团队一聊消息系统的性能,第一反应大多是吞吐量:一秒能写多少、一天能跑多少、批量开关开不开、参数还能不能再拧一拧。
这种讨论当然不算错,但它很容易越聊越窄,最后只剩一堆参数名:batchinglingerpublish delaypartition

在业务还比较单一的时候,这样聊问题不大。
消息类型少,处理链路也短,下游耗时也比较接近。这个阶段只要批处理做得合理,分区数和消费并发没有明显配错,吞吐往上走通常是自然结果。

但业务一复杂,事情就不是这样了。
比如同一套系统里,开始同时跑订单状态、库存扣减、营销触达这几类消息。它们的顺序要求、处理耗时、失败代价都不一样:有的要求按 key 保序,有的更在意并发;有的只是简单落库,有的要打多个下游;有的失败以后可以稍后重试,有的一旦重复处理就会带来业务副作用。这个时候再只盯 Producer 一次发多少,通常就不够了。

因为把高吞吐问题逼出来的,多数时候是业务把后面那串问题一起带了出来:消息怎么分发,失败以后怎么确认和重投,下游吃不动时积压会怎么长,系统又该怎么看自己是不是开始失衡。压测本身倒还排不上号。

Pulsar 值得讨论,我觉得也在这里。
关键在于它没有把高吞吐停在发送侧,继续往后延伸到消费和确认环节。参数好不好看,倒是其次。

一、业务还单一时,参数视角通常够用

高吞吐系统当然离不开 batch。
逐条发、逐条存、逐条处理,成本一定会很高。流量一上来,协议开销、系统调用、网络往返、Broker 处理成本都会被放大。只从这一层看,batch 先解决的是固定成本摊薄,这没什么问题。

所以在业务比较单一的时候,团队先去看 Producer 怎么攒批、Broker 怎么少处理几次请求,这很正常。
因为这个阶段消息的处理方式差异没那么大,链路后半段也没那么容易互相拖累。

开始变难,一般是在系统从“承接一类差不多的消息”走到“开始混跑几类差异很大的业务消息”之后。

这时候团队卡住的地方,已经不在“还能不能再多打一批”上了,更多是下面这些事:

  1. 一类消息要求按 key 顺序处理,另一类消息希望尽量并发,系统能不能同时接住。
  2. 同一批消息里只有一部分处理成功时,系统是整批重投,还是能记住已经处理到哪里。
  3. 库存、风控这类慢消息混进来时,会不会把原本很快的通知类消息一起拖住。
  4. 下游跟不上时,系统暴露出来的是短暂抖动,还是 backlog 会越堆越大。

这些问题都不是参数表能直接回答的。

二、Pulsar 真正往前做的,是把 batch 带进后面的处理链路

如果只看发送侧,很多消息系统都会做 batch。
Pulsar 更值得看的一点,是它没有把 batch 理解成“Producer 先攒一包,发出去就算完事”。官方文档把这件事说得很直白:batch 在 Broker 侧会按批次单元被跟踪和存储,到了消费阶段再拆回单条消息。
换句话说,batch 已经超出了网卡前面的一次性技巧,继续进入了后面的消息处理链路。

这样做的意义在于,后面的语义还有机会继续保住——吞吐数字好看只是顺带的。

比如按 key 分发。
如果业务要求同一个 key 的消息尽量按顺序处理,那 batch 就不能只是随手拼。前面怎么拼,后面就会影响路由和顺序。Pulsar 官方文档专门把 Key_Shared 下的 key-based batching 单独拎出来讲,原因也很现实:默认 batch 可能会破坏这类分发语义。
这件事的价值是它承认了一个现实:吞吐优化不能把分发语义直接压坏。多一个配置项只是手段。

再比如确认。
业务处理很少是整齐划一的。一批消息里,有的已经成功落库了,有的卡在下游调用,有的可能因为数据问题根本过不去。如果系统只能整批确认、整批重投,后面的治理会很快变粗。Pulsar 文档里把 batch index acknowledgment 讲得很明确:默认情况下,批里没全部确认时可能发生整批重投;开启 batch index ack 以后,Broker 会跟踪批内索引的确认状态。
本质上它是在补这件事:消息可以按批跑,但系统还是得尽量记住批里哪些消息已经处理过,这样后面的重试、补偿和排查才不会一锅端。

确认粒度从整批到逐条,差别远比想象的大。

