第 32 章

生产故障实录:10 个真实案例的根因分析

第32章:生产故障实录:10 个真实案例的根因分析

导读:生产环境最常见的 Kafka 故障模式如何排查和预防?

本章核心问题:生产环境最常见的 Kafka 故障模式如何排查和预防?

读完本章你将理解


Level 1 · 你需要知道的(1-3年经验)

故障是最好的老师

本章记录的 10 个故障案例,来源于真实的生产环境模式——有的来自笔者直接参与处理的线上事故,有的是 Kafka 社区和各大技术博客中反复出现的经典问题。每个案例遵循相同的分析框架:症状 → 排查步骤 → 根本原因 → 修复方案 → 预防措施

这个框架的重要性不亚于案例本身。生产故障发生时,人的第一反应是恐慌和盲目操作。系统性的排查方法——观察症状、形成假设、验证假设、找到根因——才是缩短 MTTR(平均恢复时间)的关键。


Level 2 · 它是怎么运行的(3-5年经验)

案例 5:磁盘空间耗尽导致集群瘫痪

症状

某 Topic 的 Compaction 机制开启,日志清理线程(log cleaner)运行正常,但磁盘空间持续减少,最终在 4 小时内耗尽磁盘空间(480GB SSD),Broker 停止接受写入。

排查步骤

# Step 1: 查看写入速率
kafka-jmx.sh \
  --object-name "kafka.server:type=BrokerTopicMetrics,name=BytesInPerSec,topic=user-events" \
  --attributes OneMinuteRate
# 输出: 1,073,741,824 bytes/s = 1 GB/s 写入速率

# Step 2: 查看 Compaction 线程处理速率
kafka-jmx.sh \
  --object-name "kafka.log:type=LogCleaner,name=cleaner-io-ratio" \
  --attributes Value
# 输出: 0.2 (20%),即 Compaction 线程以 200 MB/s 的速度清理

# Step 3: 简单计算
# 写入 1 GB/s vs 清理 200 MB/s
# 净增长: 800 MB/s = 48 GB/min
# 480 GB / 48 GB/min = 10 分钟磁盘耗尽?
# 实际 4 小时说明有部分数据因 TTL 自然过期,但 Compaction 完全跟不上

根本原因

日志压缩(Log Compaction)的清理吞吐量(200 MB/s)远低于写入速率(1 GB/s)。新数据以 5:1 的速度超过清理速度,未被清理的旧数据版本持续占用磁盘空间,最终耗尽。

根本设计问题:单线程 log cleaner(默认 log.cleaner.threads=1)在高写入场景下是严重瓶颈。

修复方案

# 立即行动:增加日志清理线程数(无需重启,动态生效)
kafka-configs.sh --bootstrap-server kafka:9092 --alter \
  --entity-type brokers --entity-name 1 \
  --add-config log.cleaner.threads=4

# 增加 Cleaner I/O 配额
kafka-configs.sh --bootstrap-server kafka:9092 --alter \
  --entity-type brokers --entity-name 1 \
  --add-config log.cleaner.io.max.bytes.per.second=536870912  # 512 MB/s

# 对高写入 Topic,降低 min.compaction.lag.ms
# 让 Compaction 更及早处理可以被清理的数据
kafka-configs.sh --bootstrap-server kafka:9092 --alter \
  --entity-type topics --entity-name user-events \
  --add-config min.compaction.lag.ms=3600000  # 1小时后即可被 Compact

# 对于热点分区,考虑独立磁盘
# 将高写入 Topic 的分区迁移到独立的 NVMe SSD

预防措施



Level 3 · 规范怎么定义的(资深)

本章的协议与规范细节已融入上述 Level 中。如需深入了解,请参阅 Kafka 官方协议文档和对应 KIP 提案。


Level 4 · 边界与陷阱(所有人)

案例 1:ISR 频繁收缩导致吞吐雪崩

症状

某支付系统的 Kafka 集群在业务高峰期出现严重吞吐下降,从正常的 500MB/s 骤降至 50MB/s。监控显示 UnderReplicatedPartitions 指标在 0 和 200+ 之间剧烈振荡,Producer 端出现大量 NOT_ENOUGH_REPLICAS 错误(acks=all 时 ISR 不足导致写入被拒绝)。

