通常包括以下内容:
- 环境配置:Region、Zone、Cluster、Environment、Color、Discovery、AppID、Host 等之类的环境变量信息,通过在线运行时平台打入到容器或物理机,供 kit 库读取使用。比如 Dev、UAT、Preprod、Prod、DR 等环境。
- 静态配置:即资源需要初始化的配置信息,比如 HTTP/gRPC server、Redis、MySQL 等,通常不建议运行时变更(很可能会导致业务出现不可预期的事故),变更静态配置和发布 bianry app 没有区别,应该走迭代发布流程。在设计上应考虑 协议卸载:将有状态、需要运行时变更的业务逻辑下沉,而避免安排在接入节点层(比如TCP Server,无状态)。
- 动态配置:应用程序可能需要比较简单的在线开关控制业务策略,会频繁的调整和使用,这类用于动态变更业务流的(比如 AB Test 的 flag,一般是基础类型 int、bool 等)配置可收归在一起,考虑结合 expvar (opens new window) 使用,与配置中心打通。
- 全局配置:通常各类依赖组件、中间件都有大量默认配置或指定配置,在各个项目里大量复制容易出现意外。所以使用配置模板来定制化常用组件,在特化应用进行局部替换。
配置传参先参考 net/http 库:
func main() {
s := &http.Server{
Addr: ":8080",
Handler: nil,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
缺点是无法获知修改公共字段是否会有副作用,字段的含义也要自行查阅文档。
改进是自行设计 config struct,建议使用 functional options:
- 符合编程直觉,可实现高度的可配置化,容易维护和扩展。
- 自文档描述,代码可读、容易上手。
- 代码直观,无歧义(比如空值)。
type Server struct {
Addr string // required
Port int // required
Protocol string // not null, default TCP
Timeout time.Duration // not null, default 30
MaxConn int // not null, default 1024
TLS *tls.Config //
}
type Option func(*Server)
func Protocol(p string) Option {
return func(s *Server) {
s.Protocol = p
}
}
func Timeout(timeout time.Duration) Option {
return func(s *Server) {
s.Timeout = timeout
}
}
func MaxConn(maxConn int) Option {
return func(s *Server) {
s.MaxConn = maxConn
}
}
func TLS(tls *tls.Config) Option {
return func(s *Server) {
s.TLS = tls
}
}
func NewServerFP(addr string, port int, options ...Option) (*Server, error) {
// 有一个可变参数 options 可以传出多个上面的函数,for-loop 设置 Server 对象。
srv := Server{
Addr: addr,
Port: port,
Protocol: "tcp",
Timeout: 30 * time.Second,
MaxConn: 1000,
TLS: nil,
}
for _, option := range options {
option(&srv)
}
//...
return &srv, nil
}
func TestFunctionalOptions(t *testing.T) {
s1, _ := NewServerFP("localhost", 1024)
s2, _ := NewServerFP("localhost", 2048, Protocol("udp"))
s3, _ := NewServerFP("0.0.0.0", 8080, Timeout(300*time.Second), MaxConn(1000))
fmt.Println(s1, s2, s3)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
在实践中应注意配置文件到配置数据之间映射的解耦:
- 仅保留 options API。
- config file 和 options struct 解耦:比如利用 gRPC 的 Protobuf 的强 schema 约束定义 Config 对象,实现语义验证、语法高亮和 lint、格式化。
[Config Web UI] <----+---------+
| ↓
[Config API] --------+--> [Config Data] ----> [System]
| ↑
[Config Language] <--+---------+
1
2
3
4
5
2
3
4
5
YAML:需要先转换成 JSON,再转成 Protobuf。Protobuf 的 Config 对象不能直接扩展方法,所以还需要加一个 Options 方法。
func ApplyYAML(s *redis.Config, yml string) error {
js, err := yaml.YAMLToJSON([]byte(yml))
if err != nil {
return err
}
return ApplyJSON(s, string(js))
}
// Options apply config to options.
func Options(c *redis.Config) []redis.Options {
return []redis.Options{
redis.DialDatabase(c.Database),
redis.DialPassword(c.Password),
redis.DialReadTimeout(c.ReadTimeout),
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Protobuf:使用 wrap struct 区分是否有值。
syntax = "proto3";
import "google/protobuf/duration.proto";
package config.redis.v1;
// redis config.
message redis {
string network = 1;
string address = 2;
int32 database = 3;
string password = 4;
google.protobuf.Duration read_timeout = 5;
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
最终实现配置注入:
func main() {
// load config file from yaml.
c := new(redis.Config)
_ = ApplyYAML(c, loadConfig())
r, _ := redis.Dial(c.Network, c.Address, Options(c)...)
}
1
2
3
4
5
6
2
3
4
5
6
# 最佳实践
实现代码变更系统功能是冗长且复杂的过程,往往还涉及 CR、测试等流程。而更改单个配置选项也可能对功能产生重大影响,且通常情况下修改配置还容易被忽略、未经测试就上线。
配置管理的目标:
- 避免复杂:依赖的通用基础中间件使用配置中心支持的全局配置化模板。
- 多样的配置:配置模板通过覆盖某些字段实现多样化。
- 区分必选项和可选项,向简单化努力:尽可能减少必要的配置项(最佳实践)。
- 以基础设施 -> 面向用户进行转变。
- 配置的防御编程。
- 权限和变更跟踪。
- 配置的版本和应用对齐。
- 安全的配置变更:逐步部署、回滚更改、自动回滚。