cloud-sre

Erigon 主网归档节点卡在 25393069:为什么最后只删 chaindata

一次 Ethereum Mainnet Erigon 归档节点现场修复记录:v3.5.0 仍在 25393069 出现 gas used mismatch,轻量修复没有马上恢复,最后保留 snapshots、删除 chaindata 后重建通过。

Jun 30, 2026
EthereumErigonarchive-nodechaindatatroubleshooting

这次故障很像“只坏了一个区块”。

Ethereum Mainnet 的 Erigon 归档节点升级到 erigontech/erigon:v3.5.0 后,执行阶段稳定卡在 25393069。 错误不是随机 I/O 抖动,也不是 peer 暂时不足,而是同一个区块反复出现 gas used mismatch。

最后有效的恢复方式不是跳过区块,也不是删除全部 /data。 我保留了本地 snapshots,只删除 /data/chaindata,让 Erigon 从已有 snapshots 重新构建执行数据库。 节点随后越过 25393069,RPC 高度提交到 25393999,执行继续往后跑。

用语说明

用语含义
Archive node / 归档节点保留历史状态的节点,可以支持旧区块 trace、receipt 和历史状态查询。
ErigonEthereum 执行客户端之一,使用 staged sync,把下载、执行、索引、trie 等阶段拆开处理。
Datadir节点的数据目录。这里是 /data,里面同时有 snapshots、chaindata、日志等内容。
chaindataErigon 的主执行数据库目录。删除它会让节点重建执行状态,但不等于删除所有 snapshots。
Snapshot预构建的链数据文件。保留 snapshots 可以避免从创世块线性重新下载和执行全部历史。
Gas used mismatch节点自己执行区块后算出的 gas used,和区块头里的 gas used 不一致。执行客户端会把这个区块判为无效。
Staged syncErigon 的同步模型。HeadersBodiesSendersExecutionTxLookupFinish 等阶段会分别推进。

现场现象

节点配置是普通 Ethereum Mainnet archive 形态:

erigontech/erigon:v3.5.0
--chain=mainnet
--datadir=/data
--prune.mode=archive

升级前的数据目录来自旧版本。 升级后,节点能打开数据库,也能看到本地 snapshots,但执行阶段卡住。

核心错误是:

gas used mismatch block=25393069 header=20304193 execution=20137672
[4/6 Execution] rw exit err="invalid block, block=25393069, invalid block, gas used by execution: 20137672, in header: 20304193"
[4/6 Execution] Execution failed err="invalid block, block=25393069 ..."
Cannot update chain head err="updateForkChoice: [4/6 Execution] invalid block, block=25393069 ..."

当时 eth_syncing 里的 stage 也显示出这个形状:

Headers     25393067
Bodies      25393067
Senders     25393067
Execution   25393067
Finish      25393067

也就是说,节点不是完全离线。 它在 25393069 附近稳定失败。

为什么不能“跳过这个区块”

区块链客户端不能安全地跳过一个执行失败的区块。

如果 25393069 的执行结果和区块头对不上,后面的 state root、receipt、trace 都没有可信基础。 对 archive node 来说更明显:后续历史状态和 trace 都依赖前面执行状态。

所以这里没有一个合理的“ignore bad block and continue”操作。 可选路线只有两类:

  1. 证明这是坏块标记、索引或局部状态问题,然后做局部修复。
  2. 放弃当前执行数据库,从可靠的 snapshots 和 block 数据重建。

先试过的轻量修复

Erigon 镜像里有 integration 工具。 先确认它存在:

docker exec erigon command -v integration
docker exec erigon integration --version

然后停掉主节点,避免两个进程同时写 /data

docker stop erigon

先看 stage:

docker run --rm \
  -v /data:/data \
  --entrypoint integration \
  erigontech/erigon:v3.5.0 \
  print_stages --datadir=/data

当时能看到 Execution 停在 25393067,本地 blocks snapshots 已经到 25391999 以后,数据库里 header/body 也有后续块。

接着清掉 bad block 标记:

docker run --rm \
  -v /data:/data \
  --entrypoint integration \
  erigontech/erigon:v3.5.0 \
  clear_bad_blocks --datadir=/data

这个命令成功清了 BadHeaderNumber 表。

随后补 Senders

docker run --rm \
  -v /data:/data \
  --entrypoint integration \
  erigontech/erigon:v3.5.0 \
  stage_senders --datadir=/data --chain=mainnet --block=25393070

再尝试执行阶段 unwind 后重跑:

docker run --rm \
  -v /data:/data \
  --entrypoint integration \
  erigontech/erigon:v3.5.0 \
  stage_exec --datadir=/data --chain=mainnet --unwind=100 --block=25393070

