前端开发都应该知道的配置中心

动态化方案一般都是比较大型的, 比如react native 、flutter 等都是从UI,运行逻辑等多方面完整的动态更新。但实际上,移动端还有很多细粒度的配置类数据需要支持动态更新的。

比如某一个文案或者广告的位置希望可以根据用户表现来随时改动,又比如你开开发了一个线上功能,但上线后才发现里面潜藏了一个严重的问题, 希望可以同过一个线上开关立即关闭此功能。

这一类需求用一句话来讲就是叫做:千万不要写死。

Android里面大家都很数据的sharedpreference,里面可以存储很多的K-V类型的数据。那我们如何来实现一套可动态更新的K-V存储机制呢? 从而将这类小型配置或AB开关准时下发到所以的客户端。

为了实现这个功能,很多公司都研发了一套自己的配置中心系统,比如阿里内部的orange系统,能狗做到秒级下发所以配置到全量客户端。

要实现这样一套系统,需要考虑到几个问题点:

  • 首先,远端配置更新后,如何实时通知给所有客户端呢?
  • 配置数据体积越来越大,下载失败率变高,如何优化数据包大小呢?
  • 配置数据的安全性如何保证?
  • 如何灰度发布配置?
  • 实时全量下发的话,CDN会存在什么问题吗?
  • Android 如何实现多进程监听配置变更?

如何通知客户端配置更新

首先我们要解决的问题是:如何才能尽快告诉客户端,配置数据以及更新了?

1.主动拉取(Pull)

第一种方式,我们可以让客户端进行主动轮询:

  • Polling定时轮询:定时发起请求到后端,拉取最新的配置数据。。
  • Long Polling长轮询:后台起一个service,持续去发去长轮询判断后端是否有配置数据更新,一旦发现有更新,再发起请求去拉取配置

关于长轮询说明下,普通的短连接是客户端发起socket请求,服务端收到后,不管有没有数据,都会立即返回。

而如果长轮询,服务端如果没有数据则会hold该请求,将socket等请求信息保存起来, 不立即返回, 等到有数据时才通过socket重新写回客户端。当客户端收到结果,或者判断请求超时,便会发起下一次的长轮询请求, 此时的超时时间一般会比正常的http请求的超时时间长, 比如一分钟,比减少请求次数。

这种方式可能能够达到比较好的实时性,但很显然,会带来非常多的流量浪费,而且对后端也会带来非常多无用的请求,造成机器资源的浪费。

2.推送(Push)

这种方式可以借助长连接

当我们发布更新配置后,可以通过长连接通道向在线用户下发配置变更通知, 用户收到通知后便会主动去拉取配置。

这种方式可以极大的降低流量损耗,只有配置变更时才会拉取数据,但也存在一些问题:

* 需要维护稳定长连接通道,存在一定的技术成本。
* 如果用户长连接断开,会导致无法接收到消息,从而不能保证实时变更。

因此,这种非方式虽然节省流量,但不能够保证100%的实时配置生效率。

3.推拉结合

既然主动拉取和推送都做不到,很自然的,我们会想到能不能两种方式结合起来,方案如下:

* 客户端与服务端保持一个长连接,当有配置变更时,立即下发;
* 为防止长连失效导致更新不及时,客户端还会作定期轮询,拉取配置;

通过这种方式,可以获得较低的流量开销和较高的更新率。业界不少企业采用的是类似的方案,比如携程的配置中心系统Apollo。

4.统一网关

这里介绍第四种更新机制,来自阿里的Orange移动配置系统,它无需任何轮询请求和长连接通道下发,而且可以做到在线用户100%的秒级更新率。

它的秘密就是利用了全集团的统一网关,这套网关同时运行在客户端(Android、iOS)和服务端。移动端网关会接管所有客户端的请求,而后端网关则会承接所有移动端发过来的请求。具体流程如下:

* 客户端的任何网络请求在经过移动端网关时,带上本地配置的版本号;
* 服务端网关收到请求后,抽取出配置版本号,发送给配置中心服务,其他参数透传给业务后端
* 配置中心服务基于当前APP的版本号,配置版本号信息,判断是否有新配置,如果有则返回新配置版本号给网关
* 当业务后端返回时,带上这个新配置版本号给客户端
* 客户端发现新配置版本号,则去CDN拉取最新配置数据,完成更新。

前端开发都应该知道的配置中心-LMLPHP

在这个流程里,我们没有为配置单独发起轮询请求,也不需要依赖长连接服务进行下发,而且借助现有的业务数据,加上网关的协议,来实现配置的动态更新。只要客户端处于活跃状态,就能立即发现配置更细你, 而且非常稳定。

