2021-05-10

清新脱俗的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)

 

此上即为整个配置文件的应用,其恰好将业务代码和配置信息完成了剥离。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值