前言
应用程序里要用到很多配置,比如监听端口、数据库相关配置、缓存相关配置等。这些配置可能来自不同的源,最常见的是本地配置文件,也可以作为命令行参数或环境变量传入,还可以托管在远程配置中心。同一项配置可能在多个不同的配置源中同时存在,如何处理优先级?在程序运行过程中,有些配置需要能够动态更新,又该如何实现?配置的源、优先级及动态更新,就是配置管理的内容。
Golang
配置层级
Viper
- 命令行参数
- 环境变量
- 本地配置文件
- 远程配置文件
- 默认值
Viper
0. 配置结构
yaml
# myconfig.yaml aaa: foo xxx: bbb: bar ccc: 100
Vipermap
为方便使用,我们可以定义如下的结构体来接收配置:
package config var Config config type config struct { Aaa string Xxx struct { Bbb string Ccc uint } }
1. 命令行参数
ViperBindPFlagsCobra
// 1. 根据需要定义命令行参数 pflags = rootCmd.PersistentFlags() pflags.StringP("aaa", "a", "", "aaa's usage") pflags.String("xxx.bbb", "", "xxx.bbb's usage") pflags.Uint("xxx.ccc", 0, "xxx.ccc's usage") // 2. Viper绑定命令行参数 viper.BindPFlags(pflags)
这样就可以通过命令行参数来传入相关配置:
// 参数aaa使用长参数格式 $ mycmd --aaa foo --xxx.bbb bar --xxx.ccc 100 // 参数aaa使用短参数格式 $ mycmd -a foo --xxx.bbb bar --xxx.ccc 100
2. 环境变量
ViperAutomaticEnv
// 设置环境变量的前缀 viper.SetEnvPrefix("MYCMD") // 将配置Key中的点号(.)和横杠(-)替换为下划线(_) viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) // 从环境变量中读取匹配的配置 viper.AutomaticEnv()
这样就可以通过环境变量来传入相关配置:
$ MYCMD_AAA=foo MYCMD_XXX_BBB=bar MYCMD_XXX_CCC=100 mycmd
3. 本地配置文件
Viper
// 在初始化cobra时,将本地配置文件路径定义为命令行参数 pflags.StringVarP(&cfgFile, "config", "c", "", "config file") // 在初始化viper时,设置本地配置文件路径 viper.SetConfigFile(cfgFile) // 读取本地配置文件 if err := viper.ReadInConfig(); err != nil { log.Fatal(err) }
这样就可以通过本地配置文件来传入相关配置:
// myconf.yaml为同目录下的配置文件,内容如上文所述 $ mycmd --config myconf.yaml
4. 远程配置文件
Viperetcd
// 注意:在实际项目中,以下参数也可以作为配置传入 provider := "etcd3" endpoint := "x.x.x.x:2379" path := "/config/mycmd/myconf.yaml" viper.AddRemoteProvider(provider, endpoint, path) viper.SetConfigType("yaml") if err := viper.ReadRemoteConfig(); err != nil { log.Println("Read remote config failed:", err) }
etcd
$ cat myconf.yaml | etcdctl put /config/mycmd/myconf.yaml
5. 默认值
Viper
viper.SetDefault("aaa", "foo") viper.SetDefault("xxx.bbb", "bar") viper.SetDefault("xxx.ccc", 100)
在所有配置源都加载后,我们可以将Viper配置装载到配置结构体变量中,方便使用:
if err := viper.Unmarshal(&config.Config); err != nil { log.Fatal("Viper unmarshal failed:", err) }
配置动态更新
以上解决了静态配置管理的问题,如何实现配置的动态更新呢?
配置的动态更新包括三个步骤:
- 更新配置源中的配置
- 更新内存中的配置
- 配置的动态生效
ViperViper
1. 更新配置源中的配置
Viper
2. 更新内存中的配置
Viper
viper.OnConfigChange(func(e fsnotify.Event) { // 回调函数 // 可在此处更新内存中的本地配置: // 1. 重新读取本地配置:ReadInConfig // 2. 装载到配置结构体:Unmarshal // 注意:为Viper及配置结构体加锁 }) viper.WatchConfig()
对于远程配置文件,可以通过周期性地重新读取来实现动态更新,示意代码如下:
go func() { for { time.Sleep(time.Second * 10) // 周期为10s // 可在此处更新内存中的远程配置: // 1. 重新读取远程配置:WatchRemoteConfig // 2. 装载到配置结构体:Unmarshal // 注意:为Viper及配置结构体加锁 } }()
3. 配置的动态生效
不同配置的使用方式不同,动态生效方式也就不同,大概可以分为两类:
- 有些配置,每次使用时都是读取最新的值,因此只要更新了内存中的配置,就会即时生效
- 有些配置,比如数据库凭据,可能需要重建连接池才能生效
在这里就不再细说了。
总结
Golang