cloud-sre
Erigon 主网归档节点卡在 25393069:为什么最后只删 chaindata
一次 Ethereum Mainnet Erigon 归档节点现场修复记录:v3.5.0 仍在 25393069 出现 gas used mismatch,轻量修复没有马上恢复,最后保留 snapshots、删除 chaindata 后重建通过。
这次故障很像“只坏了一个区块”。
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 和历史状态查询。 |
| Erigon | Ethereum 执行客户端之一,使用 staged sync,把下载、执行、索引、trie 等阶段拆开处理。 |
| Datadir | 节点的数据目录。这里是 /data,里面同时有 snapshots、chaindata、日志等内容。 |
| chaindata | Erigon 的主执行数据库目录。删除它会让节点重建执行状态,但不等于删除所有 snapshots。 |
| Snapshot | 预构建的链数据文件。保留 snapshots 可以避免从创世块线性重新下载和执行全部历史。 |
| Gas used mismatch | 节点自己执行区块后算出的 gas used,和区块头里的 gas used 不一致。执行客户端会把这个区块判为无效。 |
| Staged sync | Erigon 的同步模型。Headers、Bodies、Senders、Execution、TxLookup、Finish 等阶段会分别推进。 |
现场现象
节点配置是普通 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”操作。 可选路线只有两类:
- 证明这是坏块标记、索引或局部状态问题,然后做局部修复。
- 放弃当前执行数据库,从可靠的 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
0x1837b4f 是 25393999。
到这里,旧的 gas used mismatch block=25393069 没有再次出现。
可复用 runbook
如果 Erigon archive node 稳定卡在同一个执行块:
- 先保留现场日志,记录精确 block number、header gas、execution gas。
- 确认真实运行镜像和参数,不要只看源码目录或 Terraform 里的期望值。
- 用
eth_syncing和integration print_stages看各 stage 卡在哪里。 - 可以先尝试
clear_bad_blocks,但不要把它当成一定能恢复的修复。 - 跑
stage_exec前,确认只有一个进程会写 datadir。 - 如果走
chaindatareset,只删除/data/chaindata,保留 snapshots。 - 重启后不要只看
eth_blockNumber,还要看Execution History、Execution、TxLookup、Finish。 - 最后用“是否越过原失败块”作为验证点,而不是只看容器是否 up。
最关键的边界是:不要同时让主 Erigon 和 integration 写 /data。
这次故障的教训
固定区块的 gas mismatch,不能用普通重启来解释。 它要么是客户端执行规则问题,要么是本地执行数据库状态和当前客户端不兼容。
这次 v3.5.0 仍然会在 25393069 复现。
轻量命令能清标记、能重跑部分 stage,但没有在可接受窗口内给出明确恢复结果。
删除 chaindata 后,Erigon 从本地 snapshots 重建执行状态,最终越过了坏块。
所以这类故障的判断顺序应该是:
- 先确认是不是固定区块。
- 再确认上游 issue 里是否有同类报告。
- 先做低风险检查。
- 如果要 reset,尽量只 reset 必要层级。
这不是优雅的修复。 但在一个归档节点需要尽快恢复的现场,它是边界清楚、结果可验证的 workaround。