这里有一个现场教训:stage_exec 不是一个秒级修复命令。 它持续写 MDBX 数据,docker stats 里的 Block I/O 一直增长,超过数百 GB 仍在工作。 这不一定代表卡死,但在生产恢复窗口里,它不是一个很干净的结论。

如果用 docker run --rm 跑临时 integration 容器,还要注意两个点:

  • 容器退出后可能自动删除,最后日志会不容易拿。
  • 最好提前用 docker wait 或日志跟随命令捕捉退出码和尾部日志。

为什么切到删除 chaindata

上游 issue 里有人提到,保留 snapshots、删除 chaindata 后可以恢复 [1]

这条路线的含义不是“清空整个节点”。 关键是只删执行数据库:

/data/chaindata

不要顺手删除:

/data/snapshots
/data/downloader
/data/logs

保留 snapshots 后,节点可以从本地已有数据重建执行状态。 代价仍然存在:Execution History、trie、索引、TxLookup 等阶段要重新补。 但它比删除整个 /data 小得多。

实际执行顺序

先确认没有主 Erigon 和 integration 进程同时写 /data

docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Command}}'
ps -eo pid,stat,pcpu,pmem,etime,args | grep -E 'erigon|integration stage_exec' | grep -v grep

停止临时 execution 容器:

docker stop -t 60 <temporary-integration-container>

再次确认没有 integration stage_exec

ps -eo args | grep -E 'integration stage_exec' | grep -v grep

删除 chaindata

rm -rf /data/chaindata
test ! -e /data/chaindata && echo chaindata_deleted

启动原来的 Erigon 容器:

docker start erigon

启动后,RPC 一开始可能回到 0x0。 这是预期现象:执行数据库被重建了,stage 需要重新推进。

{
  "currentBlock": "0x0",
  "stages": [
    { "stage_name": "Execution", "block_number": "0x0" },
    { "stage_name": "TxLookup", "block_number": "0x0" },
    { "stage_name": "Finish", "block_number": "0x0" }
  ]
}

恢复过程怎么看

第一段日志会显示它复用本地 snapshots:

[1/6 OtterSync] Skipping SyncSnapshots, local preverified. Use snapshots reset to resync

然后会补执行历史:

Downloading Execution History progress=30363/36158

这段期间 eth_blockNumber 可能暂时不动。 不要急着判断失败。

之后进入 block 插入和执行:

[BlockCollector] Inserting blocks from=25392000 to=25392999
[BlockCollector] Inserting blocks from=25393000 to=25393999
[4/6 Execution] parallel starting from=25392365 to=25393999

真正的验证点是执行日志越过坏块:

[4/6 Execution] parallel executed blk=25392996
[4/6 Execution] parallel executed blk=25393199

25393199 已经大于 25393069。 这说明这次重建已经越过原来的失败点。

随后 RPC stage 提交到更高位置:

eth_blockNumber = 0x1837b4f
Execution      = 0x1837b4f
TxLookup       = 0x1837b4f
Finish         = 0x1837b4f

0x1837b4f25393999。 到这里,旧的 gas used mismatch block=25393069 没有再次出现。

可复用 runbook

如果 Erigon archive node 稳定卡在同一个执行块:

  1. 先保留现场日志,记录精确 block number、header gas、execution gas。
  2. 确认真实运行镜像和参数,不要只看源码目录或 Terraform 里的期望值。
  3. eth_syncingintegration print_stages 看各 stage 卡在哪里。
  4. 可以先尝试 clear_bad_blocks,但不要把它当成一定能恢复的修复。
  5. stage_exec 前,确认只有一个进程会写 datadir。
  6. 如果走 chaindata reset,只删除 /data/chaindata,保留 snapshots。
  7. 重启后不要只看 eth_blockNumber,还要看 Execution HistoryExecutionTxLookupFinish
  8. 最后用“是否越过原失败块”作为验证点,而不是只看容器是否 up。

最关键的边界是:不要同时让主 Erigon 和 integration 写 /data

这次故障的教训

固定区块的 gas mismatch,不能用普通重启来解释。 它要么是客户端执行规则问题,要么是本地执行数据库状态和当前客户端不兼容。

这次 v3.5.0 仍然会在 25393069 复现。 轻量命令能清标记、能重跑部分 stage,但没有在可接受窗口内给出明确恢复结果。 删除 chaindata 后,Erigon 从本地 snapshots 重建执行状态,最终越过了坏块。

所以这类故障的判断顺序应该是:

  • 先确认是不是固定区块。
  • 再确认上游 issue 里是否有同类报告。
  • 先做低风险检查。
  • 如果要 reset,尽量只 reset 必要层级。

这不是优雅的修复。 但在一个归档节点需要尽快恢复的现场,它是边界清楚、结果可验证的 workaround。

参考资料

  1. erigontech/erigon issue #22019: sync stopped around block 25393072/25393073
  2. Erigon v3.5.0 integration command README
  3. Erigon v3.5.0 release