Kafka 现场变更手册
分区扩容 + 10MB 大消息配置链
2. 10MB 大消息需要 producer / broker / consumer 全链一起放大;客户目前只做完了 broker 侧的一半,另外四项未知,其中三项默认值是 1 MiB。
1.1 命令
kafka-topics.sh --bootstrap-server $BS --alter --topic my_topic --partitions 36
--partitions 给的是目标总数,不是增量。写
36 = 「扩到 36 个」,不是「再加 36 个」。MSK 上同样是这条命令——这是 topic 层面的操作,不需要走 MSK 配置修订版(那是 broker 配置的路径)。
1.2 新分区会自动分布到整个集群吗?
会,但要说准。
新分区的 leader / replica 位置由 Kafka 自动分配,走 AdminUtils 的副本分配算法:
- 每个分区的第一个副本(= preferred leader)在 broker 列表上做 round-robin,起始位置随机
- 剩余副本按递增 shift 排布
- 有 rack 信息时(MSK 自动把
broker.rack设为 AZ)→ 遵守跨 AZ 分散约束
所以新增的 20 个分区(16 → 36)会被均匀撒到全部 12 个 broker,无需手工指定。
两个必须清楚的关键点
| 说明 | |
|---|---|
| ❗ | 不是「只落在空闲 / 新 broker 上」。若场景是「刚加了 3 台新 broker,想让新分区只去新机器」,默认算法不会这么做——它会撒到全部 15 台。要强制只落新机器,必须用 --replica-assignment 手工指定,且 --alter 时要把全部分区的 assignment 写全,不只是新增的。 |
| ✅ | 36 = 12 × 3 这个数选得好:正好每台 broker 3 个 leader,完全均衡。当前 16 分区在 12 台上是「4 台各扛 2 个 + 8 台各扛 1 个」——那 4 台的 leader 负载是别人的 2 倍,而集群平均 CPU 会掩盖这个倾斜。 |
1.3 是不是「平滑」的?分三层看
加分区完全不触发数据迁移。 新分区在目标 broker 上是全新的空分区,直接创建,零字节复制。
这与 kafka-reassign-partitions.sh(副本重分配)是完全不同的两回事——后者才是客户担心的「搬数据、吃 CPU / 网络、AWS 说 CPU > 70% 不要做」。
- 消费端最长 5 分钟(
metadata.max.age.ms默认300000)后才感知到新分区 - 感知后触发重分配。默认
RangeAssignor是 eager 协议 = stop-the-world:全组先 revoke 全部分区 → 全组停止消费 → 重新分配 - 在 10× 峰值下,停 30 秒 = 凭空制造「30 秒 × 10 倍生产速率」的 lag
缓解:切 CooperativeStickyAssignor(增量式,只影响需要迁移的那几个分区,不是全组停摆)。
⚠️ 注意:必须两轮滚动升级,不能一步切。
另一个副作用:这 5 分钟窗口内,新分区已在收数据但还没人消费 → 会看到「新分区 lag 快速上涨」的假故障。想立刻生效就滚动重启 consumer。
这是对交易类客户最要紧的一条,也是唯一可能造成正确性事故的。
默认 partitioner:
toPositive(murmur2(serializedKey)) % numPartitions
分区数从 16 变 36,同一个 key 的落点几乎必然改变。举例:某账户 key 的 murmur2 正值为 1000
| 分区数 | 1000 % N | 该 key 的消息去哪 |
|---|---|---|
| 16 | 8 | partition-8 |
| 36 | 28 | partition-28 |
后果链
- 扩容那一刻,该 key 的新消息开始写
partition-28 - 但它的旧消息还在
partition-8,且可能没消费完 - 两个分区之间没有任何顺序保证(由不同消费者并行消费)
- 下游可能先看到「撤单」再看到「下单」、同账户入金/出金乱序导致中间态负余额、订单状态机倒退
顺带一个更严重的情况
单分区 topic 扩容时,key 映射破坏率是 100%——因为 hash % 1 == 0 对所有 key 成立,扩容后除了恰好留在 partition-0 的那部分,其余 key 全部换分区。比 16 → 36 严重得多。
1.4 所以现场必须先问这一句
| 答案 | 能否随手 --alter |
|---|---|
| 无 key(走 sticky partitioner) | ✅ 可以 随时扩,零风险,唯一代价是一次 rebalance |
| 有 key 但下游不依赖有序 | ✅ 可以扩 |
| 有 key 且依赖有序 | 🔴 不能随手做 必须走下面三个方案之一 |
| 不知道 | 🔴 保守处理 按「有且依赖」处理(交易业务不能赌) |
有 key 时的三个方案(按运维成本排序)
| 方案 | 做法 | 顺序风险 | 适用 |
|---|---|---|---|
| 1. 排空后再扩 ⭐ | 停写/限速 → 等所有 group 的 LAG 严格归零(不是「接近 0」)→ --alter → 滚动重启 consumer → 恢复写入 | 0 | 明天最推荐:反正已有变更窗口(机型升级),顺手做 |
| 2. 新 topic 双写切换 | 建 topic-v2(目标分区数)→ producer 双写 → 等 v1 排空 → consumer 切 v2 → 下线 v1 | 0 | 无法停写的核心交易 topic |
| 3. 自定义 Partitioner | hash(key) % 12288 固定虚拟槽,再映射到物理分区区间;扩容只改映射表 | 大幅降低 (非完全消除) | 预期还会持续扩容的集群 |
方案 1 的验收标准为什么必须是严格 0:只要还有一条带某 key 的旧消息未消费,扩容后该 key 的新消息就可能被先消费掉,顺序就断了。
1.5 还有两个代价要一起知道
① 只增不减,硬编码拒绝
controller 直接抛 InvalidPartitionsException: would not be an increase。要减只能重建 topic(没有 in-place 路径)。
推论:分区数是单向棘轮。宁可一次算准甚至适度 over-partition,别「先加一点看看不够再加」——每次加都要付一次顺序性代价。
好在客户空间极大:
36 分区 × RF3 ÷ 12 broker = 每 broker 仅 9 个副本
AWS 对 4xlarge+ 的推荐上限 = 4000
按 12 ~ 24 个月后的目标一次定准完全可行。
② 老数据不重分布
官方原文:
这条直接决定了时间安排:
| 场景 | 加分区有用吗 |
|---|---|
| 积压已经发生,要清 lag | ❌ 几乎无用 旧积压全在老分区里,还是那批消费者慢慢啃;新空分区分到的实例在空转,短期内有效并行度反而更低 |
| 积压尚未发生,为峰值做准备 | ✅ 唯一解 |
明天既然已有变更窗口,就该把分区扩容和机型升级一起做掉,而不是留到出问题时再动。
2.1 客户当前值
brokermessage.max.bytes = 10485880 # 约 10.0 MB
一个值得注意的细节:这不是整数的 10 MiB
10 × 1024 × 1024 = 10485760
客户值 10485880
差值 120 字节
这 120 字节很可能是客户有意为 record batch header 预留的——说明他们是认真算过的,确实在传 10MB 量级的消息。
但这 120 字节踩了一个很阴的坑
replica.fetch.response.max.bytes 的官网默认值恰好是 10485760(整 10 MiB)。
也就是说,客户的 message.max.bytes 比这个默认值高出正好 120 字节:
每条满载的 10MB 消息
→ 触发 fetch response 上限的 fallback 路径(「整个 response 只装这一条」)
→ 在 12 broker 的复制拓扑上显著拖慢复制吞吐
→ 喂给 ISR 收缩风险
2.2 「决定发送数据包上限」——这里要分清两侧
message.max.bytes 是 broker 侧的接收闸门(broker 愿意接受的单个 batch 上限),它不是 producer 侧的发送上限。
producer 侧管发送上限的是 max.request.size(默认仅 1 MiB)。
两者关系:
producer max.request.size ≥ 单条消息大小
← 不满足:produce 客户端直接抛 RecordTooLargeException
broker message.max.bytes = 10485880 ✅ 客户已设对
broker socket.request.max.bytes = 100MB ✅ 有 10× 余量
message.max.bytes 设到 10MB 是对的、已经做完的一半;危险的是另一半——producer 的
max.request.size 如果还是默认 1 MiB,10MB 的消息根本发不出去。这正是明天要现场问出来的 P0 第一个问题。
2.3 完整约束链(10MB 消息必须全链一起放大)
| 参数 | 侧 | 官网默认 | 客户当前 | 应设为 |
|---|---|---|---|---|
max.request.size | producer | 1 MiB | 未知 🔴 | ≥ 12 MiB |
message.max.bytes | broker | 1048588 | 10485880 ✅ | 保持 |
replica.fetch.max.bytes | broker | 1 MiB | 未知 🔴 | ≥ 12 MiB(read-only 需重启) |
replica.fetch.response.max.bytes | broker | 10485760 | 未知 🔴 | ≥ 32 MiB(客户值超它 120B) |
max.partition.fetch.bytes | consumer | 1 MiB | 未知 🔴 | ≥ 12 MiB |
fetch.max.bytes | consumer | 50 MiB | 未知 | ≥ 上一项 |
四个「未知」的后果
四个未知里有三个默认值是 1 MiB。只要有一个没跟着放大:
| 漏掉的参数 | 后果 |
|---|---|
max.request.size | produce 直接失败(RecordTooLargeException) |
replica.fetch.max.bytes / replica.fetch.response.max.bytes | 复制退化 → ISR 收缩 → 生产被拒 |
max.partition.fetch.bytes | 消费退化,约 12× 效率损失 ← 这很可能就是「消费跟不上」的直接原因 |
3.1 现场必须问出来的(P0)
| # | 问题 | 为什么是 P0 |
|---|---|---|
| 1 | producer 的 max.request.size 是多少? | 若仍是默认 1 MiB,10MB 消息根本发不出去 |
| 2 | replica.fetch.max.bytes / replica.fetch.response.max.bytes 是多少? | 复制退化 → ISR 收缩 → 生产被拒 |
| 3 | consumer 的 max.partition.fetch.bytes 是多少? | 很可能是「消费跟不上」的直接原因(~12× 损失) |
| 4 | producer 有指定 message key 吗?key 是什么?下游依赖同一 key 严格有序吗? | 决定分区扩容能否随手 --alter |
3.2 变更窗口内的行动清单
- 核对四个未知参数的实际值
- producer:
max.request.size→ ≥ 12 MiB - consumer:
max.partition.fetch.bytes→ ≥ 12 MiB;fetch.max.bytes≥ 前者 - broker:
replica.fetch.max.bytes→ ≥ 12 MiB(read-only,必须重启,正好搭机型升级的重启) - broker:
replica.fetch.response.max.bytes→ ≥ 32 MiB(消掉那 120 字节的坑)
- 确认每个待扩 topic 的 key / 有序性要求
- 无 key 或不依赖有序 → 直接
--alter到 12 的整数倍(建议 36) - 有 key 且依赖有序 → 走方案 1:排空(LAG 严格 = 0)→
--alter→ 滚动重启 consumer - 顺手评估
CooperativeStickyAssignor迁移(两轮滚动) - 分区数按 12 ~ 24 个月目标一次定准,别分次加
3.3 一句话总结
本文档技术内容未做增删改,仅完成两大主题合并、表格格式统一、P0 问题与行动清单合并。
不含任何客户名 / 公司名 / 账号 / IP / 主机名等敏感信息。配套完整调优手册见 MSK-Tuning-Battle-Plan.html。