背景

客户端大量的硬编码导致其灵活性大大降低,一些细小的改动只能通过发布版本解决,用户升级更新迭代速度慢,时效性差等原因,催生出了有赞 App 的动态化配置中心,它可以将配置,功能,界面,数据等各种配置数据统一进行管理下发,实时生效,极大地提升了客户端的灵活性。

同时配置中心不仅仅是简单的对配置数据进行修改、读取而已,更需要在容错性、流量优化、带宽节省等各方面的优化上下功夫。本文主要提供了有赞 App 的动态化配置中心解决方案,也总结了版本迭代中所做的优化。

配置中心设计

起初有赞各 App 内都散落着一些写死的链接,但随着 WWDC 16 中,Apple 表示将继续在 iOS 10 和 macOS 10.12 里收紧对普通 HTTP 的访问限制,并且无法使用 NSAllowsArbitraryLoads 来绕过 ATS 限制。我们只能很费时费力的把各 App 内所有访问 HTTP 的链接都修改成 HTTPS,这一个小小的改动就如此麻烦,那后续大的改动更难以想象,所以我们开始思考能否将这些重复且动态的工作抽象成一个配置中心,用它来支撑各个业务。

第一版

第一版设计的相对简单,我们只是专门设计了一个 API,通过 API 请求配置数据,并且每个业务单独维护一份配置文件。

具体流程:客户端进入到前台,也就是应用程序被激活的时候,通过配置中心的接口向服务端请求最新的配置数据,如果配置文件有更新,则下发最新的配置给客户端,并且客户端本地存储这份最新的配置,用于应用运行时使用。

但随着模块化的推进和应用数量的增加,配置文件的体积和数量会逐渐增大,每一次修改都会全量下发到客户端,这部分的流量累积起来是一个非常庞大的数字。并且使用配置文件去管理配置,只能维护到最新的配置,无法做到下发指定版本的配置。

所以针对这些弊端问题,我们衍生出了第二版配置中心。

第二版

为了解决第一版产生的流量浪费和配置管理弱的问题,我们优化了配置下发和管理流程,我们采取的策略是 增量更新数据库存储配置

具体流程改动:

增量更新

增量更新的优势主要体现在业务增长的过程中。我们的配置文件可能会从几十 KB 增长到几百 KB 甚至更大,如果还继续使用全量更新,这部分流量对用户来说是非常浪费的。而使用增量更新之后,服务端只需要下发不同配置之间的差异补丁包,补丁包的大小相比于原始配置的大小是非常小的,可以节省下90%左右的流量,这是非常可观的。

增量更新我们目前使用的是 Google 出的 google-diff-match-patch ,支持 Java, JavaScript, Dart, C++, C#, Objective-C, Lua 和 Python,但是官网已经下掉了该 SDK(不明白为什么),需要到 GitHub 上搜索类似于 diff patch language:java 这样的关键词,就能找到对应平台下的 SDK 了,有人已经 fork 出来了,因为只是字符串的比较处理,所以可以放心使用。

该增量更新的原理是先通过比较首部和尾部的相同部分,目的是提升一定的效率,再比较中间差异的部分,差异的部分通过一些字符去表示该改动是 DEL 还是 ADD 还是 EQUAL ,这样最终形成的补丁包比改动部分要大一点,但是相比于全量更新已经减少很多了。

并且生成补丁包基本是毫秒级的,也不必担心接口请求的耗时问题。

服务端把前后配置的字符串进行比较即可得到一组补丁数据:

1
2
3
4
5
6
$dmp = new DiffMatchPatch();
$patches = $dmp->patch_make($oldConfigString, $latestConfigString);
$patchStrings = [];
foreach ($patches as $patchObject) {
array_push($patchStrings, $patchObject->__toString());
}

生成的补丁以官方为例,补丁内包含两个字符串之间变动部分的 location 和 length,并最终下发给客户端:

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
@@ -16,21 +16,29 @@
see
-yonder
+the
cloud
+over there
that
@@ -47,18 +47,19 @@
almost
-in
+the
shape o
@@ -86,24 +86,18 @@
By
-the mass, and 't
+golly, it
is l
@@ -129,21 +129,23 @@
et:
-Me
+I
think
-s
it
-i
+look
s li
@@ -177,12 +177,12 @@
is
-back
+shap
ed l
@@ -234,11 +234,19 @@
us:
-Ver
+It's totall
y li

