ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Pulsar 实战笔记:存算分离架构、选型要点与生产踩坑指南

Pulsar 实战笔记:存算分离架构、选型要点与生产踩坑指南

上周开会的时候,团队里还在纠结要不要去参加月底那场开源大会。结果票还没抢完,日程表里 Pulsar Developer Day 那栏已经被人圈了个红框。作为从 Pulsar 2.x 一直跟到 3.x 的深度用户,我太懂这种兴奋从哪来了——消息中间件这个领域,这几年技术演进是真快,而 Pulsar 恰好站在了“存算分离”和“云原生”这两股浪潮的交叉点上。离大会开场只剩三天,与其干等,不如把这段时间反复琢磨的架构思路、选型教训和落地实操整理成一份笔记,给同样准备在生产环境里折腾消息系统的朋友做个参照。

这个开发者日的主题是“消息中间件创新实践”,我理解下来核心就解决三件事:第一,为什么越来越多团队把消息队列从“插件式集群”迁移到“存算分离架构”;第二,在真实业务里用 Pulsar 时,哪些参数值得花钱花精力去调;第三,生产环境踩过的坑,如何在架构层面提前规避。无论你是刚接手公司消息系统的中级开发,还是已经在多集群环境下维护 Kafka 的老手,这份笔记应该都能给你一些启发。

1. 为什么消息队列集体转向“存算分离”

1.1 传统消息队列的痛点:从“小作坊”到“云原生”的落差

先说一个很多团队心照不宣的现状:公司里的 Kafka 集群从 3 台 broker 涨到 30 台 broker,表面看是业务增长,实际上是运维成本的失控。为什么这样说?因为传统消息队列的存储和计算是绑在一起的,一台 broker 既要做内存索引、又要落盘、还要跟 follower 做副本同步。数据量一旦上来,每次扩缩容都要反复评估“这台机器能不能同时扛住存储和流量”。我见过某团队峰值流量翻倍时,凌晨加班给 broker 加磁盘,结果因为“磁盘均衡”没做好,部分节点磁盘写满,整个消费链路直接雪崩。

没有 standalone 服务,所有分区数据都跟着 broker 绑定,还带来另一个问题:分区迁移极其痛苦。某次我们要把一批 topic 从老机房迁到新集群,计划要做一整天,期间还要保证消费不中断。做过类似迁移的朋友应该懂,那种“牵一发动全身”的紧绷感,很难受。这也是为什么 Kafka 社区这些年一直试图用 KRaft 和分片元数据来缓解,但存储与计算耦合的模型天生就限制了它的灵活性。

1.2 Pulsar 的解题思路:Broker 与 BookKeeper 的“貌离神合”

Pulsar 的做法是彻底拆开:Broker 无状态,负责收发消息和路由;BookKeeper 集群负责持久化存储,每个 segment 分布在多台 bookie 上,副本数可控,故障自动切换。这相当于把消息系统的“前台”和“后台”分成了两拨人马,Broker 可以随意扩展,Bookie 可以独立扩容。

这个架构在实际运维里有多香?举个我自己的案例。我们有一个线上集群,高峰期写入速率是 10 万条/秒,topic 数将近 1500。以前用传统队列,光想想磁盘 IO 和文件句柄就头大。换成 Pulsar 之后,我们只需要监控两条曲线:broker 侧的 CPU/内存,以及 bookie 侧的磁盘吞吐。哪个瓶颈先出现,就定向扩容哪一边,完全不用动另一个组件。这种感觉,就像以前开一辆混动车,发动机和电机绑在一起坏哪个都得拖车;现在变成两台独立的车,谁累了就换谁。

而且,Pulsar 的分层存储功能让我印象极深。老数据可以自动从 BookKeeper 落到对象存储里,这个动作对消费者完全透明。我一度担心“回放很久以前的数据”会很慢,实测下来只要对象存储带宽够,冷热数据切换的延迟完全在可接受范围内。这一点,对想省钱又想保底数据可回溯的团队来说,价值极其明显。

2. 选型关键考量:什么时候应该选 Pulsar

