Город IT · 2024 · Pavel Simonov
保证一致性的分布式事务:神话还是现实?
本文根据演示文稿、提纲、预告和源代码重建。没有可用的演讲录像,因此并非逐字记录。内容顺序对应2024年演讲的23张幻灯片。
原始幻灯片图片保留俄语。文字稿可按所选语言阅读;代码和 API 标识符不翻译。
目录
1 / 23分布式事务
原始幻灯片 · RU 重建文本
大家好!我是Citeck的系统架构师Pavel Simonov。今天介绍我们如何实现分布式事务,会遇到哪些困难,以及如何处理。
先从两阶段提交讲起,再介绍事务动作和事件监听器。目标是让跨微服务的操作更容易编写,同时保持数据一致。
2 / 23事务边界
原始幻灯片 · RU 重建文本
大家已经习惯了普通事务。在方法上加一个注解,就期待所有修改一起保存,出错时全部回滚。只有一个数据库时,这很直观。
图中我们启动事务,执行几次修改,最后Commit。这些修改需要一起保存。
3 / 23回滚
原始幻灯片 · RU 重建文本
如果中途出错呢?比如前两次修改成功,第三次失败。执行Rollback,数据库就回到这些修改之前的状态。我们不必自己判断哪些写入成功,再逐一撤销。
不过回滚也有边界。邮件已经发出,数据库回滚无法把它收回来。后面会再讲这类动作。
4 / 23多个数据库
原始幻灯片 · RU 重建文本
现在假设数据在两个微服务中,各有自己的数据库。我们在DB1和DB2启动事务、完成修改。接下来怎么办?
依次提交时,第一个数据库可能成功,第二个却报错。第一个Commit已经完成,普通Rollback无法撤销它,数据就对不上了。
5 / 23回到单体?
原始幻灯片 · RU 重建文本
可以保留一个数据库,回到单体。这样所有修改又能放进同一个事务,和前面的图一样。
对某些任务,这很合适。但如果操作已经涉及多个独立服务,我们仍要约定怎样把修改一起保存。
6 / 23Saga与补偿
原始幻灯片 · RU 重建文本
另一种选择是Saga。各服务分别保存修改。如果后续步骤失败,就执行补偿:用另一个操作修正前一步的结果。
补偿逻辑需要自己写,取决于业务:撤销创建的文档、恢复状态、处理其他影响。长流程可以用这种办法,但这里希望保留完整执行或回滚修改的便利。
7 / 23实际场景
原始幻灯片 · RU 重建文本
哪些场景需要它?比如Camunda管理流程,文档的业务数据在另一个服务。完成任务时还要修改文档,我们希望两项修改一起保存。
集成或使用平台业务数据的自定义微服务也有同样问题。用户看到的是一次操作,一条请求却可能涉及多个数据库。
8 / 232PC参与者
原始幻灯片 · RU 重建文本
为此我们使用两阶段提交,也就是2PC。XA标准描述怎样与事务资源交互。先看看参与者。
事务管理器TM管理事务。XA资源能准备修改,再提交或回滚,例如支持XA的数据库。还需要TM Repo,保存协调信息,以便故障后知道怎样处理未完成事务。
9 / 23准备与提交
原始幻灯片 · RU 重建文本
先修改两个资源。结束时不立即提交,而是询问每个资源:能保存你这一部分吗?这就是Prepare阶段。
只要有一个没准备好,就回滚。全部准备好,则保存提交决定,再让各资源Commit。完成后清理协调信息。
故障时,关键是提交决定是否已经保存。保存后,恢复必须完成这个决定,不能随意改成回滚。等待期间,已准备资源可能持有锁,需要考虑这个代价。
10 / 23ecos-model中的协调器
原始幻灯片 · RU 重建文本
来看我们的服务。请求通过ecos-gateway进入,涉及ecos-process和ecos-model,各自有保存业务数据的PostgreSQL。
gateway把提交协调交给ecos-model,事务管理器的仓库也在那里。ecos-model负责判断哪些资源参与、是否就绪,以及应执行什么决定。
11 / 23HTTP请求上下文
原始幻灯片 · RU 重建文本
其他服务怎么知道调用处于事务中?HTTP请求会携带事务标识,让服务知道这次调用属于哪项工作。
还会传递实例路由。图中emodel对应wbag7dya54yq,eproc对应za7iyygilrr4。服务有多个实例时,后续调用必须到达已经保存本事务状态的那个实例。
12 / 23路由传播
原始幻灯片 · RU 重建文本
响应也要传播路由。假设App A-0调用App B-0,B再调用App C-1。A还不知道C的哪个实例参与了操作。
所以B-0返回App C → C-1。A之后在同一事务里调用C,就能找到正确实例。通过负载均衡随意选一个,可能选到完全不了解这个事务的实例。
13 / 23响应元数据
原始幻灯片 · RU 重建文本
响应返回三项信息:事务资源、路由、事务动作。路由已经讲过,资源列表告诉调用方,结束事务时要把谁包括进来。
动作让服务提出:这段代码要在特定时刻执行,比如提交前或提交后。接下来看看为什么需要它。
14 / 23为何需要动作
原始幻灯片 · RU 重建文本
保存前可能要重新计算权限,保存后要发布事件或发邮件。如果立即发邮件,而事务随后回滚,收件人就会收到一个最终没发生的变更通知。
因此先注册动作,在合适阶段执行:提交前计算权限,成功提交后发送通知。
15 / 23动作描述
原始幻灯片 · RU 重建文本
描述一个动作需要什么?注册它的应用、动作标识、执行时机和顺序。
时机有三种:before commit、after commit、after rollback。这样可以收集多个服务的动作,按所需顺序执行。
16 / 23线程复用
原始幻灯片 · RU 重建文本
另一个难点是线程。A调用B后等待响应,但B处理时又可能同步回调A,而且仍在同一个事务中。
A已经在工作,有被占用的线程及其上下文。特别是回到同一个业务流程时,必须考虑这些状态。来看一个例子。
为此我们使用WebApiThreadChain。原始线程等待HTTP响应时,可以处理队列中的嵌套调用。同一事务中的回调到达A后,可以交给这个等待中的线程。它使用已经打开的流程上下文完成工作,收到B的响应后再继续原来的调用。
17 / 23BPMN示例
原始幻灯片 · RU 重建文本
这里有两个子流程。第一个接手任务,设置“一级支持处理中”,随后取消SLA 1通知并结束SLA 1。
但状态变化也启动第二条分支:检查转移条件、通知发起人、处理请求。一次状态修改,在第一条分支结束前就把我们带回流程引擎。
18 / 23回调与转移条件
原始幻灯片 · RU 重建文本
按服务来看,ecos-process调用ecos-model设置状态。ecos-model发送“状态已变更”事件,我们回到ecos-process,在同一线程继续流程。
图中转移条件检查失败。这时需要知道哪个流程正在执行、属于哪个事务。如果丢失流程执行上下文,仅有事务标识还不够。
19 / 23事务事件监听器
原始幻灯片 · RU 重建文本
这些情况需要事务事件监听器。另一个服务若必须在当前操作中响应数据创建、修改或删除,我们就在当前事务内通过HTTP(S)同步调用它。
为了让发送方知道该调用谁,我们在Zookeeper中维护订阅注册表。应用注册需要的事件、过滤条件和处理器所需的属性,并注明监听器是否参与事务。服务监听注册表的变化,及时更新接收方列表。
处理器可在事务结束前完成自己的工作。确认全部数据已保存的通知,则通过提交后的独立动作发送。
20 / 23负载测试条件
原始幻灯片 · RU 重建文本
再看负载测试。使用JMeter,非200响应或执行超过10秒都算错误,每个线程使用独立用户。
系统预先有100 000用户和10 000 000合同,以10 000用户身份持续施加一小时负载。
21 / 23负载场景
原始幻灯片 · RU 重建文本
用户执行不同操作:登录并打开首页20%,查看合同列表30%,打开带全部组件的合同页面30%。
还包括活动任务列表30%、创建文档20%、启动流程10%、执行任务50%。这些是参与各场景的用户比例,一个用户可以执行多种操作,所以无需相加为100%。
22 / 23测试结果
原始幻灯片 · RU 重建文本
一小时共20 447 914请求,221次错误或超过时限。幻灯片舍入为0.00%,更精确是约0.00108%。
平均响应时间21.88毫秒,中位数6毫秒,90%请求低于18毫秒。
23 / 23源码与结束
会议照片 (6)
Pavel Simonov演讲现场
Citeck展位前的参会者
Citeck展位前的合影
演讲:事务事件监听器
演讲:线程复用
演讲者、观众与演示屏幕