排查步骤

# Step 1: 确认哪些分区在 ISR 收缩
kafka-topics.sh --bootstrap-server kafka:9092 \
  --describe --under-replicated-partitions

# Step 2: 查看 Broker 日志中的 ISR 变动记录
grep "ISR updated" /var/log/kafka/server.log | tail -50
# 发现 ISR 变动都集中在 Broker 3

# Step 3: 检查 Broker 3 的磁盘 I/O
iostat -x 1 10 -d /dev/sdb  # Broker 3 的数据盘
# 输出: await=1200ms(正常应 < 5ms),说明磁盘响应极慢

# Step 4: 检查 RAID 控制器状态
/opt/MegaRAID/MegaCli/MegaCli64 -LDInfo -Lall -aAll
# 输出: "Degraded",RAID 5 其中一块磁盘故障,降级模式运行
# RAID 5 降级模式:每次写入需要重新计算奇偶校验,写延迟暴增 10-20x

根本原因

RAID 5 阵列中一块磁盘出现故障进入降级模式。降级模式下,RAID 控制器需要为每次写入重新计算奇偶校验,磁盘写延迟从 2ms 飙升至 1200ms。Follower Broker 的 Fetch 请求无法及时完成,延迟超过 replica.lag.time.max.ms(30 秒),触发 ISR 收缩。ISR 收缩导致 Producer 的 acks=all 请求失败(ISR 成员不足),吞吐断崖式下降。

修复方案

# 立即行动:提高 replica.lag.time.max.ms 买时间(临时措施,不能根治)
kafka-configs.sh --bootstrap-server kafka:9092 --alter \
  --entity-type brokers --entity-name 3 \
  --add-config replica.lag.time.max.ms=120000  # 临时提高到 120 秒

# 更换故障磁盘,RAID 重建(需要维护窗口)
# 重建期间磁盘性能依然受影响,监控 ISR 状态

# 长期修复:迁移到 JBOD,消除 RAID 这个单点故障

预防措施


案例 2:Consumer Rebalance 风暴

症状

微服务团队进行灰度部署,50 个 Consumer Pod 的消费者组在滚动更新期间,消费完全停止长达 15 分钟。每次有 Pod 重启,Kafka 的 Consumer Group 就触发一次完整的 Rebalance,整个消费组暂停消费直到 Rebalance 完成(约 20 秒),然后下一个 Pod 重启,再次触发 Rebalance。50 个 Pod 的滚动更新 = 50 次顺序 Rebalance = 1000 秒的停摆。

排查步骤

# 观察 Consumer Group 状态(在滚动部署期间)
watch -n 2 "kafka-consumer-groups.sh --bootstrap-server kafka:9092 \
  --group payment-consumer-group --describe"

# 发现状态在 Stable → PreparingRebalance → CompletingRebalance → Stable
# 每次循环 ~20 秒,且循环不断重复

# 查看 Broker 日志中的 Rebalance 记录
grep "Preparing to rebalance group" /var/log/kafka/server.log | wc -l
# 输出 50+,确认触发了大量 Rebalance

根本原因

问题在于两个相互叠加的设计缺陷:

  1. Eager Rebalance 策略(默认 RangeAssignor / RoundRobinAssignor):任何成员加入或离开,都会触发全组所有成员停止消费并放弃所有分区,等待重新分配。
  2. 无 Static Membership:每次 Pod 重启,Consumer 带着新生成的 member.id 重新加入组,Group Coordinator 无法识别这是同一个消费者,视为新成员加入 + 旧成员离开,两次触发 Rebalance。

修复方案

// Consumer 配置修改
Properties props = new Properties();

// 修复 1:启用 CooperativeStickyAssignor(渐进式 Rebalance)
// 只有受影响的分区会被重新分配,其他分区继续消费
props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
    "org.apache.kafka.clients.consumer.CooperativeStickyAssignor");

