Kratos 配置

2022-04-24 11:43 更新

配置

微服務(wù)或者說(shuō)云原生應(yīng)用的配置最佳實(shí)踐是將配置文件和應(yīng)用代碼分開(kāi)管理——不將配置文件放入代碼倉(cāng)庫(kù),也不打包進(jìn)容器鏡像,而是在服務(wù)運(yùn)行時(shí),把配置文件掛載進(jìn)去或者直接從配置中心加載。Kratos的config組件就是用來(lái)幫助應(yīng)用從各種配置源加載配置。

設(shè)計(jì)理念

1.支持多種配置源

Kratos定義了標(biāo)準(zhǔn)化的Source和Watcher接口來(lái)適配各種配置源。

框架內(nèi)置了本地文件file和環(huán)境變量env的實(shí)現(xiàn)。

另外,在contrib/config下面,我們也提供了如下的配置中心的適配供使用:

  • apollo
  • consul
  • etcd
  • kubernetes
  • nacos

如果上述的配置加載方式無(wú)法涵蓋您的環(huán)境,您也可以通過(guò)實(shí)現(xiàn)接口來(lái)適配您自己的配置加載方式。

2.支持多種配置格式

配置組件復(fù)用了?encoding?中的反序列化邏輯作為配置解析使用。默認(rèn)支持以下格式的解析:

  • json
  • proto
  • xml
  • yaml

框架將根據(jù)配置文件類型匹配對(duì)應(yīng)的Codec,進(jìn)行配置文件的解析。您也可以通過(guò)實(shí)現(xiàn)Codec并用?encoding.RegisterCodec?方法,將它注冊(cè)進(jìn)去,來(lái)解析其它格式的配置文件。

配置文件類型的提取,根據(jù)配置源具體實(shí)現(xiàn)不同而略有區(qū)別,內(nèi)置的file是把文件后綴作為文件類型的,其它配置源插件的具體邏輯請(qǐng)參考對(duì)應(yīng)的文檔。

3.熱更新

Kratos的config組件支持配置的熱更新,您可以使用配置中心配合config的熱更新功能,在服務(wù)不重新發(fā)布/不停機(jī)/不重啟的情況下,在線更新服務(wù)的配置,修改服務(wù)的一些行為。

4.配置合并

在config組件中,所有的配置源中的配置(文件)將被逐個(gè)讀出,分別解析成map,并合并到一個(gè)map中去。因此在加載完畢后,不需要再理會(huì)配置的文件名,不用文件名來(lái)進(jìn)行查找,而是用內(nèi)容中的結(jié)構(gòu)來(lái)對(duì)配置的值進(jìn)行索引即可。設(shè)計(jì)和編寫配置文件時(shí),請(qǐng)注意各個(gè)配置文件中,根層級(jí)的key不要重復(fù),否則可能會(huì)被覆蓋。

舉例:

有如下兩個(gè)配置文件:

# 文件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),配置文件的各層級(jí)將分別合并,在key沖突時(shí)會(huì)發(fā)生覆蓋,而具體的覆蓋順序,會(huì)由配置源實(shí)現(xiàn)中的讀取順序決定,因此這里重新提醒一下,各個(gè)配置文件中,根層級(jí)的key不要重復(fù),也不要依賴這個(gè)覆蓋的特性,從根本上避免不同配置文件的內(nèi)容互相覆蓋造成問(wèn)題。

在使用時(shí),可以用?.Value("foo.bar")?直接獲取某個(gè)字段的值,也可以用?.Scan?方法來(lái)將整個(gè)map讀進(jìn)某個(gè)結(jié)構(gòu)體中,具體使用方式請(qǐng)看下文。

使用

1.初始化配置源

使用file,即從本地文件加載: 這里的path就是配置文件的路徑,這里也可以填寫一個(gè)目錄名,這樣會(huì)將整個(gè)目錄中的所有文件進(jìn)行解析加載,合并到同一個(gè)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里面找一個(gè),以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。

2.讀取配置

首先要定義一個(gè)結(jié)構(gòu)體用來(lái)解析字段,如果您使用的是kratos-layout創(chuàng)建的項(xiàng)目,可以參考后面講解kratos-layout的部分,使用proto文件定義配置和生成struct。

我們這里演示的是手工定義結(jié)構(gòu),您需要在結(jié)構(gòu)體上用json tag來(lái)定義您配置文件的字段。

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)體中,這種方式適用于完整獲取整個(gè)配置文件的內(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ú)獲取某個(gè)字段的內(nèi)容。

name, err := c.Value("service.name").String()
if err != nil {
  panic(err)
}
fmt.Printf("service: %s", name)

3.監(jiān)聽(tīng)配置變更

通過(guò)?.Watch?方法,可以監(jiān)聽(tīng)配置中某個(gè)字段的變更,在本地或遠(yuǎn)端的配置中心有配置文件變更時(shí),執(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)
}

4.讀取環(huán)境變量

如果有配置需要從環(huán)境變量讀取,請(qǐng)使用以下方式:

配置環(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)容里使用占位符來(lá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 替換,無(wú)默認(rèn)值
    timeout: "$TIMEOUT"

5.配置解析Decoder

Decoder用于將配置文件內(nèi)容用特定的反序列化方法解析出來(lái),默認(rèn)decoder會(huì)根據(jù)文件的類型自動(dòng)識(shí)別類型并解析,通常情況不需要自定義這個(gè),您可以通過(guò)后文的實(shí)現(xiàn)Codec的方式來(lái)注冊(cè)更多文件類型。

在初始化config時(shí)加入?WithDecoder?參數(shù),可以將Decoder覆蓋為自定義的邏輯。如下代碼展示了配置自定義Decoder的方法,這里使用了yaml庫(kù)解析所有配置文件,您可以使用這種方式來(lái)使用特定的配置文件解析方法,但更推薦使用后文的實(shí)現(xiàn)Codec的方式,能同時(shí)支持多種格式的解析。

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)
  }),
)