以 iOS 为例,客户端获取补丁包后,根据补丁包内变动符号以及变动内容对本地配置文件内容进行字符串拼接或删除操作,形成最终的配置:

1
2
3
4
5
6
7
8
9
10
11
DiffMatchPatch *dmp = [[DiffMatchPatch alloc] init];
NSError *error = nil;
NSArray<NSString *> *patchStrings = configDic[@"patches"];
NSMutableArray<Patch *> *patches = [NSMutableArray array];
for (NSString *patchString in patchStrings) {
NSArray<Patch *> *patch = [dmp patch_fromText:patchString error:&error];
if (!error) {
[patches addObjectsFromArray:patch];
}
}
NSString *newCacheConfig = [[dmp patch_apply:patches toString:cacheConfig] objectAtIndex:0];

为了确保客户端最终生成的配置与服务端保持一致,客户端在打好补丁之后,用该配置生成 MD5 值,与我们在服务端下发补丁包时携带的最新配置的 MD5 值进行比较,在一致的情况下才去缓存配置,避免因为补丁造成 App 无法使用的问题。

数据库管理配置

实现增量更新的前提是我们需要有不同版本的配置记录,而用文件去管理是非常不可控的,取而代之的是我们可以通过数据库来管理,同时利于后期的横向扩展,可以针对不同平台,不同渠道,不同版本等规则下发。

目前我们是根据版本和应用来关联到配置,所以表结构设计的很简单,把应用、版本和配置放在一张表里面,后面针对多渠道和多平台等规则之后,表结构上会做一定调整。

由于需要 App 能及时获取到最新配置,我们选择在 App 激活的时机去获取配置,通过数据统计中心我们可以看到 App 当日启动次数的时段分析,再结合配置文件大小,我们可以预估所占带宽的大小,运维是非常担心你把他的带宽跑满,影响到其他业务,所以我们需要做两个优化,一个是压缩配置,一个是本地缓存。

压缩配置

为了不占用过多的带宽,我们需要在写入之前把配置压缩,读取之后解压配置。

这里我们通过 gzip 来压缩配置:

1
$data['config'] = base64_encode(gzcompress(json_encode($config)));

需要注意的是 gzip 可以设置压缩等级,范围是0 - 9,默认是6,但是提升压缩等级会占用较多 CPU 时间和内存,我们使用默认等级压缩之后,配置文件体积减少了75%,看来是配置内相同的部分比较多,所以压缩效果还是比较明显的。

这里有人会问,为什么 gzip 之后还要 base64 编码一下,因为二进制数据不能直接写到text字段里面,写进去之后读出来也是解压不了的。进行 base64 编码之后,我们的压缩配置内容会比原来多1/3的长度。

本地缓存

对于相同的配置,我们可以不用频繁地去请求数据库,而将其缓存到本地,这样后续的请求可以直接从缓存中读取配置,当然请记得设置缓存的过期时间。

我们目前用的是 Redis 缓存,它对于复杂的数据结构和操作支持的相当不错,而且操作简单,对于前期只是key-value存储的话,可以考虑使用 Redis

可操作界面

为了方便业务方修改配置,我们在内部平台上提供了一个简单的可操作界面,通过顶部的Tab栏切换查看不同应用下的配置记录。

点击 查看详情 可以看到当前版本的配置内容,目前展示的效果并不友好,后面会进行优化。

在需要修改配置的时候,我们可以通过 新增配置 给指定应用修改配置,新增时会自动在版本上 +1。

后续优化

目前有赞 App 已经实现了动态化配置中心整一套流程,但是在一些细节方面还需要不断的改进优化,比如:

  • 更加丰富的配置下发规则
  • 可操作界面配置的有效性的校验
  • 可操作界面的视觉优化
  • 等等

也欢迎大家提出自己的意见和建议,我们一起探讨学习!