Apache Pulsar 核心原理与实战:存储计算分离、订阅模型、消息确认与对比 Kafka

Apache Pulsar 核心原理与实战:存储计算分离、订阅模型、消息确认与对比 Kafka
神经蛙本文是《Kafka 核心原理与实战》的”对照篇”:不重讲 分区、副本 ISR、消费组、EOS 这些 Kafka 篇已经铺开的内容。这里只聊 Pulsar 独有、且恰恰是它相对 Kafka structural 差异所在的那些点——如果你还没读 Kafka 篇,建议先补一下”消息队列凭什么扛住高吞吐”。
一、为什么又来一个消息系统
Kafka 已经把”高吞吐分布式消息”做到了极致,但凡用过的人都知道它有三个结构性痛点,不是调参能解决的:
- 存储与计算耦合。一个 Kafka 分区就是一个文件 + 一组副本,扩容分区意味着数据要 rebalance、消费组要再均衡(rebalance storm),大集群扩一次分区能抖几分钟。
- 队列与流语义割裂。Kafka 本质是”流”(独占消费、offset 由消费者维护),要做”队列”(多个 worker 竞争消费同一条消息)只能靠起多个消费组各自存 offset,资源浪费且语义别扭。
- 多租户弱。Kafka 的 topic 之间是平铺的,没有原生的租户/命名空间隔离、配额、鉴权分层,一个热点 topic 能拖垮整个集群。
Apache Pulsar 是 Yahoo! 在 2013 年为解决上述内部消息痛点而生的,2018 年成为 Apache 顶级项目。它的核心设计哲学就是一句话:把”计算”和”存储”彻底拆开。
二、架构总览:存储计算分离
Pulsar 的架构分两层,这是它和 Kafka 最本质的区别:
| 层 | 组件 | 是否有状态 | 职责 |
|---|---|---|---|
| 计算层 | Broker | 无状态 | 接收生产请求、做协议解析、投递消息给消费者、维护元数据缓存 |
| 存储层 | Apache BookKeeper | 有状态 | 真正持久化消息,由一组 Bookie 节点组成,提供低延迟追加写 |
1 | Producer Consumer |
为什么这个拆分是杀手锏? 因为 Broker 无状态,扩容计算层 = 起一个新进程,立刻分担流量,完全不搬数据;存储层扩容 = 加 Bookie,新写入自动落到新节点。对比 Kafka 扩分区要 rebalance 整个 topic 的数据,Pulsar 的扩容是”无感”的。
三、Topic → Partition → Ledger → Bookie
Pulsar 的存储粒度比 Kafka 更细:
- 一个 Topic 可以分成多个 Partition(和 Kafka 分区概念类似,解决并行度)。
- 每个 Partition 的消息,按段(Segment)写入 BookKeeper,每一段就是一个 Ledger。
- Ledger 是追加写、只封口(seal)不修改的不可变日志,分散存储在多个 Bookie 上(默认 3 副本)。
1 | Partition-0 |
关键差异:Kafka 的一个分区文件是固定的几台机器,而 Pulsar 的 Ledger 可以轮流落到不同 Bookie,写入压力天然打散,不会出现”某个分区恰好落在慢盘机器上”的长尾。
四、四种订阅模型(Pulsar 最香的能力)
Kafka 的消费语义只有”消费组独占 + offset”。Pulsar 把消费模式抽象成了四种订阅类型,一套消息可以被多个订阅以不同方式消费,且互不影响:
| 订阅类型 | 行为 | 类比 | 典型场景 |
|---|---|---|---|
| Exclusive | 一个订阅只能有 1 个消费者,独占 | 类似 Kafka 单消费者 | 严格顺序消费 |
| Failover | 1 个主消费者 + N 个备,主挂备顶 | 主备容灾 | 需要容错的顺序消费 |
| Shared | 消息被轮询分发给多个消费者,一条只给一个人 | 真正的”队列” | 任务分发、竞争消费 |
| Key_Shared | 按消息 key 哈希分发,相同 key 必到同一消费者 | 分区消费 + 保序 | 按用户 id 保序的处理 |
Shared 订阅是 Pulsar 相比 Kafka 的”队列语义”补齐:多个 worker 同时消费同一个 topic,每条消息只被其中一个处理,天然实现竞争消费和水平扩展,而 Kafka 要做到这点得绕路。
1 | Consumer<byte[]> consumer = pulsarClient.newConsumer(Schema.BYTES) |
五、消息确认机制:单条 ack 与累积 ack
Kafka 的”确认”是 commit offset(累积的,跳过的消息就再也回不去了)。Pulsar 的 ack 灵活得多:
- 累积 ack(cumulative):和 Kafka 一样,确认某条即确认它之前的所有消息。Exclusive/Failover 订阅用这个。
- 单条 ack(individual):精确确认某一条消息。Shared/Key_Shared 订阅必须用它,否则会把没处理完的消息一起确认掉。
1 | // 累积确认(Exclusive/Failover) |
Pulsar 还内建了 重试队列 和 死信队列(DLQ),不用自己造轮子:
1 | Consumer<byte[]> consumer = pulsarClient.newConsumer(Schema.BYTES) |
- 消息 TTL:未被确认的消息超过 TTL 自动进 DLQ 或丢弃,防止 backlog 无限堆积。
- backlog(积压):Pulsar 原生暴露每个订阅的未确认消息数,监控告警比 Kafka 的 lag 更直白。
六、多租户:Tenant / Namespace 两级隔离
这是 Pulsar 在”企业级”上碾压 Kafka 的设计。Pulsar 的 topic 全名是:
1 | persistent://<tenant>/<namespace>/<topic> |
- Tenant(租户):最高隔离单位,可以设置管理员、认证角色。
- Namespace(命名空间):租户下的逻辑分组,可以设置配额(存储上限、生产/消费速率)、鉴权策略、消息 TTL、跨地域复制开关。
1 | # 创建租户并指定管理员 |
一个 Pulsar 集群可以给公司内所有团队共用,彼此配额隔离、互不干扰——这是 Kafka 平铺 topic 难以原生做到的。
七、读写链路:一次写入怎么落盘
生产链路:
- Producer 把消息发给某个 Broker(按 topic/partition 路由)。
- Broker 作为 BookKeeper 的客户端,把消息以 Entry 形式并发写入多个 Bookie。
- 写入采用 Quorum 机制:配置
ensemble(参与节点数)、write quorum(写入副本数 Qw)、ack quorum(确认副本数 Qa)。通常Qw=3, Qa=2,只要 2 个 Bookie 写成功就算成功,兼顾性能与可靠。
消费链路:
- Consumer 向 Broker 发起订阅拉取(Pulsar 是服务端 push + 客户端流控,底层仍是拉)。
- Broker 从 BookKeeper 读消息返回,Consumer 处理完发送 ack。
- ack 由 Broker 记录到 Managed Ledger(每个 cursor 一个消费位置),不依赖消费者自己维护 offset。
正因为消费位置(cursor)存在 Broker 侧,Pulsar 的消费者重启不需要重新平衡全量分区,也不会像 Kafka rebalance 那样短暂停止消费——这也是它”无状态 Broker”带来的直接红利。
八、跨地域复制(Geo-replication)
Kafka 做跨机房要么靠 MirrorMaker(异步、易丢、运维重),要么靠自己搭。Pulsar 把跨地域复制作为命名空间级别的一行配置:
1 | pulsar-admin namespaces set-clusters team-a/app1 --clusters us-west,us-east,eu-central |
开启后,写入某地域的消息会自动异步复制到配置的其它地域集群,且是按订阅粒度复制(可以只复制部分 topic)。这对全球化部署和异地容灾非常友好。
九、Pulsar vs Kafka:结构性差异对照
| 维度 | Apache Pulsar | Apache Kafka |
|---|---|---|
| 架构 | 存储计算分离(Broker + BookKeeper) | 存储计算耦合(Broker 自带日志) |
| 扩容分区 | 只加 Broker/Bookie,不搬数据 | rebalance,要搬数据、再均衡抖动 |
| 消费语义 | 4 种订阅(含真正的队列 Shared) | 消费组(流语义为主) |
| 消息确认 | 单条 + 累积,原生重试/死信 | 累积 offset,需自建重试 |
| 多租户 | Tenant/Namespace 原生隔离 + 配额 | topic 平铺,靠外部治理 |
| 跨地域 | 命名空间级配置即开 | MirrorMaker 异步复制 |
| 消息保留 | 按时间/大小,与消费解耦 | 默认按保留策略,消费不影响 |
| 适用侧重 | 多租户、灵活订阅、云原生 | 超高性能日志流、生态成熟 |
两者并非替代关系:Kafka 在”极致吞吐的不可变日志流”上生态最成熟;Pulsar 在”需要多租户、灵活队列/流混合、平滑扩容”的场景更省心。选型看团队是否需要 Pulsar 那些原生能力,而不是盲目追新。
十、消息语义与 Exactly-Once
Pulsar 提供端到端语义保障:
- 去重(idempotence):Producer 开启
enableBatching+ 序列号,Broker 侧按 Producer 级别去重,重复发送不会重复落盘。 - 事务(Transaction):Pulsar 2.7+ 支持跨 topic 的原子写入,配合 Pulsar Functions 可以实现”消费 A、处理、写入 B”的 exactly-once。
- Pulsar IO / Functions:内置连接器(source/sink)和计算框架,很多 ETL 不用再外接 Flink/Spark。
1 | PulsarClient client = PulsarClient.builder() |
十一、生产避坑清单(10 条)
- 订阅类型选错:Shared 订阅下用累积 ack 会误确认未处理的消息,务必用单条 ack。
- backlog 无限堆积:务必配置消息 TTL 和 DLQ,否则未确认消息永远占着存储。
- Bookie 磁盘 IO 打满:Bookie 对磁盘延迟极敏感,用独立 SSD,别和 Broker 混部共享盘。
- Qw/Qa 配置失衡:
Qa太小(如 1)会丢数据;Qw太大写延迟高,常用Ensemble=3, Qw=3, Qa=2。 - 主题数爆炸:Pulsar 每个 topic/订阅都有元数据,海量小 topic 会压垮 ZooKeeper/元数据存储,注意聚合。
- 跨地域复制带宽:开了多地域复制要预留专线带宽,否则复制滞后拖慢本地写入确认。
- 消费者卡住不 ack:消费逻辑异常导致不回 ack,backlog 只增不减,监控 cursor 滞后是关键。
- 命名空间配额忘了设:多租户共用时,不限制速率的租户会挤占别人,必须配 dispatch rate。
- Producer 没开压缩:文本类消息开
CompressionType.LZ4/ZSTD,吞吐和存储双赢。 - 混用持久/非持久 topic:
non-persistent://不落 BookKeeper、丢电即丢消息,只适合可丢的 metrics 类流量。
结语
Pulsar 用”存储计算分离 + 四种订阅 + 原生多租户”三板斧,精准补上了 Kafka 在扩容体验、队列/流混合语义、企业级隔离上的结构性短板。如果你正在选型一个”给全公司多团队共用、要平滑扩容、要灵活消费模式”的消息底座,Pulsar 值得认真评估;如果只是要一个极致吞吐的日志管道,Kafka 依旧是稳妥答案。下一篇可以聊聊 Pulsar 的函数计算与 Schema Registry,把”消息 → 处理 → 落地”的链路再串起来。