消费侧也一样。 如果下游本来就是按批处理的,那 Consumer 最关心的是自己希望按多少条、多少字节、等多久再拿一批消息,Producer 那边怎么攒反倒不重要。Pulsar 的 BatchReceivePolicy 就是在消费侧回答这个问题:批量接收什么时候结束,由消息条数、字节数和等待时间共同决定。
这样一来,batch 从发送端私下做的优化,变成了消费端也能明确表达处理节奏的机制。

Pulsar 更值得聊的地方,是它把 batch 后面那几件业务上迟早会撞上的事一起摆到了系统里——光说“它也支持 batch”远远不够。

消息从写入到结果兑现,走的大概是这么一条路,瓶颈也基本长在后半段:

flowchart TD
    P["Producer 攒批发送"] -- "batch / key-based batch" --> BR["Broker 批次跟踪·存储·路由"]
    BR -- "Shared / Key_Shared 分发" --> C["Consumer 按 BatchReceivePolicy 拉取"]
    C -- "拆批逐条处理" --> R{"处理结果"}
    R -- "成功" --> ACK["batch index ack 逐条确认"]
    R -- "部分失败" --> NACK["重投"]
    ACK -- "确认推进" --> BR
    NACK -- "回流" --> BR
    BR -. "出流 < 入流" .-> BL["Backlog 积压"]
    BL -. "最老未确认消息滞留 → 结果兑现变慢" .-> WARN["系统失衡"]

三、到了工程现场,先坏掉的通常不是写入峰值,而是消费闭环

写入峰值当然要看,但它通常不是最早暴露问题的地方。
Producer 打得很快,Broker 先接住,这件事本身并不难做出漂亮数字。容易出问题的,往往是消费侧和后面的积压。

原因也不复杂。
业务处理的耗时本来就不均匀,有的消息只是简单落库,有的消息要查多个依赖,有的消息失败以后还要补偿。只要这些东西混在一套承载面里,系统表面上可能还在高吞吐,实际上 backlog、重投和延迟已经慢慢长出来了。业务侧感知到的信号多半不是“系统吞吐下降”,更可能是某一类结果开始变慢、变晚,甚至一直没有兑现。

Pulsar 的订阅模型之所以值得看,也是在这里。
一旦用了 SharedKey_Shared,很多问题都会变得非常具体:ACK 粒度怎么选,失败消息怎么回投,并发拉高以后顺序损失多少,某一类消息处理很慢时 backlog 会不会把别的消息一起拖住。

所以到了工程上,“系统一天跑了多少消息”这个总量用处有限,更值得盯的是下面这些信号:

  1. 入流是不是稳定,写入延迟有没有开始抖。
  2. 出流是不是跟得上,还是只是把流量不断堆进系统里。
  3. backlog 有没有持续增大,最老未确认消息挂了多久。
  4. 延迟、重投和积压是不是一起上来了。

比 backlog 总量更有用的,往往是最老那批未确认消息已经卡了多久。
总量大不一定危险,但如果最老消息的滞留时间一直往上爬,多半说明系统已经开始失衡,不只是单纯的“忙”。这个时候再看吞吐,就不能只看峰值,得看这个峰值是不是拿更长等待时间、更粗的确认粒度或者更重的消费侧代价换来的。
对业务来说,这种失衡最后会表现成结果兑现变慢,而不是监控图上某条线不好看。

四、该看的不是跑分,而是后面的消息处理闭环

如果业务里已经出现了混跑、顺序要求、部分失败、慢消费者这些问题,那你讨论的其实已经是一整套消息处理方式,早已超出单点性能的范围。
我更愿意从这个角度看 Pulsar。原因不复杂:在它这里,下面这些事被串到了一起,而不是分散在不同层各自处理:

  1. batch 不只是发送端优化,还会继续影响路由、确认和消费。
  2. 吞吐不只看 Producer,还要看消费端能不能走完全程。
  3. 性能不只看总量,还要看系统是不是开始慢慢失衡。

最后想落下来的其实就一句话。
业务一旦复杂,高吞吐就不再只是参数问题。它后面连着分发、确认、消费、重投和观测。Pulsar 值得讨论,关键也是它把这些原本迟早要补的工程问题放到了同一条链路里,跟数字做得多大没太大关系。

反过来说,如果系统现在承接的还是一类处理方式比较接近的消息,参数优化当然仍然重要,甚至已经够用。
值得把问题讲重,是在同一套承载面已经开始混跑不同业务、不同顺序要求和不同失败代价之后。