bigywater.tech
RabbitMQ · 8

prefetch:预取数量对吞吐和公平性的影响

prefetch 到底限制什么?global 标志有什么用?太小和太大各会怎样?这篇先数 ready / unacked 看清含义,再在 loopback 和加了 10 ms 往返的链路上分别量吞吐,最后看消费者崩溃时的代价。

9718 → 29665 条/秒
loopback 上 prefetch 从 1 提到 10 的消费吞吐
78 条/秒
往返 10 ms 时 prefetch=1 的消费吞吐,约等于 1 ÷ 往返时间
1000 条
prefetch=1000 的消费者崩溃后重新入队的消息数(redelivered=True)

含义

实测 · 本机执行输出
  消费者收到了 5 条;(ready, unacked) = (15, 5)
  ack 了第 1 条之后又收到 1 条(总共 6);(ready, unacked) = (14, 5)
  再 ack 到最后一条(multiple=True)之后共收到 11 条;(ready, unacked) = (9, 5)
  $ rabbitmqctl list_channels prefetch_count messages_unacknowledged consumer_count
    Listing channels ...
    prefetch_count	messages_unacknowledged	consumer_count
    0	0	0
    5	5	1

basic.qos(prefetch_count=N) 让 broker 给每个消费者最多推 N 条未确认的消息;每确认一条才再推一条。它限制的是在途(unacked)数,不是总数。

global 标志

实测 · 本机执行输出
  list_deprecated_features(摘取): classic_queue_mirroring=removed/denied, global_qos=denied_by_default/denied, transient_nonexcl_queues=denied_by_default/denied
  默认配置 global=False: 两个消费者各收到 {'a': 5, 'b': 5},合计 10;(ready, unacked) = (30, 10)
  默认配置 global=True: 两个消费者各收到 {'a': 5, 'b': 5},合计 10;(ready, unacked) = (30, 10)
  打开 permit.global_qos 后 global=True: 两个消费者各收到 {'a': 5, 'b': 0},合计 5;(ready, unacked) = (35, 5)
  默认配置下 global=True 没有报错,效果和 global=False 一样(每个消费者各 5);只有显式打开 permit.global_qos,两个消费者才共享 5

AMQP 里 global=True 本来表示整个 channel 共享这个限制。4.3.6 把 global_qos 列为 denied_by_default 的废弃特性:默认配置下请求不报错,但效果和 global=False 一样(每消费者各 N);显式打开 deprecated_features.permit.global_qos 才共享。

自动确认

实测 · 本机执行输出
  prefetch=5 + auto_ack=True:收到 1000 条;(ready, unacked) = (0, 0)

auto_ack 的消费者没有未确认状态,prefetch 对它不起作用,1000 条一次推完。

loopback 上的吞吐

实测 · 本机执行输出
  prefetch    耗时      吞吐
  1            2058 ms     9718 条/秒
  10            674 ms    29665 条/秒
  100           635 ms    31520 条/秒
  1000          576 ms    34711 条/秒
  0(无限)         527 ms    37944 条/秒

prefetch=1 因为每条一次往返只有 9718 条/秒,提到 10 后 29665 条/秒,再提到 1000 也只有 34711 条/秒,收益递减。

有延迟的链路

实测 · 本机执行输出
  prefetch    耗时      吞吐        理论上限(= prefetch ÷ 往返 10 ms)
  1            25706 ms       78 条/秒        100 条/秒
  10            2639 ms      758 条/秒       1000 条/秒
  100            258 ms     7761 条/秒      10000 条/秒
  1000           148 ms    13488 条/秒     100000 条/秒

往返变成 10 ms 之后差距是量级的:吞吐 ≈ prefetch ÷ 往返时间(实测 78、758、7761、13488 条/秒)。跨机房的消费者不能用 prefetch=1。

代价

实测 · 本机执行输出
  消费者在线时 (ready, unacked) = (4000, 1000)
  消费者断开之后 (ready, unacked) = (5000, 0)
  把 5000 条全取出来,其中 redelivered=True 的有 1000 条(正好是被推给那个崩溃消费者的 1000 条)

prefetch 越大,一个消费者攥着的在途消息越多:它崩溃时这些消息全部重投,慢的消费者还会把消息“囤”在手里不让别人处理。

分步演示

prefetch
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / RabbitMQ 4.3.6 / Erlang/OTP 28.5 / pika 1.4.4(Homebrew 与临时虚拟环境);所有节点都是本机自己起的 RabbitMQ 进程(127.0.0.1,端口 9500 以上,每个实验独立的数据目录与节点名,不连任何外部服务);命令输出来自真实的 rabbitmqctl 与管理 API,发布与消费用 pika 的真实连接;计时用 time.perf_counter,数字每次运行会有小幅抖动。第四节 loopback 每个 prefetch 值 3 次取中位数;第五节经过自己写的代理,每个方向加 5 ms 延迟(排队后按期限发送,保持流水线),每个值只测 1 次(prefetch=1 要 25 秒);消费者每条处理完立刻 ack,处理本身为零耗时
    • 没有验证:消费者处理耗时不为零时 prefetch 与吞吐的关系(第 9 篇会涉及);quorum 队列和 stream 上的 prefetch;多消费者共享队列时的 prefetch 分布;global_qos 放开后与 quorum 队列的交互;消息体更大时的 prefetch 内存占用;客户端库自己缓冲带来的差异(pika 的 BlockingConnection 回调模式)
    京ICP备2026067502号-1京公网安备11010502063487号