如何设计一个配置中心的后端架构

配置中心能够让 App 具有更好的动态性,通过在远程下发配置来动态改变 App 的行为。假如现在需要设计这么一套系统,该如何去考虑呢,以下是我的一些分析过程。

对「配置中心」这个需求进行分解就是:第一时间把配置发送给客户端。因此我们先来构想一个最简单的场景:

客户端和服务端保持一个长链,当在后台操作配置时,会把这个配置以 K/V 形式存储,然后通知 Procesor,后者拿到 K/V 之后把它推给客户端,整个过程就完成了。这是最原始的形态,接下来会在这个基础上进行演化。

配置更新时,客户端处于离线状态怎么办

长链只能保证(尽量)客户端在线时能第一时间拿到配置中心的值,处于离线状态(比如没有打开 App)时就无能为力了,因此需要想办法到用户下一次打开 App 时可以拿到最新的值,这个简单处理就是在保存 K/V 时,额外存一个 flag 字段,用来表示这个 K/V 是否已经成功发送给客户端。

多份配置的处理

通常 K/V 对不止一份,那么多份配置,也就是多个 K/V 对,又会带来哪些变化呢。

这时就有三个问题需要我们考虑:存储、流量和同步策略。

存储

一个设备的 K/V 对通常不会超过 100 项,每对 Size 不超过 1K,也就是一台设备对应的大小上限为 100K 左右,假如设备数为 100 万,就需要 100G 的磁盘空间。这个量还是有点大的,可以优化下。考虑到一些配置项会在多个设备共存,可以把这些配置单独存储,然后把 hash 值作为 Value。假设 Key 的 size 为 30 字节,Value 为 10 字节,这样就只需 40M 的 K/V 存储空间。因此由于存储上的限制,我们的设计也要做一下调整:

但这样还是有问题,Value 的组合会很多:

比如原来 K1 的 Value 为 V1,更新之后变成了 V2,那么需要新建一组 Value,然后将其中的 V1 变为 V2,因为不知道之前的那一组 Value 是否还有其他设备在引用,这样就会逐渐累积下去,要降低这种累积的话,还要设计清除算法,复杂度就上来。

不妨参考一下 Go 语言里 Slice 的设计,Slice 内部使用了一个数组,但可以指定使用该数组的哪一部分,其实就是索引。

这样某个 Key 如果有新的 Value 了,只需在对应的 Key 后面 append 即可。此时需要同步更新设备的索引,这块可能花一点时间,如果数据都在内存的话其实也还好(由于只是存索引,因此这些数据量内存 hold 得住),持久化可以异步进行。

流量

上面已经说过,单个设备的量可以达到 100K,如果每次配置有更新就发送 100K 的数据对到达率会有一定影响,尤其在设备网络情况不佳的情况下。因此这里的目标是如何减少数据传输量,同时尽量避免提升复杂度。

对数据进行压缩

这是比较简单同时效果也不错的方法,这里需要考虑的几个点是压缩比和压缩/解压缩速率以及资源消耗。参考这篇文章,可以发现 lz4 在压缩/解压缩时间上非常有优势,同时资源占用也很少,就是压缩比不太高。而 lzma 则有更高的压缩比,因此可以参考不同的场景来选择合适的压缩方案。

这还会有一个问题,如果每次请求都进行压缩,效率就太低了,因此需要缓存,缓存的 Key 其实就是 Config Indexes 的哈希(hash('1,2;2,0'))。这样就需要对所有的 Config Index 存一份哈希值,然后根据这个哈希值去找对应的压缩后的文件。

需要考虑下缓存命中率,如果设备之间很难利用缓存,意义就不大。初步估计一下,这块问题应该不大,除非每个设备都有一份独特的配置。

还有一种常见的减少流量方案,就是使用 Diff。

使用 Diff

Diff 的话一种处理方式是把 K/V 的索引放到客户端,然后比对两个索引的 Diff,再把真正的 Value Diff 下发到客户端进行合并。这样就会有一个问题,客户端需要上报它当前的配置中心的索引值,这就涉及到上报时机,有两种方式:

  1. 客户端轮询。比较低效,也无法保证实时,但不需要维护长链,实现起来相对简单。
  2. 服务端在得知更新后,主动向客户端要当前保存在客户端的 Config Indexes,对比之后再发送 Diff。

对于第 2) 种情况,相比直接推送会多了一轮通信,同时对于两端都会增加一些复杂度(处理 Diff),但好处是可以最大成都节省数据传输。

多设备

多设备对服务端的挑战比较大,如果设备数比较多,而服务器资源比较有限,可以考虑客户端轮询的方案,不过同样要处理峰值的情况,比如某次促销可能会带来大量的瞬时并发请求。简单的处理可以用令牌桶算法:桶里的令牌数代表服务器当前的承载能力,每次请求进来消耗一个令牌,如果令牌消耗完了,请求直接拒绝,等服务器缓过来了,再往桶里加令牌。

多维度

维度也就是设备的特征,一个设备会有多个特征,比如 iOS 11v9.3.6 等,同样一个维度也会包含多台设备。

维度的设计使用 Set 会比较合适,因为顺序无关,且将来设备的维度改变后,查找起来也会很快(比如当设备升级了版本,就要把设备从旧版本的维度变为新版本的维度)。所以需要在一个合适的时机去检查设备的维度是否需要更新,同时检查是否需要发送配置,这个时机选择在连接建立完成后异步执行比较合适。

维度更新

假如设备从「维度 1」更新到了「维度 2」,怎么知道需要更新哪些 K/V ?要解决这个问题,可以给维度也建一张表,来保存 Config 信息,Key 为维度名,Value 为 Config Index。

小结

最终的流程和设计如下:

本文主要提供了一种架构设计的思路,从最核心的需求开始,随着场景的变化,来不断完善、调整设计。结合常见的考虑点和适当的数据评估,选择相对简单的方案。在细化的过程中,一些问题就会浮现出来,尽早发现,尽早解决,等到开发甚至上线才发现就麻烦了,这也是架构设计的重要性。

最后,请有限度吐槽图片质量,博主已经很努力了···

❤️