// 修复 2:启用 Static Membership(静态成员 ID)
// 使用 Pod 名称作为 group.instance.id,重启后 Group Coordinator 识别为同一成员
// 不会立即触发 Rebalance,等待 session.timeout.ms 超时后才重新分配
props.put(ConsumerConfig.GROUP_INSTANCE_ID_CONFIG,
    System.getenv("POD_NAME"));  // 例如: "payment-consumer-0"

// 配套调整:增加 session.timeout.ms
// Static Member 重启期间需要等待 session.timeout.ms 才重新分配分区
// 设置为 Pod 重启时间的 2-3 倍(假设 Pod 重启需要 30 秒)
props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 90000);  // 90 秒

效果:50 Pod 滚动部署时间从 1000 秒降至约 30 秒(仅最后一批 Pod 替换时触发一次 Rebalance)。

预防措施


案例 3:消息丢失排查

症状

下游数据分析团队发现某个时间窗口内的订单数据存在缺口:数据库中的订单记录比 Kafka 中消费到的消息少了约 3000 条。经比对,这批缺失的消息对应的是一次 Leader 故障切换前后 30 秒内产生的订单。

排查步骤

# Step 1: 确认缺失时间窗口对应的 Leader 切换事件
grep "New leader for partition" /var/log/kafka/server.log \
  | grep "orders-0" | head -10

# Step 2: 检查 Producer 的 acks 配置
# 查看 Producer 应用日志或配置文件
grep "acks" /opt/app/config/producer.properties
# 发现: acks=1

# Step 3: 模拟重现:acks=1 下 Leader 故障的数据丢失路径
# Leader 收到消息 → 写入 OS page cache → 发送 ACK
# Follower 尚未 Fetch 该消息
# Leader 宕机(消息在 page cache,未 fsync)
# 新 Leader 选出,该消息不在新 Leader 上 → 永久丢失

根本原因

acks=1 是数据丢失的根因。Leader 在收到 Producer 请求后立即返回 ACK,不等待 Follower 同步。如果 Leader 在 Follower 同步之前宕机,且消息还在 OS page cache(未 fsync),该消息永久丢失。

修复方案

// Producer 配置修改
props.put(ProducerConfig.ACKS_CONFIG, "all");          // 等待所有 ISR 确认
props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);  // 无限重试

// Broker 配置修改
// min.insync.replicas=2:要求至少 2 个 ISR 副本确认(replication.factor=3 时)
// 此设置与 acks=all 配合,确保即使 1 个 Broker 宕机,数据仍然安全
# 修改 Topic 级别配置(无需重启 Broker)
kafka-configs.sh --bootstrap-server kafka:9092 --alter \
  --entity-type topics --entity-name orders \
  --add-config min.insync.replicas=2

数据恢复

这批消息已经永久丢失,无法从 Kafka 中恢复。通过以下手段部分恢复:

  1. 对比上游数据库(Producer 在写 Kafka 之前先写 DB)
  2. 触发业务补偿流程(重新产生这 3000 条历史记录)

预防措施


案例 4:Producer OOM 导致服务不可用

症状

某微服务在发送 Kafka 消息时,主业务线程被阻塞,HTTP 请求响应超时,服务实例最终因 Kubernetes liveness probe 失败被重启。日志中出现大量 java.lang.OutOfMemoryError: Java heap space 或主线程长时间阻塞在 KafkaProducer.send() 处。

排查步骤

# Step 1: 查看 Producer 端的 buffer.memory 和 max.block.ms 配置
# 发现: buffer.memory=33554432 (32MB, 默认值)
#       max.block.ms=60000 (60秒, 默认值)

# Step 2: 查看 Broker 端的处理延迟
kafka-jmx.sh \
  --object-name "kafka.network:type=RequestMetrics,name=TotalTimeMs,request=Produce" \
  --attributes 99thPercentile
# 输出: 15000ms (P99 15秒!)

# Step 3: 分析 Producer 发送队列积压
kafka-jmx.sh \
  --object-name "kafka.producer:type=producer-metrics,client-id=payment-producer" \
  --attributes buffer-available-bytes,record-queue-time-avg,request-latency-avg