2.1 场景决定技术:用大炮打蚊子,还是杀鸡用牛刀

我一向反对“哪家技术火就无脑上哪家”的做法。Pulsar 很强,但不代表所有业务场景都得迁移。这里需要先做一道选择题:你的需求更像是“消息队列”还是“事件流平台”?

如果你只是要在微服务之间做异步解耦,topic 总量不超过 100,流量峰值也基本可预测,那传统的 RabbitMQ 或者 Kafka 完全够用。维护成本也很低,团队成员都熟。但如果你面临的是以下四种情况,Pulsar 的优势就会逐渐放大:

  • 同一个集群要服务多个团队,要求隔离性和配额管理都非常严格。
  • 业务里有多活、容灾、跨地域复制的需求。
  • 消息积压出现得又快又猛,需要靠“粗粒度存储”而不是“内存堆积”来兜底。
  • 老数据要定期回放,或者要用 stream 的方式做批流一体。

一句话:场景越复杂、越偏向基础设施化,Pulsar 的存算分离就越值钱。反过来,如果你的消息量不大、也没有多租户需求,为了“先进”去盲目替换现有方案,大概率只会换来额外的运维负担。

2.2 多租户和共享基础设施:平台化消息系统的必经之路

现在很多中大型公司都要做“消息中台”,也就是让多个业务线共用一套消息基础设施。这时候,Pulsar 的多租户机制几乎是量身定做的。它通过 tenant 和 namespace 两级划分,把不同团队的资源和权限隔离得清清楚楚。某个团队的应用可以在自己的 namespace 里创建任意 topic,但绝不能碰其他团队的。

我当初迁移到 Pulsar 的一个重要诱因,就是公司里多个项目组各自搭建小集群,浪费资源且版本混乱。后来我们用一套 Pulsar 集群承载了三个大业务线的消息通信,再配合 pulsar-admin 做 namespace 和 topic 的配额限制,效果立竿见影。每个项目组拿到一个独立的 namespace,关键指标各看各的,再也没出现过“某个上游团队刷屏把下游搞挂”的纠纷。

不过要提醒一句:多租户不是简单地“开个 tenant 就完事”,你得提前设计存储和流量策略。比如把重要的生产环境 namespace 单独放到一组 bookie 上,把测试环境的 IO 优先级调低一点。我们没有做这套隔离之前,测试环境的频繁写入确实影响过生产环境的延迟。后来通过状态分类和服务层级规范,才算真正稳定下来。

3. 从零接入 Pulsar 的实操备忘

3.1 环境准备:最轻量的方式跑起一个可调试集群

如果你还没把 Pulsar 跑起来过,最推荐的方式是用 Docker 直接拉官方镜像,先把 standalone 模式跑通。别急着在生产环境搞联邦集群,先把基本操作弄熟了再说。

# 拉取镜像并启动 standalone 模式(示例版本,可按需替换) docker run -dt \ --name pulsar-standalone \ -p 8080:8080 \ -p 6650:6650 \ apachepulsar/pulsar:3.3.0 \ bin/pulsar standalone

启动后,可以验证一下集群状态:

# 查看集群健康状态 docker exec -it pulsar-standalone bin/pulsar-admin brokers list standalone # 创建一个测试 topic docker exec -it pulsar-standalone bin/pulsar-admin topics create persistent://public/default/test-topic

这里给第一次接触 Pulsar 的读者一点背景说明:Pulsar 的 topic 完整路径是persistent://tenant/namespace/topic或non-persistent://tenant/namespace/topic。默认的 tenant 是 public,namespace 是 default,之前创建测试 topic 没写完整路径会报错。我刚入坑时在这上面卡了快一小时,后来才意识到“topics create”和“topics create persistent://...”的细微差别。

3.2 Java 客户端配置要点:参数背后都有代价

一个 Pulsar 客户端,最核心的就是 Producer 和 Consumer 的构造参数。很多人习惯照着示例代码一把梭,然后生产环境出问题。这里我挑几个容易踩坑的参数,讲讲它们的取舍逻辑。

先看 Producer:

