从延迟、积压、分区倾斜三个指标反推 Pulsar 容量规划,比盯着 CPU 内存管用得多
上个月我们团队对 Pulsar 集群做了一次容量缩减,砍掉了 30% 的 Bookie 节点,结果消息延迟反而下降了 12%。这件事让我重新审视了一个长期被忽视的问题:绝大多数团队做容量规划的方式根本就是错的。
盯着 CPU 和内存利用率做 Pulsar 容量规划,就像通过观察一个人的饭量来判断他能不能跑完马拉松——指标本身没毛病,但它跟你要回答的问题不在一个维度上。真正应该作为容量规划核心依据的,是三个业务侧指标:生产延迟、消费积压、分区倾斜度。这三个指标直接反映了消息系统的健康状态,而且它们之间存在清晰的推导链条,能告诉你瓶颈到底在哪一层。
生产延迟是 Broker 层的“诚实信号”
生产延迟(produce latency)指的是从 Producer 发送消息到收到 Broker 确认的时间差。Pulsar 的生产确认路径比较特殊:消息写入 BookKeeper 的多数派节点后才返回确认,所以这个延迟其实包含了 Broker 内部处理 + Bookie 网络往返 + Bookie 磁盘写入的总和。
我们在生产环境用 Pulsar 2.10.3 跑了一年多的数据,发现一个非常稳定的规律:当 Broker 单节点管理的 Topic 数量超过 400 个时,p99 生产延迟会在 15 分钟内从 50ms 左右飙升到 200ms 以上,而这时候 Broker 的 CPU 使用率可能才到 55%。原因是 Broker 内部有一个单线程的 Topic 调度模型(2.10 版本前尤其明显),Topic 数量增加直接导致每个 Topic 获得的调度时间片减少,CPU 并没用满,但延迟已经炸了。
这就引出了第一个容量规划阈值:按单 Broker 的 Topic 数量设限,而不是按 CPU 水位设限。我们的做法是,监控 pulsar_broker_publish_latency_le 的 p99 值,当它连续 3 分钟超过 50ms 时触发告警,然后反向检查该 Broker 上的 Topic 数量。如果 Topic 数超过 380 个,就开始做 Topic 迁移或扩容 Broker 节点。这个阈值不是拍脑袋定的,是从历史数据里找出来的拐点——超过 380 之后延迟的增速明显变陡。
Bookie 侧也类似。生产延迟的另一个组成部分是 Journal 写入延迟,而这个延迟跟 Bookie 的 Journal 磁盘 IO util 直接挂钩。我们用 NVMe SSD,实测当 Journal 盘的 avgqu-sz 持续超过 2 时,p99 生产延迟会从 10ms 以下跳到 80ms 左右。所以 Bookie 的容量规划锚点应该是 Journal 盘的队列深度,而不是磁盘空间利用率。
消费积压暴露的是消费链路的真实吞吐上限
消费积压(consumer backlog)这个指标很多人只会看总量,觉得“积压多了就是消费者不够”。但积压的增速才是容量规划的关键参数。
举个例子:我们有一条业务链路,消息生产速率稳定在 8000 msg/s,单个 Consumer 的消费速率在 2000 msg/s 左右。理论上配 4 个 Consumer 刚好打平,但实际上积压一直在涨。查下来发现,这个 Topic 有 16 个分区,但 Consumer 只连了 4 个,每个 Consumer 要拉 4 个分区的数据,而 Pulsar Consumer 内部是单线程处理单个分区的拉取回调,导致实际消费速率打了七折。
这里可以推导出第二个容量规划规则:按分区数反推 Consumer 数量下限,按积压增速判断是否需要扩容分区。我们的监控逻辑是,计算 msgBacklog / msgRateOut 的值,如果这个值持续大于 60 秒且呈上升趋势,说明消费能力跟不上生产速率。此时先检查 Consumer 数量是否已经等于分区数,如果没到,加 Consumer;如果已经到了,说明单分区的消费吞吐已经到顶,需要增加分区数。
Pulsar 的分区扩容不是无代价的——分区数增加意味着 Broker 需要维护更多的 Cursor 和 ManagedLedger,会增加 Broker 的内存和调度开销。回到第一点的逻辑,分区数增加会推高单 Broker 的 Topic 等效负载,所以分区扩容后必须重新评估 Broker 的容量。这两条链路是耦合的,不能割裂开来规划。
在积压监控上还有一个容易被忽略的点:积压的分布是否均匀。我们遇到过一种情况,总积压量只有 50 万条,看起来不多,但其中 48 万条集中在 3 个分区上,其他 13 个分区几乎没积压。这说明存在热点分区,单个分区的写入或消费出现了倾斜,这时候加再多 Consumer 也没用,因为大部分 Consumer 在空转。
分区倾斜是容量规划中最被低估的指标
分区倾斜(partition skew)指的是消息在多个分区之间分布不均的程度。Pulsar 默认的分区路由策略是 round-robin,如果没有指定 Key,消息会均匀分布;但如果 Producer 指定了 Key,同一个 Key 的消息会落到固定分区,业务上很容易制造出热点。
我们之前有个业务,用用户 ID 作为消息 Key,结果某个大客户的消息量占了总量的 35%,全打到了一个分区上。那个分区的生产延迟 p99 飙到了 500ms,其他分区只有 20ms。Broker 整体 CPU 利用率不到 40%,看起来“资源充裕”,但实际上那个热点分区的 ManagedLedger 已经扛不住了。
分区倾斜的量化方式很简单:计算每个 Topic 下各分区的消息流入速率的标准差,除以平均速率,得到变异系数。我们的经验值是,变异系数超过 0.3 就说明倾斜明显,超过 0.6 就必须介入处理。处理方式有两种:业务侧改分区 Key 策略(比如用更细粒度的 Key 或者加盐),或者运维侧给热点分区单独分配更多 Bookie 资源(通过修改放置策略把该分区的数据钉在性能更好的 Bookie 节点上)。
这个指标对容量规划的意义在于:集群总资源充足不代表每个分区都获得了足够资源。传统的 CPU/内存监控看到的是平均值或总量,完全掩盖了倾斜问题。只有把分区级别的延迟和吞吐指标拉出来单独看,才能发现真正的瓶颈在哪里。我们的容量规划模型里,分区倾斜度是一个独立的触发条件——即使全局指标一切正常,只要某个分区的变异系数超过阈值,就触发该 Topic 的分区扩容或迁移。
三个指标的联动关系与容量模型
把这三个指标串起来,可以得到一个完整的容量规划决策树:
- 生产延迟升高 → 检查单 Broker Topic 数量是否超 380 → 超了就迁移 Topic 或加 Broker;检查 Bookie Journal 盘队列深度是否超 2 → 超了就加 Bookie 节点或换更好的磁盘。
- 消费积压增速大于 0 → 检查 Consumer 数量是否等于分区数 → 不够就加 Consumer;够了就检查单分区消费速率是否达到 Consumer 线程上限 → 到了就增加分区数。
- 分区倾斜变异系数超 0.3 → 业务侧优化 Key 策略或运维侧调整放置策略 → 如果倾斜导致单分区延迟飙升,该分区独立扩容。
这个模型的核心思想是:容量规划的单位不是“节点”,而是“分区”和“Topic”。一个 32 核 64G 的 Broker 能承载多少个 Topic,不取决于它的 CPU 和内存总量,而取决于每个 Topic 的吞吐特征和调度开销。同样,一个 Bookie 能扛多少流量,不取决于它的磁盘容量,而取决于 Journal 盘的写入延迟。
我们现在的做法是,在 Prometheus 里建了三个核心告警规则:
# Broker Topic 数量与延迟联动
avg(rate(pulsar_broker_publish_latency_le{quantile="0.99"}[5m])) by (broker) > 50
and
count(pulsar_topics_count{type="persistent"}) by (broker) > 380
# 积压消费比
sum(pulsar_msg_backlog) by (topic) / sum(rate(pulsar_msg_rate_out[1m])) by (topic) > 60
# 分区倾斜度
stddev(rate(pulsar_msg_rate_in[1m])) by (topic) / avg(rate(pulsar_msg_rate_in[1m])) by (topic) > 0.3
这三个规则上线半年,集群的整体消息延迟 p99 从 120ms 降到了 45ms,而且再也没有出现过“CPU 看着挺空闲但系统就是慢”的诡异情况。
说到底,消息队列的容量规划本质上是一个排队论问题,不是资源利用率问题。你关心的是消息从进入系统到被消费出去的端到端延迟,以及这个延迟在负载变化时的稳定性。CPU 和内存只是实现这个目标的中间资源,盯着它们规划容量,等于用代理指标替代了真实目标。延迟、积压、倾斜这三个指标,才是直接描述系统服务质量的真实信号。
常见问题
分区倾斜的变异系数阈值 0.3 是怎么定的,不同业务能通用吗?
这个阈值是从我们自己的历史故障数据里反推出来的。我们统计了半年内 12 次生产延迟告警,发现当变异系数超过 0.3 时,有 10 次在 30 分钟内出现了至少一个分区的 p99 延迟超过 200ms。不同业务的消息大小、消费模式差异很大,0.3 不能直接照搬,但方法论是通用的:把你历史上出过延迟故障的时间点拉出来,回头算当时的分区变异系数,找到你的系统开始“不舒服”的拐点,那就是你的阈值。
单 Broker 的 Topic 数量上限跟 Pulsar 版本有关系吗?
关系很大。我们用的 2.10.3 版本,Broker 内部 Topic 调度是单线程模型,380 左右就是性能拐点。Pulsar 2.11 开始引入了 Topic 分片调度优化,3.0 版本进一步做了线程模型重构,单 Broker 能承载的 Topic 数量上限有显著提升。如果你用的是 3.0+ 版本,这个阈值需要重新压测确定,不能直接用 380 这个数字。但核心逻辑没变:监控生产延迟,当延迟开始非线性增长时,那个点就是你的容量上限,不管版本怎么变,这个推导方法都成立。
Consumer 数量等于分区数之后,积压还在涨,除了加分区别无他法吗?
不一定。如果业务允许,可以先尝试调大 Consumer 的 receiverQueueSize(默认 1000),这个参数控制 Consumer 单次能从 Broker 拉取的消息数量,增大它可以提升单分区的消费吞吐,对延迟不敏感的场景很有效。另外检查一下 Consumer 端的业务处理逻辑是否有瓶颈,很多时候积压是因为消费到业务处理之间的内存队列满了,而不是 Pulsar 本身的问题。如果这两个方向都试过还是不行,再考虑分区扩容。分区数不是越多越好,每增加一个分区,Broker 就要多维护一组 Cursor 和 ManagedLedger,对集群有实实在在的开销。