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

本文是《Kafka 核心原理与实战》的”对照篇”:不重讲 分区、副本 ISR、消费组、EOS 这些 Kafka 篇已经铺开的内容。这里只聊 Pulsar 独有、且恰恰是它相对 Kafka structural 差异所在的那些点——如果你还没读 Kafka 篇,建议先补一下”消息队列凭什么扛住高吞吐”。

一、为什么又来一个消息系统

Kafka 已经把”高吞吐分布式消息”做到了极致,但凡用过的人都知道它有三个结构性痛点,不是调参能解决的:

  1. 存储与计算耦合。一个 Kafka 分区就是一个文件 + 一组副本,扩容分区意味着数据要 rebalance、消费组要再均衡(rebalance storm),大集群扩一次分区能抖几分钟。
  2. 队列与流语义割裂。Kafka 本质是”流”(独占消费、offset 由消费者维护),要做”队列”(多个 worker 竞争消费同一条消息)只能靠起多个消费组各自存 offset,资源浪费且语义别扭。
  3. 多租户弱。Kafka 的 topic 之间是平铺的,没有原生的租户/命名空间隔离、配额、鉴权分层,一个热点 topic 能拖垮整个集群。

Apache Pulsar 是 Yahoo! 在 2013 年为解决上述内部消息痛点而生的,2018 年成为 Apache 顶级项目。它的核心设计哲学就是一句话:把”计算”和”存储”彻底拆开。

二、架构总览:存储计算分离

Pulsar 的架构分两层,这是它和 Kafka 最本质的区别:

层 组件 是否有状态 职责
计算层 Broker 无状态 接收生产请求、做协议解析、投递消息给消费者、维护元数据缓存
存储层 Apache BookKeeper 有状态 真正持久化消息,由一组 Bookie 节点组成,提供低延迟追加写
1
2
3
4
5
6
7
8
9
10
11
12
13
    Producer                         Consumer
│ publish ▲ subscribe
▼ │
┌──────────────┐ ┌──────────────┐
│ Pulsar │ 无状态,可随意 │ Pulsar │
│ Broker │◀──水平扩缩容──▶│ Broker │
└──────┬───────┘ └──────────────┘
│ 写入/读取 (BookKeeper 协议)
▼
┌──────────────┐
│ BookKeeper │ 有状态,只管存
│ Bookie 集群 │ (Ledger / Entry)
└──────────────┘

为什么这个拆分是杀手锏? 因为 Broker 无状态,扩容计算层 = 起一个新进程,立刻分担流量,完全不搬数据;存储层扩容 = 加 Bookie,新写入自动落到新节点。对比 Kafka 扩分区要 rebalance 整个 topic 的数据,Pulsar 的扩容是”无感”的。

三、Topic → Partition → Ledger → Bookie

Pulsar 的存储粒度比 Kafka 更细:

  • 一个 Topic 可以分成多个 Partition(和 Kafka 分区概念类似,解决并行度)。
  • 每个 Partition 的消息,按段(Segment)写入 BookKeeper,每一段就是一个 Ledger。
  • Ledger 是追加写、只封口(seal)不修改的不可变日志,分散存储在多个 Bookie 上(默认 3 副本)。
1
2
3
4
Partition-0
├── Ledger-1 (sealed) → {Bookie-A, Bookie-B, Bookie-C}
├── Ledger-2 (sealed) → {Bookie-B, Bookie-C, Bookie-D}
└── Ledger-3 (open) → {Bookie-C, Bookie-D, Bookie-E}

关键差异: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
2
3
4
5
Consumer<byte[]> consumer = pulsarClient.newConsumer(Schema.BYTES)
.topic("persistent://my-tenant/my-ns/orders")
.subscriptionName("order-workers")
.subscriptionType(SubscriptionType.Shared) // 四种之一
.subscribe();

五、消息确认机制:单条 ack 与累积 ack

Kafka 的”确认”是 commit offset(累积的,跳过的消息就再也回不去了)。Pulsar 的 ack 灵活得多:

  • 累积 ack(cumulative):和 Kafka 一样,确认某条即确认它之前的所有消息。Exclusive/Failover 订阅用这个。
  • 单条 ack(individual):精确确认某一条消息。Shared/Key_Shared 订阅必须用它,否则会把没处理完的消息一起确认掉。
1
2
3
4
5
// 累积确认(Exclusive/Failover)
consumer.acknowledge(msg);

// 单条确认(Shared/Key_Shared 必需)
consumer.acknowledge(msg.getMessageId());

Pulsar 还内建了 重试队列 和 死信队列(DLQ),不用自己造轮子:

