背景交代
我们在维护着很多个企业网络,这些网络有存储配置的平台,也有边缘的设备。
设备需要保持与平台的配置一致性,也需要保证在失联时,能够以本地的配置进行服务的恢复
当前的问题
现在整个网络的配置是稳定维护的,可以支撑住当前的业务。但是它看起来并不美妙。
主要表现出几点:
- 指令与参数是耦合下发的,在指令与参数体积差不多时,并不觉得有问题。但是如果指令与参数体积失衡时,每次重试都把两者同时下发,那么就显得蠢笨。
- 因为边缘设备也有操作配置的入口,维护多个入口时,会让一致性变得复杂,尤其是网络复杂度升高时,为了保持操作本身幂等进一步加剧了配置维护的难度。
- 逻辑胶水太多,是时候做一次整体的重设计与实现了。
一条向东流的河
设计分布式配置时,我把它想象成一条向东流的河。
河流只有一个源头,所有配置都应该从这里诞生,然后一路向下游流动。
我希望任何节点都不要主动修改已经流过来的水,而是等待新的水流抵达。因为一旦允许河水回流,就意味着状态开始相互覆盖、互相解释,最终形成一个没人能够说清楚真相的漩涡。
重构的设计原则
我把这个重构的想法与现在维护边缘设备和平台配置的同事进行了交流,总结出一些这次重构的设计原则:
- 新的配置需要是插件式的,不影响现有的配置逻辑,有效且易用后再逐步迁移
- 一个好的状态机,不是覆盖所有可能,而是屏蔽那些没有业务价值的状态。我们阅读约定就维护两种状态,正常与失联,不再将场景复杂化
- 正常时,平台为主,配置变更对平台操作,边缘设备持续订阅平台变更(Streaming)
- 失联时,平台暂停作为配置入口,配置变更对设备操作
- 基于此,达到平台为唯一真值的目的
选型的考虑
当前平台是Postgresql, 设备是自研的localdb,带来的问题是,两者之间是需要人为进行翻译维护的。 将设备改成Postgresql是不可行的
- PostgreSQL 的复制机制(Streaming Replication / Logical Replication)设计目标是数据库之间的数据一致,而不是平台与边缘设备之间的配置分发。为了配置同步去改造数据库集群,不仅耦合度高,也让数据库承担了本不属于它的职责。
- 同时,设备并不支持部署完整的集群架构,它能给到配置的资源有限,需要尽量轻量。原有的localdb满足了轻量,甚至由于Cache机制,它要比一般的本地数据服务更加高效,但是它的维护成本是高的。
配置,其实就是数据库
我们总是在讨论同步配置。实际上,真正需要同步的,不是一份 JSON,也不是一条条 RPC,而是一份可以查询、可以事务更新、可以进行增量同步的状态。
SQLite 恰好提供了这样一种能力:
- 所有配置都落在数据库中;
- 事务保证一致性;
- WAL 可以天然记录变更;
- 数据库文件本身,就是设备的完整状态。
于是,同步配置,变成了同步 SQLite。
SQLite 已经解决了一半的问题
开源社区已经维护着一套完整SQLite的工具,已经帮我们解决了事务、一致性、WAL、崩溃恢复等问题。
剩下真正属于我们的,其实只有配置流转。
- 正常时,让设备能够通过控制平面的网络,对平台的SQLite进行操作
- 失联时,能够关闭平台的SQLite操作能力,同时让设备操作本地的SQLite,设备并没有进入特殊模式,只是数据源发生了切换
- 恢复时,关闭两侧的SQLite的操作能力,备份设备增量到平台,恢复正常状态
至此,配置始终沿着同一个方向流动。
正常时,它从平台流向设备。
失联时,它暂时停留在设备。
网络恢复后,它重新汇入河流,而不会形成回流。
从始至终,河流只有一个方向。
为什么是 SQLite,而不是别的?
SQLite 并不是因为”轻量”才被选择。
真正重要的是,它把数据、一致性、事务、WAL、恢复全部封装进了一个数据库文件。
它不是一块本地缓存,而是一个完整的状态机。
这意味着,同步 SQLite,本质上就是同步整个设备状态。
当然
你觉得其他行,也行。