Go語(yǔ)言基于viper實(shí)現(xiàn)apollo多實(shí)例快速
前言
viper是適用于go應(yīng)用程序的配置解決方案,這款配置管理神器,支持多種類型、開(kāi)箱即用、極易上手。
本地配置文件的接入能很快速的完成,那么對(duì)于遠(yuǎn)程apollo配置中心的接入,是否也能很快速完成呢?如果有多個(gè)apollo實(shí)例都需要接入,是否能支持呢?以及apollo遠(yuǎn)程配置變更后,是否能支持熱加載,實(shí)時(shí)更新呢?
擁抱開(kāi)源
帶著上面的這些問(wèn)題,結(jié)合實(shí)際商業(yè)項(xiàng)目的實(shí)踐,已經(jīng)有較成熟的解決方案。本著分享的原則,現(xiàn)已將xconfig包脫敏開(kāi)源,github地址:https://github.com/jinzaigo/xconfig,歡迎體驗(yàn)和star。
下面快速介紹下xconfig包的使用與能力,然后針對(duì)包的封裝實(shí)踐做個(gè)講解
獲取安裝
go get -u github.com/jinzaigo/xconfig
Features
- 支持viper包諸多同名方法
- 支持本地配置文件和遠(yuǎn)程apollo配置熱加載,實(shí)時(shí)更新
- 使用sync.RWMutex讀寫(xiě)鎖,解決了viper并發(fā)讀寫(xiě)不安全問(wèn)題
- 支持apollo配置中心多實(shí)例配置化快速接入
接入示例
本地配置文件
指定配置文件路徑完成初始化,即可通過(guò)xconfig.GetLocalIns().xxx()
鏈?zhǔn)讲僮?,讀取配置
package main import ( "fmt" "github.com/jinzaigo/xconfig" ) func main() { if xconfig.IsLocalLoaded() { fmt.Println("local config is loaded") return } //初始化 configIns := xconfig.New(xconfig.WithFile("example/config.yml")) xconfig.InitLocalIns(configIns) //讀取配置 fmt.Println(xconfig.GetLocalIns().GetString("appId")) fmt.Println(xconfig.GetLocalIns().GetString("env")) fmt.Println(xconfig.GetLocalIns().GetString("apollo.one.endpoint")) }
xxx支持的操作方法:
- IsSet(key string) bool
- Get(key string) interface{}
- AllSettings() map[string]interface{}
- GetStringMap(key string) map[string]interface{}
- GetStringMapString(key string) map[string]string
- GetStringSlice(key string) []string
- GetIntSlice(key string) []int
- GetString(key string) string
- GetInt(key string) int
- GetInt32(key string) int32
- GetInt64(key string) int64
- GetUint(key string) uint
- GetUint32(key string) uint32
- GetUint64(key string) uint64
- GetFloat(key string) float64
- GetFloat64(key string) float64
- GetFloat32(key string) float32
- GetBool(key string) bool
- SubAndUnmarshal(key string, i interface{}) error
遠(yuǎn)程apollo配置中心
指定配置類型與apollo信息完成初始化,即可通過(guò)xconfig.GetRemoteIns(key).xxx()
鏈?zhǔn)讲僮鳎x取配置
單實(shí)例場(chǎng)景
//初始化 configIns := xconfig.New(xconfig.WithConfigType("properties")) err := configIns.AddApolloRemoteConfig(endpoint, appId, namespace, backupFile) if err != nil { ...handler } xconfig.AddRemoteIns("ApplicationConfig", configIns) //讀取配置 fmt.Println(xconfig.GetRemoteIns("ApplicationConfig").AllSettings())
多實(shí)例場(chǎng)景
在本地配置文件config.yaml維護(hù)apollo配置信息,然后批量完成多個(gè)實(shí)例的初始化,即可通過(guò)xconfig.GetRemoteIns(key).xxx()
鏈?zhǔn)讲僮?,讀取配置
#apollo配置,支持多實(shí)例多namespace apollo: one: endpoint: xxx appId: xxx namespaces: one: key: ApplicationConfig #用于讀取配置,保證全局唯一,避免相互覆蓋 name: application #注意:name不要帶類型(例如application.properties),這里name和type分開(kāi)配置 type: properties two: key: cipherConfig name: cipher type: properties backupFile: /tmp/xconfig/apollo_bak/test.agollo #每個(gè)appId使用不同的備份文件名,避免相互覆蓋
package main import ( "fmt" "github.com/jinzaigo/xconfig" ) type ApolloConfig struct { Endpoint string `json:"endpoint"` AppId string `json:"appId"` Namespaces map[string]ApolloNameSpace `json:"namespaces"` BackupFile string `json:"backupFile"` } type ApolloNameSpace struct { Key string `json:"key"` Name string `json:"name"` Type string `json:"type"` } func main() { //本地配置初始化 xconfig.InitLocalIns(xconfig.New(xconfig.WithFile("example/config.yml"))) if !xconfig.GetLocalIns().IsSet("apollo") { fmt.Println("without apollo key") return } apolloConfigs := make(map[string]ApolloConfig, 0) err := xconfig.GetLocalIns().SubAndUnmarshal("apollo", &apolloConfigs) if err != nil { fmt.Println(apolloConfigs) fmt.Println("SubAndUnmarshal error:", err.Error()) return } //多實(shí)例初始化 for _, apolloConfig := range apolloConfigs { for _, namespaceConf := range apolloConfig.Namespaces { configIns := xconfig.New(xconfig.WithConfigType(namespaceConf.Type)) err = configIns.AddApolloRemoteConfig(apolloConfig.Endpoint, apolloConfig.AppId, namespaceConf.Name, apolloConfig.BackupFile) if err != nil { fmt.Println("AddApolloRemoteConfig error:" + err.Error()) } xconfig.AddRemoteIns(namespaceConf.Key, configIns) } } //讀取 fmt.Println(xconfig.GetRemoteIns("ApplicationConfig").AllSettings()) }
封裝實(shí)踐
學(xué)會(huì)使用xconfig包后,能快速的實(shí)現(xiàn)本地配置文件和遠(yuǎn)程apollo配置中心多實(shí)例的接入。再進(jìn)一步了解這個(gè)包在封裝過(guò)程都中遇到過(guò)哪些問(wèn)題,以及對(duì)應(yīng)的解決方案,能更深入的理解與使用這個(gè)包,同時(shí)也有助于增加讀者自己在封裝新包時(shí)的實(shí)踐理論基礎(chǔ)。
1.viper遠(yuǎn)程連接不支持apollo
查看viper的使用文檔,會(huì)發(fā)現(xiàn)viper是支持遠(yuǎn)程K/V存儲(chǔ)連接的,所以一開(kāi)始我嘗試著連接apollo
v := viper.New() v.SetConfigType("properties") err := v.AddRemoteProvider("apollo", "http://endpoint", "application") if err != nil { panic(fmt.Errorf("AddRemoteProvider error: %s", err)) } fmt.Println("AddRemoteProvider success") //執(zhí)行結(jié)果: //panic: AddRemoteProvider error: Unsupported Remote Provider Type "apollo"
執(zhí)行后發(fā)現(xiàn),并不支持apollo,隨即查看viper源碼,發(fā)現(xiàn)只支持以下3個(gè)provider
// SupportedRemoteProviders are universally supported remote providers. var SupportedRemoteProviders = []string{"etcd", "consul", "firestore"}
解決方案:
安裝shima-park/agollo包: go get -u github.com/shima-park/agollo
安裝成功后,只需要在上面代碼基礎(chǔ)上,最前面加上 remte.SetAppID("appId")
即可連接成功
import ( "fmt" remote "github.com/shima-park/agollo/viper-remote" "github.com/spf13/viper" ) remote.SetAppID("appId") v := viper.New() v.SetConfigType("properties") err := v.AddRemoteProvider("apollo", "http://endpoint", "application") if err != nil { panic(fmt.Errorf("AddRemoteProvider error: %s", err)) } fmt.Println("AddRemoteProvider success") //執(zhí)行結(jié)果: //AddRemoteProvider success
2.agollo是怎么讓viper支持apollo連接的呢
不難發(fā)現(xiàn),在執(zhí)行 remote.SetAppID("appId")
之前,remote.go 中init方法,會(huì)往viper.SupportedRemoteProviders中append一個(gè)"apollo",其實(shí)就是讓viper認(rèn)識(shí)一下這個(gè)provider,隨后將viper.RemoteConfig
做重新賦值,并重新實(shí)現(xiàn)了viper中的Get Watch WatchChannel這3個(gè)方法,里邊就會(huì)做apollo連接的適配。
//github.com/shima-park/agollo/viper-remote/remote.go 278-284行 func init() { viper.SupportedRemoteProviders = append( viper.SupportedRemoteProviders, "apollo", ) viper.RemoteConfig = &configProvider{} } //github.com/spf13/viper/viper.go 113-120行 type remoteConfigFactory interface { Get(rp RemoteProvider) (io.Reader, error) Watch(rp RemoteProvider) (io.Reader, error) WatchChannel(rp RemoteProvider) (<-chan *RemoteResponse, chan bool) } // RemoteConfig is optional, see the remote package var RemoteConfig remoteConfigFactory
3.agollo只支持apollo單實(shí)例,怎么擴(kuò)展為多實(shí)例呢
執(zhí)行remote.SetAppID("appId")
之后,這個(gè)appId是往全局變量appID里寫(xiě)入的,并且在初始化時(shí)也是讀取的這個(gè)全局變量。帶來(lái)的問(wèn)題就是不支持apollo多實(shí)例,那么解決呢
//github.com/shima-park/agollo/viper-remote/remote.go 26行 var ( // apollod的appid appID string ... ) func SetAppID(appid string) { appID = appid } //github.com/shima-park/agollo/viper-remote/remote.go 252行 switch rp.Provider() { ... case "apollo": return newApolloConfigManager(appID, rp.Endpoint(), defaultAgolloOptions) }
解決方案:
既然agollo包能讓viper支持apollo連接,那么何嘗我們自己的包不能讓viper也支持apollo連接呢,并且我們還可以定制化的擴(kuò)展成多實(shí)例連接。實(shí)現(xiàn)步驟如下:
- shima-pack/agollo/viper-remote/remote.go復(fù)制一份出來(lái),把全局變量appID刪掉
- 定義
"providers sync.Map"
,實(shí)現(xiàn)AddProviders()方法,將多個(gè)appId往里邊寫(xiě)入,里邊帶上agollo.Option相關(guān)配置;同時(shí)關(guān)鍵操作要將新的provider往viper.SupportedRemoteProviders append,讓viper認(rèn)識(shí)這個(gè)新類型 - 使用的地方,根據(jù)寫(xiě)入時(shí)用的provider 串,去讀取,這樣多個(gè)appId和Option就都區(qū)分開(kāi)了
- 其他代碼有標(biāo)紅的地方就相應(yīng)改改就行了
核心代碼(查看更多):
//github.com/jinzaigo/xconfig/remote/remote.go var ( ... providers sync.Map ) func init() { viper.RemoteConfig = &configProvider{} //目的:重寫(xiě)viper.RemoteConfig的相關(guān)方法 } type conf struct { appId string opts []agollo.Option } //【重要】這里是實(shí)現(xiàn)支持多個(gè)appId的核心操作 func AddProviders(appId string, opts ...agollo.Option) string { provider := "apollo:" + appId _, loaded := providers.LoadOrStore(provider, conf{ appId: appId, opts: opts, }) //之前未存儲(chǔ)過(guò),則向viper新增一個(gè)provider,讓viper認(rèn)識(shí)這個(gè)新提供器 if !loaded { viper.SupportedRemoteProviders = append( viper.SupportedRemoteProviders, provider, ) } return provider } //使用的地方 func newApolloConfigManager(rp viper.RemoteProvider) (*apolloConfigManager, error) { //讀取provider相關(guān)配置 providerConf, ok := providers.Load(rp.Provider()) if !ok { return nil, ErrUnsupportedProvider } p := providerConf.(conf) if p.appId == "" { return nil, errors.New("The appid is not set") } ... }
4.viper開(kāi)啟熱加載后會(huì)有并發(fā)讀寫(xiě)不安全問(wèn)題
首先viper的使用文檔(鏈接),也說(shuō)明了這個(gè)并發(fā)讀寫(xiě)不安全問(wèn)題,建議使用sync包避免panic
然后本地通過(guò)-race試驗(yàn),也發(fā)現(xiàn)會(huì)有這個(gè)競(jìng)態(tài)問(wèn)題
進(jìn)一步分析viper實(shí)現(xiàn)熱加載的源代碼:其實(shí)是通過(guò)協(xié)程實(shí)時(shí)更新kvstrore這個(gè)map,讀取數(shù)據(jù)的時(shí)候也是從kvstore讀取,并沒(méi)有加鎖,所以會(huì)有并發(fā)讀寫(xiě)不安全問(wèn)題
// 在github.com/spf13/viper/viper.go 1909行 // Retrieve the first found remote configuration. func (v *Viper) watchKeyValueConfigOnChannel() error { if len(v.remoteProviders) == 0 { return RemoteConfigError("No Remote Providers") } for _, rp := range v.remoteProviders { respc, _ := RemoteConfig.WatchChannel(rp) // Todo: Add quit channel go func(rc <-chan *RemoteResponse) { for { b := <-rc reader := bytes.NewReader(b.Value) v.unmarshalReader(reader, v.kvstore) } }(respc) return nil } return RemoteConfigError("No Files Found") }
解決方案:
寫(xiě):不使用viper自帶熱加載方法,而是采用重寫(xiě),也是起協(xié)程實(shí)時(shí)更新,但會(huì)加讀寫(xiě)鎖
讀:也加讀寫(xiě)鎖
核心代碼(查看更多):
//github.com/jinzaigo/xconfig/config.go type Config struct { configType string viper *viper.Viper viperLock sync.RWMutex } //寫(xiě) //_ = c.viper.WatchRemoteConfigOnChannel() respc, _ := viper.RemoteConfig.WatchChannel(remote.NewProviderSt(provider, endpoint, namespace, "")) go func(rc <-chan *viper.RemoteResponse) { for { <-rc c.viperLock.Lock() err = c.viper.ReadRemoteConfig() c.viperLock.Unlock() } }(respc) //讀 func (c *Config) Get(key string) interface{} { c.viperLock.RLock() defer c.viperLock.RUnlock() return c.viper.Get(key) }
5.如何正確的輸入namespace參數(shù)
問(wèn)題描述:調(diào)用agollo包中的相關(guān)方法,輸入namespace=application.properties(帶類型),發(fā)現(xiàn)主動(dòng)拉取數(shù)據(jù)成功,遠(yuǎn)程變更通知后數(shù)據(jù)拉取失敗;輸入namespace=application(不帶類型),發(fā)現(xiàn)主動(dòng)拉取數(shù)據(jù)成功,遠(yuǎn)程變更通知后數(shù)據(jù)拉取也能成功。兩者輸入差異就在于是否帶類型
問(wèn)題原因:查看Apollo官方接口文檔,配置更新推送接口notifications/v2 notifications字段說(shuō)明,一目了然。
基于此說(shuō)明,然后在代碼里做了兼容處理,并且配置文件也加上使用說(shuō)明
//github.com/jinzaigo/xconfig/config.go 72行 func (c *Config) AddApolloRemoteConfig(endpoint, appId, namespace, backupFile string) error { ... //namespace默認(rèn)類型不用加后綴,非默認(rèn)類型需要加后綴(備注:這里會(huì)涉及到apollo變更通知后的熱加載操作 Start->longPoll) if c.configType != "properties" { namespace = namespace + "." + c.configType } ... } //config.yml配置說(shuō)明 namespaces: one: key: ApplicationConfig #用于讀取配置,保證全局唯一,避免相互覆蓋 name: application #注意:name不要帶類型(例如application.properties),這里name和type分開(kāi)配置 type: properties
結(jié)論
基于實(shí)際商業(yè)項(xiàng)目實(shí)踐,提升配置管理組件能力,實(shí)現(xiàn)了本地配置文件與遠(yuǎn)程apollo配置中心多實(shí)例快速接入;從xconfig包的快速上手的使用說(shuō)明到封裝實(shí)踐難點(diǎn)痛點(diǎn)的解析,雙管齊下,讓你更深入的理解,希望對(duì)你有所幫助與收獲。
到此這篇關(guān)于Go語(yǔ)言基于viper實(shí)現(xiàn)apollo多實(shí)例快速的文章就介紹到這了,更多相關(guān)Go語(yǔ)言 viper內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go?語(yǔ)言使用goroutine運(yùn)行閉包踩坑分析
這篇文章主要介紹了Go?語(yǔ)言使用goroutine運(yùn)行閉包踩坑解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11golang?gorm模型結(jié)構(gòu)體的定義示例
這篇文章主要為大家介紹了golang?gorm模型結(jié)構(gòu)體的定義示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04Go語(yǔ)言實(shí)現(xiàn)一個(gè)簡(jiǎn)單生產(chǎn)者消費(fèi)者模型
本文主要介紹了Go語(yǔ)言實(shí)現(xiàn)一個(gè)簡(jiǎn)單生產(chǎn)者消費(fèi)者模型,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12Go基礎(chǔ)教程系列之WaitGroup用法實(shí)例詳解
這篇文章主要介紹了Go基礎(chǔ)教程系列之WaitGroup用法實(shí)例詳解,需要的朋友可以參考下2022-04-04go實(shí)現(xiàn)一個(gè)內(nèi)存緩存系統(tǒng)的示例代碼
本文主要介紹了go實(shí)現(xiàn)一個(gè)內(nèi)存緩存系統(tǒng)的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-10-10關(guān)于go get 下載第三方包存儲(chǔ)路徑問(wèn)題
這篇文章主要介紹了關(guān)于go get 下載第三方包存儲(chǔ)路徑問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01