服务配置管理的一些误会

这里指的配置,泛指一切能改变服务行为的变量。其生效的位置可以在编译期、部署过程中、运行期;其存在的形式,可以是代码中,本地文件,资源文件,环境变量,数据库甚至其它服务。也正是因为这种灵活性。配置管理大概算是软件工程里人们分歧最严重的领域。而且,由于“反正无论怎么样搞,怎么样都能搞定。搞定不就行了吗?”的“结果导向”的态度的存在,这部分也常常最不受实质上的重视和控制,于是对配置项肆意随性的使用,最终将导致配置混乱,配置漂移,配置变更风暴乃至生产事故的发生。(顺便说下,我这里当然就是在曲解“结果导向”,但是我见过无数人在想说一个实现细节不重要的时候,便把“结果导向”搬出来说事儿了,所以这里说是“结果导向”是为了便于一些人理解。)

服务配管管理这个话题非常大,一篇文章显然说不完。下面我仅仅讨论几个我感觉人们误会最深的点。主要内容与理念来自《Continous Delivery》一书,但是重点不同。

代码与配置分离

“代码与配置分离”常常被从字面上理解错,从而产生两个严重的误会:

  1. 代码中,不要出现与配置文件有关的任何东西。
  2. 另外,哪部分算“代码”呢?我们要求高些,就整个代码库吧。

于是在犯了这两个误会的代码库中,连个完整的配置文件都找不到。于是你连这个服务都有哪些配置项都看不出来,要去实际环境中,把环境变量啦,本地配置文件啦统统拿出来过一遍才搞得清楚。我遇到过的,更极端的做法是样的:写一个服务维护各个环境的配置项,这个还不错,但是呢,又在CI中加入一个自定义的步骤,把已经打好的Jar包解开,把相关配置下载下来,替换掉里面的application.properties文件,然后重新封成Jar包。我一开始还以为有什么高深的不为人知的原因才这么搞的。后来我发现我想多了,他们就是不想用或是不会用Spring的Profile机制罢了。虽然所有项目都的确是“用了”Spring Boot框架的。但是真的只是用了个框架,本质上这个项目连Spring 4的功能都没用上。

再说回原题,我不想解释“代码与配置分离”是什么意思,因为这只是个处理方式,我想介绍一下这个处理方式要解决的问题是什么。它要解决的主要问题之一是:同一份代码产生的包,能够不经修改地,在不同的环境下运行,配置可以在代码之外进行控制。避免出现“因为生产的配置不一样,所以要改下代码,再打个包再发到生产”的问题。那为什么临时改下,再打个包再发生产不行呢?我乐意加班不行吗?不行,问题在于:

  • 效率低。你改下代码,再打包,要不要再测试呢?即使不要,也10来分钟过去了。能不花额外时间的事儿,就不要花1秒额外时间。加班要创造价值,不是创造问题。
  • 风险高。你说你只改配置,谁知道是不是真的呢?要不要人来检查呢?你手改的怎么保证不出错呢?
  • 成本高。这是上面两条的推论。

其实《CD》这本书,全书基本都是在说提高效率和降低风险。抓住这个主旨,再去理解“代码与配置分离”,至少不会跑得太偏,也不会纠结于怎么分,分到什么程度这种问题。

虚拟化场景下的代码与配置分离

到了虚拟化的环境,情况会略有不同,部署的单位是镜像,那么问题来了,镜像里面应该是纯代码?还是代码与配置的合体?这个复杂些,两个说法都不对。

镜像本身是包含运行环境的,比如OS,JDK,一部分环境变量什么的。所以环境相关的配置,好多都是和OS, JDK相关的,本就应该放在镜像内部,不允许随意从外部修改的。非要从镜像里拿出来,当然技术上是完全可行的,但是没有意义和价值,而且效率会变低。拿出来统一管理所有配置?不不,统一是手段,不是目的。餐具有筷子和勺子,是为了吃饭方便高效,但是吃一碗汤面,不需要为了用筷子和勺子,就非把面条单独捞出来放一个碗。这叫脱了裤子放屁。 配置项到底是在镜像里还是镜像外只是手段。你要看你的问题解决了没有,目的有没有达成。而且,配置放镜像里,就无法统一管理了吗?就无法从外面控制了吗?

