一个租户可以拖慢所有人
一个 Kafka 集群通常不是给一个团队用的。借阅服务、库存服务、推荐服务、数据分析任务共享同一批 broker,谁的写入突然放大,谁就会占掉网络、页缓存和磁盘 IO。问题不在于「谁对谁错」,而在于共享资源没有边界。
多租户要解决三件事,顺序不能反:
- 限速:单个租户的流量不能超过约定值。
- 隔离:一个租户过载时,其他租户的延迟不被拖垮。
- 归责:出问题时能按租户把指标切开,知道是谁。
配额(quota)只能解决第一件和第三件的一部分。它不会拒绝请求,也不会把租户赶到别的 broker 上;它的语义是「你超了,我让你等」。这一点必须先说清楚,否则后面所有配置都会被误读成限流熔断。
配额的三个维度
Kafka 的客户端配额有三个可调维度:
| 维度 | 配置项 | 单位 | 约束的是什么 |
|---|---|---|---|
| 生产字节速率 | producer_byte_rate | 字节/秒 | 客户端写入的未压缩字节 |
| 消费字节速率 | consumer_byte_rate | 字节/秒 | 客户端读取的字节 |
| 请求占比 | request_percentage | 百分比(0–100 之外也接受) | 客户端占用的请求处理线程时间 |
此外还有 controller_mutation_rate(4.x)和 connection_creation_rate,分别约束元数据变更与建连速率,平台治理会用,业务团队一般碰不到。
配额的作用对象有两种:
- client-id:由客户端自己声明,
ProducerConfig.CLIENT_ID_CONFIG或client.id。它可以被伪造,适合「同一个应用内区分不同模块」。 - user:SASL 认证后的 principal,客户端改不了。适合真正的租户边界,前提是开了认证(13.2)。
两者还可以组合成 (user, client-id) 的精确配额。匹配顺序上,组合配置优先于单一维度;没配的那一档落到默认配额(quota.producer.default 等)。这意味着给某个 client-id 单独调大配额,不会影响同一 user 下的其他 client-id,但前提是那个 client-id 没有从更宽的 user 配额继承限制。
超限是降速,不是拒绝
这是本节最重要的一句话。达到配额后:
- broker 不会返回错误码,消息不会进死信,连接不会断。
- broker 在响应里带一个
throttle_time_ms,客户端看到后自行暂停向该 broker 发请求。 - 生产者的
send()仍然是成功的,只是get()返回得晚;消费者的poll()变慢。 - 对于无法在响应里表达的场景,broker 还会直接静默(mute)这个连接一段时间。
所以配额对上层表现为延迟上升,不是错误率上升。监控如果只看错误率,配额打满时一切正常;要看 produce-throttle-time、端到端延迟和吞吐,才看得出被限速。
还有一个容易被忽略的边界:配额在请求处理之后才生效(响应被延迟),所以一条超大消息不会被配额切碎,它会一次性写进去,然后让这个客户端在下一个窗口里还债。配额不是消息大小上限,max.request.size / message.max.bytes 才是。
本机实测:把 1 KB/s 配额压到一个 client-id 上
环境:Temurin 25.0.5+8 / kafka-clients 4.3.0 / apache/kafka:4.3.0 KRaft 单节点,宿主端口 39192。实验程序 QuotaLab 用 client.id 指定身份,向单分区主题 library.loan.created 同步发送 16 条 1024 字节的消息,每条都 acks=all 并等待 RecordMetadata,最后从 producer.metrics() 里读 throttle 指标。
启动 broker 的命令与卷一相同,只把 EXTERNAL 端口换成 39192,这里不再重复。建主题:
docker exec mq-rel13-quota sh -c 'unset JMX_PORT; \
/opt/kafka/bin/kafka-topics.sh --bootstrap-server 127.0.0.1:9092 \
--create --topic library.loan.created --partitions 1 --replication-factor 1'
第一组:不配配额
export JAVA_HOME=~/.jdks/jdk-25.0.5+8/Contents/Home
CP="$HOME/.m2/repository/org/apache/kafka/kafka-clients/4.3.0/kafka-clients-4.3.0.jar:\
$HOME/.m2/repository/org/slf4j/slf4j-api/2.0.20/slf4j-api-2.0.20.jar:\
$HOME/.m2/repository/org/slf4j/slf4j-simple/2.0.20/slf4j-simple-2.0.20.jar"
$JAVA_HOME/bin/java -cp "out:$CP" QuotaLab 127.0.0.1:39192 quota-lab 16 1024
本机实测(单次运行;容器内 4 vCPU,/proc/loadavg 为 0.40/0.84/0.97;数字随调度波动,不作基准):
[send 00] partition=0 offset=0 latency=317 ms
[send 01] partition=0 offset=1 latency=5 ms
...
[send 15] partition=0 offset=15 latency=3 ms
[result] records=16 payload_bytes=1024 elapsed_ms=385
[result] client_side_records_per_sec=41.56
[metric] produce-throttle-time-avg=0.0
[metric] produce-throttle-time-max=0.0
[metric] record-send-total=16.0
第一条 317 ms 是建连与元数据,后面每条 3–5 ms。16 条约 17 KB,385 ms 内发完。
第二组:给同一个 client-id 配 1024 B/s
docker exec mq-rel13-quota sh -c 'unset JMX_PORT; \
/opt/kafka/bin/kafka-configs.sh --bootstrap-server 127.0.0.1:9092 \
--alter --entity-type clients --entity-name quota-lab \
--add-config producer_byte_rate=1024'
输出与确认:
Completed updating config for client quota-lab.
Quota configs for client-id 'quota-lab' are producer_byte_rate=1024.0
不改任何客户端代码,只改 client.id 对应的服务端配置,再跑一次:
[send 09] partition=0 offset=25 latency=85 ms
[send 10] partition=0 offset=26 latency=1128 ms
[send 11] partition=0 offset=27 latency=2114 ms
[send 12] partition=0 offset=28 latency=3143 ms
[send 13] partition=0 offset=29 latency=4110 ms
[send 14] partition=0 offset=30 latency=5120 ms
[send 15] partition=0 offset=31 latency=3 ms
[result] records=16 payload_bytes=1024 elapsed_ms=15943
[result] client_side_records_per_sec=1.00
[metric] produce-throttle-time-avg=782.15
[metric] produce-throttle-time-max=5114.0
[metric] record-send-total=16.0
三件事同时发生:
- 总耗时从 385 ms 变成 15943 ms,约 41 倍。
- 客户端吞吐从 41.56 条/秒降到 1.00 条/秒,正好对上 1024 B/s ÷ 1024 B。
- 16 条全部 ack,offset 从 16 连续到 31,没有一条失败、没有一条进重试。
第 10–14 条的延迟以约 1 秒为步长递增,是典型的配额静默:客户端每发一条就被要求等一个窗口的额度,未还完的债累加到下一次。最后一条只等 3 ms,因为前面的等待已经把窗口内的超额抹平。
第三组:换个 client-id,同一时刻不受影响
配额绑定的是 client-id,不是主题,也不是 broker:
[send 15] partition=0 offset=47 latency=3 ms
[result] records=16 payload_bytes=1024 elapsed_ms=265
[result] client_side_records_per_sec=60.38
[metric] produce-throttle-time-avg=0.0
[metric] produce-throttle-time-max=0.0
同一个 broker、同一个主题、几乎同一时间,other-lab 265 ms 发完,throttle 为 0。这说明配额的作用域是身份,不是资源。反过来也成立:如果你给一个主题限速,却让所有客户端共用 client.id,那限的就是「所有用这个 id 的进程加起来」。
指标名在 4.3.0 里变了
网上大量文章让查 kafka.producer:type=producer-metrics,client-id=... 下的 throttle-time。在 4.3.0 的源码里这个名字已经不在生产者侧了:
// Sender.java:967
public static Sensor throttleTimeSensor(SenderMetricsRegistry metrics) {
Sensor produceThrottleTimeSensor = metrics.sensor("produce-throttle-time");
produceThrottleTimeSensor.add(metrics.produceThrottleTimeAvg, new Avg());
produceThrottleTimeSensor.add(metrics.produceThrottleTimeMax, new Max());
return produceThrottleTimeSensor;
}
// SenderMetricsRegistry.java:123
this.produceThrottleTimeAvg = createMetricName("produce-throttle-time-avg",
"The average time in ms a request was throttled by a broker");
this.produceThrottleTimeMax = createMetricName("produce-throttle-time-max",
"The maximum time in ms a request was throttled by a broker");
所以看板里要写 produce-throttle-time-avg。这也是本书反复强调「版本相关断言必须当场验证」的原因:同一个指标,换一个大版本就可能查不到。
默认配额与继承
配额不是「配了才有」。没有单独配置的客户端会落到 broker 的默认档;默认档通常给得很宽,目的是不误伤正常业务。本机第一组实验就是证据:同一个 broker、同一个主题,没配额的 client-id 16 条 385 ms 发完,produce-throttle-time-avg=0.0,看不出任何限制。
也就是说,默认档不构成租户边界。想给一整个 user 下的所有 client-id 一个总闸,就配 user 配额:
docker exec mq-rel13-quota sh -c 'unset JMX_PORT; \
/opt/kafka/bin/kafka-configs.sh --bootstrap-server 127.0.0.1:9092 \
--alter --entity-type users --entity-name libraryd \
--add-config producer_byte_rate=8192,consumer_byte_rate=16384'
注意 --add-config 的多个键值要写在同一个字符串里,用逗号连接,不要拆成两个 --add-config:4.3.0 的 CLI 只接受一个这种参数,拆开会报参数重复。这条在卷一的工具章节提过一次,配配额时最容易踩。
优先级可以记成三层:(user, client-id) 组合 > 单一维度 > quota.*.default。给某个租户临时扩容时,优先改组合配额,不要动 user 配额,后者会连带放大该 user 下所有服务。
配额管不到的地方
把配额当成多租户的全部,是设计上最常见的误判。它至少有五个管不到的地方:
| 场景 | 配额能不能管 | 说明 |
|---|---|---|
| 单条超大消息 | 不能 | 请求先被处理,配额只延迟响应;大小上限靠 max.request.size |
| 磁盘被写满 | 不能 | 要靠 retention、配额主题容量、独立的 log dir |
| 消费延迟被拖垮 | 部分能 | 需要同时配 consumer_byte_rate,否则读不设限 |
| 故障扩散 | 不能 | 一个租户打满页缓存,其他租户照样抖;要物理隔离 |
| 配额本身被绕过 | 不能 | client-id 可伪造;真正的租户边界要用 SASL user |
前面三条可以靠配置补齐,后两条只能靠架构。配额是最后一道闸,不是隔离墙。
多租户隔离的五种做法
按成本从低到高排:
一、约定 client-id 前缀 + 一组配额。 例如 loan-svc-producer、inventory-svc-producer,每个前缀一条 quota 配置。落地最快,但只能防「无意打爆」,防不了「故意改 client.id」。
二、按 SASL user 配配额。 认证之后 principal 由 broker 认定,客户端改不了。需要先做 13.2 的认证,配额才有意义。这是大多数平台的起点。
三、逻辑隔离:主题前缀 + 独立消费组 + ACL。 租户 A 只能写 tenantA. 前缀的主题,只能读自己的组。它隔离的是权限与命名,不是资源。
四、物理隔离:独立集群或独立 broker 池。 大租户单独一套 broker,配额只作为兜底。成本最高,但只有它能隔离尾延迟和故障。
五、入口层限流。 在应用侧用令牌桶先削峰,broker 配额做兜底。两层都要有:应用层能给出友好的降级,broker 层能防住应用层失效。
本书的建议是二 + 三起步,四按租户等级升级。只做一,等于把边界交给客户端自觉。
配额配置的两个操作细节
第一,配额按 broker 计。三节点集群里,每个 broker 都独立执行同一条 producer_byte_rate;分区分散在三个 broker 上时,客户端的总吞吐上限接近三倍。做容量规划时不能用「单 broker 配额 × 1」。本机是单节点,所以上面测到的就是集群上限。
第二,改配额是动态的,不需要重启。kafka-configs.sh --alter 写进元数据后立即生效,已有连接也会被重新计费。但客户端侧的 produce-throttle-time 是滑动统计,改完立刻看指标可能还是旧值,等一个统计窗口再看。
另外,删除配额用 --delete-config producer_byte_rate,不要用 --add-config producer_byte_rate=-1:负值不是「取消」,含义在不同版本里并不一致。
三个常见误配
一是给主题限速。 Kafka 的客户端配额没有「按主题」这一维度。想限制某个主题的总写入量,只能限制写它的那些 client-id,或者用 ACL 把主题收归一个专用账号,再给账号配额。
二是用 request_percentage 代替字节速率。 它约束的是请求处理线程时间,对大消息、小消息混跑的负载不线性;一个租户用小消息刷请求数,字节速率没超,request_percentage 先打满。两种都要配。
三是消费配额配得比生产小一个数量级。 消费端要读的字节包括所有订阅主题,配小了会让 poll() 变慢、lag 上升,看起来像消费者性能问题,实际是配额。排查时先看 consumer_byte_rate 有没有被改过。
落地清单
- 先定身份,再谈配额:client.id 由平台统一分配,格式固定为
<租户>-<服务>-<角色>。 - 生产和消费都配:只配
producer_byte_rate,消费端仍可以把 broker 读垮。 - 配额值从实测基线来:先跑一周的 p99 字节速率,再乘 1.5–2 留余量;拍脑袋的值只会逼业务改 client.id。
- 监控三件套:
produce-throttle-time-avg、端到端 p99、按 client-id 切分的字节速率。 - 配额变更走审批:它是容量承诺,不是调试开关。
- 大租户直接上物理隔离:配额救不了尾延迟。
本节判据
- 超过配额时 broker 返回什么? 答:不返回错误,返回
throttle_time_ms让客户端自行等待;消息仍然成功写入。 producer_byte_rate的作用域是什么? 答:按 (user, client-id) 维度、在每个 broker 上独立生效,不是整个集群一个总额。- 为什么给 client-id 配了配额,另一个 client-id 不受影响? 答:配额绑定身份,不绑定主题或 broker;本机
other-lab在同一时刻仍是 265 ms 发完、throttle 为 0。 - 配额能防止一条超大消息吗? 答:不能。请求先被处理再延迟响应,大小要由
max.request.size和message.max.bytes约束。 - 多租户只做 client-id 配额,最大的缺口是什么? 答:client-id 可伪造,且配额不隔离故障与尾延迟;真正的边界要靠 SASL user、ACL 与物理隔离。
阅读导航:上一节 12.3 消费端性能调优 | 下一节 13.2 SASL/ACL 与传输加密
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。