消息中间件这个领域,这几年被聊得最多的两个词:一个是 Kafka,另一个就是 Apache Pulsar。COSCon‘25 同场活动 Pulsar Developer Day 倒计时只剩 3 天,说实话,我对这场活动的期待值还挺高的。消息中间件是后端架构里绕不开的一环,从异步削峰到事件驱动,从数据管道到多租户隔离,核心场景几乎都被它包圆了。Pulsar 能在 Kafka 已经这么强势的情况下持续拿到开发者关注,靠的不是概念,而是真正把“存储与计算分离”落到了工程实践里。这篇文章我不打算做那种面面俱到的官方介绍,就把我自己从接触 Pulsar 到把它用进生产环境的过程、架构理解、踩坑经历,以及这次 Pulsar Developer Day 值得关注的点,一次性摊开来聊。
1. 消息中间件为什么突然成了必修课
1.1 从同步调用到异步解耦
如果你经历过早期单体应用的开发,肯定对“一个接口调到底”的同步链路不陌生:用户下单,订单服务同步调用库存服务,再调支付服务,再调积分服务,最后还要发短信。整个请求链路只要有一个服务变慢,用户那边就是一片转圈等待,数据库的连接池也容易被拖垮。这种架构最大的问题不是代码难写,而是服务之间的耦合太深:任何一个下游抖动都会反向传导到上游,最后形成雪崩。
消息中间件解决的问题,本质上是把“服务之间直接说”变成“通过一个可信的中间人转达”。生产者把事件丢进消息队列就返回,消费者按自己的节奏去处理。好处有三层:第一是异步化,调用方不需要傻等;第二是削峰填谷,突发流量先落到队列里,消费者按吞吐能力慢慢消费;第三是解耦,上下游之间不需要知道对方的存在,只要约定好消息格式就能各自演进。这三件事说起来简单,真正在架构里落地的时候,选哪一款中间件、怎么设计 Topic、怎么保证消息不丢不重,全都变成了实打实的工程问题。
1.2 老三样之外,Pulsar 凭什么有姓名
业内消息中间件的选择,过去很长时间基本是 Kafka、RabbitMQ、RocketMQ 三足鼎立。Kafka 在日志采集和数据管道领域几乎是事实标准,吞吐量高、生态全,但在业务消息场景里并非没有短板。用过大规模 Kafka 集群的人应该都有体会:分区数量一旦上去,Rebalance 可能引发整个消费组的停顿;Broker 既管路由又管存储,扩容时数据迁移的操作成本很高;消息积压以后想要临时加消费者,又得先扩分区,而分区扩了又不能随便缩。这些痛点不影响 Kafka 的强大,但确实让“云原生 + 大规模业务消息”的团队开始寻找另一种思路。
Pulsar 吸引我的核心,是它把存储层彻底抽离了出来。Broker 只负责消息路由和元数据管理,真实数据写入底层的 Apache BookKeeper 集群。这个设计带来的直接变化是:Broker 变成了无状态的服务,扩容一个 Broker 不需要搬数据;存储节点和计算节点可以独立扩容;消息以 Segment 的方式存储在 BookKeeper 里,配合分层存储还能把历史数据自动卸载到对象存储。加上原生支持多租户、跨地域复制、四种订阅模式这些能力,Pulsar 更适合那种“一个集群服务多个业务团队、需要长期保留消息、还要做容灾”的复杂环境。
当然,Pulsar 不是银弹。它的运维复杂度比单机版 Kafka 要高,BookKeeper 引入的组件也更多,小团队如果只是为了接几个业务消息,直接用云上的托管 Kafka 或 Pulsar 也完全合理。但如果你在选型调研期,或者已经遇到了 Kafka 分区膨胀、Rebalance 抖动、跨机房复制困难这类问题,Pulsar 值得花力气认真学一学。这次 COSCon‘25 同场活动的 Pulsar Developer Day,很大程度上就是给这批“正在认真评估消息中间件的人”准备的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Pulsar 架构拆解:存储与计算分离是怎么回事
2.1 两层架构:Broker 和 BookKeeper
Pulsar 的架构听起来复杂,拆开其实就是两层:无状态的 Broker 层和分布式的 BookKeeper 存储层。Broker 不保存消息数据,它只做三件事:接收生产者的消息,把消息写入 BookKeeper;从 BookKeeper 读取消息并推送给消费者;维护 Topic、租户、命名空间这些元数据。因为 Broker 无状态,你可以随时加机器横向扩展,新加的 Broker 不需要做数据重分布。这跟 Kafka 的“一台 Broker 管一批分区”很不一样。
BookKeeper 是存储层的核心,它把消息按 Ledger(账本)的方式组织,一个 Ledger 又由多个 Segment 组成,Segment 会均匀分布到多个 Bookie 节点上,并按照配置做多副本冗余。写入的时候,客户端(准确说是 Broker 内部的存储客户端)把消息追加到 Ledger,只要大多数副本写入成功就算确认。这个机制保证了即使某个 Bookie 节点宕机,数据也不会丢。
我用一个比较生活化的类比来理解这套架构:Broker 是超市的前台收银台,它负责接待顾客、传达信息;BookKeeper 是超市的巨型仓库,货物都存放在仓库里,收银台本身不囤货。以前 Kafka 的模型是每个收银台自己带一个小仓库,分区的数据跟着收银台走,哪天这个收银台要搬位置,货物也得跟着搬。Pulsar 把仓库独立出来之后,收银台增加多少台都没关系,货架扩容也只跟仓库有关。这个思路放到云原生环境里特别顺手:Broker 可以随流量弹性伸缩,Bookie 可以随存储量独立扩容,两者互不拖累。
2.2 消息模型与订阅模式
Pulsar 的消息模型用几个核心概念就能讲清楚:Topic 是消息的目的地,生产者往 Topic 发消息,消费者从 Topic 收消息。为了实现吞吐量扩展,Topic 可以拆成多个分区,分区本质上是更小的消息流,消息会按照 key 或轮询策略路由到不同分区。与传统消息系统相比,Pulsar 多了一个 Cursor(游标)的概念,每个订阅在 Broker 端都维护自己的消费位置,消息删除不是“消费者读完就删”,而是根据所有订阅的进度来判断,这为消息重放提供了基础。
订阅模式是我觉得 Pulsar 做得比大多数中间件精细的地方,一共有四种:
| 订阅模式 | 消费者数量 | 消息分发方式 | 适用场景 |
|---|---|---|---|
| Exclusive(独占) | 1 个 | 同一时刻只有一个消费者 | 严格顺序消费、审计日志 |
| Failover(灾备) | 多个 | 同一时刻一个主消费者,其余备份 | 顺序性要求高,需要高可用 |
| Shared(共享) | 多个 | 消息轮流分发给所有消费者 | 任务队列、并行处理 |
| Key_Shared(按键共享) | 多个 | 相同 key 的消息固定发给同一消费者 | 按订单号/用户ID 保证局部顺序 |
我第一次看这四种模式的时候,最大的感受是:Pulsar 把“到底怎么消费”这个决策权完全交给了业务方。如果你需要某个 Topic 的消息全局有序,用 Exclusive;如果既要有序又要高可用,用 Failover;如果对顺序没要求只求吞吐,用 Shared;如果要兼顾局部顺序和并行扩展,用 Key_Shared。这种灵活度在 Kafka 里需要靠多分区 + 分区键 + 消费者分配策略去绕,在 Pulsar 里直接做成订阅级的配置,确实省心很多。
除了订阅模式,Pulsar 还有一个 Reader 接口。Consumer 是“用订阅游标消费、自动管理位置”,Reader 则是“从指定 Message ID 开始读取,完全由应用自己管理位置”。做数据回放、离线分析、按时间点补数据的时候,Reader 比 Consumer 好用得多。生产上我经常看到有人用 Consumer 强行做重放,结果游标被搞乱,其实换成 Reader 就清爽了。
2.3 分层存储与跨地域复制
分层存储(Tiered Storage)是 Pulsar 一个很容易被忽略但价值极高的特性。BookKeeper 虽然存储能力强,但毕竟还是本机磁盘,成本和容量都有上限。Pulsar 允许你设置一个阈值,比如“超过 Retention 时间”“消息达到一定大小”之后,把 Segment 自动卸载到 S3、OSS、GCS 这类对象存储上。消费者读取历史消息的时候,Broker 会透明地从对象存储拉回来,应用层完全无感。
这意味着什么?原来 Kafka 想保留几天甚至更久的数据,磁盘成本会让人肉疼;Pulsar 可以把热数据放在 BookKeeper,冷数据放对象存储,成本直接降一个数量级。对于有审计、重放、数据湖对接需求的团队,这个能力等于给了你一个“无限长的消息河流”。
跨地域复制则是把单集群的能力扩展到多机房。Pulsar 原生支持在多个集群之间做异步复制,两个机房的数据可以双向同步,任意一个机房故障,另一个机房还能继续提供服务。搭配多租户设计,不同业务团队可以在同一个集群里有独立的命名空间和配额,互不干扰。多租户是我实际用过之后觉得特别舒服的点:以前自建 Kafka 给多个团队用,要么一个团队一个集群,运维成本高;要么共用集群,互相争资源。Pulsar 的 namespace 天然把隔离做得比较干净,配额、权限、策略都能分开配。
3. 我用 Pulsar 跑通第一个生产项目的完整复盘
3.1 环境准备:从单机到集群
最早我接触 Pulsar,用了官方提供的本地单机模式,一条命令 bin/pulsar standalone 就能把 Broker 和 Bookie 都拉起来,适合跑通概念验证。但要注意,standalone 模式默认用的是临时数据目录,重启可能丢数据,千万别拿它当生产用。
后来我们在测试环境搭集群,用的是 Docker Compose 方案。一个可复用的最小配置大致包含:一个 ZooKeeper、一个 Bookie、一个 Broker。如果是多节点生产环境,我会建议用 Helm Chart 部署在 Kubernetes 上,或者直接用官方的二进制包做裸机部署,关键是 Broker 和 Bookie 要分开扩容,别把两者部署在同一批机器上,否则存储和计算争抢资源,故障域也混在一起。
部署完成之后有几步验证特别重要:第一步,检查 Bookie 是否已经注册到元数据服务;第二步,用 pulsar-admin topics list 确认命名空间能正常操作;第三步,跑一次简单的生产和消费测试,确认写入和读取链路都通。很多新手部署完发现生产消费都不报错但消息就是丢,大概率是 Bookie 副本数配置不匹配或者 ZooKeeper 连接地址写错了。
3.2 生产者消费者代码实战
我第一个生产项目是用 Python 写的,任务是接收业务系统的订单变更事件,同步到下游的搜索索引和数据仓库。Pulsar 的 Python 客户端用起来很直接,下面是简化后的生产者代码:
python复制import pulsar
client = pulsar.Client('pulsar://localhost:6650')
producer = client.create_producer(
'persistent://public/default/order-events',
send_timeout_millis=30000
)
message = {
"order_id": "202501010001",
"status": "paid",
"updated_at": "2025-01-01T12:00:00Z"
}
producer.send(
json.dumps(message).encode('utf-8'),
properties={"source": "order-service"}
)
producer.close()
client.close()
消费者这边,我用的是 Shared 订阅,因为下游搜索引擎的更新任务不需要严格顺序,多个消费者并行处理吞吐更好:
python复制import pulsar
client = pulsar.Client('pulsar://localhost:6650')
consumer = client.subscribe(
'persistent://public/default/order-events',
subscription_name='search-index-sub',
consumer_type=pulsar.ConsumerType.Shared,
initial_position=pulsar.InitialPosition.Earliest
)
while True:
msg = consumer.receive()
try:
process_event(json.loads(msg.data().decode('utf-8')))
consumer.acknowledge(msg)
except Exception:
consumer.negative_acknowledge(msg)
这段代码里有几个细节值得展开。initial_position=Earliest 表示这个新订阅如果有历史消息,从头开始消费,这个参数在创建订阅之后不能随便改。negative_acknowledge 是消息处理失败时的重试机制,被否定确认的消息会进入重投流程,配合 retryLetterTopic 可以做死信处理。如果项目里出现了“消息丢了”的错觉,先看一下是不是消费失败后直接 acknowledge 了,这是新手最容易踩的坑。
3.3 订阅模式怎么选
订阅模式的选择,我建议在写代码之前先想清楚业务约束。拿我那个项目举例:订单事件同步到搜索索引,因为同一个订单的修改顺序不能乱,按 order_id 做局部顺序就行,所以用 Key_Shared 其实更稳妥。后来我调整成了 Key_Shared,指定 key=order_id,这样同一个订单的多次变更始终由同一个消费者处理,顺序天然有保障。
反过来,如果是一个纯任务队列,比如“批量发送通知”,每条消息之间毫无关联,Shared 就是最好的选择。如果是一个数据总线,下游只有一个消费者做全量存储,Exclusive 简单直接。Failover 模式适合“消费者应用本身有状态,需要主备切换”的场景,比如某个消费者负责实时计算,挂了不能接受重复消费,就用 Failover。
我还想提醒一点:订阅模式的调整会影响消费者的并发模型。不要在核心链路里频繁切换模式,而是把模式当成业务设计的一部分。团队里最好约定一套规则,什么类型的 Topic 用什么订阅,沉淀成文档,不然每个人按自己的理解写,后面排查消息积压会非常痛苦。
4. 生产环境调优与踩坑实录
4.1 我踩过的四个坑
第一个坑是自动创建 Topic 导致的命名空间混乱。Pulsar 默认允许生产者往不存在的 Topic 发消息时自动创建,开发环境很方便,生产环境如果没有关闭这个开关,线上会冒出大量命名奇怪的 Topic,维护成本直线上升。后来我在生产环境关闭了自动创建,走审批制,由管理员用 pulsar-admin topics create 统一创建。
第二个坑是消费端 Ack 超时导致重复消费。Pulsar 的 Consumer 如果设置了 ack_timeout_millis,超过时间没有收到 Ack,Broker 会重新投递消息。这个机制本来是为了防止消费者宕机丢消息,但如果业务处理时间本身就超过超时阈值,每条消息都会被投递两次,下游如果没做幂等,数据就乱了。所以要么把 Ack 超时时间设得足够大,要么干脆不设置 Ack 超时,而用消费端心跳和负确认来兜底。
第三个坑是 Bookie 磁盘 IO 打满。Pulsar 写入链路对磁盘性能很敏感,如果 Bookie 用的是机械盘或者云盘 IOPS 不够,写入延迟会急剧升高,Broker 端的生产延迟也会跟着涨。排查的时候先看 Bookie 的 IO 等待指标,再看 Journal 目录是不是和数据目录共用同一块磁盘。Pulsar 官方推荐 Journal 用单独的 SSD,数据目录可以放宽,这个配置在新手部署时经常被忽略。
第四个坑是客户端版本和 Broker 版本不一致。Pulsar 的客户端兼容性总体不错,但有些 2.x 和 3.x 混用的情况会碰到协议或行为上的差异,比如某个新特性的配置在旧客户端不生效。建议线上环境统一客户端版本,升级 Broker 之前先在测试环境用目标版本客户端跑一轮。
我把识别和恢复的方法整理成表格,方便排查:
| 问题现象 | 可能原因 | 快速处理建议 |
|---|---|---|
| 消费积压不断增长 | 消费者处理能力不足或订阅模式不匹配 | 增加消费者实例,或改用 Shared / Key_Shared |
| 消息重复消费 | Ack 超时设置过短 / 消费者崩溃 | 调整 Ack 超时,下游做好幂等 |
| 生产端写入延迟高 | Bookie 磁盘 IO 瓶颈 | 隔离 Journal 目录,换 SSD,增加 Bookie |
| Topic 数量泛滥 | 自动创建未关闭 | 关闭 autoUpdate,建立 Topic 审批流程 |
| 历史消息查不到 | Retention 设置过短 / 存储卸载过早 | 根据业务保留需求设置 Retention 和分层阈值 |
4.2 参数调优思路与监控
Pulsar 的调优参数很多,我自己的原则是:先监控,再调参,别凭感觉乱改。最重要的监控指标有三个:积压量(Backlog),就是某条 Topic 的消费未确认消息数量;生产端的写入延迟和消费端的处理延迟;Bookie 节点的磁盘和 IO 利用率。这三个指标能反映集群健康度的 80%。
参数层面,有几个我每次都会确认一遍:
retentionTime:消息被消费之后保留多久。默认可能比较短,有审计和重放需求的一定要调大。retentionSize:消息保留的容量上限。跟retentionTime配合,哪个先满足就触发清理。ttl:未消费消息允许存多久,超过后即使没有消费者也会被删除。清理积压时很有用,但误删就麻烦了。maxUnackedMessagesPerConsumer:单个消费者允许的最大未确认消息数,防止消费者处理不过来还一直拉取。receiverQueueSize:消费者本地缓存的消息条数,调大能提升吞吐,但也会增加内存占用和消息丢失风险。
调参的过程不要急着一次性全上。我会先在测试环境用压测工具模拟峰值流量,观察积压曲线和延迟分布,再逐个参数调整并对比。生产环境改配置也要从小的增量开始,比如先改一个 namespace 的 retention,观察稳定后再扩大范围。这套方式看起来慢,但比“直接改全局参数然后半夜被告警叫醒”稳妥太多。
5. 倒计时 3 天:Pulsar Developer Day 我准备怎么逛
5.1 COSCon‘25 同场活动的看点
COSCon 是开源圈一年一度的大聚会,今年 COSCon‘25 同场设置 Pulsar Developer Day,本质上是在开源大会的大环境里,给消息中间件方向单独开一个专场。从过去几届 Pulsar Developer Day 的惯例来看,这类活动的形式一般是主题演讲加动手实践,不会只有 PPT 念稿,更多是带着真实的工程案例来分享。倒计时 3 天的节点,活动议程应该已经排得比较满了,大概率会覆盖几个方向:Pulsar 新版本的特性解读、生产环境的架构实践、性能调优经验、Kafka 迁移到 Pulsar 的案例,以及周边生态的集成。
我自己比较期待的是关于 Pulsar 3.x 的议题。Pulsar 近几年的迭代速度很快,新版本在 Broker 元数据服务、存储引擎、客户端稳定性上都有不少改进,这些官方 Release Notes 里看不出来的细节,往往需要一线研发团队出来讲才有真的体感。另外,如果现场有 Workshop 环节,带一台能跑 Docker 的电脑去,跟着动手体验一下集群部署和 Topic 管理,比听十个演讲都来得实。
5.2 带什么问题去现场最值
参加这种技术活动,如果只是坐在台下听,收获会打折扣。我的习惯是提前准备两三个自己真实遇到的问题,利用茶歇和自由交流时间找讲师或同场开发者聊。比如我们团队现在用的是自建 Kafka,近期正在评估是否迁移 Pulsar,我大概会带着这些问题去现场请教:
- 生产环境从 Kafka 迁到 Pulsar,Topic 和分区如何对应,消费组迁移有没有成熟的工具链?
- 当 Topic 数量达到几百到上千,Pulsar 的元数据压力大概在什么量级,单集群的合理容量如何评估?
- 跨地域复制在真实网络环境下,延迟和带宽的表现如何,两边数据不一致时怎么排查?
- 分层存储的卸载阈值和冷读性能,有没有经验值可以参考?
这类问题在文档里很难找到标准答案,因为答案跟你的集群规模、业务场景、机器配置都强相关。现场遇到有实战经验的人,聊十分钟往往比闷头看两周文档都有用。就算你的项目里暂时用不到 Pulsar,带着架构设计的问题去听,也能学到别的团队怎么处理存储、扩展、容灾这些问题。
5.3 我的活动参与路线
距离活动还有 3 天,我给自己定的准备计划很简单:先把 Pulsar 官方文档的架构章节重新过一遍,再看一遍顺手梳理出两个方向上最核心的问题,一个留给主题演讲时听,一个留给交流环节问。到了现场,我一般不会每个环节都赶场,而是先跟着主题演讲把整体技术趋势搞清楚,再挑一个动手实践环节深度参与,最后留足时间在展区和交流区待着。
很多人参加技术大会喜欢把日程塞满,其实收获反而不高。我个人体会是,一天活动能有一个技术点真正弄明白,能认识一个可以以后保持联系的技术同路人,就已经值回票价了。尤其是 Pulsar 这种偏基础架构的技术,讲义上的内容会后都可以看回放,但现场交流、动手体验、和其他团队面对面聊出来的细节,是没办法回放的。希望活动当天能看到更多关于消息中间件真实落地的分享,也期待通过这次活动,有更多人愿意认真研究 Pulsar 的架构和设计思路。
