事件溯源在物联网设备数据同步中应用案例 - eventstore


我们是一家小型软件咨询公司,专门从事涉及函数式编程的项目工作。那是 2015 年,我们的一个客户是(实际上仍然是)一家大型汽车连锁店。
他们打电话给我们是因为他们的 IT 战略发生了变化:他们经营的商店被分成多条车道,每条车道一次为一辆车提供服务。每条车道的尽头是一个金属达文波特办公桌,上面有各种硬件——打印机、诊断设备等。2015 年之前,达文波特包含一台固定式 PC。客户正准备摆脱这些 PC,而是为每位员工发放便携式设备(基本上是坚固耐用的平板电脑)。
现在有一个问题:当员工(和他们的设备)搬到不同的车道(或不同的商店)时,他们需要重新配置它以连接到本地 davenport 的设备。这是一个繁琐的过程,需要手动输入设备背面的 9 点式 MAC 地址等。 所以客户要求我们开发一个可以自动化这个过程的软件,有以下要求:

  • 每台设备只能配置一次;然后它的配置应该自动在所有设备上可用。
  • 该软件应该在容量有限的不可靠无线网络的环境中运行。
  • 该组织无法操作中央服务器。

这些要求意味着设备需要在本地存储配置数据以确保可用性并在它们之间传达配置更改。
早些时候,我们意识到我们无法使用通常的持久性方法将当前配置状态存储在数据库中。在永远在线的环境中保持一致性已经够难了,但是随着设备进出网络,很明显这将是无法管理的。
我们的想法围绕着事实的想法,即真实的事物。
“Davenport 123 连接到 IP x 上的 Acme PrintAce”不是事实:它只适用于特定时间段,现在可能不再适用。
但是,
“安妮特·穆勒 (Annette Mueller) 于 2021 年 12 月 6 日表示,Davenport 123 连接到 IP x 的 Acme PrintAce”
——这是我们可以存储的事实,如果我们在场见证的话。
见证的事实纯粹是累积的,这大大简化了同步和一致性:当两个设备相遇时,它们只需要在各自相对的设备还不知道时交换那些事实。
事实并不是孤立存在的。如果“Hans Mayer 于 2021 年 12 月 6 日声明,Davenport 123 连接到 IP y 的 Highres WriteQueen”,那么该事实与前一个有关。这两个事实都采用有关 Davenport 123 配置当前状态的属性陈述的形式。它们可以通过以下方式相关联:
  • 一个事实“取代”了另一个。
  • 这两个事实是相互矛盾的。

系统如何区分两者?好吧,如果一个事实是在已知另一个事实的情况下做出的,那么第二个事实将取代第一个事实,因为它暗示了因果关系。如果不是,即如果两个事实相互独立,就会发生冲突。
 
当我们开始设计系统时,我们沉迷于冲突。我们做了两个假设:
  • 冲突很少发生,因为我们假设用户在进行配置更改时会站在设备附近。
  • 对于双向冲突,有正确的方法和错误的解决方法——我们需要确保系统选择正确的方法。

结果证明这两个假设都是错误的,直到系统运行时我们才注意到这一点。
...
我们年轻而天真:我们没有考虑同步算法的不合理有效性,它会在任何人有机会解决冲突之前将两个相互冲突的事实传播到许多设备。这是一个 UI 失败——用户不知道为什么他们会看到这个选择,并且很难弄清楚谁应该按下什么按钮。
例如,我们迅速转向自动创建“合并事实”,例如,更喜欢最近的事实而不是旧的事实。不幸的是,这几乎以灾难告终,因为这发生在所有具有相互矛盾的事实的设备上,结果证明是相当多的。他们会创建多个合并事实,这些事实也必须合并,从而启动无休止的合并级联.
我们做了两个改变:
  • 没有更多的自动合并事实
  • 面对一个Conflict值,系统会确定性地选择其中一个值

我们完全希望用户在系统选择错误值时抱怨,但这种情况从未发生。用户只需更改配置,手动添加合并事实。
查看数据,尽管我们假设物理上接近,但我们对冲突发生的频率感到惊讶。尽管我们从未真正弄清楚为什么这些冲突如此频繁,但我们也不应该感到惊讶:在呈现手动合并 UI 时,我们假设只有一个系统,而实际上是一个分布式系统在工作。
 
由于设备之间只有弱连接和不可靠的网络连接,我们不得不在本地存储事实。我们决定使用 SQLite,它以其简单性、可扩展性和可靠性而闻名。

使用合适的数据库还有另一个令人愉快的副作用:一些用户在犯了配置错误时要求“倒转时间”。我们通过收集与给定地点相关的所有事实并按时间顺序显示它们的列表来实现这一点。然后用户可以选择过去的时间点。系统现在可以简单地收集与该地点相关的所有事实,并使用它来创建一个虚拟数据库。
我们使用了一个 monad 来实现数据库的依赖注入。最初,这是为了在没有磁盘存储的情况下进行测试。在这里,我们能够重新使用此功能来创建数据库接口的临时实现,该接口只能访问截至该时间点的事实子集。由此,系统可以使用现有代码重建那个时间点的状态,并在当前状态之上生成新的事实,以重新创建过去的状态。
因此,我们的“事实数据库”的设计已经方便地提供了实现这个全新的时间机器所需的所有部分。
 
“事实数据库”设计的一个关键方面是它支持跨设备的高效同步。各种事件会触发此类同步:配置更改或设备进入 WiFi 网络。发生这种情况时,相关设备会广播一个 UDP 数据包,请求同步伙伴。
一旦找到,系统就会将所有事实组装成每个设备上的Merkle 树。然后这些设备协作以广度优先的方式向下遍历 Merkle 树。Merkle 树在每个节点都有一个哈希码,用于标识其子树中的所有数据。在每一层,双方确定设备之间匹配的哈希值,将它们从遍历中移除。底部是需要转移到另一方的事实列表。
顶部哈希值包含在触发同步的 UDP 广播中:任何接收到它的设备都可以将它与自己的顶部哈希值进行比较。如果它们匹配,则不需要同步。
 
应该很容易将这个系统从根本上看作是一个事件溯源系统。只需将“事件”替换为“事实”,事件溯源中的技术和技术就适用。
详细点击标题见原文