跳转到内容
use-cases / the-webhook-fan-out-you-didnt-have-to-build / hero
PIPE · HTTP 扇出

你无需自建的 webhook 扇出

Stripe 用 ?n=12 把事件主体 POST 到一个 pipe 路径。十二个订阅者用 ?n=12 GET 同一个路径。pipe 在所有人连接好之前一直保留消息,然后一次性向全部十二个推流。没有 broker,没有 consumer group,没有 DLQ。

阅读 pipe 文档
use-cases / the-webhook-fan-out-you-didnt-have-to-build / mechanism

一个 URL。十二个读者。每条连接独立背压。

Hoody Pipe 是带多接收方模式的 HTTP 流。在发送方和接收方 URL 上都追加 ?n=N,pipe 会等到 N 个读者都接入后,再把主体同时镜像给所有人。慢的那些只压自己的连接——快的那些继续流。

01STEP 01 · 接入

Stripe POST 到 pipe

你的 Stripe 端点把事件主体直接转发到 /api/v1/pipe/billing?n=12。连接在 pipe 等待十二个接收方集合期间被阻塞。

02STEP 02 · 集合

十二个订阅者接入

每个订阅者都是容器中的一个 curl 循环,用 ?n=12 GET 同一个路径。pipe 把主体保留在内存中,直到第十二个读者连接——没有磁盘上的队列,没有要刷盘的内容。

03STEP 03 · 推流

扇出,然后忘掉

所有人连接好后,主体一次性流向全部十二个。慢读者把背压施加在自己的套接字上;其他人继续走。最后一个读者断开时,pipe 忘掉这条消息。

stripe-webhook.sh
# Sender side — your Stripe webhook handler.
# Pipe holds the body until 12 readers are attached.
curl -X POST "https://api.hoody.com/api/v1/pipe/billing?n=12" \
  -H "Authorization: Bearer $HOODY_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary "@stripe-event.json"

# Receiver side — one of twelve subscriber containers.
# Same path, same n. Streams the event body when fan-out is ready.
curl -N "https://api.hoody.com/api/v1/pipe/billing?n=12" \
  -H "Authorization: Bearer $HOODY_TOKEN" | ./billing-handler.sh

# n must match on both ends — mismatch returns 400.
# Default n=1 is point-to-point. n=12 is fan-out for twelve.

两端用同一个 URL、同一个查询字符串。发送方的连接就是 broker;读者的连接就是 consumer group。背压是按套接字计的,而不是按主题——因为根本没有主题,只有一份在传输的主体和十二个套接字在拉它。

use-cases / the-webhook-fan-out-you-didnt-have-to-build / fleet

订阅者像进程一样来去

增加一个订阅者就是再起一个容器里多跑一条 curl。移除一个就是杀掉那条 curl。没有 broker 配置,没有 consumer-group 再均衡,没有 DLQ 要清空——集群拓扑就是此刻碰巧在跑的那些进程。

subscribers diff · 最近 5 分钟无需再均衡
  • + 新增
    WHKAUD
    起了两个新容器
  • − 移除
    MIX
    容器被杀,curl 退出
  • = 不变
    BILANLLOGFRDSUCSLACRMCDNNOT
    仍在同一条 n=12 路径上

pipe 不追踪谁订阅了。它只是为每个事件等 n 个连接。两端把 n 调高,下个事件就等新的人头数。没有要被破坏的成员状态,因为根本没有成员关系。

use-cases / the-webhook-fan-out-you-didnt-have-to-build / advantages

你不再需要运营什么

下面每一行都是当协议本身就是 broker 时消失的一类工作。无需配置基础设施,无需学抽象——只有 curl 和 ?n。

  • 无需配置队列

    没有 SQS,没有 Kafka 集群,没有 RabbitMQ exchange。pipe 就是队列,而它一次只为一条消息存活。

  • 无需 consumer-group 簿记

    没有 offset、没有 commit、没有再均衡。pipe 把主体保留到 n 个套接字接入——这就是全部协调模型。

  • 无需 DLQ 清空

    如果发送方在 n 个读者就绪前断开,pipe 会超时(5 分钟 TTL),Stripe 会重试。没有要照看的毒消息桶。

  • 无需逐语言 SDK

    订阅者是 curl 循环。发送方是 curl。协议是 HTTP。任何能命中 URL 的东西都能加入这个集群。

  • 按套接字背压

    慢读者只压自己的连接。其他十一个继续以全速推流。整个机群没有头部阻塞。

  • 默认遗忘

    最后一个读者断开的瞬间,主体就消失了。无需设保留策略,无需排日志压缩,无需写 GDPR 删除请求作业。

use-cases / the-webhook-fan-out-you-didnt-have-to-build / punchline

十二个订阅者,一个 URL,没有 broker。

下面这些 broker,是在 HTTP 扇出还不是一个查询参数之前你必须装的东西。右侧是替代它们的——一个路径、一个 n、十二个互不知道彼此组成集群的 curl 进程。

你不用再跑的东西
  • AWS Lambda + SQS/SNS
  • Apache Kafka
  • RabbitMQ exchanges
  • 自定义 webhook 路由器
你改成调用什么
POST /api/v1/pipe/billing?n=12

十二个接收方用同一个 n GET 同一个路径。pipe 就是 broker——并在最后一个读者挂断的瞬间忘掉这条消息。

阅读扇出章节
use-cases / the-webhook-fan-out-you-didnt-have-to-build / replaces

这取代了什么

如果你为了把一个 webhook 广播给 N 个消费者去抓下面任何一个,pipe 模型用两次 curl 调用就能干同样的活——一边是发送方,另一边是接收方。

  • AWS Lambda + SQS/SNS每个主题一个 broker,每个订阅者一份 IAM
  • Apache Kafka集群、ZooKeeper/KRaft、consumer group
  • RabbitMQ exchangesbinding、vhost、每环境一个管理 UI
  • 自定义 webhook 路由器一个要维护的服务,一套要发明的重试
  • Pub/Sub 服务按消息计费,GCP/AWS 锁定
  • HTTP 扇出插件每套技术栈一个网关插件,无背压
use-cases / the-webhook-fan-out-you-didnt-have-to-build / cta

你以前要花一个 sprint 才能写出的扇出,现在是一个查询参数。追加 ?n=12 然后发布。

阅读 pipe 文档
use-cases / the-webhook-fan-out-you-didnt-have-to-build / related

阅读其他内容