前语

viper是适用于go应用程序的装备解决方案,这款装备管理神器,支撑多种类型、开箱即用、极易上手。

本地装备文件的接入能很快速的完结,那么关于长途apollo装备中心的接入,是否也能很快速完结呢?如果有多个apollo实例都需求接入,是否能支撑呢?以及apollo长途装备改变后,是否能支撑热加载,实时更新呢?

拥抱开源

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

下面快速介绍下xconfig包的运用与能力,然后针对包的封装实践做个讲解

获取装置

go get -u github.com/jinzaigo/xconfig

Features

  • 支撑viper包诸多同名办法
  • 支撑本地装备文件和长途apollo装备热加载,实时更新
  • 运用sync.RWMutex读写锁,解决了viper并发读写不安全问题
  • 支撑apollo装备中心多实例装备化快速接入

接入示例

本地装备文件

指定装备文件途径完结初始化,即可经过xconfig.GetLocalIns().xxx()链式操作,读取装备

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

长途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装备,支撑多实例多namespace
apollo:
  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 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
    }
    //多实例初始化
    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 package
var 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.go
var (
  ...
  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

golang基于viper实现apollo多实例快速接入

然后本地经过-race实验,也发现会有这个竞态问题

golang基于viper实现apollo多实例快速接入

进一步剖析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.go
type 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字段阐明,一望而知。

golang基于viper实现apollo多实例快速接入

基于此阐明,然后在代码里做了兼容处理,而且装备文件也加上运用阐明

//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地址:github.com/jinzaigo/xc…。欢迎体会与star。

参考资料

  • www.liwenzhou.com/posts/Go/vi…
  • www.liwenzhou.com/posts/Go/ap…
  • github.com/spf13/viper
  • github.com/shima-park/…
  • www.apolloconfig.com/#/zh/usage/…