Emmett:用事件溯源实现工作流的架构设计思路


介绍一个划时代的、革命性的、足以载入软件工程史册的伟大发明——Emmett,一个旨在让你的业务应用开发“更丝滑”的框架。

当然,它现在还只是个社区项目,但别担心,它的野心可不小。它的目标,是让那些折磨你、让你秃头、让你怀疑人生的多步骤业务流程,变得像喝杯咖啡一样轻松。

怎么做到的?靠一个叫“工作流引擎”的东西。听起来是不是很高级?别急,这玩意儿的底层,是事件溯源——没错,就是那个你听说过但一直没搞懂、觉得太“学术”而敬而远之的玩意儿。但在这里,它被包装成了“天然的解决方案”,仿佛不采用它,你就对不起自己写的每一行代码。

为什么我们需要这个“工作流引擎”?因为人类太容易失败了!

让我们先来点“痛点共鸣”。作者说,多步骤流程在业务应用中无处不在。他举了几个例子:酒店团体退房、代码审查、客户开户、事故响应、数据导入。哦,多么熟悉的场景!尤其是那个“事故响应”,简直是每个SRE(站点可靠性工程师)的噩梦。想象一下,凌晨三点,警报响起,你从梦中惊醒,揉着惺忪的睡眼,打开电脑,发现一个复杂的跨团队协调流程卡在了某个神秘的步骤。你不知道它卡在哪,不知道之前发生了什么,甚至不知道重启服务会不会让它从头再来一遍,导致客户被重复扣款。这种时候,你最想做的不是解决问题,而是想把设计这个流程的人从名单里删掉。

作者深谙此道,他指出,这些流程需要“持久化执行”,因为“它们需要存活的时间,远超我们的想象”。一个文档审批流程可能要持续几天,涉及人类的“审批”——也就是拖延、度假、忘记密码。一个事故响应可能持续数小时,协调着不同团队的“有效沟通”——也就是互相甩锅。当事情失败时(注意,是“当”,不是“如果”),我们需要知道它停在哪、为什么停。一次网络抖动,不应该让一个正在进行的退房流程中途夭折;一次服务重启,不应该让系统忘记它已经走过的步骤;一个bug修复后,不应该需要你手动去数据库里翻找那些“卡住”的流程实例。这简直是每个后端开发者的终极噩梦。

DIY?别天真了,那只是通往生产地狱的单程票

作者接着说,你当然可以用Emmett现有的抽象(比如命令处理器、消费者)自己实现一套。听起来很美好,对吧?自己动手,丰衣足食。但作者立刻泼了一盆冷水:“DIY解决方案总有漏洞。” 他说,我们通常只处理了“幸福路径”,加点重试逻辑,然后就满怀信心地上线了。

结果呢?生产环境总能用你从未想到的边缘情况来教育你。

比如,部署时消息丢失了;部分失败后流程卡死了;修复bug后无法恢复;最致命的是,当老板问“订单X怎么了?”,你只能支支吾吾,打开各种日志和数据库,像侦探一样拼凑线索。这不就是我们每天的真实写照吗?作者用一种略带嘲讽的口吻暗示:别以为自己能写出完美的代码,生产环境才是真正的“代码审查者”,而且它从不留情。

现成的工具?哦,它们强大,但复杂得让你想哭

那么,为什么不直接用Temporal、Restate或AWS Step Functions这些成熟的工具呢?作者承认,这些工具确实能解决问题,保证流程完成,提供可见性。但他话锋一转,开始大吐苦水:这些工具带来了“认知负担”。写一个简单的审批流程,你需要学习活动(activities)、工作流(workflows)、工作节点(workers)、任务队列(task queues),还有那该死的“确定性重放”要求。你那个简单的“发送审批邮件”操作,瞬间变成了一个需要配置重试策略、超时和心跳的“活动”。测试?你需要它们的测试服务器、时间跳跃API和模拟活动。这简直是强迫症患者的噩梦。

作者讽刺道:“这些工具要求你学习它们的整个编程模型,即使你只需要一个简单的流程。” 你得到了强大的功能,但“复杂性的代价”却要提前支付,不管你是否真的需要那些花里胡哨的功能。这就像为了切一个苹果,你得先学会操作一台精密的CNC机床。

我们真正需要的是什么?一个“自然”的东西

