数据损坏这种事,在数据湖里并不少见。尤其用过 Hudi 的朋友应该都知道,它是一个基于 HDFS 或者 S3 这类存储上的数据湖框架,能支持流式写入、增量读取。但你有没有想过,万一哪一天表里的文件丢了一部分,或者某个 commit 的元数据对不上了,整张表会不会直接就废了?别慌,这篇文章就是来帮你把“体检”和“急救”的思路理顺的。我会用最接地气的方式,带着你从文件系统到 Hudi 的元数据,一层一层排查,再教你怎么做备份、怎么恢复,保证你读完能上手。

一、先说说数据损坏这件事

1.1 什么情况下会出问题

Hudi 表说白了就是一堆 Parquet 文件加上一些记录操作的时间线文件。任何一层出了问题都可能导致数据读不出来:

  • 磁盘坏道、网络抖动,导致写入文件不完整。
  • 不小心删掉了某个分区目录。
  • 覆盖写的时候,旧文件被清理掉,但新文件还没写成功。
  • HDFS 的 NameNode 损坏,或者 S3 上的桶被误清空。
  • 多个任务同时写一张表,又没有配置好并发控制,把时间线搞乱了。

这些情况听着吓人,但只要我们有预案,大多数都能救回来。

1.2 你能从这篇文章里得到什么

我会先带你检查 Hudi 表的“健康状态”,包括文件系统层面和 Hudi 元数据层面。然后给你一套完整的备份和恢复方案,全部用 Shell 命令演示,不需要额外安装复杂的工具,只要你有 Hudi CLI 或者能跑 Spark 的环境就行。文中的示例都是同一个技术栈:Shell + Hudi CLI,方便你直接复制去试。

二、动手之前先摸清家底

2.1 Hudi 表到底长什么样

一张 Hudi 表在存储上通常有三个部分:

  • 数据文件:一般是 Parquet 或者 HFile,放在表目录下的各个分区路径里。
  • .hoodie 目录:这是 Hudi 的元数据目录,里面存放了 commit、clean、rollback 等操作的时间线文件。
  • 可选元数据表:如果开启了 hoodie.metadata.enable=true,会有一个 .hoodie/metadata 子目录,用于加速查询和文件索引。

可以把 .hoodie 想象成表的“体检报告目录”,每次提交都会往里面写一个新文件,比如 20240801100000.commit 表示一次提交完成。如果这个目录里的信息丢了,表就找不到哪些文件是当前有效的了。

2.2 几种常见的“病危”信号

  • 读表时报 FileNotFoundException,说明某个数据文件不存在。
  • 跑批任务报 CommitNotFoundException,说明时间线文件缺失。
  • 用 Spark 查询时返回的结果行数忽多忽少,说明提交信息对不上。
  • 文件大小异常,比如很多 0 字节文件,大概率是写入中断。

三、文件系统层面怎么体检

3.1 检查文件是否存在

最直接的办法就是列出表目录下的所有文件。你可以在 HDFS 上执行 hdfs dfs -ls -R,或者在 S3 上执行 aws s3 ls --recursive。我们这里统一用命令行操作。

# 技术栈:Shell(文件系统命令)

# 先看看表目录整体结构,假设表放在 /data/hudi/ogs
hdfs dfs -ls -R /data/hudi/ogs

# 如果输出一堆目录和文件,说明路径还在
# 我们可以数一下文件数量,跟平时对比一下
hdfs dfs -count /data/hudi/ogs

# 如果发现某个分区目录不见了,比如 d=2024-08-01
hdfs dfs -ls /data/hudi/ogs/d=2024-08-01

如果分区目录没了,先别急着重新生成,看看有没有备份。没备份的话,只能从底层存储的快照去恢复。

3.2 用 fsck 检查文件一致性

HDFS 有自己的文件系统检测命令,叫 fsck。它能告诉你哪些 block 丢了,哪些副本不健康。对于 S3 来说,没有这个命令,但你可以用 aws s3api head-object 检查每个 key 是否存在且能正常读取。

# 技术栈:Shell(HDFS 文件系统检查)

# 对 Hudi 表所在的目录做 fsck,只检查不修复
hdfs fsck /data/hudi/ogs -files -blocks

# 如果看到类似 MISSING BLOCK 的信息,说明有文件损坏
# 这时候可以尝试用 hdfs fsck -delete 把损坏文件标记删除,
# 但注意删除前必须确认备份已存在,否则数据就真没了。
# 修复前建议先用 -list-corruptfileblocks 看下有哪些坏文件
hdfs fsck /data/hudi/ogs -list-corruptfileblocks | grep -i corrupt

