在蚂蚁金服工作的时候,见到和使用了很多设计糟糕的系统,其中涉及最糟糕的叫做一个 AntX 的东西,现在想起来还会吓得发抖。
具体的实现已经记不太清了,因为自己从来也没有真正掌握过。只是记得这个其实是一个动态配置的系统,要使用它,在你的应用中要写入3个模板文件,其中一个模板会先进行 render,再去用这些变量 render 另外一个模板;render 的过程用到的变量是在一个 web 界面上配置的,并不存在于代码中;实际 render 出来的结果,有一些是程序编译的时候注入实际变量,貌似有一部分还是在运行时进行注入的。
复杂的模板文件、每一个模板还是使用不同的语法、需要在 web 配置、配置区分编译时和运行时变量、区分不同的环境变量。这听起来就是一个灾难了,我相信整个公司也找不到几个人能把这个最终配置生成的过程说的明明白白。最终大家也承认这套系统无法继续维护了,程序中已经存在有很多变量其实是没有用到的,但是程序跑的好好的,谁也没有动力去梳理一遍。最后,大家决定写一个新的配置系统来替代它。
Config 是面向用户的东西,应该像 UI 一样追求简洁,易懂,避免歧义。因为配置错误而导致的事故数不胜数,其实,很多都是由配置的作者以及使用者的理解有代沟造成的。这篇博客就来讲一讲我觉得不错的配置实践。
There should be one– and preferably only one –obvious way to do it.
软件的输入有很多种,命令行参数、环境变量、stdin、配置文件等都可以作为配置项去控制软件的行为。很多软件对同一个配置提供多种配置方式,增加了复杂度。
比如 Etcd 的这个 BUG:grpc gateway 在使用 –config-file 的时候默认是 false,不使用 –config-file 的时候是 true。即使 –config-file=/dev/null 也会变成 false。当时花了很长时间去排查。
我觉得对于服务端的软件,其实大部分配置都要求从配置文件中配置就可以。在 lobbyboy 这个项目中验证了一下自己的想法。lobbyboy 作为一个 server 端软件,所有的配置只能从配置文件输入,除了 -c
可以改变配置文件的 Path 之外没有别的命令行参数。
Nginx 的命令行也没有多少参数,大部分配置都是通过配置文件来控制的。
当然对于 Client 端软件来说,就比较复杂了,很多参数同时支持环境变量、args、xxxrc 文件配置,会比较用户友好一些。比如 iredis 的配置文件读取顺序是:
- Options from command line
$PWD/.iredisrc
~/.iredisrc
(this path can be changed withiredis --iredisrc $YOUR_PATH
)/etc/iredisrc
- default config in IRedis package.
客户端软件的配置最好遵守 XDG Base Directory Specification。
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
让配置更加简单一些,在设计的时候,可以考虑这几个问题:
- 这个配置是否有必要?用户在大部分场景下是否用默认值就足够了?
- 多个配置是否可能用一个配置没有歧义地讲清楚?
- 可以这样考虑:添加一个新的配置的时候,如何使用最少的语言将这个配置解释清楚?
在 UI 的设计中,每一个像素都是重要的,每一个空间都要想办法争取节省,添加太多没必要的东西,将会把UI变得不直观。配置也是,配置不是一个无限大的文件,配置的添加是会带来成本的,不是程序运行的成本,而是人的成本,这比运行的性能更加重要。
说到动态配置,很多时候我都在怀疑这到底是不是一个伪需求。现在的应用都被设计成无状态的,可以随时重启的。那么我修改配置的时候,是否可以改一下配置文件然后重新部署?当然,如果有上千个实例的话,修改速度可能会成为一个问题。
有动态配置功能的时候,一个误区是开发者会将所有“感觉将来可能会修改”的配置都放到动态配置中。其实我觉得所有的配置都可以先作为 hardcode,如果发现需要修改的多了,再移动到配置文件或者动态配置中。
Json 绝对是一个糟糕的配置语言。它的优点在于机器读取和解析没有歧义,不像 Yaml 那样。
但是它对于人类编辑来说,实在太不友好了:
- json 只是易于解析但是太难编辑,非常容易出错。比如 [] 不允许在末位添加
,
,否则会出现格式错误。这样,你每次编辑的时候,添加一行会出现两行 diff. - json in json 更是噩梦。我发现一个规律,就是在有 json 的地方,就会有 json in json(指的是 json 里面有一些 value,是字符串,字符串只是添加了转义的 json)。甚至有可能有 json in json in json.
- 比如很多list都设计成
items:[{key: name, value:jack}]
,一个list 这已经就已经有两层的嵌套了。 - Json 对于 line editor,比如 sed,awk 也不够友好。
- 不支持注释,导致无法在文档里面给出一个 self-explain 的 example.
If the implementation is hard to explain, it’s a bad idea.
If the implementation is easy to explain, it may be a good idea.
很多配置的名字都和实现绑定,可能是一个思维定式。为一个配置想出来一个好的名字,可以尝试忘记实现的细节,想一想如何能够直白、没有歧义地、容易被记住地解释这个配置。
一个典型的例子是,配置尽量不要使用双重否定。比如 disallow_publish
,最好使用 allow_publish
.
uWSGI 里面有一个配置叫做 harakiri
,是日语的“切腹自尽”的意思,表示一个进程在规定的时间内如果没有完成请求的处理,就会退出。比较贴切。
Redis 的 save 触发时间的配置我觉得设计的很好:
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 |
################################ SNAPSHOTTING ################################ # # Save the DB on disk: # # save <seconds> <changes> # # Will save the DB if both the given number of seconds and the given # number of write operations against the DB occurred. # # In the example below the behaviour will be to save: # after 900 sec (15 min) if at least 1 key changed # after 300 sec (5 min) if at least 10 keys changed # after 60 sec if at least 10000 keys changed # # Note: you can disable saving completely by commenting out all "save" lines. # # It is also possible to remove all the previously configured save # points by adding a save directive with a single empty string argument # like in the following example: # # save "" save 900 1 save 300 10 save 60 10000 |
可惜的是,这种设计好像没有什么模式可以遵循。
后记:其实这篇文章放在草稿箱里面很久了,只是有一些想法,但是不知道怎么描述比较合适。配置如同给变量起名字一样,是一个难以说清的话题。本文也只是潦草的表达了一些凌乱的想法,看了设计服务端软件配置的 4 条建议 这篇文章,决定将草稿箱里面沉睡已久的文章发出来。欢迎读者交流沟通。
小 typo:
s/valye:jack/value:jack
感谢,已改正
不支持用 “// …” 注释的 JSON 配置文件,写起来特别特别蛋疼。
是的,有时候用
{"#": "comment", "#": "comment"}
这样来替代。Pingback: 程序 Hot reload config 的实现方式 | 卡瓦邦噶!