1
2
3
4
5
6
7
8
9
10
Consumer<byte[]> consumer = pulsarClient.newConsumer(Schema.BYTES)
.topic("orders")
.subscriptionName("order-workers")
.subscriptionType(SubscriptionType.Shared)
.enableRetry(true) // 开启重试主题
.deadLetterPolicy(DeadLetterPolicy.builder()
.maxRedeliverCount(3) // 重试 3 次
.deadLetterTopic("orders-dlq") // 进死信主题
.build())
.subscribe();
  • 消息 TTL:未被确认的消息超过 TTL 自动进 DLQ 或丢弃,防止 backlog 无限堆积。
  • backlog(积压):Pulsar 原生暴露每个订阅的未确认消息数,监控告警比 Kafka 的 lag 更直白。

六、多租户:Tenant / Namespace 两级隔离

这是 Pulsar 在”企业级”上碾压 Kafka 的设计。Pulsar 的 topic 全名是:

1
persistent://<tenant>/<namespace>/<topic>
  • Tenant(租户):最高隔离单位,可以设置管理员、认证角色。
  • Namespace(命名空间):租户下的逻辑分组,可以设置配额(存储上限、生产/消费速率)、鉴权策略、消息 TTL、跨地域复制开关。
1
2
3
4
5
6
7
8
# 创建租户并指定管理员
pulsar-admin tenants create team-a --admin-roles team-a-admin

# 创建命名空间并限流
pulsar-admin namespaces create team-a/app1
pulsar-admin namespaces set-namespace-message-ttl team-a/app1 --messageTTL 7d
pulsar-admin namespaces set-dispatch-rate team-a/app1 \
--msg-dispatch-rate 1000 --byte-dispatch-rate 1048576

一个 Pulsar 集群可以给公司内所有团队共用,彼此配额隔离、互不干扰——这是 Kafka 平铺 topic 难以原生做到的。

七、读写链路:一次写入怎么落盘

生产链路:

  1. Producer 把消息发给某个 Broker(按 topic/partition 路由)。
  2. Broker 作为 BookKeeper 的客户端,把消息以 Entry 形式并发写入多个 Bookie。
  3. 写入采用 Quorum 机制:配置 ensemble(参与节点数)、write quorum(写入副本数 Qw)、ack quorum(确认副本数 Qa)。通常 Qw=3, Qa=2,只要 2 个 Bookie 写成功就算成功,兼顾性能与可靠。

消费链路:

  1. Consumer 向 Broker 发起订阅拉取(Pulsar 是服务端 push + 客户端流控,底层仍是拉)。
  2. Broker 从 BookKeeper 读消息返回,Consumer 处理完发送 ack。
  3. 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
2
3
4
PulsarClient client = PulsarClient.builder()
.serviceUrl("pulsar://localhost:6650")
.enableTransaction(true) // 开启事务
.build();

十一、生产避坑清单(10 条)

  1. 订阅类型选错:Shared 订阅下用累积 ack 会误确认未处理的消息,务必用单条 ack。
  2. backlog 无限堆积:务必配置消息 TTL 和 DLQ,否则未确认消息永远占着存储。
  3. Bookie 磁盘 IO 打满:Bookie 对磁盘延迟极敏感,用独立 SSD,别和 Broker 混部共享盘。
  4. Qw/Qa 配置失衡:Qa 太小(如 1)会丢数据;Qw 太大写延迟高,常用 Ensemble=3, Qw=3, Qa=2。
  5. 主题数爆炸:Pulsar 每个 topic/订阅都有元数据,海量小 topic 会压垮 ZooKeeper/元数据存储,注意聚合。
  6. 跨地域复制带宽:开了多地域复制要预留专线带宽,否则复制滞后拖慢本地写入确认。
  7. 消费者卡住不 ack:消费逻辑异常导致不回 ack,backlog 只增不减,监控 cursor 滞后是关键。
  8. 命名空间配额忘了设:多租户共用时,不限制速率的租户会挤占别人,必须配 dispatch rate。
  9. Producer 没开压缩:文本类消息开 CompressionType.LZ4/ZSTD,吞吐和存储双赢。
  10. 混用持久/非持久 topic:non-persistent:// 不落 BookKeeper、丢电即丢消息,只适合可丢的 metrics 类流量。

结语

Pulsar 用”存储计算分离 + 四种订阅 + 原生多租户”三板斧,精准补上了 Kafka 在扩容体验、队列/流混合语义、企业级隔离上的结构性短板。如果你正在选型一个”给全公司多团队共用、要平滑扩容、要灵活消费模式”的消息底座,Pulsar 值得认真评估;如果只是要一个极致吞吐的日志管道,Kafka 依旧是稳妥答案。下一篇可以聊聊 Pulsar 的函数计算与 Schema Registry,把”消息 → 处理 → 落地”的链路再串起来。