6.配置處理Resolver

Resolver用于對(duì)解析完畢后的map結(jié)構(gòu)進(jìn)行再次處理,默認(rèn)resolver會(huì)對(duì)配置中的占位符進(jìn)行填充。您可以通過(guò)在初始化config時(shí)加入?WithResolver?參數(shù),來(lái)覆蓋resolver的行為。

c := config.New(
  config.WithSource(
    file.NewSource(flagconf),
  ),
  config.WithResolver(func (input map[string]interface{}) (err error)  {
    // 在這里對(duì)input進(jìn)行處理即可
    // 您可能需要定義一個(gè)遞歸的函數(shù),來(lái)處理嵌套的map結(jié)構(gòu)
    return 
  }),
)

7.支持其它格式的配置文件

首先實(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
}

然后注冊(cè)該Codec 這里由于我們把注冊(cè)代碼?encoding.RegisterCodec(codec{})?寫在了包的?init?方法中,所以在包被import的時(shí)候,將會(huì)運(yùn)行這個(gè)?init?方法,也就是進(jìn)行注冊(cè)。所以您可以在代碼入口(比如?main.go?)對(duì)它進(jìn)行注冊(cè)

import _ "path/to/your/codec"

隨后,config組件就能把上面代碼中?const Name = "myyaml"?這部分作為格式類型名,調(diào)用該Codec解析這個(gè)文件。

kratos-layout

理念

1.項(xiàng)目結(jié)構(gòu)

layout中涉及到配置文件有以下部分,簡(jiǎn)單介紹一下它們的作用

  • cmd/server/main.go 這個(gè)是服務(wù)的入口,我們默認(rèn)使用了內(nèi)置的config/file組件從本地文件系統(tǒng)讀取配置文件,默認(rèn)會(huì)讀取相對(duì)路徑?configs?目錄,您可以修改這個(gè)文件里?config.New()?參數(shù)中使用的配置源,從其它配置源(比如配置中心)進(jìn)行加載配置。配置在這里將被加載到?conf.Bootstrap?結(jié)構(gòu)體中,這個(gè)結(jié)構(gòu)體的內(nèi)容可以通過(guò)依賴注入,注入到服務(wù)內(nèi)部的其它層,比如server或data,這樣各層就能讀取到各自需要的配置,完成自己的初始化。
  • configs/config.yaml 這是一個(gè)示例配置文件,configs目錄的內(nèi)容通常不參與服務(wù)的生產(chǎn)環(huán)境運(yùn)行,您可以用它來(lái)進(jìn)行本地開(kāi)發(fā)時(shí)的配置文件的加載,方便應(yīng)用能本地能跑起來(lái)調(diào)試,不要將生產(chǎn)環(huán)境的配置放在這里。
  • internal/conf 在這里放配置文件的結(jié)構(gòu)定義,我們?cè)谶@里使用?.proto?文件來(lái)進(jìn)行配置定義,然后通過(guò)在根目錄執(zhí)行?make config?,就可以將對(duì)應(yīng)?.pb.go?文件生成到相同目錄下供使用。在初始狀態(tài)下,這個(gè)?conf.proto?所定義的結(jié)構(gòu),就是?configs/config.yaml?的結(jié)構(gòu),請(qǐng)保持兩者一致。
  • make config Makefile中的這個(gè)指令,用于生成?.proto?定義的配置對(duì)應(yīng)的?.pb.go?文件(就是調(diào)了一下protoc),要記得每次修改定義后,一定要執(zhí)行這個(gè)指令來(lái)重新生成go文件

2.配置生成命令

我們已經(jīng)把根據(jù)proto生成結(jié)構(gòu)體的指令預(yù)置在Makefile里面了,通過(guò)在項(xiàng)目根目錄下執(zhí)行?make config?即可生成。它實(shí)際上是調(diào)用了?protoc?工具,掃描internal目錄下的proto文件進(jìn)行生成。

3.使用Protobuf定義配置

正如前文所說(shuō),我們可以在代碼中直接用struct來(lái)定義配置結(jié)構(gòu)進(jìn)行解析。但您可能會(huì)發(fā)現(xiàn),我們的最佳實(shí)踐項(xiàng)目模板kratos-layout中采用了Protobuf來(lái)定義配置文件的結(jié)構(gòu)。通過(guò)Protobuf定義,我們可以同時(shí)支持多種格式如?json?、?xml?或者?yaml?等多種配置格式統(tǒng)一解析,這樣在讀配置時(shí)會(huì)變得非常方便。

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?工具來(lái)生成對(duì)應(yīng)的?.pb.go?代碼,也就是相應(yīng)的Go struct和序列化反序列化代碼,供我們使用。

使用

1.定義

修改?internal/conf/config.proto?文件的內(nèi)容,在這里使用Protobuf IDL定義你配置文件的結(jié)構(gòu)。您也可以在這個(gè)目錄下創(chuàng)建新的proto文件來(lái)定義額外的配置格式。

2.生成

在項(xiàng)目根目錄執(zhí)行下面的命令即可生成用來(lái)解析配置文件的結(jié)構(gòu)體:

make config

執(zhí)行成功后,您應(yīng)該能看到?config.pb.go?生成在?config.proto?文件的旁邊,您就可以使用里面的結(jié)構(gòu)體,比如?Bootstrap?來(lái)讀取您的配置。

3.使用

讀取配置項(xiàng)、監(jiān)聽(tīng)配置變更和其它高級(jí)用法等使用方面的內(nèi)容,與前文介紹的一致,這里就不再贅述。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)