当然,还有一些配置,比如密码,密钥什么的,安全起见,就不可以放在在镜像内,加密过的也不行。否则镜像一旦泄漏,别人直接就能把你的服务原模原样地跑起来,这个风险是不可承受的。

所以总结一下:镜像里应该要包含并固化所有环境的多数配置,通过一个参数决定运行哪套配置,少数有特殊要求的配置要独立出来。但是建议保持从镜像外部改变现有配置项的值的能力。以便可以在不重新构建的情况下,直接调整运行时参数来解决一些比较紧急的问题。但是尤其要注意的是,所有这些紧急调整的参数,一定要签入代码库以固化到镜像中。

注意:上述这些行为,Spring通通自带支持,默认就这样,你不需要为这些事儿写一行代码。除非你放屁要脱裤子,那是要写不少代码来支持的。

配置应该放哪里

能放配置项的值的地方非常多,我们先来看下都有哪些地方可以放置配置项及值。

  • 代码里。
  • Jar包的resources。比如:application-xxx.properties文件等。
  • Docker file
  • Host机上挂载的文件
  • K8s的配置文件
  • Host机上环境变量
  • Java, Docker, K8S等的启动参数
  • 各种云平台自身中。如Aliyun, OpenShift

正是因为可以配置的地方太多了,所以必须严格控制好“应该”在哪儿放配置,否则非常容易失控。在没有控制的情况下,多数人会选择自己已经知道的方式或是最方便的方式去做配置变更,而不是去探求最合理的方式去做配置变更。
所以我们先讨论一下,配置有哪几类,分类的维度会有如下几种:

  • 谁来决定值是什么?开发,产品,运维?
  • 要不要随时修改? 实时生效?
  • 是否敏感(需要保密或脱敏)?
  • 是否因环境而不同?
  • 是否有足够的权限。比如你需要有一个Kafka的Topic,这个可以是一种配置,也可以自动化配置,但是如果运维不给你权限的话,你配置这个东西的方式会有极大的不同。

上面并不是所有的维度,但是仅仅上面的维度就有2^4*N种情况,N是公司需要做配置的角色数。当然,你并不需要2^4*N种配置的方式,配置的方式是越少越好。但是要能满足以上所有情况。并且,还要满足以下对配置管理的要求:

  • 版本控制。是的,配置本身也要有版本控制。不是随手改的。至于放在代码之外,或与代码放一起都可以。如果你不想搞得太复杂,就和代码放一起。放在代码之外的话,你还要管理好配置的版本与代码的版本的兼容的对应关系。有了版本,也好做回滚。版本控制的另一个潜台词是配置共享的,团队新来一个成员,应该是可以用得到这些配置,并一键把服务按配置好的参数跑起来的。不需要满世界问人,有哪些参数要配置啊?要配置成什么值啊?你把值拷我一下啊?
  • 统一配置的能力。比如Kafka,常常是多个服务都要配置这么个地址(或ZK的,一个意思)。那你是感觉每个服务各自配置自己的好呢?还是用全公司统一的一个配置好呢?(假设公司只有一套Kafka)
  • 抽象配置存储方式的能力。比如密码,你本地环境的,开发环境的密码,可能会感觉放properties文件就好了。生产的是要通过环境变量的。然后有一天,公司打算用Vault了。你的代码,你的配置定义,应该是都不需要修改才对。这样,你的配置系统,才是与配置存储的方式是解耦的。

好,上面说了一堆要求了。方案呢?很简单。放配置项的值就三个地方,

  • Properties文件:开发负责。通过发版,或Config的Refresh Scope来变更。
  • 数据库:产品负责变更的配置,一般要有相配套的后台页面在运行时修改。
  • 环境变量。环境变量的注入方式运维团队自己决定就好。因为这个是他们负责的。