PulsarClient client = PulsarClient.builder() .serviceUrl("pulsar://localhost:6650") .build(); Producer<String> producer = client.newProducer(Schema.STRING) .topic("persistent://public/default/test-topic") .sendTimeout(30, TimeUnit.SECONDS) .blockIfQueueFull(true) .maxPendingMessages(2000) .create();

注意两个参数:

  • sendTimeout设成 30 秒,意味着如果 broker 在 30 秒内没有确认收到消息,客户端就会报错。这个值不能设太长,否则堆积消息会占满内存;也不能太短,否则遇到 broker 滚动重启时会出现大量误报。
  • blockIfQueueFull(true)配合maxPendingMessages,意思是当生产者堆积的消息超过 2000 条,客户端会阻塞而不是丢消息。这非常关键——宁可让业务线程等一会,也别在核心链路上静默丢消息。

再来看 Consumer,这里最大的坑是“订阅模式”。Pulsar 支持 exclusive、shared、failover 和 key_shared 四种模式。很多初学朋友以为用 shared 就行,但 shared 模式不保证顺序,而且处理失败的消息时如果没正确 negative acknowledge,很容易产生乱序和重复。对于要求分区内有序的场景,用failover或key_shared更安全。

Consumer<String> consumer = client.newConsumer(Schema.STRING) .topic("persistent://public/default/test-topic") .subscriptionName("my-subscription") .subscriptionType(SubscriptionType.KeyShared) .ackTimeout(30, TimeUnit.SECONDS) .receiverQueueSize(1000) .subscribe();

这里receiverQueueSize是个很有意思的参数。设得越大,消费者本地缓冲越多,吞吐越高,但一旦消费者崩溃,丢失的“已接收未确认”消息也会增加。生产环境平衡点通常是 100~1000,具体要看单条消息的大小。

3.3 生产环境必须处理的三类问题

第一类是消费者处理速度跟不上,也就是常说的“lag”。Pulsar 的落盘机制非常快,理论上消费者可以追得上,但实际问题往往出在消费者代码里的某个外部调用上。我曾见过下游 HTTP 调用超时导致消费变慢,Pulsar 客户端不断重试,消息反而越积越多。解决办法是在消费端加一个“最大并发数控制”和“熔断重试”逻辑。

第二类是“消息堆积报警”带来的误判。Pulsar 的消息积压指标跟 Kafka 不一样,它统计的是“未确认消息数”,而不是“磁盘上的积压量”。因为 Pulsar 可以把消息快速写入 BookKeeper 然后删除 broker 内存,所以积压的持久化消息不一定会立即引发 broker 内存报警。一线同学如果不了解这点,容易误以为系统“假死”,其实只要消费者性能一恢复,积压会很快被消化掉。

第三类是“数据丢失”之外还有“数据膨胀”。Pulsar 默认保留策略可能比你预期的多很多。你没设置 retention,老消息会在所有消费者确认后自动删除。但如果你设置了 retention,并且对象存储里还做了备份,那磁盘和存储桶成本会不断累积。建议上线前就把每个 namespace 的 retention 和 TTL 规则想清楚。

4. Pulsar Developer Day 上值得划重点的方向

4.1 活动看点:从提案讨论到生产案例分享

这个开发者日的日程我没有拿到完整终版,但从过往经验推测,有几个方向基本年年都是热点。第一波肯定是核心内核团队讲未来版本规划。比如 Pulsar 3.x 之后在协议性能、broker 元数据管理上的改动,更新点都是“少踩一个坑就是赚到”。

第二波就是各家公司的生产案例分享。这类分享的价值不在于“它用了 Pulsar 多厉害”,而在于“他在什么场景下决定不继续用 Kafka,以及切到 Pulsar 之后架构怎么调”。在我看来,这种“决策过程”比“结果展示”更值得听。建议听的时候带着自己的问题去,比如:你们怎么解决小消息高吞吐场景的瓶颈?分区数超过 5000 之后 bookie 的资源管理有什么变化?这些问题平时群里问也没人回,现场反而能抓到一线工程师聊几句。

4.2 值得自己上手摸索的冷门能力

除了听分享,我建议私下花点时间体验 Pulsar 的两大“隐藏能力”。

