关于后端:Golang如何优雅接入多个远程配置中心

42次阅读

共计 8894 个字符,预计需要花费 23 分钟才能阅读完成。

本文基于 viper 实现了 apollo 多实例疾速接入,授人以渔,带着大家读源码,详解实现思路,封装成本人的工具类并且开源。

前言

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

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

拥抱开源

带着下面的这些问题,结合实际商业我的项目的实际,曾经有较成熟的解决方案。本着分享的准则,现已将 xconfig 包脱敏开源:github 地址,欢送体验和 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

装置胜利后,只须要在下面代码根底上,最后面加上 remote.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 即可:

//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

而后本地通过 -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:

//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 字段阐明,高深莫测。

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

//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 包的疾速上手的应用阐明到封装实际难点痛点的解析,并行不悖,让你更深刻的了解,心愿对大家有帮忙。

正文完
 0