清新脱俗的config loader
前因
平日里,大家都在为了需求和业务所奔波,而对一些琐碎的小case却无空关心。因为公司内有很多个大大小小的项目,而这些项目又都在同一个仓库中统一管理,不同的项目很有可能去引入同一个配置文件。这就好比项目1和项目2,同时都在用MongoDB的同一个db。大家对db的引用的命名千奇百怪,同事A叫这个db link为db_addr,同事B又称它为seach_mdb_address。实际上他们的地址都一样。业务上并无伤大雅,都可以实现想要的功能。但弊端也很明显:
1)新人不容易上手
2)代码定义了很多同样的配置
3)不利于代码的检索
4)代码理解起来困难
5)配置和代码杂糅在一起,难以分离(这是典型的混元形意写法)
后续
直到一位同事入职组内,他也早对这种用法“不满”,于是他加班加点的整理出所有的相同配置项,将其分为测试环境和正式环境(共两份)。
var TestFile = []byte(`
es_http_addr: http://es.gateway.funshareapp.com:9200
es_cluster_addr: es.gateway.funshareapp.com`)
mongo.Module().WithAddr(configx.C.MongoMainAddr).WithMinPoolSize(configx.C.MongoPoolMinSize)
这种隔离式的配置方式,很大程度上减少了代码量。想引用db的配置,只需要使用configx.C.MongoMainAddr,即call的就是mdb的address。原理上就是使用了viper.ReadConfig,将配置文件的内容在项目启动前,init阶段就装载到了全局的configx中。configx解决的问题
1)新人好上手,开箱即用
2)配置相关代码需要重复定义
3)代码好检索,直接使用ide的reference就能看到相关配置的引用及出处
4)代码似乎也没那么难理解了。
但对于我这种事多的“老油条”来讲,回过头看这个配置文件,总是觉得哪里别扭,但是又讲不出所以然。
转折
经过我长时间的思考,终于,我还是没想出来哪里不对劲,于是也只好安心的继续写bug了。
直到手里接到了一个需求,也算是折腾个小游戏吧。游戏自然少不了的就是游戏收益计算(游戏内金币)。这时候产品说了:小师傅,这个游戏暂时是需要200金币/局,后续可是会变的,而且有些信息会经常变,我不想对游戏有很大的影响,你最好不要让用户感知到我们在修改游戏信息。听到这,我恍然大悟,所需不正是在线配置平台所提供的吗??!游戏内有些信息可能不需要变动,有些常常需要变动,而也有些配置偶尔修改即可。
- 游戏所连接的数据库链接就可能偶尔变动,甚至不需要变动
- 单场游戏时长,游戏门票要经常性修改,这属于运营的行为
鉴于此需求,我才想到上文所提及的“别扭”的原因,是代码和配置都杂糅在了业务中,修改可并不简单,这需要我们上线才能生效,这严重缩减了程序员小哥哥的下午茶的时间啊。
说干就干
配置理解起来不难,即项目中,填好了所有的配置,代码就能按照这份配置吭哧吭哧的干活,你修改了配置,代码干活的行为或者方式也会相应的改变。一个好的系统,当你修改了配置后,会自动的reload你的配置,而无需“中场休息”,这也正是我想做的。
首先我先构思出一个大致的规划,配置以yaml的形式出现,里面放着一些不会轻易变动的配置信息,比如redis地址,mongo地址和log存放地址等。但是怎么做一个动态的配置呢?好吧,不拐弯抹角了,就用Apollo(携程的动态配置平台)吧。
Apollo以监听的形式,去监听对配置文件的修改,一旦监听到配置文件有改动,就会push到client端,以保证配置的实时生效。
那么我们就有了大致的方向了,静态yaml存放不动的配置,Apollo的地址以及namespace。
CODE
#配置文件
app:
ports:
ws: 7001
http: 7002
grpc: 7003
redis:
rds:
address: "127.0.0.1:6379"
db: 0
maxIdle: 32
poolSize: 128
connectTimeout: 300ms
idleTimeout: 60s
readTimeout: 100ms
writeTimeout: 100ms
mongo:
mdb:
dsn: "127.0.0.1:27017"
apollo:
app: appname
env: prod
ip: 127.0.0.1:8080
file: .apollo
package conf
import (
"awesomeProject4/conf/mongo"
"awesomeProject4/conf/redis"
"errors"
"log"
"os"
"path"
"path/filepath"
)
type Config struct {
App struct {
Ports map[string]int `mapstructure:"ports"`
} `mapstructure:"app"`
Redis redis.Conf `mapstructure:"redis"`
Mongo mongo.Conf `mapstructure:"mongo"`
}
const (
configFolderName = "conf"
RdsRedisName = "rds"
MongoName = "mongo"
WebsocketPortName = "ws"
HttpPortName = "http"
GrpcPortName = "grpc"
)
var (
// C is the global config
C Config
// Env is one of dev/prod
Env = "prod"
errNotFound = errors.New("not found")
)
func init() {
if env := os.Getenv("ENV"); env != "" {
Env = env
}
loadConfig()
validateConfig()
}
func validateConfig() {
err := C.Redis.Ensure([]string{RdsRedisName})
if err != nil {
log.Fatalln(err)
}
err = C.Mongo.Ensure([]string{MongoName})
if err != nil {
log.Fatalln(err)
}
}
func loadConfig() {
configDir, err := getDir(configFolderName)
if err != nil {
log.Fatalf("getDir(\"%s\") error: %v", configFolderName, err)
}
configFileName := Env + ".conf.yaml"
configPath := path.Join(configDir, configFileName)
err = LoadConfig(configPath, &C)
if err != nil {
log.Fatalf("load config error: %v", err)
}
}
func getDir(dir string) (string, error) {
for i := 0; i < 3; i++ {
if info, err := os.Stat(dir); err == nil && info.IsDir() {
return dir, nil
}
dir = filepath.Join("..", dir)
}
return "", errNotFound
}
// redis引用
rc := conf.C.Redis.Get(conf.MatchRedisName)
conn := rc.Get()
conn.Close()
// Apollo的引用
ac := conf.NewConfigServer()
ac.Add(ns)
ac.UnmarshalConfig(&cfg)
此上即为整个配置文件的应用,其恰好将业务代码和配置信息完成了剥离。