文章摘要
文章指出,开发者常需通过Webhook同步外部系统的用户数据,但这一过程远比想象复杂:需处理签名验证、去重、事件顺序、数据导入和定时对账等问题,最终往往演变为一个需要持续维护的隐性系统。
文章总结
好的,这是根据您的要求,对原文进行的中文重述,保留了核心细节,并删减了与主题无关的冗余内容。
标题:Webhook 的困境
核心内容重述:
作者在三个不同的公司,为三个不同的服务商,三次构建了几乎相同的系统。这个系统没有正式名称,也从未出现在产品路线图上,但它的核心问题始终如一:关于你自家客户的真实数据,却存在于别人的数据库中。用户信息在身份提供商那里,订阅状态在 Stripe,邮件退信在邮件服务商那里。你的产品需要在本地拥有这些真实数据,因此你订阅了 Webhook,并试图在本地保存一份副本。
第一次构建时,作者以为只需一个下午就能完成一个解析 JSON 并更新数据库的接口。但这个下午最终变成了一周。首先需要添加签名验证,因为一个能修改数据库的开放接口是个安全漏洞。接着需要去重表,因为 Webhook 的投递是“至少一次”,事件可能重复送达。然后,处理器需要加入缓冲,因为 membership.created 事件有时会在其依赖的 user.created 事件之前到达。之后,还需要一个数据导入器,因为 Webhook 只通知订阅后发生的事件,并且它需要与实时事件竞争,因此又引入了锁机制。最后,还需要一个凌晨3点运行的核对定时任务,它会爬取服务商的列表 API,与本地数据对比,并悄悄修复不一致的地方。
作者坦诚地指出,这个定时任务本质上是一份“书面忏悔”,它承认:“我不信任自己构建的副本,也无法知道它何时出错,所以我只能每晚从头开始重新推导。” 这种不信任是有原因的。数据偏差从不主动宣告,往往是通过客户支持工单才被发现。例如,一个客户几个月前取消了订阅,但本地数据库仍显示为“活跃”,因为某个 customer.subscription.deleted 事件在 Stripe 和作者的系统之间丢失了,而没有任何机制能够察觉。
作者最终意识到,他真正在重建的是一个“有序的日志”。每次集成都是试图将一串无序、可能丢失的通知流,还原成它来源的那个有序、完整、最新的历史记录。而荒谬之处在于,这个历史记录是存在的,它就在服务商内部,是他们渲染仪表盘和提供重放工具的基础。服务商将自己的有序日志拆解成一个个独立的 HTTP POST 请求,通过一个既不保证顺序也不保证送达的通道发送给作者,然后作者再在本地重新组装。这就像拼图游戏,制造商有原图,却把它剪碎,一次寄一片,途中还丢了几片,有的寄了两遍,盒子上也没有任何提示。当拼好的图与原图不符时,服务商的支持团队反而会问作者缺了哪片。作者不知道,而这正是问题的全部:没有任何东西会通知你数据出现了缺口。
这并非任何服务商的错误,他们的 Webhook 功能完全符合文档描述。问题在于 Webhook 的本质:它是一种“通知”,告诉你“某事发生了,这是关于它的一个 POST 请求”。通知是触发副作用的好方法,但却是传输数据集的糟糕方式。不知从何时起,我们开始用它来做后者,却没有意识到任务已经改变。
Webhook 如何成为常态?
Webhook 的普及是因为它对服务商和消费者来说都是成本最低的方案。但它的“低成本”从未区分两种截然不同的任务:1. 触发副作用(如发送收据);2. 在本地保持服务商数据的正确副本。任务一是 Webhook 的初衷,而任务二正是作者三次都在做的事。对于任务二,Webhook 所缺乏的所有特性(顺序、完整性、初始引导、可验证性)恰恰是必需的。我们选择了2007年那个现成的工具,然后花了十五年时间来弥补它的不足。
“山谷”困境
作者借用进化生物学中的“适应性地形”概念来比喻。Webhook 用于数据复制是一个“局部最优解”,证据就是围绕它产生的堆积如山的变通方案:签名方案、去重存储、幂等处理器、重试队列、死信队列、Webhook 日志和重放工具,以及作者凌晨3点的定时任务。这些变通方案甚至催生了整个产业,如 Svix、Hookdeck、AWS 的 EventBridge/SQS/Lambda 组合,以及各种连接器平台。这些工具本身是优秀的工程,但正是这种优秀让“山谷”变得舒适,以至于没人抬头寻找更高的山峰。
不过,一些服务商已经开始改变。Stripe 保留了30天的事件日志并提供了可列表的 API,WorkOS 也推出了推荐用于数据一致性的 Events API。这表明,提供有序、可拉取的日志是解决数据复制问题的正确方向,但至今没有一个统一的协议。
更好的方案:翻转箭头
作者提出了一个设想:与其让服务商推送新信息,不如让消费者主动询问服务商自上次检查以来有什么新信息。具体来说,服务商可以为每个数据集合提供一个 URL,该 URL 返回一个有序的、基于游标(cursor)的变更日志。不提供游标则从头开始读取(即初始引导),提供游标则从中断处继续。整个同步状态就是一个游标。
这种方案能解决 Webhook 的所有痛点: - 去重表:不再需要,因为每个事件都包含对象的完整状态,重复应用结果相同。 - 排序缓冲:不再需要,因为日志本身是有序的。 - 初始导入器:不再需要,新消费者只需从日志开头读取即可。 - 丢失的删除事件:不可能发生,因为删除操作会作为墓碑事件存在于日志中,直到被读取。 - 端点、签名和隧道:根本不需要,所有连接都由消费者发起。
此外,当日志读取到末尾时,服务商还可以提供一个当前状态的校验和,让消费者能立即验证本地副本的正确性,从而取代凌晨3点的核对定时任务。
第四次构建
如果这样的日志接口存在,作者第四次构建这个系统将只是一个简单的循环:读取日志,根据“upsert”和“delete”操作更新本地数据库,并保存最后一个游标。这只需要二十行代码,没有路由、没有密钥轮换、没有队列、也没有定时任务。本地副本自带正确性证明。
目前还没有服务商提供这样的接口。作者将这个设想草拟成了一个名为 SCROLL 的协议草案,旨在定义统一的变更日志接口。作者希望那些同样经历过 Webhook 困境的人能阅读并指出其中的问题,因为“异议是期望的回应,而沉默才是失败的模式”。
评论总结
根据评论内容,主要围绕Webhooks在状态同步中的问题及替代方案展开讨论,观点如下:
支持Webhooks的观点(评分:无) - Webhooks简单通用,适合触发副作用(如发送收据、启动构建),但用于状态同步存在缺陷(评论7、11) - 关键引用:"Webhooks are simple and ubiquitous, and that's both a weakness and a strength"(评论7);"The primary use case for Webhook is for triggering the side effect"(评论11)
批评Webhooks的观点(评分:无) - Webhooks缺乏可靠性保证(非至少一次、非至多一次、非顺序保证),需结合轮询或事件API(评论4、6、8) - 关键引用:"Webhooks aren't at-least-once, nor at-most-once, nor are they guaranteed in-order"(评论6);"With SCROLL, consumers are responsible for choosing when to ask a provider for updates"(评论4)
支持SCROLL/轮询方案的观点(评分:无) - 提供游标或时间戳的轮询端点可解决数据一致性问题,类似CouchDB复制协议或Braid-HTTP订阅(评论8、15、16) - 关键引用:"I agree that webhooks should come hand-in-hand with a 'records updated since' endpoint"(评论8);"The proposed feed solution looks a lot like the CouchDB replication protocol"(评论15)
反对SCROLL/轮询方案的观点(评分:无) - 轮询增加网络流量和延迟,且无法及时感知数据变化;持久连接对服务器和CDN不友好(评论4、11、20) - 关键引用:"SCROLL will lead to an increase in unnecessary network traffic"(评论4);"With the proposed solution every consumer will have a persistent connection... inefficient unless you have a very high volume of events"(评论20)
其他观点(评分:无) - 问题本质是推拉模型权衡,区块链或事件溯源可提供更强一致性(评论12、18) - 关键引用:"The core tradeoff here is 'Push vs. Pull'"(评论18);"you should really consider a permissioned blockchain"(评论12)