微服務配置中心, 這個方案 Go 里用起來不輸 SpringCloud
微服務架構設計模式里有一條講到,要設計可配置的服務。把服務從單體架構細分成微服務后,所有配置屬性都集中存儲在一個位置,更易于管理。這個集中存儲管理配置的地方,就是配置中心。
使用配置中心還有一個好處就是,往往都支持應用配置的熱更新,這樣就不需要像修改本地配置那樣進行發版部署了。
但是這么好的事兒就沒有缺點了嗎?當然有,除非有基礎設施支持,否則它需要額外的人力進行設計和運維。不過好在有各種開源框架比如 Spring Cloud Config,能使服務接入配置中心,沒有什么侵入性。至少在表面使用上感覺不到有變化。
那么在 Go 里有沒有類似的方案呢?經過我這周的試驗探索,還真發現了,這個方案落地也很簡單,今天就跟大家簡單說說。更詳細的還得是大家上手操作起來才能感受到,本方案涉及的代碼太多,可以給我的公眾號:網管叨bi叨,發送消息【go-config】獲取項目下載地址。
分享軟件開發和系統架構設計基礎、Go 語言和Kubernetes。
有人可能會說遠程配置中心,我就把配置放在 ETCD 上,項目啟動的時候拉下來不就行了?先別著急,咱先看看隔壁家 Spring 是怎么實現這個事兒,有沒有我們可以學習的地方。
Spring 的配置和配置中心
用過 Spring 的同學都接觸過,在 Java 的項目里都有一個resources?目錄,這個目錄里一般都會有類似名字叫application.properties 的配置文件。
配置文件
也有可能配置文件的后綴名是.yaml?,那么屬性配置的格式就是YAML格式的。
在這些配置文件里設置的屬性配置,都可以通過可以通過@Value注解注入到對象的屬性上,比如假設我們在配置文件里設定了訂單的折扣為95折的配置
order:
discount: 95
那在代碼里,我們就能像下面這樣,把屬性配置的值綁定到類實例的屬性上:
public class CoffeeOrderController {
@Value("${order.discount}")
private Integer discount;
......
}
后來,因為微服務流行起來了,大家又對自己的服務拆得樂此不疲,所以 Spring 家族里后來又有了 SpringCloud,像我們知道的知名廠商 Alibaba、Netflix 都按它這個標準開源自己內部使用的組件,就有了我們天天看到的各種資料推廣文里面的 SpringCloud-Netflix,SpringCloud- Alibaba這些。
SpringCloud- Alibaba在國內因為阿里的關系使用更廣泛一些,它里面提供的配置中心方案是一個叫 Nacos 的組件,因為有 SpringCloudConfig 這個標準存在,不管各個廠商的遠程配置中心是用什么組件,都需要實現 SpringCloudConfig 里的標準。
最直觀的好處就是,比如說我把應用的屬性配置放到了遠程的 Nacos 上,比如這樣:
遠程配置中心 Nacos
但是在應用程序我們仍然可以繼續使用 @Value注解拿到放在遠程配置中心的屬性值。如果本地和遠程配置中心都有的話,以本地磁盤里的配置優先。
是不是很方便?這就類似應用里使用的是一個門面模式,下層加載使用的組件提供的driver來完成項目配置的載入。
那在 Go 里面有沒有類似的方案呢?有,雖然沒有 SpringCloud 這個支持的組件那么全,但是支持 ETCD 和 Consul 做配置中心,也夠用了。
Go 項目的配置和配置中心
聊到 Go 項目的配置和配置中心,我見過的幾十個項目里,是的,前幾年待過的兩個拿融資多的創業公司里,項目就是很多,不停地嘗試各種方向的業務,不然投資人那不好說啊,咳咳。有的,做做沒有效果就放棄了 T—_—T。
說回來,咱們配置的事兒,在這幾十個項目里基本上分成兩大派,有用 Viper 或者另一個Yaml開源庫直接操作本地文件的。還有一派是直接讀 ETCD ,拿下來把字節流轉到本地配置對象的。
那有沒有一種方案能兼容本地配置和遠程配置中心兩種模式的?
我看了一下 Viper 是支持從遠程 ETCD 或者 Consul 取配置的。
但是呢,經過我的試驗,發現官網的給的例子有BUG,從 ETCD 上根本讀不了配置,更別提熱更新了,這點我們先按下不表,我先給大家介紹下 Viper 的基本使用。
主要是我也沒從頭用過,以前用的項目架子里是別人搭好的,哈哈~,不過你們面試的時候可別這么說大實話,今天看完我的文章,至少配置中心這塊的架構選型,我是可以吹吹的,你們呢?
怎么安裝 Viper 包什么的,我就不說了,官網上都有,文末會附上官網的鏈接,下面直接上代碼。假如,不是假如,我真在項目配置文件里寫了個數據庫連接信息的YAML配置。
database:
type: mysql
dsn: "user:pass@tcp(localhost:30306)/db_name?charset=utf8&parseTime=True&loc=Local"
maxopen: 100
maxidle: 10
maxlifetime: 300
然后用 Viper 怎么讀這個配置呢?這里直接在配置文件目錄下用一個 Go 的 init 函數,在函數里把配置用 Viper 反序列化到一個全局變量里,供項目使用。
type databaseConfig struct {// 配置屬性跟類型字段不同名是要加下面這個tag
Type string `mapstructure:"type"`
DSN string `mapstructure:"dsn"`
MaxOpenConn int `mapstructure:"maxopen""`
MaxIdleConn int `mapstructure:"maxidle"`
MaxLifeTime time.Duration `mapstructure:"maxlifetime"`
}
var Database *databaseConfig
func init() {
// 獲取當前文件的路徑
_, filename, _, _ := runtime.Caller(0)
// 配置文件目錄的路徑
configBaseDir := path.Dir(filename)
vp := viper.New()
vp.AddConfigPath(configBaseDir)
vp.SetConfigType("yaml")
err := vp.ReadInConfig()
if err != nil {
panic(err)
}
vp.UnmarshalKey("database", &Database)
Database.MaxLifeTime *= time.Second
}
除了把配置項反序列化到結構體類型里,還能通過類似 Spring 里的@Value那種方式讀單個配置項的值。
vp.Get("database.type")
不過我更傾向于反序列化到結構體這種方式,使用起來更方便,同時熱更新配置時這種方式也更方便些。
項目里實例化數據庫連接的時候,就可以像這樣,用上我們的配置啦。
// 具體完整實例代碼,實在太多
// 請給我的公眾號:網管叨bi叨,發送消息【go-config】來領取。
db, err := gorm.Open(config.Database.Type, config.Database.DSN)
if err != nil {
panic(err)
}
db.DB().SetMaxOpenConns(config.Database.MaxOpenConn)
db.DB().SetMaxIdleConns(config.Database.MaxIdleConn)
db.DB().SetConnMaxLifetime(config.Database.MaxLifeTime)
if err = db.DB().Ping(); err != nil {
panic(err)
}
下面我們接著來說,Viper 使用遠程配置中心的情況。這里我給大家安利下我的 ETCD 集群 K8s 搭建教程:用Kubernetes搭建Etcd集群和WebUI,不然如果你本地沒有 ETCD 的話,不太好實踐,除非…你嚯嚯下你們公司測試環境的ETCD,嘿嘿,保命要緊,還是在自己電腦上搭建吧。
另外安利下我的 K8s 教程,上面用的Nacos也是我用 K8s 搭建的,在教程里都有,在公眾號網管叨bi叨回復k8s就能拿到教程,絕對實用。
下面我們給項目加一個 redis 連接信息的配置
redis:
address: "localhost:6579"
password: "DFgsdfhshf"
dbnumber: 0
maxactive: 100
maxidle: 20
把這個配置放到遠程的ETCD 配置中心里:
ETCD 中的配置
后來我按照官網的例子,死活讀不到我這個key 對應的配置,在網上查了一下,究其原因,是因為 Viper 依賴 crypt 庫,而 crypt 截至目前還不支持新版 ETCD 的 API。
ETCD 的 KV 中可以存儲加密的數據,Viper 在獲取的時候通過 crypt 自動解密,這個初衷是好的,但是公司里的配置中心基本上都是內網訪問,再則加密存儲的話,我就不能像上面這樣直接在客戶端里進行KV編輯了,有什么辦法呢?
看網上有技術大佬分析,可以通過重新實現remoteConfigFactory接口;
type remoteConfigFactory interface {
Get(rp RemoteProvider) (io.Reader, error)
Watch(rp RemoteProvider) (io.Reader, error)
WatchChannel(rp RemoteProvider) (<-chan *RemoteResponse, chan bool)
}
這個接口的具體實現我就不放上來了實在是太多,可以自己下載項目去看,下載鏈接獲取方式,給我的公眾號「網管叨bi叨」發送消息【go-config】獲取項目下載鏈接。
使用 Viper 讀取遠程配置,還需要匿名導入它提供的一個庫。
_ "github.com/spf13/viper/remote"
下面演示一下使用 Viper 讀取遠程配置和熱更新配置的代碼。
type RedisConfig struct {
Address string `mapstructure:"address"`
Password string `mapstructure:"password"`
DbNumber int `mapstructure:"dbnumber"`
MaxActive int `mapstructure:"maxactive"`
MaxIdle int `mapstructure:"maxidle"`
}
var Redis *RedisConfig
func init() {
// 初始化 Viper 和上面例子里的一樣,這里省略
...
代碼里省略一切error處理
// 告訴Viper遠程ETCD里的KV在哪里找
err := vp.AddRemoteProvider("etcd", "http://127.0.0.1:32379", "root/config/viper-test/config")
vp.SetConfigType("yaml")
err = vp.ReadInConfig()
err = vp.ReadRemoteConfig()
vp.UnmarshalKey("database", &Database)
Database.MaxLifeTime *= time.Second
vp.UnmarshalKey("redis", &Redis)
// 這里簡單輸出一下 redis 的配置,就不做其他演示了
fmt.Printf("Redis Config: %v\n", Redis)
// 監聽KV變更,進行熱更新
go watchRemoteConfig(vp)
}
監聽配置變更,進行熱更新這塊,我暫時實現的簡單點,用了下輪詢,后面有好的方法了再更新。
func watchRemoteConfig(vp *viper.Viper) {
for {
time.Sleep(5 * time.Second)
err := vp.WatchRemoteConfigOnChannel()
if err != nil {
zlog.Error("Read Config Server Error", zap.Error(err))
return
}
// 監控遠程配置的變更
vp.UnmarshalKey("redis", &Redis)
fmt.Printf("Redis Config: %v\n", Redis)
}
}
這里演示的配置熱更新我就是簡單向控制臺輸出了一下 Redis 的配置,啟動后我試了一下,在ETCD里把配置修改后能直接在項目里變更過來,下面是我把Redis配置的端口從 6579 改成 6580 的一個演示。
配置動態更新
總結
今天給大家講了微服務配置中心的實現方案,先介紹了下 SpringCloudConfig 標準下的使用方案,因為Spring生態比較完整,對這方面支持的比較好,像ETCD、Consul甚至Git什么的都支持拿來做配置中心。
Go 里邊的 Viper 庫也很強大,只是用 Etcd 當配置中心的時候需要我們自己做些擴展,雖然沒有那么開箱即用,但是研究問題動手解決的過程還是很有意思的。
對了,Viper 支持同時使用本地和遠程配置,本地配置優先級高于遠程,大家不要弄混了。
這里我再給個建議像是服務器啟動參數 server.port,application.name這類幾乎是項目創建完后就不會再改的配置,放在本地配置文件就好。