前言

viper是实用于go应用程序的配置解决方案,这款配置管理神器,反对多种类型、开箱即用、极易上手。

本地配置文件的接入能很疾速的实现,那么对于近程apollo配置核心的接入,是否也能很疾速实现呢?如果有多个apollo实例都须要接入,是否能反对呢?以及apollo近程配置变更后,是否能反对热加载,实时更新呢?

拥抱开源

带着下面的这些问题,结合实际商业我的项目的实际,曾经有较成熟的解决方案。本着分享的准则,现已将xconfig包脱敏开源,github地址:https://github.com/jinzaigo/xconfig,欢送体验和star。

上面疾速介绍下xconfig包的应用与能力,而后针对包的封装实际做个解说

获取装置

go get -u github.com/jinzaigo/xconfig

Features

  • 反对viper包诸多同名办法
  • 反对本地配置文件和近程apollo配置热加载,实时更新
  • 应用sync.RWMutex读写锁,解决了viper并发读写不平安问题
  • 反对apollo配置核心多实例配置化疾速接入

接入示例

本地配置文件

指定配置文件门路实现初始化,即可通过xconfig.GetLocalIns().xxx()链式操作,读取配置

package mainimport (    "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

近程apollo配置核心

指定配置类型与apollo信息实现初始化,即可通过xconfig.GetRemoteIns(key).xxx()链式操作,读取配置

单实例场景

//初始化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())

多实例场景

在本地配置文件config.yaml保护apollo配置信息,而后批量实现多个实例的初始化,即可通过xconfig.GetRemoteIns(key).xxx()链式操作,读取配置

#apollo配置,反对多实例多namespaceapollo:  one:    endpoint: xxx    appId: xxx    namespaces:      one:        key: ApplicationConfig   #用于读取配置,保障全局惟一,防止互相笼罩        name: application        #留神:name不要带类型(例如application.properties),这里name和type离开配置        type: properties      two:        key: cipherConfig        name: cipher        type: properties    backupFile: /tmp/xconfig/apollo_bak/test.agollo #每个appId应用不同的备份文件名,防止互相笼罩
package mainimport (    "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    }    //多实例初始化    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())}

封装实际

学会应用xconfig包后,能疾速的实现本地配置文件和近程apollo配置核心多实例的接入。再进一步理解这个包在封装过程都中遇到过哪些问题,以及对应的解决方案,能更深刻的了解与应用这个包,同时也有助于减少读者本人在封装新包时的实际实践根底。

1.viper近程连贯不反对apollo

查看viper的应用文档,会发现viper是反对近程K/V存储连贯的,所以一开始我尝试着连贯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")//执行后果://panic: AddRemoteProvider error: Unsupported Remote Provider Type "apollo"

执行后发现,并不反对apollo,随即查看viper源码,发现只反对以下3个provider

// SupportedRemoteProviders are universally supported remote providers.var SupportedRemoteProviders = []string{"etcd", "consul", "firestore"}

解决方案:

装置shima-park/agollo包: go get -u github.com/shima-park/agollo

装置胜利后,只须要在下面代码根底上,最后面加上 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")//执行后果://AddRemoteProvider success

2.agollo是怎么让viper反对apollo连贯的呢

不难发现,在执行 remote.SetAppID("appId") 之前,remote.go 中init办法,会往viper.SupportedRemoteProviders中append一个"apollo",其实就是让viper认识一下这个provider,随后将viper.RemoteConfig 做从新赋值,并从新实现了viper中的Get Watch WatchChannel这3个办法,里边就会做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 packagevar RemoteConfig remoteConfigFactory

3.agollo只反对apollo单实例,怎么扩大为多实例呢

执行remote.SetAppID("appId")之后,这个appId是往全局变量appID里写入的,并且在初始化时也是读取的这个全局变量。带来的问题就是不反对apollo多实例,那么解决呢

//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连贯呢,并且咱们还能够定制化的扩大成多实例连贯。实现步骤如下:

  1. shima-pack/agollo/viper-remote/remote.go复制一份进去,把全局变量appID删掉
  2. 定义"providers sync.Map",实现AddProviders()办法,将多个appId往里边写入,里边带上agollo.Option相干配置;同时要害操作要将新的provider往viper.SupportedRemoteProviders append,让viper意识这个新类型
  3. 应用的中央,依据写入时用的provider 串,去读取,这样多个appId和Option就都辨别开了
  4. 其余代码有标红的中央就相应改改就行了

外围代码(查看更多):

//github.com/jinzaigo/xconfig/remote/remote.govar (  ...  providers sync.Map)func init() {  viper.RemoteConfig = &configProvider{} //目标:重写viper.RemoteConfig的相干办法}type conf struct {  appId string  opts  []agollo.Option}//【重要】这里是实现反对多个appId的外围操作func AddProviders(appId string, opts ...agollo.Option) string {    provider := "apollo:" + appId    _, loaded := providers.LoadOrStore(provider, conf{        appId: appId,        opts:  opts,    })    //之前未存储过,则向viper新增一个provider,让viper意识这个新提供器    if !loaded {        viper.SupportedRemoteProviders = append(            viper.SupportedRemoteProviders,            provider,        )    }    return provider}//应用的中央func newApolloConfigManager(rp viper.RemoteProvider) (*apolloConfigManager, error) {  //读取provider相干配置  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开启热加载后会有并发读写不平安问题

首先viper的应用文档(链接),也阐明了这个并发读写不平安问题,倡议应用sync包防止panic

而后本地通过-race试验,也发现会有这个竞态问题

进一步剖析viper实现热加载的源代码:其实是通过协程实时更新kvstrore这个map,读取数据的时候也是从kvstore读取,并没有加锁,所以会有并发读写不平安问题

// 在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")}

解决方案:

写:不应用viper自带热加载办法,而是采纳重写,也是起协程实时更新,但会加读写锁

读:也加读写锁

外围代码(查看更多):

//github.com/jinzaigo/xconfig/config.gotype Config struct {    configType string    viper      *viper.Viper    viperLock  sync.RWMutex}//写//_ = 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参数

问题形容:调用agollo包中的相干办法,输出namespace=application.properties(带类型),发现被动拉取数据胜利,近程变更告诉后数据拉取失败;输出namespace=application(不带类型),发现被动拉取数据胜利,近程变更告诉后数据拉取也能胜利。两者输出差别就在于是否带类型

问题起因:查看Apollo官网接口文档,配置更新推送接口notifications/v2 notifications字段阐明,高深莫测。

基于此阐明,而后在代码里做了兼容解决,并且配置文件也加上应用阐明

//github.com/jinzaigo/xconfig/config.go 72行func (c *Config) AddApolloRemoteConfig(endpoint, appId, namespace, backupFile string) error {    ...    //namespace默认类型不必加后缀,非默认类型须要加后缀(备注:这里会波及到apollo变更告诉后的热加载操作 Start->longPoll)    if c.configType != "properties" {        namespace = namespace + "." + c.configType    }    ...}//config.yml配置阐明namespaces:    one:        key: ApplicationConfig   #用于读取配置,保障全局惟一,防止互相笼罩        name: application        #留神:name不要带类型(例如application.properties),这里name和type离开配置        type: properties

论断

基于理论商业我的项目实际,晋升配置管理组件能力,实现了本地配置文件与近程apollo配置核心多实例疾速接入;从xconfig包的疾速上手的应用阐明到封装实际难点痛点的解析,并行不悖,让你更深刻的了解,心愿对你有所帮忙与播种。

开源我的项目xconfig,github地址:https://github.com/jinzaigo/xconfig。欢送体验与star。

参考资料

  • https://www.liwenzhou.com/posts/Go/viper/
  • https://www.liwenzhou.com/posts/Go/apollo/
  • https://github.com/spf13/viper
  • https://github.com/shima-park/agollo
  • https://www.apolloconfig.com/#/zh/usage/apollo-user-guide
本文参加了SegmentFault 思否写作挑战赛,欢送正在浏览的你也退出。