# buffer-available-bytes 接近 0
# record-queue-time-avg = 58000ms (接近 max.block.ms)

根本原因

Broker 端处理延迟飙升(因 I/O 问题),Producer 发送的消息无法及时得到响应,大量消息积压在 RecordAccumulator 的发送缓冲区。当 buffer.memory=32MB 的缓冲区被填满后,KafkaProducer.send() 方法阻塞主业务线程最长 max.block.ms=60秒,导致业务线程无法继续处理 HTTP 请求,服务实际上进入了死锁状态。

修复方案

// 方案 1: 增加缓冲区,延长阻塞时间(治标)
props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 134217728L);  // 128MB
props.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 5000);          // 5秒超时后快速失败

// 方案 2: 异步发送 + 背压感知(治本)
// 使用 Future + 回调,不阻塞主线程
Future<RecordMetadata> future = producer.send(record, (metadata, exception) -> {
    if (exception != null) {
        // 背压:Kafka 繁忙,触发业务层限流
        rateLimiter.decrementPermits();
        log.warn("Kafka send failed, applying backpressure", exception);
    }
});

// 方案 3: 专用 Kafka 发送线程池(完全隔离主线程)
ExecutorService kafkaExecutor = Executors.newFixedThreadPool(4);
kafkaExecutor.submit(() -> producer.send(record));

背压指标监控

- alert: KafkaProducerBufferLow
  expr: kafka_producer_buffer_available_bytes / kafka_producer_buffer_total_bytes < 0.1
  for: 2m
  labels:
    severity: warning
  annotations:
    summary: "Kafka Producer 发送缓冲区不足 10%,即将触发阻塞"

案例 6:消费积压后 Offset 丢失

症状

某批处理任务的 Consumer Group 在维护停机 8 天后重新启动,发现不是从停机前的位置继续消费,而是从最新位置(auto.offset.reset=latest)开始,跳过了停机期间产生的数百万条消息。

排查步骤

# 尝试查看 Consumer Group 的历史 Offset
kafka-consumer-groups.sh --bootstrap-server kafka:9092 \
  --group batch-processor-group --describe
# 输出: "Consumer group 'batch-processor-group' does not exist."
# Offset 记录完全消失!

# 查看 __consumer_offsets topic 的 retention 配置
kafka-configs.sh --bootstrap-server kafka:9092 \
  --describe --entity-type brokers | grep offset.retention
# 输出: offset.retention.minutes=10080 (7天)

# 停机时间 8 天 > 7 天 offset retention → Offset 已过期删除

根本原因

Kafka Broker 的 offset.retention.minutes(默认 7 天)控制着 Consumer Group Offset 的保留时间。如果一个 Consumer Group 超过 7 天没有任何消费活动(所有成员都离线),其 Offset 记录会被 Broker 删除。Consumer 重启后找不到历史 Offset,回退到 auto.offset.reset 配置,如果是 latest,则跳过了所有积压消息。

修复方案

# 立即行动:尝试从备份系统或上游重新产生消息(业务层修复)
# 或者:在停机期间保持一个"心跳" Consumer 定期 commit offset

# 配置修复:增加 Offset 保留时间到 30 天
kafka-configs.sh --bootstrap-server kafka:9092 --alter \
  --entity-type brokers --entity-name 1 \
  --add-config offset.retention.minutes=43200  # 30 天

# 对关键 Consumer Group 设置 earliest 作为 fallback
# 这样即使 Offset 丢失,也是从最早数据开始,而非跳过数据
# props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
# 注意:earliest 可能导致重复消费,需要业务层幂等支持

预防措施


案例 7:分区数过多导致 Controller 压力

症状

50 个 Topic,每个 Topic 1000 个分区,总计 50,000 个分区。当任意 Broker 重启时,Controller 的 CPU 飙升至 100%,重启恢复时间长达 10-15 分钟(正常应在 1-2 分钟内完成)。期间其他 Broker 无法完成元数据同步,部分 Producer 和 Consumer 出现连接超时。

排查步骤

