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)去死磕;想要绝对的有序,就只能老老实实回到单机单线程排队的龟速时代。

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

相关推荐
字节跳动数据平台29 分钟前
火山引擎知识引擎VeKnow进化之路(上):从“人有数可用”到“AI有智可循”
大数据
Flynn Liang3 小时前
Apache Superset 重要特性详解(三):语义层 / 权限 / 缓存 / 嵌入的实战最佳实践(2026 版)
大数据·clickhouse·数据可视化·架构设计·superset·数据分析平台
小田学Python14 小时前
工具调用:让 Agent 从“说”到“做”
大数据·大模型·agent·ai工具·deepseek
GrowingIO技术社区18 小时前
用户行为分析怎么做?从业务目标到改进验证的完整流程
大数据·数据挖掘·数据分析
瑞克-桑切斯1 天前
docker Harbor 单机部署手册
大数据·运维·elasticsearch·docker·容器
演艺大赏1 天前
NiFi 起不来、登不上、节点被挤掉:一份按日志定位的排错清单
大数据
freejackman1 天前
【实战三】撕开脱敏盲区:深度解析 CASE WHEN 与窗口函数血缘
大数据·数据库·sql
wafrdggh1 天前
2026电商快递批量查询怎么用?固乔助手模板设置评测
大数据·数据库·小程序
大大大大晴天2 天前
OLTP、OLAP、数据湖、数据集市、湖仓一体到底是什么关系?
大数据
正在走向自律2 天前
AI Agent 落地这一年:从“能跑起来”到“敢放出去”
大数据·人工智能·ai agent·智能体·mcp·a2a