Webhook的陷阱:为何我们总在重复造轮子
The Valley of Webhooks

我在三家公司为不同服务商构建了三次同样的系统,却从未在路图上找到它的名字。Webhook本是为了触发副作用而生,却被我们强行用来同步数据。于是,签名验证、去重表、缓冲队列、引导导入和凌晨三点的对账Cron成了标配。这并非某个服务商的Bug,而是整个行业陷入了局部最优的陷阱:用无数优秀的工程技巧去修补一个根本不适合的工具。当Stripe和WorkOS开始提供有序日志API时,我们是否该重新思考,把推送改为拉取,让数据同步回归本质?
通知是触发副作用的好方法,却是传输数据集的糟糕方式,而我们不知不觉间把这两项工作混为一谈。
HN 评论区
92- toomim
这篇文章很好地总结了使用 Webhook 进行状态同步时存在的问题。我还注意到,文中提出的解决方案是一个名为 SCROLL 的伪 IETF 风格草案协议,它与我今年 11 月在 IETF 127 上提交的另一个实际 IETF 草案“Braid-HTTP Subscriptions”惊人地相似。
两个草案都要求通过 GET 请求加上特定头部来发起订阅:
Scroll 请求:
GET /scroll/feed/customers
Prefer: stream
Braid 请求:
GET /customers
Subscribe:
在两种系统中,GET 请求都会保持响应开放以流式传输事件。SCROLL 返回 application/x-ndjson。而 Braid 订阅则使用 209 Multiresponse 状态码,内容类型为 application/http-history。这使得它们不仅能支持 JSON,还能支持其他格式。你可以发送 CSV、PNG、XML、HTML、纯文本或任何媒体类型的状态更新。
作者提到很难获得采用。其实,Webhook 之所以如此普遍,是因为它们就是最基础的 HTTP 标准。要想让新方案被采用,我们也必须将其融入最基础的 HTTP 标准中。因此,我们需要前往 IETF,以通用方式扩展 HTTP 以支持状态同步。它应该适用于任何现有的 HTTP 媒体类型(不仅仅是 JSON),任何资源/URL(不仅仅是特殊的 /scroll/* URL),以及任何标记时间戳的方式(不仅仅是 SCROLL 中提出的有序字符串)。
这样,我们就可以将这些功能内置到 HTTP 中,进而内置到所有我们常用的基础库、工具和代码中,你就不必再……
- alt227
我最近在使用 Quickbooks API 时也遇到了完全相同的问题。你根本无法信任它的响应或 Webhook。
例如,在创建用户或发票时,有时它会返回错误,但实际上实体已经创建成功了。这意味着你必须在创建完所有内容后手动检查,以确认是否真正创建成功。
此外,Quickbooks 有时更新很慢,并且在执行一些后台操作时会锁定公司文件。这意味着你无法立即进行存在性检查,而且检查本身有时也会报错或超时。这本质上意味着你需要不停地检查,直到能够正确地将你的数据库与他们的数据库进行对账。但在每分钟处理数百甚至数千笔交易的情况下,这种状态永远无法达到。你只能永远处于试图追赶却永远无法赶上的状态。
当我向 Quickbooks 的开发支持团队提出这个问题时,他们的回复简直是:“确保在我们的系统中正确创建数据是你们的工作”。
我们究竟是怎么走到这一步的,竟然开始接受那些永远无法被信任的系统?
- tlonny
我更喜欢使用游标分页(cursor paginated)的 API 请求,而不是 Webhook。显而易见的缺点是,为了避免被 429 限流,你需要一个合理的轮询频率——这意味着你会失去对新事件的实时响应能力。
因此,我认为 Webhook 仍然有一席之地,但应仅作为一个简单的“提醒”(poke)发送给客户端,告知其某些内容已变更,以此作为默认低频轮询的补充。
这样我们可以兼得两者之长:
1. 无需费心去处理提醒的去重或重试——如果你错过了一个 Webhook,下次轮询时很快就能恢复。
2. 无需任何本地特定的隧道或工具——本地应用使用默认的轮询间隔就能正常工作。
3. 无需为每个客户端维持一个长连接。
4. 拥有 OP 在博客文章中提到的所有优点。
- bytesandbots
按照提议的解决方案,无论事件频率如何,每个消费者都将与服务器保持持久连接。除非你有极高频率的事件流入,否则这种设置似乎效率低下。许多 CDN 网络对连接可保持的时长有限制。而且数据提供商也不会偏好服务持久请求。
文中列出的问题包括签名、去重、缓冲、引导(bootstrap)和定时任务(cron)。除了签名和引导之外,所有问题都可以通过在每个 Webhook 负载中增加一个计数器来解决。该计数器每次都会递增。当你收到一个 Webhook 且计数器不匹配时,消费者可以从事件 API 获取缺失的数据。
我同意作者的观点,即提供商仅仅声称“至少一次投递”是不够的。他们应该提供无需架构设计图就能落地的解决方案。
引导(Bootstrap)更适合通过批量事件 API 来实现,这样你就不必为每个请求单独调用一次。它可以支持 after/cursor 分页。那些在我们内部 Kafka 中行之有效的解决方案,可能并不适合在互联网上跨服务使用。