# 查看集群总分区数
kafka-topics.sh --bootstrap-server kafka:9092 --list | wc -l  # 50 个 Topic
kafka-topics.sh --bootstrap-server kafka:9092 --describe \
  | grep "PartitionCount" \
  | awk '{sum += $2} END {print "Total partitions:", sum}'
# 输出: Total partitions: 50000

# 查看 Controller 在 Broker 重启时的耗时
grep "Sent LeaderAndIsr" /var/log/kafka/server.log | wc -l
# Broker 重启时,Controller 需要发送 50,000 条 LeaderAndIsr 请求
# 每条请求需要经过网络往返和持久化 → 大量时间

# 查看 Controller Epoch 更新耗时
kafka-jmx.sh \
  --object-name "kafka.controller:type=ControllerStats,name=LeaderElectionRateAndTimeMs" \
  --attributes 99thPercentile

根本原因

Kafka Controller(在 ZooKeeper 模式下)是单线程处理 Leader 选举的。50,000 个分区在 Broker 重启时,Controller 需要串行处理 50,000 个 LeaderAndIsr 请求,每个请求需要写入 ZooKeeper 并等待 ACK。ZooKeeper 的写入延迟(通常 1-5ms)累计 50,000 次 = 50-250 秒,导致恢复时间极长。

修复方案

短期

# 评估哪些 Topic 分区数可以减少(需要业务评估)
# 分区数的合理上限:每个 Broker 1000-3000 个分区

# 合并低流量 Topic
# 将 50 个小 Topic 合并为 5 个大 Topic(使用 Consumer 端过滤区分)

长期:迁移到 KRaft 模式

KRaft(Kafka Raft)模式下,Controller 是分布式的(多个 Controller 节点),元数据存储在内部 Kafka Topic 而非 ZooKeeper,可以高效处理数百万个分区。

# Kafka 3.7+ 支持从 ZooKeeper 模式迁移到 KRaft
# 完整迁移流程(需要维护窗口,约 1-2 小时)

# Step 1: 生成 Cluster ID
KAFKA_CLUSTER_ID=$(kafka-storage.sh random-uuid)

# Step 2: 配置 KRaft(详见官方迁移文档)
# server.properties 中设置 process.roles=broker 或 controller

预防措施


案例 8:SSL 证书过期引起集群分裂

症状

某天清晨,监控告警:UnderReplicatedPartitions 从 0 跳升至 150+,ActiveControllerCount 短暂变为 0,Consumer 端出现大量 SSL handshake failed 错误。

排查步骤

# Step 1: 检查 SSL 证书状态
for broker in kafka-1 kafka-2 kafka-3 kafka-4 kafka-5; do
    echo "=== $broker ==="
    openssl s_client -connect $broker:9092 2>/dev/null | \
    openssl x509 -noout -dates
done

# 输出:
# === kafka-1 === notAfter=Dec 31 23:59:59 2023 GMT  (已过期!)
# === kafka-2 === notAfter=Dec 31 23:59:59 2023 GMT  (已过期!)
# === kafka-3 === notAfter=Dec 31 23:59:59 2023 GMT  (已过期!)
# === kafka-4 === notAfter=Dec 31 23:59:59 2024 GMT  (有效)
# === kafka-5 === notAfter=Dec 31 23:59:59 2024 GMT  (有效)

# 3 个 Broker 的证书同一天过期!
# Broker 4 和 5 拒绝与 Broker 1/2/3 建立 SSL 连接(证书过期)
# 集群被分裂为两个互不通信的子集群

根本原因

5 个 Broker 中的 3 个使用了同一批次签发的证书,且证书有效期恰好到期。Broker 之间的 SSL 连接(REPLICATION 监听器)因证书过期被拒绝。Broker 1/2/3 无法向 Broker 4/5 同步副本,ISR 大幅收缩,Controller 也因 Quorum 问题出现短暂异常。

修复方案

# 立即行动:为 Broker 1/2/3 更新证书
# 使用 Vault 或 cert-manager 快速签发新证书
vault write -format=json pki/issue/kafka-broker \
  common_name="kafka-broker-1.internal" ttl="8760h" \
  > /tmp/new-cert.json

