编程 Apache Pulsar 深度实战:当消息系统把存储与计算彻底拆开——从分层分片架构、BookKeeper 低延迟存储引擎到 Pulsar Functions 与分级存储的完整工程指南(2026)

2026-07-09 08:16:40 +0800 CST views 6

Apache Pulsar 深度实战:当消息系统把存储与计算彻底拆开——从分层分片架构、BookKeeper 低延迟存储引擎到 Pulsar Functions 与分级存储的完整工程指南(2026)

如果你用过 Kafka,一定经历过这样的深夜:为了加几个分区做扩容,整个集群 rebalance 几小时,消费lag 一路飙升;或者为了修一条慢消费者,不得不把几十 GB 的 Partition 整体迁移。问题的根源不是你不会用,而是 Kafka 把"计算"和"存储"焊死在了同一个进程里。Apache Pulsar 从设计第一天起就把这两件事拆开了——Broker 变成无状态的计算节点,BookKeeper 变成专门的低延迟存储层。本文从架构原理讲到生产实战,带你把 Pulsar 真正跑稳。

一、背景:消息中间件的"不可能三角"

在讲 Pulsar 之前,我们先把问题说清楚。一个生产级的消息/流平台,至少要同时满足三件事:

  1. 弹性扩容:流量翻倍时,加机器就能扛,而不是"加机器先来一场 rebalance 灾难"。
  2. 多租户隔离:一个集群要同时服务订单、日志、风控多个团队,彼此不能互相挤占。
  3. 无限留存 + 实时消费:既要能像 Kafka 一样实时流处理,又要能像数据库一样回看三个月前的任意一条消息。

传统方案在这三点之间反复妥协:

方案扩容代价多租户留存/回看
KafkaPartition 与存储绑定,扩分区=数据迁移,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(租户):最顶层隔离单元,比如 paymentssearchlogs 各占一个 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());

背后完整链路:

  1. Producer 把消息发给拥有该 topic 的 Owner Broker(通过服务发现拿到)。
  2. Broker 把消息交给 Managed Ledger(每个 topic 一个,负责管理 ledger 的生命周期)。
  3. Managed Ledger 把消息作为 Entry 写入 BookKeeper 的当前 Ledger,按 Qw/Qa 规则复制到多个 bookie。
  4. 达到 Qa 后,Broker 给 Producer 返回 MessageId(由 ledgerId + entryId 组成,全局唯一且有序)。
  5. 消息同时被异步追加到各 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; });

几个容易踩的坑:

  1. sendTimeout 设成 0 表示无限等待,生产环境务必设一个有限值,否则 bookie 抖动时业务线程会被拖死。
  2. blockIfQueueFull(false) 时,缓冲满了会抛 ProducerQueueIsFullError——订单场景宁可阻塞也不要丢,所以上面选了 true
  3. 消息超过 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 写入侧:批量 + 压缩 + 分块

参数建议值说明
batchingMaxPublishDelay1~10ms越大吞吐越高、延迟越高,按业务容忍度调
compressionTypeLZ4 / ZstdLZ4 速度接近无压缩,Zstd 压缩率更高
enableChunking单条 >2MB 必开大消息切片写,避免单条超限
maxPendingMessages1000~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 策略里设):

场景QwQa容忍度说明
订单/交易32允许 1 台挂默认,安全与性能平衡
日志/埋点21允许 1 台挂省带宽,丢一条日志可接受
强一致要求33允许 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。消费者在任一机房都能读到全量数据,非常适合"两地三中心"容灾。

注意三点:

  1. 复制是异步的,跨机房有秒级延迟,金融核心链路不要依赖它做强一致。
  2. 复制走的是 Pulsar 自己的 replicator topic,会在目标集群生成 orders 的影子订阅,不会和你的业务 subscription 冲突。
  3. 若要避免"回环复制",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 年的几个明显趋势:

  1. Pulsar 成为"AI 事件总线":Agent 之间的消息、工具调用日志、训练数据回流,天然需要多租户 + 强留存 + 回看,Pulsar 比 Kafka 更契合。
  2. 与湖仓打通:通过 Pulsar IO + 分级存储,消息流直接落地 Iceberg/Delta,实时与离线一条管道。
  3. Serverless 化:Pulsar Functions 配合 Knative/容器,走向"按消息量计费"的事件计算。
  4. 运维进一步自动化:基于 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 用户了。

推荐文章

npm速度过慢的解决办法
2024-11-19 10:10:39 +0800 CST
Linux查看系统配置常用命令
2024-11-17 18:20:42 +0800 CST
前端如何给页面添加水印
2024-11-19 07:12:56 +0800 CST
程序员茄子在线接单