跳到主要内容
自己写 webhook 不是省钱,是把成本搬到了凌晨三点

自己写 webhook 不是省钱,是把成本搬到了凌晨三点

算账先生
算账先生

· 阅读约 6 分钟

Webhook 这东西,大多数人把账从一开始就算错了。他们以为自己要写的是一个 HTTP 客户端加个重试循环,顶多再配个签名,两三天弄完,一年省下几百美金的订阅费。这笔账听着是赚的。实际上你写的这个东西,核心是一个状态机——而且是那种“结果可能永远不确定”的状态机。写错的状态机不会在上线那天崩,它专挑半年后的凌晨三点崩。你正睡着,一条“我明明发了、对方说没收到”的消息把你从床上拽起来,你顶着起床气去查,发现日志根本没接全,客户在群里等着,老板在私聊里问怎么回事。你为了省那点订阅费,正在凌晨三点打一份没结钱的工。

为什么说它是状态机而不是 HTTP 客户端?因为你每次发出去的请求,结果从来不是二值的。成功、失败,那是课本写的。真实世界里还有第三个状态:不知道。超时了,对方到底收没收到?可能收到了,只是响应包丢了;可能压根没收到;可能收到且正在处理,处理完了你才知道。这个“不知道”才是 webhook 里最贵的一笔账——你的重试逻辑必须为它兜底,而兜底的唯一办法就是重发,重发就制造重复,重复就得去重。账是一层层欠下来的。

说到重试,Svix 公开的那套时间表可以拿来算一算:第一次立即发,然后 +5 秒、+5 分钟、+30 分钟、+2 小时、+5 小时、+10 小时、再 +10 小时。最后一次尝试大概落在 27 小时 35 分钟之后。一条消息折腾一天多才放弃。这 27 个半小时里,发送方和接收方都晾在那——发送方不知道最后能不能到,接收方不知道下一次重试什么时候拍过来。等你用户量上来,某次流量峰值过后,几万条半死不活的消息躺在队列里,按同一张时间表醒过来,齐刷刷砸向那个本来就很脆弱的接收方。正经的生产级发送器要加 jitter 和 per-endpoint 速率限制,不为优雅,为了别死在自己手里。

——插一句不相干的,我见过太多团队在这个地方翻车,姿势都一模一样:简单重试循环上线,前两个月风平浪静,然后某天合作伙伴 API 抽风两小时,几百万条 webhook 全进了重试队列。等对方缓过来了,队列里那些消息按固定时间表一批批醒,把刚喘过气的接收方再打挂一次。这种事故的账从来没人建项的时候算进去,因为不在“开发成本”那栏,在“运维成本”那栏——而运维成本这栏,多数人压根没建。

回正题。就算重试机制做扎实了,还有个坎绕不过去:at-least-once delivery。发送方必须重试,因为“不知道”必须兜底;只要重试,接收方就一定会收到重复消息。这不是 bug,是分布式系统的基本物理。接收端去重不是加分项,是必选项。怎么做?把 message ID 和业务变更放进同一个数据库事务。先写 ID,再处理业务。这是唯一安全的顺序。见过有人反着做的,省了半行代码,后来用一整晚去清重复数据。

签名这事,说大不大,要命的时候是真要命。

webhook 签名必须覆盖原始请求体,不能是你解析完再序列化出来的 JSON。为什么?JSON 这玩意儿 parse 一遍再 dump 一遍,字节大概率长得不一样——字段顺序、空格、浮点精度,随便一个变了签名就对不上。接收方拿到消息,第一件事验签,验过了再去 parse。顺序反过来,等于给攻击者开了个伪造消息的口子。还有两点:时间戳要查,超出重放窗口直接拒;HMAC 比较用 constant-time,别用普通的字符串相等。

先验签再 parse——这句话听着像废话,但我自己栽过。当时写了个先 parse 再验签的实现,有天签名校验因为字段类型问题挂了,我们先把 JSON 解出来抛了异常,客户端收到 500,默认重试机制立刻又打过来三轮。一群人对着日志喊“上游数据有问题”,喊了大半天,最后发现是自己处理顺序有问题。这种账,算不清。

好,现在到核心问题:自己写还是买。

把它当真账算一遍。一个中级工程师,按国内一线城市行情,综合成本一天两千到三千块吧,这是保守的。写一个能跑的 webhook 发送器——重试循环、签名、基础日志——两三天够了。不贵。但接下来你要加什么:重试队列持久化,重启不能丢消息;重试状态监控,哪条卡在哪一步得看得见;签名密钥轮换;手动 replay,客户说没收到你得能重发;endpoint 自动禁用与恢复,对方连续失败要不要停、停了什么时候恢复。这些加起来不是两三天,是两三周打底。还只是“写完”,没算“养着它”。

养着它才是真正吞钱的地方。客户发工单:“我们没收到 event evt_8813。”你怎么办?如果你的答案是“让工程师去日志里查一下”,那这笔账就不对了。工程师查一条消息,快则二十分钟,慢则半天;按时薪乘一下,一个工单的成本可能就超了一年订阅费。更别说很多公司工程师根本不乐意碰这事,工单转来转去,排期排三天,客户已经炸了。所以 build vs buy 真正该问的不是“开发要多久”,而是这句话:“event evt_8813 到底发没发出去、对方返回了什么,现在谁能回答?” 如果答案是需要工程师加日志权限去查,那这个支持成本必须算进 build 的账里。算完你会发现,“自己写更省钱”这个结论,多数时候是没把这一栏算进去。

Svix Dispatch 这类东西值不值?如果它把 durable delivery、重试、签名、速率限制、尝试日志、手动 replay、endpoint portal 都打包好,那对中小团队来说,订阅费大概率比自建的运维成本便宜。这不是广告,是一道算术题:你的工程师时间贵,还是订阅费贵。但反过来说,你就接几个 webhook,一年发不出几万条消息,自己写个带重试的 HTTP 调用也不丢人——关键是你得知道自己省略了什么。怕的是你根本不知道自己漏了什么,还以为自己都做完了。

webhook 表面上的开发成本,写个 POST 加个重试,趋近于零。真正的成本在状态管理上:重试队列的状态、超时的不确定状态、重复投递的去重状态、签名验证的安全状态。你每写一行代码,都是在替未来的自己付账——有些一次付清,有些分期。分期的那些会利滚利,最后滚成凌晨三点的一通电话。

这笔账,你自己算。咱下篇见。

算账先生
算账先生

用算账视角看 AI 工具这门生意——红利、认知差、护城河,泼完冷水给一条窄路。

查看主页 →