# 更新 Keystore(需要重启 Broker,或使用 SSL reload 机制)
# Kafka 3.x 支持动态 SSL 证书更新(无需重启)
kafka-configs.sh --bootstrap-server kafka-4:9092 --alter \
  --entity-type brokers --entity-name 1 \
  --add-config "ssl.keystore.location=/etc/kafka/ssl/new-keystore.jks,ssl.keystore.password=newpassword"

预防措施


案例 9:跨 AZ 延迟飙升拖慢 HW 推进

症状

业务团队反映,Kafka 消息的端到端延迟(P99)从正常的 20ms 飙升至 2 秒以上,持续约 1 小时。期间 Producer 吞吐量正常,Consumer 也在消费,但消费到的消息都是 1-2 秒前产生的。

排查步骤

# Step 1: 检查 ISR 状态(分区分布在 3 个 AZ)
kafka-topics.sh --bootstrap-server kafka:9092 --describe --topic orders \
  | grep "Isr:"
# 发现 ISR 完整,没有副本落后

# Step 2: 检查各 Broker 的 Fetch 延迟(区分 AZ)
# AZ-A: Broker 1, 2 → fetch latency P99: 15ms (正常)
# AZ-B: Broker 3, 4 → fetch latency P99: 18ms (正常)
# AZ-C: Broker 5, 6 → fetch latency P99: 2100ms (异常!)

# Step 3: 检查 AZ-C 的网络延迟
ping kafka-broker-5  # from AZ-A
# 正常应该 < 1ms,实际输出: 52ms (AZ-C 有额外 50ms 网络延迟)

# Step 4: 理解 HW (High Watermark) 的推进机制
# HW = min(所有 ISR 成员的 LEO)
# AZ-C 的 Follower 因为 50ms 网络延迟,Fetch 速度变慢
# Leader 的 HW 只能推进到最慢 ISR 成员的 LEO
# Consumer 只能消费到 HW,因此消费延迟 = AZ-C 的 Fetch 延迟

根本原因

AZ-C 因为云平台网络问题出现额外 50ms 延迟。由于 AZ-C 的 Follower 仍在 ISR 中(50ms < replica.lag.time.max.ms=30s),Leader 的 HW 推进被 AZ-C 的 Follower 拉慢。所有 Consumer 只能消费到 HW,端到端延迟等于 AZ-C 的 Fetch 延迟。

修复方案

立即行动

# 将 AZ-C 的 Follower 手动从 ISR 中移除(临时措施)
# 注意:这会降低数据安全性,需要权衡
kafka-configs.sh --bootstrap-server kafka:9092 --alter \
  --entity-type topics --entity-name orders \
  --add-config unclean.leader.election.enable=true  # 不推荐,仅紧急情况

# 更好的方案:调低 replica.lag.time.max.ms
# 使 50ms 延迟的 AZ-C Follower 更快被踢出 ISR
kafka-configs.sh --bootstrap-server kafka:9092 --alter \
  --entity-type brokers --entity-default \
  --add-config replica.lag.time.max.ms=5000  # 从 30s 降低到 5s

长期架构优化

// Consumer 端配置:优先消费同 AZ 的副本(Kafka 2.4+)
props.put(ConsumerConfig.CLIENT_RACK_CONFIG, "az-a");  // Consumer 所在 AZ

// Broker 配置:副本感知(Replica Rack Awareness)
// 确保 ISR 中有来自不同 AZ 的副本
// broker.rack=az-a (Broker 配置)
# Topic 创建时启用 rack aware 分配
kafka-topics.sh --bootstrap-server kafka:9092 \
  --create --topic orders \
  --partitions 12 \
  --replication-factor 3 \
  --replica-assignment 1:3:5,2:4:6,1:4:5,...  # 每个 partition 的副本分布在不同 AZ

案例 10:Kafka on Kubernetes Pod 重启导致数据丢失

症状

