Apache Pulsar 深度实战:当消息系统把存储与计算彻底拆开——从分层分片架构、BookKeeper 低延迟存储引擎到 Pulsar Functions 与分级存储的完整工程指南(2026)
如果你用过 Kafka,一定经历过这样的深夜:为了加几个分区做扩容,整个集群 rebalance 几小时,消费lag 一路飙升;或者为了修一条慢消费者,不得不把几十 GB 的 Partition 整体迁移。问题的根源不是你不会用,而是 Kafka 把"计算"和"存储"焊死在了同一个进程里。Apache Pulsar 从设计第一天起就把这两件事拆开了——Broker 变成无状态的计算节点,BookKeeper 变成专门的低延迟存储层。本文从架构原理讲到生产实战,带你把 Pulsar 真正跑稳。
一、背景:消息中间件的"不可能三角"
在讲 Pulsar 之前,我们先把问题说清楚。一个生产级的消息/流平台,至少要同时满足三件事:
- 弹性扩容:流量翻倍时,加机器就能扛,而不是"加机器先来一场 rebalance 灾难"。
- 多租户隔离:一个集群要同时服务订单、日志、风控多个团队,彼此不能互相挤占。
- 无限留存 + 实时消费:既要能像 Kafka 一样实时流处理,又要能像数据库一样回看三个月前的任意一条消息。
传统方案在这三点之间反复妥协:
| 方案 | 扩容代价 | 多租户 | 留存/回看 |
|---|---|---|---|
| Kafka | Partition 与存储绑定,扩分区=数据迁移,rebalance 风暴 | 弱(靠 ACL 勉强隔离) | 留存靠 retention,但回看历史要重放,成本高 |
| RabbitMQ | 队列级扩展,但吞吐天花板明显 | 靠 vhost 隔离 | 消息确认即删除,基本不留存 |
| RocketMQ | 接近 Kafka 的模型,CommitLog 顺序写 | 弱 | 接近 Kafka |
Pulsar 的解法是架构层面的分家:把"谁在处理消息"(Broker,无状态)和"消息存在哪"(BookKeeper,有状态存储)彻底分离。这一刀切下去,三个问题同时被解开——这正是它 2013 年在 Yahoo 内部诞生、2018 年成为 Apache 顶级项目的根本原因。
一句话记住 Pulsar:Broker 管"路",BookKeeper 管"仓库",ZooKeeper 管"户口本"。
二、核心概念:先搞懂 Pulsar 的"三级地址"
Pulsar 里每个 Topic 的完整名字都是三段式:
persistent://<tenant>/<namespace>/<topic>
│ │ │
│ │ └── 具体主题
│ └── 命名空间(策略/配额的最小单位)
└── 租户(公司里的一个大团队)
- Tenant(租户):最顶层隔离单元,比如
payments、search、logs各占一个 tenant。 - Namespace(命名空间):tenant 下的子空间,是所有运维策略(配额、留存、复制、鉴权)的作用边界。你不会对单个 topic 设配额,而是对 namespace 设。
- Topic:真正读写消息的地方。一个 topic 可以拆成多个 Partition 来水平扩展。
Pulsar 的 topic 分两种持久性:
persistent://:消息落盘到 BookKeeper,这是生产默认。non-persistent://:只走内存,Broker 宕机即丢,适合"丢了也无所谓"的监控指标类数据。
2.1 订阅模型:四种"吃法"
同一个 topic 可以被多个消费者以不同方式"吃"消息,这是 Pulsar 比 Kafka 消费者组灵活得多的地方:
Exclusive : 一个 subscription 只能有 1 个消费者,多连报错。等同 Kafka 单消费者。
Failover : 1 个主消费者 + N 个备,主挂了备顶上。适合"必须有序且不能断"的场景。
Shared : 消息轮询分发给 N 个消费者,一条消息只被一个人处理。适合"能并行"的任务队列。
Key_Shared : 按消息 key 哈希分发,相同 key 永远落同一个消费者,且保序。订单号维度并行处理神器。
// 四选一,只改 subscriptionType 即可
consumer = client.newConsumer()
.topic("persistent://payments/orders/create")
.subscriptionName("fraud-check")
.subscriptionType(SubscriptionType.Key_Shared) // ← 就是这一行
.subscribe();
2.2 游标(Cursor)与 Backlog
Kafka 用 offset 记录消费进度,Pulsar 用 Cursor(游标)。每个 subscription 在 BookKeeper 里都有自己的游标位置,互不影响。由此引出两个关键概念:
- Backlog:某个 subscription 还没消费的消息堆积量。它不占用"别人"的空间,因为消息本体存在 BookKeeper,游标只是个指针。
- Retention:即使所有消费者都 ack 了,消息默认还会按 namespace 的 retention 策略多留一段时间,供新 subscription 或 Pulsar SQL 回看。
三、架构深度剖析:存储与计算分离到底怎么落地的
这是全文最核心的一节。我们把它拆成"写入路径"和"读取路径"两条线来看。
3.1 三层组件
┌──────────┐ ┌──────────────────────┐ ┌─────────────────┐ ┌──────────┐
│ Producer │───▶│ Broker (无状态) │───▶│ BookKeeper │ │ ZooKeeper│
│ Consumer │◀───│ 只做路由/协议/限流 │◀───│ (有状态存储) │ │ 元数据 │
└──────────┘ └──────────────────────┘ └─────────────────┘ └──────────┘
- Broker:无状态。它不存消息,只负责接收 producer 的请求、把消息写给 BookKeeper、从 BookKeeper 读消息返给 consumer,以及维护各种限流、协议转换。因为无状态,挂了随时换一个顶上,topic 的所有权(ownership)可以瞬间漂移到别的 Broker。
- BookKeeper:有状态的低延迟日志存储。Pulsar 的每条消息都是一条 Entry,写进 BookKeeper 的 Ledger(账本) 里。
- ZooKeeper / 元数据服务:存 tenant/namespace/topic 的"户口本"——谁拥有哪个 topic、有哪些 subscription、cursor 在哪。Pulsar 3.x 起也支持用 Etcd 风格的元数据服务,但 ZooKeeper 仍是主流。
3.2 BookKeeper 的四个核心概念(必须背下来)
Ledger(账本) : 一段只追加(append-only)的日志,写满或关闭后不可变。
Entry(条目) : Ledger 里的一条记录,对应 Pulsar 的一条消息(或一批)。
Fragment(片段) : Ledger 内部按"写入集合"划分的片段,每个片段写到一组 bookie 上。
Ensemble(集合) : 当前片段写入的 bookie 机器列表。
写入时的两个关键参数:
- Write Quorum(Qw):一条消息要写到几个 bookie 上。比如 Qw=3 表示写 3 份。
- Ack Quorum(Qa):只要几个 bookie 确认就算写成功。比如 Qa=2 表示 3 份里写成功 2 份即可返回。
教科书配置是 Qw=3, Qa=2:既能容忍 1 台 bookie 挂掉不影响写入,又能保证数据有 3 副本。
Producer ──▶ Broker
│ 选出 Ensemble = [B1, B2, B3]
├─▶ 写 B1 (Qw=3)
├─▶ 写 B2 (Qa=2 达成 → 立即返回 Producer 成功✔)
└─▶ 写 B3 (异步补齐第三副本)
注意一个巧妙点:Broker 只要拿到 Qa 个 ack 就返回成功,第三副本可以异步补齐。这正是 BookKeeper 能做到毫秒级写入延迟的秘诀——它不要求"全部落盘才返回"。
3.3 写入路径全貌
// 一个典型的同步发送,背后发生的事远比你看到的多
MessageId id = producer.send("order-1001-created".getBytes());
背后完整链路:
- Producer 把消息发给拥有该 topic 的 Owner Broker(通过服务发现拿到)。
- Broker 把消息交给 Managed Ledger(每个 topic 一个,负责管理 ledger 的生命周期)。
- Managed Ledger 把消息作为 Entry 写入 BookKeeper 的当前 Ledger,按 Qw/Qa 规则复制到多个 bookie。
- 达到 Qa 后,Broker 给 Producer 返回
MessageId(由 ledgerId + entryId 组成,全局唯一且有序)。 - 消息同时被异步追加到各 subscription 的 Cursor 待消费队列。
3.4 读取路径与"无状态 Broker 怎么读历史"
消费者来拉消息时,Broker 自己不存数据,于是它去 BookKeeper 按 MessageId 顺序读 Entry,再返回给消费者。由于 BookKeeper 是 append-only 日志且按 ledger 有序,读历史 = 从某个 ledger 的某个 entry 往后顺序扫,非常高效。
两个对性能影响巨大的设计:
- Read Cache / PageCache:Broker 和 bookie 都会缓存热点 segment,热数据基本走内存。
- Managed Ledger 的"游标缓存":活跃的 subscription 游标会被 Broker 常驻内存,避免每次拉取都查 ZooKeeper。
3.5 为什么"分离"等于"弹性"
Kafka 扩容要迁移 Partition(数据在 broker 本地),Pulsar 扩容只是把 topic 的所有权换一个 Broker:
扩容前:Broker1 拥有 topic-A 的 4 个 bundle
扩容后:Broker1、Broker2 各分担 2 个 bundle(bundle 是 namespace 哈希环上的一段)
bundle 是 namespace 在哈希环上切分出的"片",默认 16 个。Broker 之间会通过 LoadShedding(负载卸载) 自动把 bundle 从忙的节点漂到闲的节点,没有任何数据拷贝——因为数据都在 BookKeeper 里。这就是存储计算分离带来的"加机器即扩容"。
四、代码实战:从 0 到生产级
理论讲完,直接上代码。本节所有例子都能在本地 Docker 起的 standalone 上跑通。
4.1 三行命令起一个 Pulsar
docker run -it -p 6650:6650 -p 8080:8080 \
apachepulsar/pulsar:3.3.0 \
bin/pulsar standalone
6650是二进制协议端口(客户端连这个)8080是 HTTP 管理端口(pulsar-admin和 REST API)
验证一下:
# 列出所有 tenant
docker exec <container> bin/pulsar-admin tenants list
# 默认会输出 [ "public", "pulsar" ]
4.2 Java Producer:同步、异步、批量、压缩一站打尽
Maven 依赖:
<dependency>
<groupId>org.apache.pulsar</groupId>
<artifactId>pulsar-client</artifactId>
<version>3.3.0</version>
</dependency>
最朴素的同步发送:
import org.apache.pulsar.client.api.*;
public class BasicProducer {
public static void main(String[] args) throws Exception {
PulsarClient client = PulsarClient.builder()
.serviceUrl("pulsar://localhost:6650")
.build();
Producer<byte[]> producer = client.newProducer()
.topic("persistent://public/default/orders")
.create();
MessageId id = producer.send("order-1001-created".getBytes());
System.out.println("sent, msgId=" + id); // 形如 ledgerId:entryId
producer.close();
client.close();
}
}
但生产环境你绝对不会用上面这种"发一条等一条"的同步写法。下面是经过压测调优的生产级 Producer:
Producer<byte[]> producer = client.newProducer()
.topic("persistent://public/default/orders")
// —— 批量:攒一批再发,吞吐的关键 ——
.enableBatching(true)
.batchingMaxPublishDelay(10, TimeUnit.MILLISECONDS) // 最多等 10ms
.batchingMaxMessages(1000) // 或攒够 1000 条
// —— 压缩:带宽换 CPU,LZ4 几乎零损耗 ——
.compressionType(CompressionType.LZ4)
// —— 发送超时与积压队列 ——
.sendTimeout(10, TimeUnit.SECONDS)
.maxPendingMessages(5000) // 异步发送时本地缓冲上限
.blockIfQueueFull(true) // 缓冲满了就阻塞,而不是丢
// —— 大消息分块:单条超过 2MB 必须开 ——
.enableChunking(true)
.create();
// 异步发送(高吞吐标配),用 CompletableFuture 不阻塞业务线程
CompletableFuture<MessageId> future = producer.sendAsync(payload);
future.thenAccept(msgId -> metrics.record(msgId))
.exceptionally(ex -> { alert(ex); return null; });
几个容易踩的坑:
sendTimeout设成 0 表示无限等待,生产环境务必设一个有限值,否则 bookie 抖动时业务线程会被拖死。blockIfQueueFull(false)时,缓冲满了会抛ProducerQueueIsFullError——订单场景宁可阻塞也不要丢,所以上面选了true。- 消息超过
maxMessageSize(默认 5MB,含批量)会报错。超过 2MB 的单条消息必须.enableChunking(true),Pulsar 会把它切成多块顺序写入。
4.3 结构化消息:用 Schema 替你管序列化
裸 byte[] 早晚出事。Pulsar 内置 Schema 体系,能自动做 JSON/AVRO 序列化并在 Broker 端做兼容性校验:
public class Order {
public String orderId;
public long userId;
public double amount;
public long createTime;
}
Producer<Order> producer = client.newProducer(Schema.JSON(Order.class))
.topic("persistent://public/default/orders-struct")
.create();
producer.send(new Order("O-1", 1001L, 99.9, System.currentTimeMillis()));
消费者侧用同样的 Schema 反序列化,类型安全:
Consumer<Order> consumer = client.newConsumer(Schema.JSON(Order.class))
.topic("persistent://public/default/orders-struct")
.subscriptionName("risk-sub")
.subscriptionType(SubscriptionType.Key_Shared)
.keySharedPolicy(KeySharedPolicy.autoSplitHashRange()) // 按 key 哈希分片
.subscribe();
while (true) {
Message<Order> msg = consumer.receive();
try {
Order o = msg.getValue();
riskEngine.check(o);
consumer.acknowledge(msg); // 处理成功才 ack
} catch (Exception e) {
consumer.negativeAcknowledge(msg); // 失败:退回重投,不 blocking 其他消息
}
}
negativeAcknowledge 是 Pulsar 比 Kafka seek 优雅的地方:它把这条消息标记为重投,而不影响整个 partition 的 offset。
4.4 批量消费与重试队列
单条 receive() 太慢,生产用 batchReceive:
Consumer<byte[]> consumer = client.newConsumer()
.topic("persistent://public/default/orders")
.subscriptionName("batch-sub")
.batchReceivePolicy(BatchReceivePolicy.builder()
.maxNumMessages(100) // 一次最多拿 100 条
.maxNumBytes(1024 * 1024) // 或累计 1MB
.timeout(100, TimeUnit.MILLISECONDS) // 或等 100ms
.build())
.subscribe();
while (true) {
Messages<byte[]> msgs = consumer.batchReceive();
for (Message<byte[]> m : msgs) { /* 批量落库 */ }
consumer.acknowledge(msgs); // 批量 ack
}
带死信队列的"自动重试"订阅:
Consumer<byte[]> consumer = client.newConsumer()
.topic("persistent://public/default/orders")
.subscriptionName("dlq-sub")
.deadLetterPolicy(DeadLetterPolicy.builder()
.maxRedeliverCount(3) // 最多重试 3 次
.deadLetterTopic("persistent://public/default/orders-dlq") // 进死信
.build())
.subscribe();
重试 3 次仍失败的消息自动进 orders-dlq,主流程不再被脏数据拖死——这是构建健壮消费系统的标准姿势。
4.5 Go 客户端:云原生时代的标配
Pulsar 官方 Go client 接口与 Java 高度一致:
package main
import (
"context"
"fmt"
"github.com/apache/pulsar-client-go/pulsar"
)
func main() {
client, err := pulsar.NewClient(pulsar.ClientOptions{
URL: "pulsar://localhost:6650",
OperationTimeout: 30 * 1e9, // 30s
MessageChan: make(chan pulsar.ConsumerMessage, 100),
})
if err != nil { panic(err) }
producer, err := client.CreateProducer(pulsar.ProducerOptions{
Topic: "persistent://public/default/orders",
})
if err != nil { panic(err) }
// 带 key 的发送,配合 Key_Shared 可实现按订单号保序
_, err = producer.Send(context.Background(), &pulsar.ProducerMessage{
Payload: []byte("order-created"),
Key: "order-1001",
})
fmt.Println("sent:", err)
}
Go 侧消费与并发处理:
consumer, _ := client.Subscribe(pulsar.ConsumerOptions{
Topic: "persistent://public/default/orders",
SubscriptionName: "go-worker",
Type: pulsar.Shared, // pulsar.Exclusive / Failover / KeyShared
})
for consumer.IsConnected() {
msg, err := consumer.Receive(context.Background())
if err != nil { continue }
go func(m pulsar.Message) {
// 并发处理
consumer.Ack(m)
}(msg)
}
4.6 Pulsar Functions:把"流处理"塞进消息系统
很多团队为了"消费→转换→再投"专门搭一套 Flink/Spark Streaming,杀鸡用牛刀。Pulsar Functions 让你直接在 Broker 边写轻量流处理逻辑,不用起额外集群。
Python 版(极简,适合数据清洗):
# uppercase_function.py
def process(input, context):
# input 是上游 topic 的消息体(bytes/str)
return input.upper()
Java 版(带状态,适合计数/聚合):
import org.apache.pulsar.functions.api.Context;
import org.apache.pulsar.functions.api.Function;
public class WordCount implements Function<String, Void> {
@Override
public Void process(String input, Context context) {
for (String word : input.split("\\s+")) {
// 用 Pulsar 内置的 KeyValue 状态存储做累加
context.incrCounter(word, 1);
}
return null;
}
}
提交运行(一次命令,无需额外基础设施):
bin/pulsar-admin functions create \
--py uppercase_function.py \
--classname uppercase_function \
--tenant public --namespace default \
--name upper-fn \
--inputs persistent://public/default/raw \
--output persistent://public/default/upper
Pulsar Functions 支持 at-least-once 和 effectively-once(基于 BookKeeper 的 checkpoint),对大部分 ETL、告警、富化场景完全够用。
4.7 Pulsar IO:把数据库"接"进来
配套的 Pulsar IO(Connector)让你用一条命令把 MySQL、PostgreSQL、Redis、S3、Elasticsearch 等接成 source/sink:
# 把 MySQL 的 orders 表变更流,实时同步进 Pulsar topic
bin/pulsar-admin sources create \
--archive /path/to/pulsar-io-jdbc-3.3.0.nar \
--name mysql-source \
--tenant public --namespace default \
--destination-topic-name mysql-orders
# 把 topic 数据写进 Elasticsearch 做检索
bin/pulsar-admin sinks create \
--archive /path/to/pulsar-io-elastic-3.3.0.nar \
--name es-sink \
--tenant public --namespace default \
--inputs mysql-orders \
--sink-config '{"elasticSearchUrl":"http://localhost:9200","indexName":"orders"}'
五、性能优化:把吞吐和延迟都榨出来
Pulsar 出厂配置偏"稳"不偏"快"。要上生产,下面这些旋钮必须动。
5.1 写入侧:批量 + 压缩 + 分块
| 参数 | 建议值 | 说明 |
|---|---|---|
batchingMaxPublishDelay | 1~10ms | 越大吞吐越高、延迟越高,按业务容忍度调 |
compressionType | LZ4 / Zstd | LZ4 速度接近无压缩,Zstd 压缩率更高 |
enableChunking | 单条 >2MB 必开 | 大消息切片写,避免单条超限 |
maxPendingMessages | 1000~5000 | 异步发送本地缓冲,太小会频繁 block |
经验值:开启 LZ4 批量后,典型 JSON 业务消息吞吐可从 5w/s 提到 20w+/s,带宽下降 60%~70%。
5.2 Broker 侧:bundle 与限流
# namespace 默认 16 个 bundle,热点 topic 可以提高到 64/128
bin/pulsar-admin namespaces set-bundle-split-boundary \
public/default --boundaries 64
# 给 subscription 限流,防止一个慢消费者拖垮整个 topic
bin/pulsar-admin namespaces set-subscription-dispatch-rate \
public/default --msg-dispatch-rate 100000 --byte-dispatch-rate 10485760
# Key_Shared 下,按 key 哈希分发;热点 key 可通过 hash 范围分裂缓解
bin/pulsar-admin topics unload persistent://public/default/orders # 触发重新负载均衡
Backlog 配额也很关键——防止某个消费者挂掉后 backlog 无限增长把 BookKeeper 撑爆:
bin/pulsar-admin namespaces set-backlog-quota public/default \
--limitSize 10G --limitTime 7d --policy producer_request_hold
producer_request_hold 表示 backlog 超限后阻塞生产者而不是丢弃,适合"宁可慢不能丢"的订单场景;日志类场景可用 producer_exception 直接抛异常。
5.3 BookKeeper 侧:磁盘分离是命门
BookKeeper 的瓶颈几乎永远在磁盘 IO。生产部署铁律:journal 盘和 ledger 盘物理分离。
journal 盘 : 放 WAL(写前日志),必须 NVMe SSD,要的是低延迟 fsync
ledger 盘 : 放数据副本,可以用稍便宜的高吞吐 SSD,要的是顺序写带宽
bookkeeper.conf 关键配置:
# journal 用独立的 NVMe 盘
journalDirectories=/nvme1/bookkeeper/journal
# ledger 数据盘(可多块)
ledgerDirectories=/data1/bookkeeper/ledger,/data2/bookkeeper/ledger
# 写 journal 时强制 fsync 的条数/bytes,越小越安全越慢
journalSyncData=true
journalFlushWhenQueueEmpty=true
# 读路径的 PageCache 大小
dbStorage_writeCacheMaxSizeMb=1024
dbStorage_readAheadCacheMaxSizeMb=1024
Qw/Qa 的权衡表(在 broker.conf 或 namespace 策略里设):
| 场景 | Qw | Qa | 容忍度 | 说明 |
|---|---|---|---|---|
| 订单/交易 | 3 | 2 | 允许 1 台挂 | 默认,安全与性能平衡 |
| 日志/埋点 | 2 | 1 | 允许 1 台挂 | 省带宽,丢一条日志可接受 |
| 强一致要求 | 3 | 3 | 允许 0 台挂 | 最慢但最稳 |
5.4 分级存储(Tiered Storage):冷热分离省大钱
订单要回看半年,但近 7 天才是热数据——全放 BookKeeper 的 SSD 太贵。Pulsar 的分级存储把老 ledger 自动 offload 到 S3 / 对象存储,Broker 读历史时透明地从对象存储拉取。
broker.conf 开启 S3 offload:
managedLedgerOffloadDriver=S3
s3ManagedLedgerOffloadBucket=pulsar-cold-storage
s3ManagedLedgerOffloadRegion=ap-east-1
s3ManagedLedgerOffloadServiceEndpoint=s3.ap-east-1.amazonaws.com
然后按 namespace 设定"多久/多大的数据搬到冷存储":
# 超过 7 天、或大于 100GB 的 ledger 自动 offload 到 S3
bin/pulsar-admin namespaces set-offload-policies public/default \
--offload-after-hours 168 \
--offload-after-size 107374182400
# 也能手动触发某个 topic 的 offload
bin/pulsar-admin topics offload persistent://public/default/orders 100
分级存储之后,BookKeeper 只保留热数据,存储成本可下降一个数量级,而消费者回看历史对业务代码完全透明(还是同一个 consumer.receive())。
六、跨地域复制:多机房容灾
Pulsar 的 Geo-Replication 是 namespace 级、全互联(full-mesh) 的异步复制,配置极简:
# 假设有两个集群 cluster-beijing、cluster-shanghai
bin/pulsar-admin namespaces set-clusters public/default \
--clusters cluster-beijing,cluster-shanghai
设完之后,写到北京集群 public/default/orders 的消息,会自动异步复制到上海集群的同名 topic。消费者在任一机房都能读到全量数据,非常适合"两地三中心"容灾。
注意三点:
- 复制是异步的,跨机房有秒级延迟,金融核心链路不要依赖它做强一致。
- 复制走的是 Pulsar 自己的 replicator topic,会在目标集群生成
orders的影子订阅,不会和你的业务 subscription 冲突。 - 若要避免"回环复制",Pulsar 内置去重,自动识别并丢弃从其他集群同步过来的消息。
七、生产部署与运维:上 Kubernetes
单机 standalone 只能体验,生产请用 Pulsar Operator / Helm Chart,核心组件各自独立扩缩容:
# 伪代码:values.yaml 核心思路
components:
broker: { replicaCount: 3, resources: { requests: { cpu: "4", memory: "8Gi" } } }
bookie: { replicaCount: 3, storage: { journal: nvme, ledger: ssd } }
zookeeper: { replicaCount: 3 }
autorecovery: { enabled: true } # bookie 挂了自动补副本
必须监控的指标(接 Prometheus + Grafana):
# 延迟类
pulsar_broker_publish_latency # 写入端到端延迟,P99 > 50ms 要告警
pulsar_broker_end_to_end_latency # 生产到消费的完整延迟
# 堆积类
pulsar_subscription_backlog # 任一 subscription backlog 持续增长 = 消费跟不上
pulsar_storage_size # BookKeeper 用量,配合分级存储看增长斜率
# 健康类
pulsar_broker_load # bundle 分布是否倾斜
bookie_journal_sync_latency # journal 盘 fsync 延迟,NVMe 应 < 1ms
一套最小可用告警:backlog 连续 5 分钟上涨且 > 100w、P99 端到端延迟 > 200ms、任一 bookie journal 延迟 > 5ms。
八、Pulsar vs Kafka:怎么选不踩坑
别信"谁取代谁"的营销话术,看场景:
选 Pulsar 当:
· 需要多租户强隔离(一个集群服务全公司)
· 扩容频繁、受不了 rebalance(流量波动大的业务)
· 既要实时流、又要长期留存回看(事件溯源、审计)
· 想要 Functions/IO 省掉额外流处理集群
· 跨机房容灾开箱即用
选 Kafka 当:
· 生态已经深度绑定(Connector、ksqlDB、Flink 集成成熟)
· 纯吞吐、单租户、团队能接受分区再均衡
· 社区资料/简历人才更多,招聘更省心
性能上,二者单分区吞吐在 NVMe 上都能到 100w+/s 量级,差异主要在运维模型而非裸性能。Pulsar 用复杂度换弹性,Kafka 用简单换掌控力。
九、总结与展望(2026 及以后)
回看开头那个"不可能三角",Pulsar 用存储计算分离这一刀切出了新空间:
- Broker 无状态 → 扩容零数据迁移、负载自动均衡;
- BookKeeper 低延迟日志存储 → Qw/Qa 可配的强一致、毫秒级写入;
- 分级存储 → 热数据走 SSD、冷数据落对象存储,成本与体验兼得;
- Functions / IO / Geo-Replication → 把流处理、连接器、容灾都收编进消息平台本身。
2026 年的几个明显趋势:
- Pulsar 成为"AI 事件总线":Agent 之间的消息、工具调用日志、训练数据回流,天然需要多租户 + 强留存 + 回看,Pulsar 比 Kafka 更契合。
- 与湖仓打通:通过 Pulsar IO + 分级存储,消息流直接落地 Iceberg/Delta,实时与离线一条管道。
- Serverless 化:Pulsar Functions 配合 Knative/容器,走向"按消息量计费"的事件计算。
- 运维进一步自动化:基于 Etcd 的元数据服务、Operator 自动扩缩容、AI 辅助的 backlog 预测调度正在成熟。
如果你正被 Kafka 的 rebalance 折磨,或者被"多团队共用一个消息集群"搞得焦头烂额,Pulsar 值得你花一个周末认真试一次——它的学习曲线前陡后平,一旦跑顺,那种"加机器即扩容、丢消息几乎不可能"的踏实感,是很多团队再也回不去的理由。
动手清单:①
docker run起 standalone;② 跑通 4.2 的 Java Producer;③ 用 Key_Shared + 死信队列写一个健壮消费者;④ 配一次分级存储 offload;⑤ 用 Helm 在测试 K8s 起 3 Broker / 3 Bookie。五步走完,你已经超过 80% 的"只会docker-compose up"的 Pulsar 用户了。