那么,我们真正需要的是什么?作者给出了他的答案:我们需要一种“感觉自然”的持久化执行。这意味着能“写普通的代码”,测试就是“调用函数”,模式要“匹配我们对领域的思考方式”。简单流程应该简单,复杂功能在需要时再引入。这听起来是不是很像“敏捷开发”和“渐进式增强”的口号?作者称之为“学习阶梯”,提供安全的默认设置和扩展点。

简而言之,一个既能让新手上手,又能让高级用户发挥的“无障碍模型”。这目标听起来很美好,但实现起来?谁知道呢。

事件溯源:被“重新发现”的救世主

接下来,作者开始神化“事件溯源”。他说,持久化执行的核心是三点:在每个决策点持久化进度、从最后已知状态恢复、知道发生了什么。传统的做法很复杂,但事件溯源提供了一个“更直接的方式”。它把每个业务决策存储为不可变事件。对于工作流,这意味着每个输入和输出都成为流中的一个事件。这不仅是存储,更是恢复机制。作者兴奋地宣布,他们可以“扩展这个模型”,用事件存储作为消息存储,存储所有消息。命令(运行业务逻辑的意图)和事件(已发生的事实)都被存起来。

然后,他举了一个“团体退房”的例子:店员发起退房,系统记录事件,然后安排两个单独的退房命令。如果处理到一半进程崩溃了,重启后怎么办?作者得意地说:“读取最后一个成功的检查点,继续处理。不需要特殊的恢复代码!工作流甚至不知道它崩溃过。”

这听起来确实很酷,但背后的代价是“双跳延迟”和“数据重复存储”,作者在后面轻描淡写地提了一句,仿佛那不是问题。

为什么事件溯源让工作流变简单?因为它自带“上帝视角”

作者继续吹捧事件溯源的好处:时间旅行调试(逐个重放事件看决策序列)、假设分析(修复bug后重放看是否成功)、天然的审计日志(每个决策都有记录)、错误分析(存储错误作为消息)。他说,Emmett已经提供了这些“原语”,缺的只是一个“协调模式”。这听起来像是在说:“看,我们不是从零开始,我们只是把已有的乐高积木搭成了一个新模型。”

他还特意提到,这个设计灵感来自Yves Reynhout的“工作流模式”,仿佛给自己的发明找到了一个高贵的血统。

解决方案概览:又是熟悉的“决定/演化”模式

终于到了技术细节。Emmett的工作流遵循和命令处理器相同的思维模型:接收命令,基于状态做决定,产生新消息。但它还能对事件做出反应。模式有三个函数:decide(业务逻辑)、evolve(构建状态)、initialState(初始状态)。每个工作流实例有自己的事件流,既是收件箱也是发件箱。

作者强调,这设计“自我包含”,一切都在一个地方。他又一次提到Yves Reynhout,仿佛在说:“看,我们不是抄的,我们是‘借鉴’。”

技术设计:双跳模式,为“可观测性”而生的延迟

技术设计部分揭示了真相:输入消息先到源流,路由函数决定工作流实例,然后存储到工作流流中(第一跳),消费者再从工作流流中取出(第二跳),重建状态,调用decide,存储输出,最后发送命令/发布事件。作者承认,这增加了延迟。但他辩解说,对于后台进程,这“微不足道”。对于延迟敏感的操作,用命令处理器。这就像在说:“我们的方案有延迟,但没关系,因为你不该用它来做需要快速响应的事。” 这逻辑是不是有点微妙?

另外,消息在源流和工作流流中各存一份,作者称之为“有意的重复”,可观测性的好处“超过了存储成本”。这听起来像是在为自己的设计缺陷找借口。

何时不该用工作流?几乎所有简单场景!

最后,作者诚实地说,工作流不是万能的。简单请求-响应用命令处理器,读模型用投影,简单消息处理用反应器,同步操作也用命令处理器。工作流只适用于“多步骤、长时间运行、跨流协调”的流程。这等于说,它的适用范围其实很窄。

作者还展望了未来:可能支持Kafka、RabbitMQ等消息系统,可能增加Saga模式,可能提供付费工具如“可视化工作流调试器”和“业务流程分析仪表盘”,并问社区是否愿意为此付费。

这最后一句,暴露了真正的目的:让Emmett可持续发展。这整个宏大的技术叙事,最终落脚点竟然是“求赞助”。这讽刺的意味,不可谓不浓。