Kafka 部署在 Kubernetes 集群中,某次 Kubernetes 节点驱逐(Node Eviction)后,Broker Pod 被调度到新节点,重启后发现该 Broker 上的所有数据消失,表现为大量 UnderReplicatedPartitions 和 Consumer 侧的数据重复消费(因为其他副本重新同步)。

排查步骤

# Step 1: 检查 PVC 状态
kubectl get pvc -n kafka
# 发现 kafka-data-kafka-0 处于 Lost 状态

# Step 2: 检查 StorageClass
kubectl get storageclass
kubectl describe pvc kafka-data-kafka-0 -n kafka
# 发现: StorageClass: local-storage, AccessModes: ReadWriteOnce
# ReadWriteOnce 只允许单节点挂载,Pod 调度到新节点后,PVC 无法跟随

# Step 3: 检查部署方式
kubectl get pods -n kafka
# 发现: kafka-0, kafka-1, kafka-2 都是 Deployment(不是 StatefulSet!)
# Deployment 不保证 Pod 名称稳定,也不保证 PVC 与 Pod 的绑定关系

根本原因

两个根本错误:

  1. 使用 Deployment 而非 StatefulSet 部署 Kafka:Deployment 不能保证 Pod 与 PVC 的稳定绑定关系
  2. StorageClass 使用 ReadWriteOnce(本地存储):节点驱逐后,Pod 调度到新节点,原 PVC 绑定在旧节点,新 Pod 创建新的空 PVC,数据消失

修复方案

# 正确的 Kafka on Kubernetes 配置
apiVersion: apps/v1
kind: StatefulSet    # 必须是 StatefulSet,不是 Deployment
metadata:
  name: kafka
  namespace: kafka
spec:
  serviceName: kafka-headless
  replicas: 3
  podManagementPolicy: OrderedReady
  updateStrategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app: kafka
  template:
    metadata:
      labels:
        app: kafka
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: kafka
          image: apache/kafka:3.7.0
          env:
            - name: KAFKA_LOG_DIRS
              value: /var/kafka/data
          volumeMounts:
            - name: kafka-data
              mountPath: /var/kafka/data

      # Pod Disruption Budget 确保滚动更新期间至少 2 个 Broker 可用
  
  # VolumeClaimTemplates 确保每个 Pod 有稳定的 PVC 绑定
  volumeClaimTemplates:
    - metadata:
        name: kafka-data
      spec:
        accessModes:
          - ReadWriteOnce
        storageClassName: gp3-encrypted    # 使用支持跨节点挂载的 StorageClass
        resources:
          requests:
            storage: 500Gi
---
# Pod Disruption Budget:防止 Kubernetes 同时驱逐多个 Broker Pod
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: kafka-pdb
  namespace: kafka
spec:
  minAvailable: 2    # 始终保持至少 2 个 Broker 运行
  selector:
    matchLabels:
      app: kafka

StorageClass 选型:

预防措施

故障根因分布与预防系统

回顾这 10 个案例,故障根因可以分为 5 大类:

类别 案例 典型根因
存储与磁盘 1, 5 RAID 降级、Compaction 不足
配置错误 3, 6 acks=1、offset.retention 默认值过低
规模化问题 7, 9 分区数过多、跨 AZ 延迟影响 HW
基础设施 8, 10 证书过期、K8s 存储配置错误
客户端行为 2, 4 Rebalance 风暴、Producer 背压处理不当

预防优先级

  1. 基础设施自动化:证书自动续期、存储配置规范化(消灭案例 8、10)
  2. 配置规范化acks=allmin.insync.replicas=2offset.retention.minutes=43200(消灭案例 3、6)
  3. 监控告警体系:磁盘使用率、ISR 变动频率、Rebalance 频率(早期发现案例 1、2、5)
  4. 架构评审:分区数上限审查、跨 AZ 延迟测试(预防案例 7、9)

任何一个故障案例,在事后复盘时都会发现"如果早一步做了某件事,这个故障就不会发生"。构建 Kafka 可靠性的核心,是将这些"事后明显的"预防措施,变成日常工程实践的一部分。

本章评分
4.8  / 5  (3 评分)

💬 留言讨论