其它地方,什么Docker file啦,K8S配置啦,启动参数啦,原则上统统不放配置。
至于实现,用Spring自带的properties支持就可以达成效果,通过profile来区分环境。日后如果要用Vault,与Spring Cloud Config集成也简单。

应该谁来配置?

这其实就是上面一节中的一点。但是我感觉非常重要,所以想单独拿出来再说一遍。因为“谁来配置”,基本上决定了配置的方式甚至能进而影响服务的设计乃至架构。有人可能从字面上看到这个问题就反感。因为一般情况下,一个事儿,一旦聊到“谁”来做这个问题,大都就开始推诿扯皮了,因为很多人的心态就是“多一事儿不如少一事儿。”,而不是考虑这个事儿“应该”谁来做。这里的“应该谁来配置”的重点,恰恰就是“应该”是谁。而不是“谁”来做比较快,比较简单。

那么配置到底“应该”是谁来配置呢?一般而言,谁有权力决定这个值应该是什么,谁来配置。注意这里的“权力”是发言权、专家权,而不是职权。那么谁有专家权呢?

比如,API的错误率的告警阈值,传统的做法就是负责系统监控的运维人员来决定一个全公司统一的值。比如5%,高于这个就告警。但是每个API的情况不一样啊。比如12306的图片验证API,错误率正常情况下肯定不止5%,谁不得试了两三次的?你配置个统一的阈值必然没办法普遍适用。 (注意,不要抬杠说这里验证失败也应该是200,这里只是用这个接口举个例子)

于是有的公司改进了,专门做个页面,允许每个Team的负责人去配置每个API自己的阈值。于是又有新的问题了,比如:

  • 负责人说,我也不知道每个API正常情况下,错误率是多少啊?那个谁谁谁,你来拉下数据。
  • API改进了,用异步操作了,总是直接返回结果,然后错误率忘改了,然后一直没有任何告警。

Team Lead是专家吗?Team Lead其实也不可能事无巨细地知道一切,不可能实时跟进API的变更,也不可能不顾实际情况地强行配置一个值,那和一开始由运维配置一个全局的值没有本质区别。本质问题是什么呢?Team Lead其实也决定不了一个API的错误率。只有写这个API的人才能根据自己对API的了解做出判定。即开发本人,确定每一个API的合适的错误率告警阈值。对于非常专业的工作,实际做的那个人,才是专家,而且必须是专家,不然他是做不好的。这里的专家并不是方方面面的全才,而是指在自己负责做的这一个小部分,在整个公司范围内,他必须是专家,至少是之一。

当然,这么做是有两个假设,一是假设开发都是认真负责的,不会配置个99%然后就安心地去睡大觉了,其次Team Lead也要愿意相信Team Member的判断。另外,“合适”并不表示“符合预期要求”。比如开发人员确定了一个数据查询接口,由于bug多,其错误率本来就高,认为告警阈值配置到30%才真表示有问题。Team Lead的责权在于,决定让开发人员改进接口,把错误率降到3%,才允许上线。

重点来了,如果一个东西让开发去配置,那方式就多了。而且一般不用单独做个配置页面这么重。常见的有:

  • 配置文件。比如properties文件。
  • 数据库migration脚本。比如Flyway或是DbMigration来修改数据库表中的配置信息。
  • 甚至代码本身

这里说的在代码中做配置并不是hardcode在代码里,具体的例子可以参考bucket4jbucket4j-spring-boot-starter这两个项目中的示例。对于需要由开发来配置的参数,这种方式势必会是方案之一。

注意:这两个框架我并没有用过,而且是做限流,不是做告警阈值配置的。只是按自己解决问题的思路,找到的现成的和上面说的方式比较一致的框架,提一下给大家做个类比参考,我也省下自己去写example的时间。

最后再强调一点,我写这篇文章,并不是要鼓吹在“代码里做配置”这种方式。 而是在讲,做一件事的时候,最好先把思路打开,别把思路局限在一个小圈圈里找方案。“代码里做配置”,只是一个代表,代表着那些从一开始,由于一些成见、思维定势而被排除在可选项外的可选方案之一。

发表回复