配置分类
环境变量(配置)
在部署、运行时能获取到的信息,没必要放到配置文件里。可通过K8S注入到容器或者物理机,供kit 库读取使用。
- Region:区域
- Zone:可用区
- Cluster:集群
- Environment:环境,由集群本身决定,而不是取决于应用
- Color:染色信息,可用于分发流量,实现多租户
- Discovery:注册中心?课程没说清楚
- AppID:应用ID中的部分信息
- Host
静态配置
需要初始化的配置信息,比如 http/gRPC server、redis、mysql 等。
这类资源在线变更配置的风险非常大,不鼓励 on-the-fly 变更,很可能会导致业务出现不可预期的事故。
变更静态配置和发布 bianry app 没有区别,应该走一次迭代发布的流程。
动态配置
- 应用程序可能需要一些在线的开关,来控制业务的一些简单策略,会频繁的调整和使用,我们把这类是基础类型(int, bool)等配置,用于可以动态变更业务流的收归一起,同时可以考虑结合类似 https://pkg.go.dev/expvar 来结合使用。
expvar 包提供了一种标准化接口用于公共变量,例如针对 server 中的操作计数器; expvar 以 JSON 格式通过 HTTP 的
/debug/vars
来暴露这些变量; 针对这些公共变量的 set 或 modify 操作具有原子性; 内部通过并发安全的map存放各种键值对,可用于暴露应用运行指标,例如默认提供的内存状态。 - 业务配置最好做到管理后台,运营同事才好使用
全局配置
通常,我们依赖的各类组件、中间件都有大量的默认配置或者指定配置,在各个项目里大量拷贝复制,容易出现意外,不好维护。所以我们使用全局配置模板来定制化常用的组件,然后再特化的应用里进行局部替换。
可选函数模式
Go 中没有Java里的方法重载,导致配置可选参数时,需要些提供很多不同命、不同参数的方法来进行初始化(例如Redis)。
如何解决:使用可选函数模式,区分必填参数和非必填参数。
方式一
参数仅用于一个对象。
代码语言:javascript复制type Option func(log *logrus.Logger)
func Level(level logrus.Level) Option {
return func(log *logrus.Logger) {
log.Level = level
}
}
func Output(w io.Writer) Option {
return func(log *logrus.Logger) {
log.Out = w
}
}
...
func NewLogrusLogger(options ...Option) log.Logger {
logger := logrus.New()
// 默认值
logger.Level = logrus.DebugLevel
logger.Out = os.Stdout
logger.Formatter = &logrus.JSONFormatter{}
for _, option := range options {
option(logger)
}
return &LogrusLogger{
log: logger,
}
}
方式二
可选参数用于多个对象,或者还需要在其他地方使用可选参数,可将可选参数单独封装为一个对象。
代码语言:javascript复制todo gorm db
自定义扩展配置
如果想要用户可以自定义一些配置,可以看看 grpc 的配置定义,主要的思路就是把 option 从函数修改为接口,并提供一些实现类(也可以空实现,交给子类自己重写,例如下面的EmptyCallOption
),自定义配置可以继承实现类,并增加额外的字段。在使用这些可选参数时,通过类型转化得到扩展参数。
type CallOption interface {
before(*callInfo) error
after(*callInfo, *csAttempt)
}
type EmptyCallOption struct{}
func (EmptyCallOption) before(*callInfo) error { return nil }
func (EmptyCallOption) after(*callInfo, *csAttempt) {}
func Header(md *metadata.MD) CallOption {
return HeaderCallOption{HeaderAddr: md}
}
type HeaderCallOption struct {
HeaderAddr *metadata.MD
}
func (o HeaderCallOption) before(c *callInfo) error { return nil }
func (o HeaderCallOption) after(c *callInfo, attempt *csAttempt) {
*o.HeaderAddr, _ = attempt.s.Header()
}
配置文件
Protobuf 作为强 schema 的描述文件,也可以方便扩展,是不是用于配置文件定义也可?
当然可以,kratos就是这么干的,理由如下:
- 强类型。
- 可添加约束,提前暴露问题。例如在配置中心页面录入配置时,就可以进行校验。
- pb可做lint
- pb的反序列化支持解析多种格式的配置文件,例如yaml、json。
如何区分零值?
可使用pb里的包装类:https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/wrappers.proto
配置文件的反序列化对象如何赋值给上诉的可选函数配置?
方式一:直接拷贝
反序列化对象和配置对象定义成一样的结构,赋值时直接拷贝。
不可取,这样强耦合了。并且需要暴露配置对象,这是不能保证的,有的三方包仅提供可选函数配置。
方式二:统一使用可选函数配置
不管是根据配置文件进行配置,还是其他地方需要配置,都使用可选函数模式,不要提供多种初始化方式,这样基础库才精简。
使用时,额外写个反序列化对象转option数组的函数,但不能封装到kit库,因为这个反序列对象是使用者通过pb定义的,大家都不同。
代码语言:javascript复制// Options apply config to options.
func (c *Config) Options() []redis.Options {
return []redis.Options{
redis.DialDatabase(c.Database),
redis.DialPassword(c.Password),
redis.DialReadTimeout(c.ReadTimeout),
}
}
func main() {
// instead use load yaml file.
c := &Config{
Network: "tcp",
Addr: "127.0.0.1:3389",
Database: 1,
Password: "Hello",
ReadTimeout: 1 * time.Second,
}
r, _ := redis.Dial(c.Network, c.Addr, c.Options()...)
}
个人认为反序列化对象转option数组的函数意义不大,还是直接给可选函数赋值简单。
另外毛老师说kratos讲配置定义的pb文件生成go代码时,直接额外生成转option的函数,这是不可行的,因为option是基础库里定义的,kratos现在也并没有这么做。
最佳实践
代码更改系统功能是一个冗长且复杂的过程,往往还涉及Review、测试等流程,但更改单个配置选项可能会对功能产生重大影响,通常配置还未经测试。
修改配置其实是一件比较危险的事情,很多时候我们缺乏足够的敬畏,因为现在在线的配置中心越来方便,所以修改的成本越来越低,大家就越来越随意,所以我们需要对配置的修改慎重一些。
配置的目标:
- 避免复杂
- 多样的配置简
- 单化努力
- 以基础设施 -> 面向用户进行转变
- 配置的必选项和可选项
- 配置的防御编程
- 权限和变更跟踪
- 配置的版本和应用对齐
- 安全的配置变更:逐步部署、回滚更改、自动回滚
参考
GO 编程模式:FUNCTIONAL OPTIONS——陈皓
Go工程化(六) 配置管理
Post Views: 5