配置文件的增量更新,压缩与加密

很多开发者担心自己开发的功能上线后悔出现问,所以会加上各种各样的配置开关,随着版本的不断迭代,配置数据肯定会越来越大,如果每次数据更新,都需啊哟去全量下载,那消耗的流量会越来越多,而且,数据包越大,下载更新失败率会也会逐步变高。

因为,我们还要想办法减少下载配置数据包大小,一般会从两个角度考虑

1.增量更新
2.压缩

除此之外,有些配置数据是敏感的,所以应该要实现加密。

增量更新

增量算法有很多,我们可以结合具体场景来选择。

由于我们的配置都是纯文本,所以可以优先考虑文本Diff算法,如Google的diff-match-patch,就是专门针对纯文本的高性能Diff/Patch算法。而且它提供了多种语言版本实现,包括Java、Objective-C、Python、Dart等,

diff-match-patch内部是基于Myers算法,这个算法就是我们天天用的 git diff 和 RecyclerView 里的 DiffUtil的实现原理。而且diff-match-patch在这个基础上还做了不少性能优化。

当然,除了纯文本Diff,我们也可以考虑二进制Diff算法,如BsDiff算法,一般apk的更新、Tinker补丁包更新都可以采用这个增量算法。

压缩

对于文本压缩,用的最多的就是Gzip了,Http协议传输也是用的Gzip压缩,兼具压缩率和性能。所以此处也可以考虑Gzip压缩,接入成本最低,效果也不错。另外也可以考虑zStd压缩和Brotli压缩,感兴趣的读者可以去深入了解下。腾讯云CDN服务目前已经支持了Gzip压缩和Brotli压缩,当文本文件大小在256Byte - 2048KB之间时,采用Gzip压缩;更大的文件则采用Brotli压缩。

加密

不少情况下,我们会用配置中心来下发一些敏感数据。比如双十一活动,某个优惠券的生效时间戳,如果用户分析出来了,可以人为修改配置数据从而导致程序运行异常。因此,为了保护下发的敏感配置数据,我们需进行加密。

第一种方式,下发链路加密。我们可以对下发的配置数据压缩包进行整体加密,客户端拿到后,流式解密和解压到本地文件。以后客户端读取配置时可以直接读取明文。这种方式的问题就是配置数据是以明文形式落盘(存储到磁盘)的,这可能存在数据泄漏风险。

第二种方式,单Value加密。我们会对每个Key对应的Value进行加密,然后下发到客户端,解压后存储在磁盘里的是明文Key+密文Value,在需要读取某个配置时,再实时解密并缓存在内存里,从而降低数据泄漏风险。

在选择具体加密方式方面,考虑到客户端的解密耗时,可以使用类似AES/CBC/PKCS7Padding加密算法,密钥可以存储到客户端保护起来(为了安全可以放到so 内部,通过混淆提高安全性,或者采用白盒加密方案,将密钥与算法一起编译生成代码从而隐藏密钥),也可以支持动态更新。

另外,Value加密后是二进制数据,此时要利用Base64编码,把二进制数据转成文本写入配置文件中。

灰度、回滚及CDN压力

前面说了,我们的配置中心能够做到秒级下发,那这里会存在一个问题:如果配置有问题,可能会立即导致大规模线上故障。因此,这里需要支持配置的灰度发布。

一般可以支持多维度灰度,比如基于用户的App版本区间、Uid或者DeviceId、城市、渠道等维度信息来进行灰度。灰度发布后开始观察线上数据表现,确定逻辑运行正常且稳定,如果没问题,才可以全量发布。

那如果出了问题呢?那就要进行回滚。这里我们可以通过重新发布一个历史版本的配置数据来实现回滚。

另外,全量发布后,我们会将更新的配置数据压缩发布到CDN。这时,大量客户端在检测到更新后,会立即访问CDN下载对应的更新配置数据。如果用户量级很大,需要考虑会出现的CDN请求高峰,要确保CDN能承受对应的访问压力,不能被打挂。而且,由于CDN同步需要一段时间,所以此时肯定会有很多CDN发生回源,对源服务器也会造成一定的压力。因此,在每次发布后,可以结合具体场景及用户QPS,有选择地考虑对CDN进行预热。

小结

配置中心逐步成为各家公司的标配,一套好的配置中心系统,不仅能够支持各种维度的配置下发,如业务配置、技术配置、开关配置等,也要能够做到实时下发,能够尽快触达所有用户。尤其在一些用户量较大的场景下,要能够考虑流量压力,保证CDN等基础设施能够稳定运行。

07-20 09:09