微服務(wù)或者說云原生應(yīng)用的配置最佳實(shí)踐是將配置文件和應(yīng)用代碼分開管理——不將配置文件放入代碼倉庫,也不打包進(jìn)容器鏡像,而是在服務(wù)運(yùn)行時,把配置文件掛載進(jìn)去或者直接從配置中心加載。Kratos的config組件就是用來幫助應(yīng)用從各種配置源加載配置。
Kratos定義了標(biāo)準(zhǔn)化的Source和Watcher接口來適配各種配置源。
框架內(nèi)置了本地文件file和環(huán)境變量env的實(shí)現(xiàn)。
另外,在contrib/config下面,我們也提供了如下的配置中心的適配供使用:
如果上述的配置加載方式無法涵蓋您的環(huán)境,您也可以通過實(shí)現(xiàn)接口來適配您自己的配置加載方式。
配置組件復(fù)用了?encoding
?中的反序列化邏輯作為配置解析使用。默認(rèn)支持以下格式的解析:
框架將根據(jù)配置文件類型匹配對應(yīng)的Codec,進(jìn)行配置文件的解析。您也可以通過實(shí)現(xiàn)Codec并用?encoding.RegisterCodec
?方法,將它注冊進(jìn)去,來解析其它格式的配置文件。
配置文件類型的提取,根據(jù)配置源具體實(shí)現(xiàn)不同而略有區(qū)別,內(nèi)置的file是把文件后綴作為文件類型的,其它配置源插件的具體邏輯請參考對應(yīng)的文檔。
Kratos的config組件支持配置的熱更新,您可以使用配置中心配合config的熱更新功能,在服務(wù)不重新發(fā)布/不停機(jī)/不重啟的情況下,在線更新服務(wù)的配置,修改服務(wù)的一些行為。
在config組件中,所有的配置源中的配置(文件)將被逐個讀出,分別解析成map,并合并到一個map中去。因此在加載完畢后,不需要再理會配置的文件名,不用文件名來進(jìn)行查找,而是用內(nèi)容中的結(jié)構(gòu)來對配置的值進(jìn)行索引即可。設(shè)計和編寫配置文件時,請注意各個配置文件中,根層級的key不要重復(fù),否則可能會被覆蓋。
舉例:
有如下兩個配置文件:
# 文件1
foo:
baz: "2"
biu: "example"
hello:
a: b
# 文件2
foo:
bar: 3
baz: aaaa
hey:
good: bad
qux: quux
?.Load
?后,將被合并為如下的結(jié)構(gòu):
{
"foo": {
"baz": "aaaa",
"bar": 3,
"biu": "example"
},
"hey": {
"good": "bad",
"qux": "quux"
},
"hello": {
"a": "b"
}
}
我們可以發(fā)現(xiàn),配置文件的各層級將分別合并,在key沖突時會發(fā)生覆蓋,而具體的覆蓋順序,會由配置源實(shí)現(xiàn)中的讀取順序決定,因此這里重新提醒一下,各個配置文件中,根層級的key不要重復(fù),也不要依賴這個覆蓋的特性,從根本上避免不同配置文件的內(nèi)容互相覆蓋造成問題。
在使用時,可以用?.Value("foo.bar")
?直接獲取某個字段的值,也可以用?.Scan
?方法來將整個map讀進(jìn)某個結(jié)構(gòu)體中,具體使用方式請看下文。
使用file,即從本地文件加載: 這里的path就是配置文件的路徑,這里也可以填寫一個目錄名,這樣會將整個目錄中的所有文件進(jìn)行解析加載,合并到同一個map中。
import (
"github.com/go-kratos/kratos/v2/config"
"github.com/go-kratos/kratos/v2/config/file"
)
path := "configs/config.yaml"
c := config.New(
config.WithSource(
file.NewSource(path),
)
)
如果想用外部的配置中心,可以在contrib/config里面找一個,以consul為例:
import (
"github.com/go-kratos/kratos/contrib/config/consul/v2"
"github.com/hashicorp/consul/api"
)
consulClient, err := api.NewClient(&api.Config{
Address: "127.0.0.1:8500",
})
if err != nil {
panic(err)
}
cs, err := consul.New(consulClient, consul.WithPath("app/cart/configs/"))
if err != nil {
panic(err)
}
c := config.New(config.WithSource(cs))
不同的配置源插件使用方式略有差別,您可以參考它們各自的文檔或examples。
首先要定義一個結(jié)構(gòu)體用來解析字段,如果您使用的是kratos-layout創(chuàng)建的項(xiàng)目,可以參考后面講解kratos-layout的部分,使用proto文件定義配置和生成struct。
我們這里演示的是手工定義結(jié)構(gòu),您需要在結(jié)構(gòu)體上用json tag來定義您配置文件的字段。
var v struct {
Service struct {
Name string `json:"name"`
Version string `json:"version"`
} `json:"service"`
}
使用之前創(chuàng)建好的config實(shí)例,調(diào)用.Scan方法,讀取配置文件的內(nèi)容到結(jié)構(gòu)體中,這種方式適用于完整獲取整個配置文件的內(nèi)容。
// Unmarshal the config to struct
if err := c.Scan(&v); err != nil {
panic(err)
}
fmt.Printf("config: %+v", v)
使用config實(shí)例的.Value方法,可以單獨(dú)獲取某個字段的內(nèi)容。
name, err := c.Value("service.name").String()
if err != nil {
panic(err)
}
fmt.Printf("service: %s", name)
通過?.Watch
?方法,可以監(jiān)聽配置中某個字段的變更,在本地或遠(yuǎn)端的配置中心有配置文件變更時,執(zhí)行回調(diào)函數(shù)進(jìn)行自定義的處理
if err := c.Watch("service.name", func(key string, value config.Value) {
fmt.Printf("config changed: %s = %v\n", key, value)
// 在這里寫回調(diào)的邏輯
}); err != nil {
log.Error(err)
}
如果有配置需要從環(huán)境變量讀取,請使用以下方式:
配置環(huán)境變量配置源env:
c := config.New(
config.WithSource(
// 添加前綴為 KRATOS_ 的環(huán)境變量,不需要的話也可以設(shè)為空字符串
env.NewSource("KRATOS_"),
// 添加配置文件
file.NewSource(path),
))
// 加載配置源:
if err := c.Load(); err != nil {
log.Fatal(err)
}
// 獲取環(huán)境變量 KRATOS_PORT 的值,這里用去掉前綴的名稱進(jìn)行讀取
port, err := c.Value("PORT").String()
除了上面使用Value方法直接讀的方式,也可以在配置文件內(nèi)容里使用占位符來把環(huán)境變量中的值渲染進(jìn)去:
service:
name: "kratos_app"
http:
server:
# 使用 service.name 的值
name: "${service.name}"
# 使用環(huán)境變量 PORT 替換,若不存在,使用默認(rèn)值 8080
port: "${PORT:8080}"
# 使用環(huán)境變量 TIMEOUT 替換,無默認(rèn)值
timeout: "$TIMEOUT"
Decoder用于將配置文件內(nèi)容用特定的反序列化方法解析出來,默認(rèn)decoder會根據(jù)文件的類型自動識別類型并解析,通常情況不需要自定義這個,您可以通過后文的實(shí)現(xiàn)Codec的方式來注冊更多文件類型。
在初始化config時加入?WithDecoder
?參數(shù),可以將Decoder覆蓋為自定義的邏輯。如下代碼展示了配置自定義Decoder的方法,這里使用了yaml庫解析所有配置文件,您可以使用這種方式來使用特定的配置文件解析方法,但更推薦使用后文的實(shí)現(xiàn)Codec的方式,能同時支持多種格式的解析。
import "gopkg.in/yaml.v2"
c := config.New(
config.WithSource(
file.NewSource(flagconf),
),
config.WithDecoder(func(kv *config.KeyValue, v map[string]interface{}) error {
return yaml.Unmarshal(kv.Value, v)
}),
)
Resolver用于對解析完畢后的map結(jié)構(gòu)進(jìn)行再次處理,默認(rèn)resolver會對配置中的占位符進(jìn)行填充。您可以通過在初始化config時加入?WithResolver
?參數(shù),來覆蓋resolver的行為。
c := config.New(
config.WithSource(
file.NewSource(flagconf),
),
config.WithResolver(func (input map[string]interface{}) (err error) {
// 在這里對input進(jìn)行處理即可
// 您可能需要定義一個遞歸的函數(shù),來處理嵌套的map結(jié)構(gòu)
return
}),
)
首先實(shí)現(xiàn)Codec,這里以yaml為例
import (
"github.com/go-kratos/kratos/v2/encoding"
"gopkg.in/yaml.v3"
)
const Name = "myyaml"
func init() {
encoding.RegisterCodec(codec{})
}
// codec is a Codec implementation with yaml.
type codec struct{}
func (codec) Marshal(v interface{}) ([]byte, error) {
return yaml.Marshal(v)
}
func (codec) Unmarshal(data []byte, v interface{}) error {
return yaml.Unmarshal(data, v)
}
func (codec) Name() string {
return Name
}
然后注冊該Codec 這里由于我們把注冊代碼?encoding.RegisterCodec(codec{})
?寫在了包的?init
?方法中,所以在包被import的時候,將會運(yùn)行這個?init
?方法,也就是進(jìn)行注冊。所以您可以在代碼入口(比如?main.go
?)對它進(jìn)行注冊
import _ "path/to/your/codec"
隨后,config組件就能把上面代碼中?const Name = "myyaml"
?這部分作為格式類型名,調(diào)用該Codec解析這個文件。
layout中涉及到配置文件有以下部分,簡單介紹一下它們的作用
configs
?目錄,您可以修改這個文件里?config.New()
?參數(shù)中使用的配置源,從其它配置源(比如配置中心)進(jìn)行加載配置。配置在這里將被加載到?conf.Bootstrap
?結(jié)構(gòu)體中,這個結(jié)構(gòu)體的內(nèi)容可以通過依賴注入,注入到服務(wù)內(nèi)部的其它層,比如server或data,這樣各層就能讀取到各自需要的配置,完成自己的初始化。
.proto
?文件來進(jìn)行配置定義,然后通過在根目錄執(zhí)行?make config
?,就可以將對應(yīng)?.pb.go
?文件生成到相同目錄下供使用。在初始狀態(tài)下,這個?conf.proto
?所定義的結(jié)構(gòu),就是?configs/config.yaml
?的結(jié)構(gòu),請保持兩者一致。
.proto
?定義的配置對應(yīng)的?.pb.go
?文件(就是調(diào)了一下protoc),要記得每次修改定義后,一定要執(zhí)行這個指令來重新生成go文件我們已經(jīng)把根據(jù)proto生成結(jié)構(gòu)體的指令預(yù)置在Makefile里面了,通過在項(xiàng)目根目錄下執(zhí)行?make config
?即可生成。它實(shí)際上是調(diào)用了?protoc
?工具,掃描internal目錄下的proto文件進(jìn)行生成。
正如前文所說,我們可以在代碼中直接用struct來定義配置結(jié)構(gòu)進(jìn)行解析。但您可能會發(fā)現(xiàn),我們的最佳實(shí)踐項(xiàng)目模板kratos-layout中采用了Protobuf來定義配置文件的結(jié)構(gòu)。通過Protobuf定義,我們可以同時支持多種格式如?json
?、?xml
?或者?yaml
?等多種配置格式統(tǒng)一解析,這樣在讀配置時會變得非常方便。
layout中使用了如下的?.proto
?文件定義配置文件的字段:
syntax = "proto3";
package kratos.api;
option go_package = "github.com/go-kratos/kratos-layout/internal/conf;conf";
import "google/protobuf/duration.proto";
message Bootstrap {
Server server = 1;
}
message Server {
message HTTP {
string network = 1;
string addr = 2;
google.protobuf.Duration timeout = 3;
}
message GRPC {
string network = 1;
string addr = 2;
google.protobuf.Duration timeout = 3;
}
HTTP http = 1;
GRPC grpc = 2;
}
我們可以看出,Protobuf的定義結(jié)構(gòu)清晰,并且可以指定字段的類型,這在后續(xù)的配置文件解析中可以起到校驗(yàn)的作用,保證加載配置文件的有效性。
在定義好結(jié)構(gòu)后,我們需要用?protoc
?工具來生成對應(yīng)的?.pb.go
?代碼,也就是相應(yīng)的Go struct和序列化反序列化代碼,供我們使用。
修改?internal/conf/config.proto
?文件的內(nèi)容,在這里使用Protobuf IDL定義你配置文件的結(jié)構(gòu)。您也可以在這個目錄下創(chuàng)建新的proto文件來定義額外的配置格式。
在項(xiàng)目根目錄執(zhí)行下面的命令即可生成用來解析配置文件的結(jié)構(gòu)體:
make config
執(zhí)行成功后,您應(yīng)該能看到?config.pb.go
?生成在?config.proto
?文件的旁邊,您就可以使用里面的結(jié)構(gòu)體,比如?Bootstrap
?來讀取您的配置。
讀取配置項(xiàng)、監(jiān)聽配置變更和其它高級用法等使用方面的內(nèi)容,與前文介紹的一致,這里就不再贅述。
更多建議: