Skip to content

Unexpected Offset

Errors

bash
[2025-04-28 08:22:01,606] INFO [MetadataLoader id=100] initializeNewPublishers: the loader is still catching up because we still don't know the high water mark yet. (org.apache.kafka.image.loader.MetadataLoader)
[2025-04-28 08:22:01,706] INFO [MetadataLoader id=100] initializeNewPublishers: the loader is still catching up because we still don't know the high water mark yet. (org.apache.kafka.image.loader.MetadataLoader)
[2025-04-28 08:22:01,792] ERROR Encountered fatal fault: Unexpected error in raft IO thread (org.apache.kafka.server.fault.ProcessTerminatingFaultHandler)
kafka.common.UnexpectedAppendOffsetException: Unexpected offset in append to __cluster_metadata-0. First offset 57876665 is less than the next offset 57876669. First 10 offsets in append: List(57876665, 57876666, 57876667, 57876668, 57876669, 57876670, 57876671, 57876672, 57876673, 57876674), last offset in append: 57912402. Log start offset = 57054787
        at kafka.log.UnifiedLog.$anonfun$append$2(UnifiedLog.scala:797)
        at kafka.log.UnifiedLog.append(UnifiedLog.scala:1724)
        at kafka.log.UnifiedLog.appendAsFollower(UnifiedLog.scala:682)
        at kafka.raft.KafkaMetadataLog.appendAsFollower(KafkaMetadataLog.scala:99)
        at org.apache.kafka.raft.KafkaRaftClient.appendAsFollower(KafkaRaftClient.java:1145)
        at org.apache.kafka.raft.KafkaRaftClient.handleFetchResponse(KafkaRaftClient.java:1127)
        at org.apache.kafka.raft.KafkaRaftClient.handleResponse(KafkaRaftClient.java:1550)
        at org.apache.kafka.raft.KafkaRaftClient.handleInboundMessage(KafkaRaftClient.java:1676)
        at org.apache.kafka.raft.KafkaRaftClient.poll(KafkaRaftClient.java:2251)
        at kafka.raft.KafkaRaftManager$RaftIoThread.doWork(RaftManager.scala:64)
        at org.apache.kafka.server.util.ShutdownableThread.run(ShutdownableThread.java:127)
[2025-04-28 08:22:01,807] INFO [MetadataLoader id=100] initializeNewPublishers: the loader is still catching up because we still don't know the high water mark yet. (org.apache.kafka.image.loader.MetadataLoader)
bash
kafka.common.UnexpectedAppendOffsetException: Unexpected offset in append to __cluster_metadata-0. First offset 57876665 is less than the next offset 57876669.

简单讲就是:

Kafka 在 Raft 协议同步 __cluster_metadata-0 日志分区的时候,收到了 一个不连续的 offset,导致写入失败并触发了 fatal fault(致命错误),进而导致 broker 进程进入退出或崩溃流程。

具体解释:

  • __cluster_metadata-0 是 Kafka 3.x/4.x 使用 KRaft 模式(无 Zookeeper)时的重要内部元数据日志。
  • Raft 协议要求日志是严格连续的,不能跳号或者回退。
  • 错误提示中:
    • 当前 Broker 认为自己下一个要写入的 offset 是 57876669
    • 但是收到的 append 里,第一个 offset 是 57876665(比预期的小了 4)。
  • 这违反了 Raft 的日志一致性要求,所以抛出 UnexpectedAppendOffsetException

造成这种错误的常见原因包括:

  1. 元数据日志损坏:比如磁盘写坏,或者之前异常宕机导致数据没同步好。
  2. 不正确的数据恢复:比如手动拷贝了 Kafka 数据目录,但 offset 不匹配。
  3. 集群中的 broker 节点数据不一致:一台 broker 滞后太多或者被强行回滚过数据。
  4. 强制降级/升级 Kafka 版本后元数据不兼容
  5. 磁盘 snapshot 和实际日志 offset 对不上(更底层一点,比如 snapshot 57876669,但实际日志回到了 57876665)。

解决思路:

下面的操作之前,一定要做好完整的备份,尤其是 $KAFKA_LOG_DIR/__cluster_metadata-0/ 目录。

  • 尝试修复(风险低):
    • 删除这个 broker 上的 __cluster_metadata-0 对应的 local log 文件(通常在 logs/__cluster_metadata-0 目录)。
    • 重启 broker。
    • Kafka 会根据 Raft 协议,从 leader(controller 节点)重新同步日志。
  • 强制清除日志并重新加入(风险中):
    • 设置启动参数,跳过损坏日志(比如加 kafka.storage.force.truncate.log=true,不过不同版本支持情况不同)。
    • 重启。
  • 最极端的恢复(风险高):
    • 删除整个 broker 的数据目录(彻底清空),让它作为新节点重新加入 Raft 集群。
    • 这种方式适合副本数够多(比如 3 副本集群,坏了 1 个)时使用。

当前状况

  • foobar-kafka-broker-0 Pod 起不来
  • 没办法 kubectl exec 进去
  • 但是还能 kubectl 操作 Pod 资源本身,比如修改挂载的存储(PVC)

通过 PVC 把数据清理掉

因为 Kafka 的数据是挂在 PVC(持久卷)上的,所以我们可以不需要进去 Pod,直接在 Kubernetes 级别清理数据

第 1 步:找到 foobar-kafka-broker-0 用的 PVC

执行:

bash
kubectl get pod foobar-kafka-broker-0 -o yaml | grep claimName

你会看到类似:

txt
claimName: data-foobar-kafka-broker-0

比如它叫 data-foobar-kafka-broker-0

第 2 步:挂载 PVC 到一个临时 Pod

因为 broker-0 自己起不来,我们搞一个临时容器来操作挂载的 PVC。

新建一个临时 pod(比如叫 debug-pod):

yaml
apiVersion: v1
kind: Pod
metadata:
  name: debug-pod
spec:
  containers:
  - name: debug
    image: busybox
    command:
    - sh
      - -c
      - "sleep 3600"
    volumeMounts:
    - mountPath: /data
      name: kafka-data
  restartPolicy: Never
  volumes:
  - name: kafka-data
    persistentVolumeClaim:
      claimName: data-foobar-kafka-broker-0

保存成 debug-pod.yaml,然后应用:

bash
kubectl apply -f debug-pod.yaml

第 3 步:清理数据

进入 debug-pod

bash
kubectl exec -it debug-pod -- sh

进去以后就能看到 Kafka 的数据了,比如在 /data 下面。

bash
mv $KAFKA_LOG_DIR/__cluster_metadata-0 $KAFKA_LOG_DIR/__cluster_metadata-0-bak

NFS 直接操作

bash
kubectl get pvc | grep kafka
bash
kubectl get pv xxx -o yaml