fsck 的结果很多,你主要看 MISSING BLOCKCORRUPT 字样。另外你也可以加上 -locations 参数查看 block 分布,判断是否因为节点故障导致副本数不够。

3.3 校验文件内容是否完整

文件存在不代表内容没坏。常见的做法是用 Parquet 工具读取文件页脚,或者用 Hudi CLI 的 validate 命令。这里我们先用 Shell 检查文件大小,再结合 Hudi CLI 做内容级校验。

# 技术栈:Shell(文件大小对比)

# 假设之前每次 commit 生成的 parquet 文件大概在 128MB 左右
# 现在发现有个文件只有 10KB,极可能是损坏的
hdfs dfs -ls /data/hudi/ogs/d=2024-08-01/*.parquet

# 还可以按目录统计总大小,跟备份记录对比
hdfs dfs -du -s -h /data/hudi/ogs/d=2024-08-01

如果文件大小明显异常,那就尝试用 parquet-tools 读取一下:

# 技术栈:Shell(parquet-tools 需要额外安装,这里演示命令)

# 读取 parquet 文件的 schema,能读出来说明头部没坏
parquet-tools schema /data/hudi/ogs/d=2024-08-01/part-00000-xxx.parquet

# 读取前 10 行数据,如果不报错说明基本完整
parquet-tools head -n 10 /data/hudi/ogs/d=2024-08-01/part-00000-xxx.parquet

四、Hudi 特有的表完整性检查

文件系统只是敲门砖,真正的重点在 Hudi 自己的元数据。我们要确保时间线是完整的,每个 commit 都有对应的数据文件。

4.1 看时间线

Hudi 的每个 commit / delta commit 都会在 .hoodie 目录下生成一个文件。你可以直接列出来看:

# 技术栈:Shell(hadoop fs 命令)

# 列出 .hoodie 目录下所有提交记录
hdfs dfs -ls /data/hudi/ogs/.hoodie

# 按时间排序,可以看到最近一次 commit 是什么时候
hdfs dfs -ls /data/hudi/ogs/.hoodie/*.commit | sort -k6,7

# 查看 commit 内容,这个文件是 JSON 格式的,有提交的写操作详情
hdfs dfs -cat /data/hudi/ogs/.hoodie/20240801100000.commit | head -20

如果发现时间线有空洞,比如某个时间点之前的 commit 都存在,但是中间有一个 .commit 文件丢失,那这之后的读取就会出问题。

4.2 用 Hudi CLI 做深度校验

Hudi CLI 是一个专门用来管理 Hudi 表的工具,可以校验表的一致性,列出损坏的 commit。安装好的话,直接执行 hudi-cli 进入交互模式,再执行命令。为了脚本化,我们也可以把命令写成一行。

# 技术栈:Shell(Hudi CLI)

# 进入 hudi-cli 后,连接目标表
hudi-cli -- table --table /data/hudi/ogs

# 在交互模式里执行如下命令(也可以写成脚本文件批量执行)

# 1. 查看整个表的 commit 时间线
commits show

# 2. 校验表元数据是否一致
metadata validate

# 3. 检查 file system view,它会比对元数据里的文件列表和实际存储的文件
file-system-view --table /data/hudi/ogs --partition-path d=2024-08-01

上面命令中,metadata validate 是比较重要的一步。它会读取 .hoodie/metadata 目录中的文件映射,和实际的文件系统做对比。如果发现某个文件在元数据中登记了,但实际不存在,就会报出来。

如果你不想进入交互模式,也可以用如下非交互方式:

# 技术栈:Shell(Hudi CLI 非交互模式)
# 将多条命令写入一个脚本文件,再通过管道传进去

cat > /tmp/hudi_check.txt <<'EOF'
connect --path /data/hudi/ogs
metadata validate
commits show
quit
EOF

hudi-cli < /tmp/hudi_check.txt

五、备份方案怎么做才靠谱

备份这件事,其实比恢复简单。关键是你要定期执行,并且备份完要验证。下面我给出一个适合大多数场景的 Shell 备份脚本,用 HDFS 的 distcp 把整张表复制到备份目录。

5.1 冷备份(表不变动时直接复制)

冷备份的意思就是先停掉所有写入任务,再把表目录完整复制到另一个目录。这样能拿到一个一致性快照。

# 技术栈:Shell(HDFS distcp)

#!/bin/bash
# 表名和备份根目录
TABLE_NAME="ogs"
BACKUP_BASE="/backup/hudi"
TAB_DIR="/data/hudi/${TABLE_NAME}"
BACKUP_DIR="${BACKUP_BASE}/${TABLE_NAME}_$(date +%Y%m%d%H%M%S)"

# 1. 确保备份根目录存在
hdfs dfs -mkdir -p "${BACKUP_BASE}"

# 2. 使用 distcp 做分布式复制,保留块信息和权限
hdfs distcp -update -delete "${TAB_DIR}" "${BACKUP_DIR}"

# 3. 输出备份完成的标记
echo "Backup done at ${BACKUP_DIR}" >> /var/log/hudi_backup.log

脚本里的 -update 表示增量复制新文件,-delete 表示删除源目录中已经不存在的文件。注意这两个选项不要随意动,否则备份可能越积越大或者留下旧文件。

5.2 增量备份(针对时间线文件)

如果表一直在写入,又不能停,那就做增量备份。你有两个思路:

  • 定期只备份 .hoodie 目录和最近一段时间的新数据文件。
  • 用 Hudi CLI 把某个 commit 之后产生的文件列表导出,再复制这些文件。
# 技术栈:Shell(按 commit 时间备份)

#!/bin/bash
# 获取最近的 commit 时间
LAST_COMMIT=$(hdfs dfs -ls /data/hudi/ogs/.hoodie/*.commit | awk '{print $NF}' | sed 's/.*\///' | sort | tail -1)
echo "最近 commit: ${LAST_COMMIT}"

# 把该 commit 相关的数据文件列表保存下来
# 这里用 hudi-cli 的 commit verify... 实际我们可以简化,
# 直接基于文件时间戳找出最近一天生成的数据文件
hdfs dfs -ls -R /data/hudi/ogs \
  | grep "2024-08-01" \
  | awk '{print $NF}' \
  > /tmp/recent_files.txt

# 用 distcp 复制这些文件到增量备份目录
hdfs dfs -mkdir -p /backup/hudi/ogs_incremental/20240801
while read -r file_path; do
  hdfs dfs -cp "$file_path" "/backup/hudi/ogs_incremental/20240801/" 
done < /tmp/recent_files.txt

注意这个增量脚本比较简单,实际生产环境建议用 hudi-clicommit show 获取准确的 commit 文件列表,不要用文件时间戳,因为可能包含历史文件。

5.3 备份后验证

备份完一定要验证,否则真出事时发现备份是坏的,哭都来不及。验证步骤分两块:

  • 目录结构完整:备份目录下 .hoodie 存在,数据文件数量与源表一致。
  • 能正常读出数据:用 Hudi CLI 连接备份目录,执行 commits show 看能不能列出 commit。
# 技术栈:Shell(备份验证)

# 校验文件数量是否一致
source_count=$(hdfs dfs -count /data/hudi/ogs | awk '{print $2}')
backup_count=$(hdfs dfs -count /backup/hudi/ogs_20240801120000 | awk '{print $2}')
echo "源文件数: ${source_count}, 备份文件数: ${backup_count}"

if [ "$source_count" -ne "$backup_count" ]; then
  echo "文件数量不一致,备份失败!"
  exit 1
else
  echo "文件数量一致,备份成功。"
fi

# 用 hudi-cli 连接备份表看时间线
hudi-cli -- table --table /backup/hudi/ogs_20240801120000 --action commits show

六、恢复操作实战

恢复是重头戏,我分几种情况讲。

6.1 从备份恢复

这是最幸福的场景,直接复制回去就行。

# 技术栈:Shell(HDFS distcp 恢复)

#!/bin/bash
# 源表已经坏了,先从最近的备份恢复
BACKUP_DIR="/backup/hudi/ogs_20240801120000"
TAB_DIR="/data/hudi/ogs"

# 备份目录必须存在,且里面有 .hoodie
if [ ! -d "$BACKUP_DIR/.hoodie" ]; then
  echo "备份不完整,.hoodie 目录缺失!"
  exit 1
fi

# 先把损坏的表目录移到另一个位置作为证据保存(别直接删除)
hdfs dfs -mv /data/hudi/ogs /data/hudi/hudi_corrupt_$(date +%Y%m%d%H%M%S)

# 然后从备份把表复制回原路径
hdfs dfs -cp -p "$BACKUP_DIR" "$TAB_DIR"

# 恢复完成后立刻验证
hudi-cli -- table --table "$TAB_DIR" --action metadata validate

-cp -p 会保留文件权限和时间戳,比较稳妥。

6.2 没有完整备份怎么办

如果只备份了数据文件,没有备份 .hoodie 目录,或者 .hoodie 目录损坏了,需要重建元数据。Hudi 提供了 repair 功能,可以把历史上所有 commit 重新整理。但前提是数据文件上自带 Hudi 的 _hoodie_commit_time 等字段,这样工具才能识别属于哪次提交。

# 技术栈:Shell(Hudi CLI repair)

# 连接表目录
hudi-cli -- table --table /data/hudi/ogs

# 在交互模式执行:
# repair --mode modify // 这个命令会重建文件系统视图
# 不过更常用的是 spark-sql 中的 refresh table,但这里我们保持 Shell 技术栈

实际上 Hudi CLI 里有 repair 子命令,可以修一些不一致。如果连 .hoodie 里的 commit 文件都没了,那恢复起来非常麻烦。一个可行的思路是:用 Spark 读取所有 parquet 文件,提取 _hoodie_commit_time 字段,重新生成一个最小的时间线。这属于“重建”而非“恢复”,这里我们只提一下,不展开。

6.3 恢复后的自检

恢复完不是能查询就算完,还要做几项自检。

# 技术栈:Shell(自检)

# 1. 检查目录文件数
hdfs dfs -count /data/hudi/ogs

# 2. 检查时间线最后一个 commit 是否存在
hdfs dfs -ls /data/hudi/ogs/.hoodie/*.commit | tail -3

# 3. 用 Hudi CLI 验证表能读取
hudi-cli -- table --table /data/hudi/ogs --action metadata validate | grep "No conflicts"

# 4. 找一个分区执行文件系统视图校验
hudi-cli -- table --table /data/hudi/ogs --action file-system-view --partition-path d=2024-08-01

如果这些命令都没有报错,说明表已经基本恢复。但保险起见,你可以再跑一次全量读,比如用 Spark SQL 的 select count(*) 对比恢复前后的业务记录数。

七、应用场景和优缺点

7.1 什么场景下需要这套方法

  • 数据湖里有重要业务表,公司要求数据必须可恢复。
  • 每天有大量流式写入,突然某天任务失败,导致部分分区文件被清理。
  • 团队经常做表结构演进或数据重写,操作失误概率高。
  • 底层存储(HDFS/S3)出现过故障,怀疑有数据块丢失。

7.2 数据损坏与恢复方案对比

方案 优点 缺点
冷备份 恢复简单,一致性高 需要停写,备份耗时长,占用空间大
增量备份 备份快,节省空间 恢复时依赖多个备份段,操作复杂
Hudi 元数据修复 可以在没有完整备份时抢救数据 只能修复元数据,不能找回已删除的数据文件
底层存储快照 实现最安全,可以秒级恢复 需要存储系统支持,比如 HDFS snapshot 或 S3 版本控制

实际生产环境通常混合使用:平时做增量备份,每天一次冷备份,同时开启底层存储的快照功能。万一真出事,先用快照恢复,再用增量备份找到最新 commit。

7.3 注意事项

  • 备份文件不要放在同一个节点或同一个故障域。HDFS 备份到另一个集群,S3 备份到另一个桶,或者不同 region。
  • 恢复操作必须在确认备份完整之后再执行。想象一下,你第一次恢复失败,第二次又去恢复,结果把本来就完好的数据给覆盖了,那就悲剧了。
  • 定期做“恢复演练”。很多团队备份了半年,从来没试过恢复,真出事时才发现备份目录里少了 .hoodie 关键文件。
  • Hudi 表写入时最好开启 hoodie.cleaner.policy 的保留策略,别让 cleaner 在你还想保留历史版本的时候把文件清理掉。
  • 如果是多写并发,一定要开启乐观锁或使用单写入者模式,否则时间线会上乱七八糟,那时候就不是备份能解决的问题了。

八、文章总结

数据损坏并不可怕,可怕的是没有检查手段,也没有恢复预案。通过这篇文章你应该掌握了几个核心要点:

  • 文件系统层面用 fsckduls 这些命令做体检。
  • Hudi 层面要重点检查 .hoodie 时间线和元数据表,用 hudi-climetadata validate 做一致性比对。
  • 备份方案要覆盖数据文件和元数据,缺一不可。
  • 恢复操作要严谨,先备份坏表,再恢复,最后验证。
  • 没有完美无损方案,定期演练才是保住数据的最佳防线。

以后遇到 Hudi 表报错,先别慌。按照这套“体检 → 备份 → 恢复 → 自检”的流程走,至少能帮你保住 95% 以上的数据。记住,数据湖的稳定性不是靠运气,而是靠一整套防患于未然的习惯。现在就可以拿一张测试表试试这些命令,等真派上用场的时候,你会感谢自己读过这篇文章。