konfig:採用ConfigMap實現線上配置熱更新

進德發表於2020-12-05

前言

利用kubernetes部署應用越來越流行,而執行在kubernetes中的服務需要的各種各樣的配置如何才能實現熱更新?難道需要在kubernetes中再部署zookeeper或者etcd之類的服務麼?本文采用的方案是利用ConfigMap作為服務配置的持久化方案,並利用kubernetes提供的watch能力主動發現ConfigMap更新並及時更新到服務的配置中。這樣運維人員只需要利用kubernetes的控制檯(cli或者web)修改線上服務的配置,比如修改日誌等級、降級、調整閾值等等。

本文引用原始碼https://github.com/jindezgm/konfig/blob/master/konfig.go

實現

konfig是利用kubernetes的一個ConfigMap實現的一個配置樹,雖然ConfigMap.Data是map[string]string型別,但是konfig會對ConfigMap.Data中的值做進一步(遞迴)yaml解析,前提條件是值是以"---\n"開頭。這樣設計的目的是讓konfig支援多級配置,而ConfigMap只有一級在一些使用場景並不友好。當然,利用一級也是可以實現多級配置,只是把多級體現在key上,例如:"a.b.c", "c.d",筆者認為視覺上不太優雅。

konfig支援以多種型別獲取相同的配置,可以根據需要轉換成指定型別,如下介面定義所示:

// 所有介面都會返回配置的版本號,即ConfigMap.ResourceVersion,keys是多級的key,當keys為空時表示根,即整個ConfigMap.
type Interface interface {
    // 如果keys已經被註冊某種型別(參看下面的RegValue介面),則返回指定型別的值,否則返回原生型別的值。
	Get(keys ...string) (interface{}, int64)
    // 獲取布林型
	GetBool(keys ...string) (bool, int64)
    // 獲取64、32位整型、
	GetInt64(keys ...string) (int64, int64)
	GetInt(keys ...string) (int, int64)
	GetInt32(keys ...string) (int32, int64)
    // 獲取64、32位浮點型
	GetFloat64(keys ...string) (float64, int64)
	GetFloat32(keys ...string) (float32, int64)
    // 獲取字串
	GetString(keys ...string) (string, int64)
    // 將指定keys下的值註冊為一種型別,配合Get()介面使用可以將keys下的值轉換為註冊的型別返回,其中tag是成員變數的tag名稱,比如json
    RegValue(ptr interface{}, tag string, keys ...string) (interface{}, int64)
    // 獲取指定型別的value,konfig會將map[string]interface{}轉換為value物件,其中tag是成員變數的tag名稱,比如json.
    GetValue(ptr interface{}, tag string, keys ...string) int64
    // 將指定的keys下值掛載/解除安裝到環境變數,
    MountEnv(keys ...string)
	UnmountEnv()
    // 獲取版本號
	Revision() int64
}

konfig在Get指定型別的配置時,除了原生型別外,盡最大努力將原生型別轉為指定型別,以下是konfig支援的型別轉換:

  • int,int32,int64:支援浮點型轉整型、支援字串轉整型(string->float64->intxx)、支援布林型轉整型(true:1,false:0)
  • float32,float64:支援字串轉浮點型
  • bool:支援整型、浮點型轉布林型(非零:true,零:false),支援字串轉布林型(不區分大小寫的"True":true,不區分大小寫的"False":false)
  • string:支援所有型別轉字串,採用fmt.Sprintf("%v")返回

konfig保證了單個介面的原子性,但是如果連續呼叫兩次介面可能會返回兩個不同的版本,說明在兩次呼叫介面之間ConfigMap發生了變化。如果兩次呼叫介面獲得的配置引數對於版本一致性要求比較高的話,就需要重新呼叫,直到所有的配置的版本相同為止。這種情況發生的概率比較低,並且絕大部分重新呼叫一次就可以解決,因為ConfigMap的更新頻率極低。但是,這種方法貌似有點醜陋,使用者可以將此類的配置定義一種struct,然後通過GetValue()一次性拿到所有的配置。筆者常用的方法就是把整個ConfigMap定義為一個型別,然後一次性讀取所有的配置。如下程式碼所示:

import "github.com/jindezgm/konfig"

// 定義自己的配置型別
type MyConfig struct {
    Bool   bool   `json:"bool"`
    Int    int    `json:"int"`
    String string `json:"string"`
}

kfg, _ := konfig.NewWithClientset(...)
var my MyStruct 
var rev int64

// 應用引用配置的功能實現
for {
    // 版本發生更新時重新獲取配置
    if r := kfg.Revision(); r > rev {
        // 此處的keys為空,這是將整個ConfigMap.Data對映為MyStruct
        rev = kfg.GetValue(&my, "json")
    }
    // 使用配置
    ...
}

因為GetValue()介面會執行一次類似Unmarshal的過程,所以是有一定開銷的,適用於呼叫頻率不高的場景。如果需要高頻呼叫,建議應用快取配置(如上程式碼),並根據revision決定是否呼叫該介面。如果應用想省去這些麻煩的操作,那就呼叫RegValue()介面將型別註冊到konfig,由konfig按需解析,在配合Get()介面就可以滿足高頻呼叫的場景了。如下程式碼所示:

// 將"my"下的所有值註冊為MyConfig型別
kfg.RegValue(&MyConfig{}, "json", "my")
for {
    // 每次引用直接呼叫Get,konfig保證一致性、隔離性以及原子性
    value, _ = kfg.Get("my")
    my := value.(*MyConfig)
    ...
}

在一些場景,某個功能點只需要引用一個配置項,使用者每次引用時可以直接呼叫介面,不用在自己的程式碼中快取配置(當revision變大再讀取配置更新快取),因為konfig的讀取效能還是有保證的。如下程式碼所示,按配置列印:

for {
    if p, _ := kfg.GetBool("print"); p {
        fmt.Println("Hello world")
    }
}

當然,如果習慣讀取環境變數的方法獲取配置,而容器更新環境變數又會造成容器重啟,那麼可以用MountEnv()介面將配置掛載到環境變數,如下程式碼所示:

// keys為空,將ConfigMap.Data掛載到環境變數. 需要注意的是,MountEnv()的keys下應該只有一級配置,如果是多級,konfig會用fmt.Sprintf("%v")進行格式化
kfg.MountEnv() 
defer kfg.UnmountEnv()
for {
    if strings.ToLower(os.Getenv("print")) == "true" {
        fmt.Println("Hello world")
    }
}

konfig支援兩種建立方式:1.利用Clientset;2.利用SharedInformerFactory。前者適用於應用無需不訪問kubernetes場景,需要為konfig單獨建立一次clientset,這部分可以參考官方例項程式碼;後者適用於應用需要訪問kubernetes的場景,那麼konfig與應用共享Informer。無論哪一種情況,都需要授權pod讀取ConfigMap的許可權。

不足

  1. 當前konfig遞迴解析只支援yaml格式,其實json也很容易支援,感覺不是很必要;
  2. konfig不支援回撥,即ConfigMap更新後,將變化的部分回撥給使用者,當前的解決方式是通過revision解決;
  3. GetValue()雖然能夠一次獲取多個配置,但是需要所有的配置都在一個鍵下,如果一次獲取配置樹不同分支的多個配置,konfig還不支援,感覺可以用flag.FlagSet實現;
  4. MountEnv()不會比較兩次配置的差異,然後刪除新ConfigMap中沒有的配置,他只是簡單的將每次ConfigMap中的值設定到環境變數中;而UnmountEnv()也不會刪除環境變數,只是不再更新環境變數而已;這些都可以實現,只是暫時還沒有看到必要性;

相關文章