Pub/Sub 的消费顺序和高并发

最近在用 Apache Beam 搞一个基于 Pub/Sub 的实时风控 POC,被几个关于"消息顺序"和"并发"的底层逻辑给绕进去了。顺着这些线索扒了扒底层的设计机制,发现分布式系统里根本没有魔法,全都是残酷的 Trade-off。

今天把这几个直击灵魂的问题记录下来,如果你也做流处理,大概率也会遇到同样的困惑。

1. 既然 Pub/Sub 是乱序的,Beam 的 Watermark 怎么敢说"数据到齐了"?

我们知道 Beam 最核心的机制就是 Watermark(水位线)。如果 Watermark 到了 12:01:00,就意味着 Beam 拍着胸脯说:"事件时间在 12:01:00 之前的数据我已经全见过了,可以关窗结算了。"

但这就有个巨大的悖论:Pub/Sub 默认是完全乱序投递的。

一条 12:00:50 发出的消息,完全有可能比一条 12:01:10 发出的消息更晚到达消费端。甚至,Pub/Sub 连自己系统打上的 publish_time 的顺序都不保证。

既然乱序,Beam 消费完了前面几条消息,凭什么敢断定后面排队的消息里,就没有时间戳更早的?

真相其实有点扎心:对于 Pub/Sub,Beam 的 Watermark 本质上是"猜"的,它给不了严格保证。

Beam 的 Pub/Sub Connector 底层并不会去扫描整个队列看最老的消息,而是去拉取 Pub/Sub 后台提供的一个统计信息:oldest_unacked_message_publish_time(最老未确认消息的发布时间),然后再减去一个安全余量,以此作为估算的 Watermark。

所以,对于基于 Pub/Sub 的流处理,Watermark 从来都不是 100% 准确的。业务上唯一的解法是:设定一个合理的 AllowedLateness(允许迟到宽限期)兜底,并且在对账要求高的场景下,第二天跑个 Batch Job 重新修正数据。

2. 能不能强行让 Pub/Sub 变有序?

可以。Google 在前几年给 Pub/Sub 加了一个功能叫 Ordering Keys

发消息的时候带上 ordering_key="CORP-ALPHA",消费端开启按键排序开关。这样,同一家企业(CORP-ALPHA)的交易流,就能保证严格按发布顺序投递了。

但这引出了一个新问题:这种有序是"局部"的。

"同 Key 有序,不同 Key 乱序" 是什么意思?

想象你去银行办业务。大家不排一个长队,而是按公司名字去不同的窗口排队。

  • CORP-ALPHA 公司的人在 1 号窗口排队,保证了 1 号必须在 2 号前面办完。
  • CORP-BETA 公司的人在 2 号窗口排队。
    至于 CORP-ALPHA 的 2 号和 CORP-BETA 的 1 号谁先办完?不知道,也没必要知道。

这就叫局部有序。它既保证了单个实体内的顺序,又没妨碍系统整体的并发。

3. 全局有序的死胡同:把所有 Key 设成一样行不行?

既然同 Key 有序,那如果我强行把所有消息的 ordering_key 都硬编码成 "GLOBAL_KEY",是不是就能实现全局严格排序了?

理论上可以,但在生产上这么干,系统会原地爆炸。

Pub/Sub 的高吞吐是靠底层无数台机器水平扩展出来的。当你把所有消息塞进同一个 Key,Google 底层只能把它们路由到同一台机器的同一个通道里排队处理。

后果有两个:

  1. 吞吐量暴跌 :官方文档明确写了,单 Key 的极限吞吐量是 1MB/s。在这个动辄 GB 级的数据时代,这速度跟断网没区别。
  2. 队头阻塞(Head-of-line blocking):如果某一笔交易因为网络抖动处理失败了,触发了重试。那么排在它后面的成千上万条所有账号的交易,必须全部站着等。一人生病,全家瘫痪。

所以,在分布式队列里,强求全局有序是一条死胡同。

4. 隔壁的 Kafka 怎么就能做到"既有序,又高并发"?

其实 Kafka 的底层哲学跟 Pub/Sub 的 Ordering Key 是一模一样的:放弃全局有序,只保局部有序。

Kafka 把一个 Topic 物理切分成了多个 Partition(分区)。发消息时按 Key 做 Hash,路由到固定的 Partition。

  • 因为一个 Partition 同一时刻只能被 Consumer Group 里的一个线程消费,所以它天然保证了严格顺序。
  • 因为你有多个 Partition 可以分配给多台机器,所以它实现了并发。

Kafka 只是把这个"排队逻辑"做得更重、更物理化了。

5. 灵魂暴击:单 Partition 的 Kafka 能不能起线程池做并发?

总有不信邪的开发会问:既然单 Partition 只能分给单线程,导致并发度上不去。那我能不能把这唯一的线程当成一个"搬运工",一次性拉 100 条消息下来,然后在内存里开一个 ThreadPool 去并发处理?

千万别这么干。

如果你这么干了,你会面临两个灾难:

  1. 顺序瞬间粉碎:扔进线程池的消息,处理快慢全看 CPU 调度,你辛辛苦苦维护的 Partition 级别有序直接白给。
  2. Offset 提交地狱 :假设你拉了 Offset 0-9。线程池里 Offset 5 处理成功了,但 Offset 2 连库失败了。这时候你往 Kafka 提交什么 Offset?
    • 提交 5?下次重启,2 就彻底丢了。
    • 不提交?下次重启,0-9 重新跑一遍,疯狂重复消费。

总结

折腾了一圈发现,分布式系统设计其实非常诚实:

想要高吞吐和高可用,就必须接受物理传输层的乱序,把排序的苦差事交给应用层(比如 Beam 的 Window 和 Watermark)去死磕;想要绝对的有序,就只能老老实实回到单机单线程排队的龟速时代。

不存在既要又要还要的魔法。

相关推荐
名不经传的养虾人1 小时前
从0到1:企业级AI项目迭代日记 Vol.77|隔离不只是数据,还有进程、上下文和依赖
大数据·人工智能·ai编程·企业ai·多agent协作
cn分享汇2 小时前
2026免费空间大的网盘软件有哪些推荐,主流网盘拆解
大数据·网络·人工智能
TDengine (老段)2 小时前
TDengine Node.js 与 C# 连接器 — Web 服务与 .NET 集成
大数据·数据库·node.js·c#·.net·时序数据库·tdengine
莉莉周的成长实验室3 小时前
2026年七夕海报用什么AI工具可以做?品牌运营实测3个高频场景
大数据·人工智能
宸津-代码粉碎机4 小时前
告别手动Jar部署!生产级无损热部署方案,彻底解决OOM与更新失效问题
java·大数据·开发语言·人工智能·python
xlrqx4 小时前
长治家电清洗培训基地如何挑选及行业基本培训标准科普
大数据·python
AI星桥小王子5 小时前
支持人物一致性的 AI 视频生成方案分享:怎么能解决 “人脸漂移” 痛点
大数据·人工智能
数智化管理手记6 小时前
手工统计指标误差大、效率低?指标管理系统如何告别人工算数痛点?
大数据·运维·数据库·人工智能·云计算
晓子文集13 小时前
Tushare接口文档:期货日线行情(fut_daily)
大数据·数据库·金融数据·量化投资·tushare