第一个是 Pulsar Functions,也就是在消息流上直接做轻量计算。官方定位是“无服务器计算”,你不需要单独起一个 Flink 或 Spark 集群,就能对消息做过滤、转换、汇聚。虽然复杂逻辑我还是建议走 Flink,但简单 ETL 和路由转发,用函数能省下很大一笔机器资源。

第二个是 Pulsar 的跨地域复制。它利用 BookKeeper 的存储能力和 replicator 机制,把消息从一个集群同步到另一个集群。我们做过跨两个机房的主备方案,切换时只要把客户端连接切过去,消息不丢不重(前提是处理好幂等)。云原生时代,这种能力几乎是多活系统的基础,比在应用层做双写要优雅得多。

5. 常见故障与排查速查表

我整理了一个问题排查表,都是真实环境里反复出现的典型问题。这也许是全文最“干货”的部分,建议收藏一下。

故障现象常见原因优先排查手段
消费者一直不消费,消息积压上升订阅模式设置为 exclusive,而多个消费者都订阅同一个 subscription用pulsar-admin topics stats看 Subscription 的消费者数量
生产者耗时忽高忽低,偶发超时BookKeeper 磁盘写入抖动,或 bookie 数量不够检查 bookie 的 journal 和 ledger 延迟,确认是否有慢盘
指定 key 的消息无顺序用了 Shared 订阅模式,导致同一 key 被不同消费者抢占改用 KeyShared 或 Failover 订阅
积压消息恢复后,重复消息增多消费者崩溃前ackTimeout设置过短,导致消息不断重发延长 ackTimeout,并确保消费逻辑具备幂等
老消息回放速度非常慢分层存储冷数据读取带宽不足检查对象存储限流,或通过预取机制把冷数据预热到热存储
集群扩容后流量分配不均没有做 topic 均衡,新增 broker 未分担原来的负载使用 load balancer 的卸载机制,或手动调整 topic 归属

这里特别想说一下“消息重复”的问题。很多人一听到“at-least-once”就觉得不能接受,但实际业务里几乎都会做幂等设计。问题是,Pulsar 默认重试可能非常密集,给下游压力特别大。我的做法是控制在 30 秒到 1 分钟的重试间隔,并对“连续失败”做死信投递。

另外,关注一个很容易被忽略的指标:Broker 连接数。有很多客户端因为配置了长连接池且没有回收机制,导致 broker 的连接数缓慢增长,最终触发文件描述符耗尽。这个问题的排查路径很隐蔽,我们当时查了两天,才从netstat -s和日志里的Too many open files找到线索。

6. 分享一些我自己踩坑后总结的经验

要说这几年跟 Pulsar 打交道最大的体会,那就是“可观测性比高可用更值得优先建设”。很多架构问题发生的时候,不是没做副本策略,而是监控看不到背书的数据结构,等发现已经雪崩了。所以不管你是正准备接入还是已经在用,建议把这些指标接到你的监控大盘:

  • 生产者 PublishRate、PublishLatency。
  • 消费者 ConsumerRate、AckRate。
  • Subscription 的积压未确认数。
  • Bookie 的 Journal 写入延迟、Ledger 写入吞吐。
  • Broker 的 CPU 负载、连接数、线程池利用率。

还有一个小技巧:充分利用 Pulsar 的 topic 级别策略。比如某个业务需要“延迟消费”,直接在 topic 上配置一个延迟投递的时间,不必在消费端自己睡线程。再有就是数据模型设计,尽量以“实体ID+事件类型”作为 key 来做分桶,会显著减少 KeyShared 模式下的哈希倾斜。

最后想再多说一句:消息中间件没有银弹,Pulsar 也一样。它的存算分离模型解决了扩展性问题,但也带来了“系统组件变多、排障链路变长”的新挑战。如果你手里正好有成体系的监控告警,又有耐心做好容量规划,Pulsar 会是极其趁手的底层工具。如果你只是四五台机器的小业务,那就让它在测试环境里先跑起来,多玩几次 standalone mode,再做决定也不迟。

活动现场见。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进