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. 选型关键考量什么时候应该选 Pulsar2.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 是 publicnamespace 是 default之前创建测试 topic 没写完整路径会报错。我刚入坑时在这上面卡了快一小时后来才意识到“topics create”和“topics create persistent://...”的细微差别。3.2 Java 客户端配置要点参数背后都有代价一个 Pulsar 客户端最核心的就是 Producer 和 Consumer 的构造参数。很多人习惯照着示例代码一把梭然后生产环境出问题。这里我挑几个容易踩坑的参数讲讲它们的取舍逻辑。先看 ProducerPulsarClient client PulsarClient.builder() .serviceUrl(pulsar://localhost:6650) .build(); ProducerString 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更安全。ConsumerString 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再做决定也不迟。活动现场见。