周末的日程表上,最让我期待的一件事,就是去 COSCon'25 开源集市上找 Apache Pulsar 的展台。作为一个从 Pulsar 2.x 时代就开始在生产环境折腾消息队列的老用户,这几年我对它是又爱又恨。爱的是它的架构确实先进,多租户、分层存储、跨地域复制这些特性在同类的消息中间件里几乎找不到对手;恨的是它的调优成本和排障成本都不低,尤其是某些消费模式在不恰当的版本和配置下,会冒出一堆让人摸不着头脑的怪问题。借着这次开源集市的机会,我正好把最近踩过的一个大坑——Pulsar 的 KeyShared 模式不消费的问题——完整复盘一遍,也算给准备去展台交流的朋友们提前交个底。
这篇内容我会分成两个部分来聊:先说说 Pulsar 这个项目本身为什么值得你在 COSCon'25 的集市上多停留几分钟,再重点拆解 KeyShared 模式在实际生产里不消费的 bug 现象、排查思路和最终修复方案。看完之后,你至少能在展台前和技术 committer 聊出几个有深度的问题,而不是只拿几张贴纸就走。
1. 为什么这周末要去 COSCon'25 开源集市的 Pulsar 展台
1.1 先搞清楚 Pulsar 到底解决了什么问题
很多人第一次接触 Pulsar 的时候,第一反应是“这不又是一个 Kafka 吗”。如果你也这么想,那说明你还停留在消息队列 1.0 的认知层面。Pulsar 最重要的是把计算和存储彻底拆开了,它底层的存储引擎是 Apache BookKeeper,Broker 层只负责消息的路由、订阅管理、鉴权这些事情,不存数据。这意味着什么?意味着你在扩容的时候不用像 Kafka 那样把整个分区迁来迁去,只要加 Broker 节点就能水平扩展;数据落盘也可以做到真正的多副本强一致,不像某些系统那样把“可能丢失消息”当作 trade-off 写在文档里。
再说多租户,Pulsar 里的 namespace 和 topic 可以做到非常细粒度的策略隔离,企业里不同部门、不同业务线共用一个集群是完全可行的,这在成本和运维资源上能省下一大截。还有一个我在生产里特别点赞的功能是分层存储,你可以把很久以前的消息自动卸载到对象存储里,本地只保留热数据,这让 topic 的保留期从“几天”直接变成“几十年”,而且完全不影响在线读写性能。
如果你所在的团队正在做微服务架构改造、事件驱动架构落地,或者需要一套能同时处理流式和队列场景的消息平台,Pulsar 绝对应该在你的选型清单里。在 COSCon'25 开源集市上,Pulsar 社区的伙伴们会直接把整个运行架构用可视化的方式展示出来,你甚至可以去他们的展台看真实的集群监控面板,了解生产环境的部署规模。
1.2 COSCon'25 开源集市上 Pulsar 社区准备了什么
COSCon 是开源社主办的中国开源年会,开源集市是历届大会里最有烟火气的一块区域。跟正式的会议室演讲不同,集市上的项目方会把展位布置得很活络,你可以直接看到 committer 和 Maintainer 本人在那里坐着,手里拿着贴纸、文化衫,随时准备跟你从架构设计聊到社区八卦。
Pulsar 在今年 COSCon'25 开源集市上的展台,按照社区提前公布的信息,会有几个比较实在的内容。首先是项目全景图展示,从 Broker 到 BookKeeper,再到 Pulsar Proxy、Function、IO Connector,你可以在现场看一遍完整的组件地图,理清楚每个模块的作用。其次是现场答疑,社区会安排有生产环境经验的 contributor 轮值,你有什么部署、升级、性能调优的问题,直接带到现场聊,比在 GitHub 上等 issue 回复要高效得多。
我听说还会有针对新手的快速上手演示,用 Docker 在五分钟内拉起一个本地 Pulsar 集群,生产消息、消费消息、查看监控一条龙走完。对于想入门但一直没动手的人来说,这种 hands-on 的体验比看文档有效多了。
1.3 集市交流时最容易问出价值的问题
说实话,在开源集市的展台前,匆匆忙忙问一句“Pulsar 和 Kafka 有什么区别”,得到的答案只能是大路货。真正有价值的交流,是把你自己遇到的真实场景抛出来。我建议你逛展之前,先想清楚自己团队当前在消息中间件上最痛的点是什么,是消费积压不可控?是分区扩容要搬迁?还是消息回溯的能力不够?带着具体问题去,你才能从 committer 嘴里听到那些写在源码注释之外的经验。
比如你可以问“KeyShared 模式在消费者数量动态变化的时候,哈希区间是怎么重新分配的”,这个问题隔三差五就会在社区里被翻出来。你也可以问“BookKeeper 的读写线程数到底该怎么配置才能压满磁盘 IO”,这种问题没有标准答案,但现场能聊出来的实战调整思路,比你看十篇性能调优文章都管用。后面我会详细讲的 KeyShared 不消费问题,其实就是这类“问题不在文档里、只在事故复盘里”的典型案例。
2. KeyShared 模式:Pulsar 里最容易被误用的消费模式
2.1 KeyShared 想解决什么问题
要说清楚 KeyShared 不消费的 bug,先得明白它到底是怎么运作的。Pulsar 默认提供了好几种订阅模式:Exclusive 是独占,一个订阅下只有一个消费者能拉消息,其他消费者闲着;Shared 是所有人都能拉,消息在消费者之间均匀分发;Failover 是主备模式,主消费者挂了备消费者接管。KeyShared 这个名字听起来很高级,简单理解就是:相同 key 的消息,永远只发给同一个消费者。这样做的好处是你能在多个消费者并行处理的前提下,保住某个 key 维度上的消息顺序。
举个例子,订单系统里同一笔订单的“创建”“支付”“发货”三个事件,key 都是订单号。如果你用普通的 Shared 模式,这三条消息很可能被分给三个不同的消费者,订单状态就乱了。用 KeyShared 模式,只要 key 相同,它们会一路严格有序地进入同一个消费者,你的处理逻辑完全不用考虑并发竞争。
很多团队一看到“保序又并行”这个卖点,就迫不及待地把线上所有 topic 都切成 KeyShared。这个模式确实解决了 Shared 模式的无序问题,但它背后的哈希分配机制比看起来要复杂得多,一旦配置不当或者遇到特定版本的缺陷,就会出现我在标题里提到的“不消费”情况。
2.2 从哈希分配看 KeyShared 的实现原理
KeyShared 的核心机制是对消息 key 做哈希计算,然后按照哈希值的范围把消息分给不同的消费者。最常见的哈希策略有两种,一种是 AUTO_SPLIT,Broker 会根据当前消费者的数量,把整个哈希环自动切分成 N 段,每个消费者负责其中一段;另一种是 STICKY,哈希环的切分范围比较固定,消费者变更时不轻易做重新分配。
AUTO_SPLIT 模式的逻辑看起来很美:消费者多了,分段就多;消费者少了,分段就少。但这里有一个隐藏的难点:分段变化的时候,Broker 必须把原来属于某个消费者的哈希区间完整地转移给另一个消费者。谁转移给谁、什么时候转移、转移过程中那些还没消费的消息怎么办,这一套流程在 Pulsar 某些版本里并没有处理得足够健壮。
KeyShared 对订阅名称也有要求。同一个订阅下的消费者如果想启用 KeyShared,必须显式地在客户端配置里声明使用 KeyShared 订阅类型,而且所有消费者的配置要一致。如果有一个消费者没启用,Broker 端在创建订阅时会直接报错或者做出很奇怪的行为,这种坑在初用者身上非常常见。
2.3 什么场景才适合 KeyShared
这里我建议所有团队在选型前冷静三秒钟。KeyShared 不是默认选项,它的适用场景有明确边界。第一种是消息处理确实需要 key 级别的顺序保证,同时单 key 的数据量又不够大,不值得单独开一个分区;第二种是消费者数量可以接受动态调整,并且业务能容忍短暂的重新平衡。
什么场景不适合?高吞吐的日志传输、无顺序要求的事件流、对消息乱序不敏感的任务队列,这些都应该老老实实用 Shared 模式。因为 KeyShared 的哈希分配本身有计算开销,Broker 在路由时要多做一步哈希计算和区间匹配,吞吐量会受一定影响。更重要的是,一旦某个 key 的消息特别多,它所在的哈希区间对应的消费者会成为热点,其他消费者在那边闲着,没有什么负载均衡机制能帮你自动拆掉这个热点。
我见过不少团队把 KeyShared 当万能药,最后在压测阶段就发现消费速度上不去,因为数据分布天然倾斜。你去看 Pulsar 官方文档,它也会反复强调 KeyShared 不是 Shared 的替代品,而是为特定场景设计的一个补充选项。
3. 实战复盘:一次 KeyShared 模式不消费 bug 的完整排查
3.1 故障现象:积压和空闲同时出现
上个月我们生产环境突然收到一条告警,某个核心业务 topic 的消息积压量在半个小时内从几百涨到几十万。这个 topic 用的是 KeyShared 订阅模式,8 个消费者在跑。按道理来说,几十万积压对于 8 个消费者来说也就几分钟的事情,但诡异的是,消费者这边完全看不到消息在流动,消费速率为零,CPU 占用率几乎为 0,日志里也没有任何报错。
这种“Broker 端积压严重,消费者端却闲得要命”的状态,就是标准的不消费状态。先说清楚,这不是消费者业务逻辑卡住了,因为如果是业务处理慢,你会在消费者日志里看到数据一直在拉取和处理,只是处理不过来。而我们的现象是消费者进程完全空闲,它在等数据,但数据却不来。这说明问题出在 Broker 让谁来消费这一层,也就是订阅模式的调度逻辑上。
我当时的第一个预感就是 KeyShared 的哈希区间出了问题,这种问题我在社区 issue 里见过很多次。但为了不冤枉它,我还是按部就班地走了一遍完整的排查流程。
3.2 确认消费状态:先排除业务侧假死
排查的第一步,不是去看 Broker 日志,而是先确认消费者到底处于什么状态。因为“不消费”这件事,变量太多了,有可能是网络断流、连接被 Broker 踢掉、consumer 自己进入了某种阻塞状态等等。
我先在 8 个消费者节点上看了进程状态,jstack 了一把,发现所有消费者线程都阻塞在 Pulsar 客户端的内部拉取调用上,没有活跃的业务处理线程。这个信号很关键,说明消费者完全没有拿到消息。然后我检查了 Pulsar 客户端的连接状态,通过 admin 接口查询这个订阅的消费者列表,看到 8 个消费者全部在线,而且都处于 active 状态。
这就更迷惑了,消费者在线、连接正常、没有报错,但就是拉不到消息。从表面配置和数据上看一切正常,一定是底层某个环节发生了“假活”的情况。接着我需要确认这 8 个消费者是不是都拿到了哈希区间。KeyShared 模式下,broker 的抽象里每个消费者都对应一个或多个哈希区间,消息只会投递给哈希区间匹配的消费者。如果某个哈希区间没有对应任何在线消费者,那这个消息就会被一直挂起。
3.3 从 Broker 侧看游标:揪出失联消费者
既然消费者侧证明不了什么,我立刻切到 Broker 上去看订阅游标的状态。这里用的是 pulsar-admin 的命令行工具。我先查看了这个 topic 的订阅详情,重点关注游标中记录的消息的 range 信息。
在正常情况下,8 个消费者的 KeyShared 订阅会把哈希区间均匀切分成 8 段,每段对应一个消费者。但我在输出里看到了一个让我后背发凉的信息:Broker 内部记录的哈希区间有 9 段,而不是 8 段。多出来的那段区间,它的 owner 是一个已经不存在的消费者。换句话说,之前可能有一个第 9 个消费者因为某种原因退出了,但它负责的哈希区间没有被 Broker 重新分配,而是成了一个“无主区域”。
消息积压的那段时间,业务方正好在这个 topic 上重启过一次消费者应用。重启过程中,旧的消费者连接有一个优雅退出的短暂窗口,但新的消费者还没来得及建立连接。正常情况下,KeyShared 模式应该感知到消费者数量变化,把无主区间重新分配给现有消费者。但在我们使用的 Pulsar 版本里,这个重分配动作没有正确触发。本来这个 bug 不会持续很久,因为 client 端会自动重连并触发重新协商,但偏偏我们某个消费者在重连的时候,网络抖动导致它被 Broker 判定为异常,连接被关闭,哈希区间又被卡在了半路。
3.4 根因定位:哈希区间没有随消费者变化而重分配
到这里,根因已经比较清楚了:Broker 的内存里有一块哈希区间,它的 owner 消费者已经不存在,但 Broker 既没有把它分配给别的消费者,也没有触发重新平衡。结果就是,任何 key 的哈希值落在这个无主区间范围内的消息,都会被 Broker 判定为“没有可投递的消费者”,于是这些消息就一直留在 backlog 里,越积越多。
为什么会出现哈希区间不重分配的问题?这涉及 Pulsar 的 KeyShared 实现细节。在 AUTO_SPLIT 策略下,Broker 维护了一个 Consumer 和 HashRange 的映射关系。当消费者增减时,Broker 需要重新计算哈希区间的切分,这个过程要加锁、要更新游标元数据、还要向所有消费者推送新的 hash range 信息。任何一步失败,都可能导致区间表和实际的消费者列表不一致。而且这个不一致的状态不会自动自愈,除非你重启订阅或者手动触发一次重平衡。
我们用的这个版本里还有一个额外的触发条件:消费者在重连过程中使用了不同的客户端版本或者配置了不同的 KeyShared 策略,导致 Broker 在协商时把新连接当成了完全新的消费者,而不是旧消费者的续接。这就会在消费者列表里出现一个“断开但未清理”的残留项,它的哈希区间也就成了幽灵区间。
3.5 修复与规避:升级加配置,两手都要硬
定位到根因之后,我做了几步处理。最直接的办法是重启所有消费者,让 Broker 重新做一次 KeyShared 协商,把哈希区间重新分配。这个操作在几分钟内恢复了消息消费,积压以肉眼可见的速度下降,业务恢复了正常。但这是治标,不是治本。我随后在测试环境复现了一遍操作,确认只要消费者重连时发生连接抖动,故障就可能再次出现。
真正的修复方案有两部分。第一,升级 Pulsar 版本到包含 KeyShared 重平衡修复的版本。这个问题在社区里并不是个秘密,早在 2.10 之后的一些版本中,社区就针对 KeyShared 的区间管理逻辑做了很多修补,尤其是在消费者异常断开后的清理机制上。第二,在代码层面做防御。我给消费者客户端设置了合理的 reconnect 策略,避免频繁重连;同时把 KeyShared 策略固定为 STICKY 模式,减少 AUTO_SPLIT 模式在消费者变化时触发重平衡的复杂度。
如果你在公网或者不可靠的网络环境里使用 Pulsar,我建议慎用 AUTO_SPLIT,因为消费者连接越不稳定,它需要做的重平衡就越多,出错的概率也就越高。这个话题我在 COSCon'25 展台上一定会跟社区伙伴再好好对线一次,看看最新版本在这块的健壮性到底改善了多少。
4. KeyShared 常见问题速查与避坑清单
4.1 典型问题速查表
那次故障之后,我把 KeyShared 常见的坑整理成了一张速查表,分享给团队和身边用 Pulsar 的朋友。这里也给正在读这篇内容的你:
| 问题现象 | 可能原因 | 处理建议 |
|---|---|---|
| 消费者在线但消息不消费 | 哈希区间无主,消费者列表与区间表不一致 | 重启消费者触发重协商,或升级含修复的版本 |
| 部分消费者负载特别高 | key 数据倾斜,热点 key 集中在某个哈希范围 | 先评估是否适合用 KeyShared,必要时改用 Shared + 业务侧排序 |
| 消费者启动时报错 | 订阅已存在,但客户端未启用 KeyShared 策略 | 所有消费者统一设置 KeyShared 订阅类型,且配置一致 |
| 消息偶发乱序 | 同一个 key 的哈希落在不同区间 | 确认是否启用 Batching,KeyShared 与批处理有交互限制 |
| 积压时消费者只有个别在拉 | 消费者数量变化后重平衡未完成 | 检查 Broker 日志中的 KeyShared 重平衡信息,手动触发或升级 |
这张表不能解决所有问题,但能够帮你在故障发生时快速收敛排查范围。Pulsar 的官方文档把 KeyShared 描述得很轻巧,但你在生产环境里用它之前,必须把上面这些边界情况都想清楚。
4.2 关键配置检查清单
除了排查问题,我更推荐你在上线前就把 KeyShared 的配置检查好。首先是订阅名称,同一个 KeyShared 订阅下的所有消费者必须使用同一个 subscriptionName,而且建议在客户端显式传 KeyShared 策略,不要依赖默认值。
其次是允许消费者数量变化的频率。如果你知道自己的消费者会频繁扩缩容,那最好把 KeyShared 的哈希策略配置成 STICKY,并且设置合理的 hashRange 分配策略,让区间变化尽可能小。还要设置消息的 key 路由规则,确保业务里真正需要保序的消息 key 是有限且可控的,不要让每个消息都生成一个唯一 key,那样 KeyShared 就退化成了 Shared,而且多付出了哈希开销。
我在生产里的建议是:消息 key 的基数控制在消费者数量的几十倍以内,太多会导致区间重排时大规模消息重路由,太少会负载不均。这个经验不一定适合所有人,但你可以在测试环境通过监控消费者之间的消息量差值来验证,差值长期超过两倍就该调整了。
4.3 从这坑里学到的调试方法
这次排查 KeyShared 不消费的过程,也让我养成了几个调试 Pulsar 的好习惯。第一,任何“看起来没问题但确实有问题”的场景,先去看 Broker 端的订阅游标元数据,而不是盯着消费者日志。消费者在线不代表它能拿到消息,Broker 的视角才是最终真相。第二,Pulsar 的 admin 命令是排查的利器,topic stats、subscription stats、consumer 详情这三个命令要成为你的肌肉记忆。
第三,也是最重要的:不要在生产环境使用你从来没在故障注入测试中验证过的消费模式。KeyShared 这个模式在常规操作时非常稳定,但一旦发生消费者异常断连、网络分区、Broker 重启这些故障时,它的表现就非常依赖版本质量。你在 COSCon'25 展台前问不问这个问题,社区 committer 都会告诉你同样的话:新版本一定比老版本稳,升级请务必重视。
5. 集市现场:聊技术也聊周边
5.1 展台互动与主题分享
这次 COSCon'25 开源集市上,Pulsar 展台除了常规的项目展示,我了解到社区还准备了一个很有意思的互动环节,叫“消息漂流记”。他们会模拟一个消息从生产者发布,经过 Pulsar 集群的 Broker、BookKeeper,最后到消费者消费的完整链路,你在现场可以亲手点击每一步看到消息的实时状态。这种互动方式非常直观,尤其适合从来没接触过 Pulsar 的新手。
如果你是对 KeyShared 这类话题感兴趣的资深用户,展台也会贴出过去一年社区处理的高频 issue 合集,其中就包括了我这次遇到的 KeyShared 不消费问题。你完全可以指着 issue 列表里的某个问题,直接问在场的 contributor 是怎么修的、修复之后如何验证的。这种程度的交流,才是逛开源集市最大的收获。
我也听说展台旁有专门的 One More Thing 环节,每天会有一位 Pulsar 的 committer 做一次 15 分钟的闪电分享,主题包括 Pulsar 在 AI 场景下的应用、Pulsar 和 Flink 的集成实践、以及云原生环境下的部署最佳实践。时间不长,信息密度很高,建议你提前查好日程,别错过。
5.2 给到访者的一些小建议
作为每年都逛 COSCon 集市的老油条,我给你几条实用建议。第一,早点到,开源集市的数量有限,Pulsar 这种热门项目的周边基本第一天上午就会发光,想要文化衫的千万别睡懒觉。第二,带个自己的项目名片或者 GitHub 主页二维码,跟 committer 交流的时候直接亮出来,比口头介绍自己做了几个项目高效太多了。你甚至有可能因为一次现场交流,获得一个参与社区贡献的入口。第三,参与现场互动之前,稍微准备一两个有深度的问题,千万不要只去拍个照就走,那真的浪费了这个机会。
如果是刚接触 Pulsar 的新手,我建议你在展台先看一轮十分钟的 demo,然后拿一份社区整理的学习路线图,上面会标注好推荐的文档、样例代码和入门项目。拿回去以后按照路线图走一遍,基本就能在你自己的电脑上把 Pulsar 跑起来了。
5.3 为什么每个开源爱好者都该逛一次开源集市
很多人觉得开源大会听各种 keynote 就是全部了,但我个人觉得开源集市才是开源精神的实体化体现。在集市上,你看到的不只是代码和项目,而是一群愿意花周末时间把技术做成有趣内容的人。Pulsar 的展台也好,COSCon'25 的其他项目展台也好,它们都在用各自的方式告诉你:开源这件事,是可以很具体地参与到里面去的。
你在 GitHub 上给一个项目点 star、提 issue、提交 PR,可能感觉还是很遥远。但在集市上,你和一个 committer 面对面聊了十分钟,他说“这块代码是你提的那个 issue 改的”,那种连接感是完全不同的。这也是为什么我会建议所有做技术的人都至少去逛一次开源集市——你可能不会立刻成为某个项目的核心贡献者,但你会开始理解:这些每天都在用的工具,背后是一群什么样的人,他们在想什么,他们的热爱从哪里来。
6. 写在最后:带着问题去集市
回头看我这次 KeyShared 不消费的排查过程,从现象出现到最终定位,前后花了几个小时。真正难的不是修复,而是在“消费者在线但消息不来”这种矛盾现象面前保持冷静,一层层剥开表象。Pulsar 是一个上限很高的项目,它的架构设计足够先进,但它对使用者的要求也不低。你需要理解它的存储、路由、订阅机制,才能在遇到问题时不被表象迷惑。
这次去 COSCon'25 开源集市,我给自己定的目标是跟 Pulsar 社区的 committer 聊透三件事:KeyShared 最新版本的区间重平衡实现、Pulsar 是否计划支持更细粒度的消费路由策略、以及社区对生产环境长期运行版本的建议。如果你在集市上看到一个在展台前蹲着看监控面板的人,那大概率就是我,欢迎过来一起聊聊你在 Pulsar 上踩过的坑。
这周末,带上你的问题,到开源集市走一趟吧。你带走的,可能不只是贴纸和纪念品,还有对某个开源项目更清晰的理解,甚至是一个参与贡献的起点。