七叶笔记 » golang编程 » Viper 加载远程配置的填坑之旅

Viper 加载远程配置的填坑之旅

关于 viper[1],无须多言,它是 Golang 社区里最流行的配置文件工具,除了常见功能之外,它还支持很多高级功能,比如可以加载远程配置,正好我最近在研究 etcd[2],于是我打算把二者结合起来,没想到就此开启了填坑之旅。

按照 文档[3] 上的介绍,只需启动一个 goroutine 执行 WatchRemoteConfig[4] 即可:

Watching Changes in etcd

可惜当我套用如上代码时,却发现在我的环境里根本没法用。究其原因,是因为 viper 依赖 crypt[5] ,而 crypt 截至目前还 不支持[6] 新版 etcd 的 api。

viper 使用 crypt 做什么呢?在文档中可以找到如下描述:

也就是说,KV 中可以存储加密的数据,viper 在获取的时候通过 crypt 自动解密。虽然此功能的出发点很好,但是在绝大多数场景下,etcd 中的数据通过 acl + tls 来保护就足够了,并不需要存储加密数据。此外,别的不说,如果真的都存储加密数据,那么至少我们想通过 etcdkeeper[7] 之类的工具修改 etcd 数据就不容易了。

于是我琢磨着有没有变通的方法,当我在查阅 原始代码[8] 的时候,发现一个奇怪的问题:

如上所示:Get 和 Watch 两个操作几乎一摸一样,内部都是调用后端的 Get 方法来获取数据。此时回头看看开头关于 WatchRemoteConfig 的例子的话,你会发现虽然语意上是想通过 Watch 监控,但是后端实际上执行的却是 Get,所谓的 Watch 监控其实是通过 Get 轮训实现的!官方代码为什么要这么干呢?当我 blame[9] 后终于发现了 原因[10]

原本 Get 和 Watch 两个操作是各自独立的,当在 for 循环里调用 WatchRemoteConfig 的时候,实际上就相当于在 for 循环里调用后端 Watch[11] ,偏偏 crypt 的 Watch 实现在每次调用的时候都会生成一个新的 goroutine,并且无法退出,于是乎 goroutine 的数量就失控了,因为问题出在 crypt 身上,viper 无法根治,为了掩盖此问题,不得不把 Watch 改成了 Get,并引入一个 WatchRemoteConfigOnChannel[12] 方法来实现更完善的监控。

了解了前因后果之后,我决定跳过 crypt,自己实现加载远程配置的功能,其实就是实现 viper 中的 remoteConfigFactory 接口:

 type remoteConfigFactory interface {
 Get(rp RemoteProvider) (io.Reader, error)
 Watch(rp RemoteProvider) (io.Reader, error)
 WatchChannel(rp RemoteProvider) (<-chan *RemoteResponse, chan bool)
}
  

代码如下所示,参考了原始 remote.go[13] 的实现方式:

 package remote

import (
 "bytes"
 "context"
 "io"
 "time"

 "github.com/spf13/viper"
 "go.etcd.io/etcd/clientv3"
)

type Config struct {
 viper.RemoteProvider

 Username string
 Password string
}

func (c *Config) Get(rp viper.RemoteProvider) (io.Reader, error) {
 c.RemoteProvider = rp

 return c.get()
}

func (c *Config) Watch(rp viper.RemoteProvider) (io.Reader, error) {
 c.RemoteProvider = rp

 return c.get()
}

func (c *Config) WatchChannel(rp viper.RemoteProvider) (<-chan *viper.RemoteResponse, chan bool) {
 c.RemoteProvider = rp

 rr := make(chan *viper.RemoteResponse)
 stop := make(chan bool)

 go func() {
  for {
   client, err := c.newClient()

   if err != nil {
    time.Sleep(time.Duration(time.Second))
    continue
   }

   defer client.Close()

   ch := client.Watch(context.Background(), c.RemoteProvider.Path())

   select {
   case <-stop:
    return
   case res := <-ch:
    for _, event := range res.Events {
     rr <- &viper.RemoteResponse{
      Value: event.Kv.Value,
     }
    }
   }
  }
 }()

 return rr, stop
}

func (c *Config) newClient() (*clientv3.Client, error) {
 client, err := clientv3.New(clientv3.Config{
  Endpoints: []string{c.Endpoint()},
  Username:  c.Username,
  Password:  c.Password,
 })

 if err != nil {
  return nil, err
 }

 return client, nil
}

func (c *Config) get() (io.Reader, error) {
 client, err := c.newClient()

 if err != nil {
  return nil, err
 }

 defer client.Close()

 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
 resp, err := client.Get(ctx, c.Path())
 cancel()

 if err != nil {
  return nil, err
 }

 return bytes.NewReader(resp.Kvs[0].Value), nil
}
  

实际调用代码和以前类似,只需额外触发一下 WatchRemoteConfigOnChannel 即可:

 func main() {
 endpoint := "#34;
 path := "/config/test"

 viper.RemoteConfig = &remote.Config{}

 v := viper.New()
 v.AddRemoteProvider("etcd", endpoint, path)
 v.SetConfigType("toml")
 v.ReadRemoteConfig()
 v.WatchRemoteConfigOnChannel()

 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  fmt.Fprint(w, v.GetString("service.password"))
 })

 http.ListenAndServe(":8080", nil)
}
  

最后再次强烈推荐通过 etcdkeeper 作为 etcd 的 web 前端工具,真好看真方便:

etcdkeeper

一切就绪后,你可以试着修改 etcd 里的数据,甚至重启 etcd 服务后再修改 etcd 里的数据,你会惊喜的发现应用代码无需轮训就能实时感知到数据变化,完美!

参考资料

[1]

viper:

[2]

etcd:

[3]

文档: #watching-changes-in-etcd—unencrypted

[4]

WatchRemoteConfig:

[5]

crypt:

[6]

不支持:

[7]

etcdkeeper:

[8]

原始代码:

[9]

blame:

[10]

原因:

[11]

Watch:

[12]

WatchRemoteConfigOnChannel:

[13]

remote.go:

相关文章