web3-infra
把 10 TB Avalanche Archive Node 迁移到 30 TB 本地 NVMe:一次低停机切换复盘
一次 Avalanche C-Chain archive/indexing 节点迁移记录:从高成本 io2 切换到 i7ie 本地 NVMe RAID0,用两阶段 rsync 保留约 10 TB 数据、NodeID 和监控,并正确理解后续 bootstrap。
这次切换的目标不是从零部署一个 Avalanche 节点,而是把一个已经积累约 10 TB 数据的 archive/indexing 节点,从高成本的 EBS io2 迁移到带本地 NVMe instance storage 的 EC2,同时尽量缩短停机时间,并保留原来的 NodeID、数据库和监控关系。
最终落地的是一台 i7ie.12xlarge:4 块 7.5 TB 本地 NVMe 组成 RAID0,原始容量 30 TB,格式化后约 27.3 TiB。迁移完成后,数据使用约 9.6 TB,剩余约 18 TB。
这条路线换来了很高的本地存储性能,但也带来一个必须写在最前面的限制:EC2 一旦 stop 或 terminate,本地 NVMe 数据就会丢失。reboot 可以,stop 不可以。
为什么选择本地 NVMe
旧节点使用大容量 io2。它的优点是持久、可以独立于实例保留,也容易制作 EBS snapshot;缺点是当容量和 IOPS 都很高时,月度成本会非常明显。
i7ie 这类 storage-optimized 实例把 NVMe SSD 直接挂在宿主机上。对于区块链数据库,它有三个吸引人的特点:
- 不经过 EBS 网络路径,随机读写延迟更低。
- 多块本地盘可以组成 RAID0,聚合吞吐和 IOPS。
- NVMe 容量包含在实例价格中,不再单独支付同规模 io2 的容量和 IOPS 费用。
代价也同样明确:
- local instance storage 不能像 EBS 一样脱离实例长期保存。
- stop/start 会把实例调度到其他宿主机,本地数据随之消失。
- RAID0 没有冗余,任意一块盘故障都会破坏整个阵列。
- 稀缺规格不一定随时有容量,需要先解决实例供给问题。
因此,这不是“更安全的磁盘”,而是“用更强的本地性能换取更严格的运行约束”。
先确保拿到正确规格
30 TB 方案依赖 i7ie.12xlarge 自带的 4 块本地 NVMe。目标区域里最初没有即时容量,所以通过 EC2 Fleet 持续请求指定实例规格,而不是临时降级到较小实例。
这里有两个关键原则:
- 不为了尽快启动而降级到 15 TB 方案。archive 节点后续扩容困难,容量不足时并不能给本地 RAID 在线追加一块 EBS 就自然解决。
- Fleet 获得实例后,立即删除 Fleet 但保留实例,避免后续补充出多余容量;同时开启实例停止保护。
等待容量期间,旧节点继续运行,不提前进入停机窗口。
把四块 NVMe 组成 RAID0
实例就绪后,先识别 Amazon EC2 NVMe Instance Storage,确认四块数据盘没有和系统 EBS 盘混淆,再创建阵列:
mdadm --create /dev/md0 \
--level=0 \
--raid-devices=4 \
/dev/nvme1n1 /dev/nvme2n1 /dev/nvme3n1 /dev/nvme4n1
文件系统使用 ext4,并把 reserved blocks 调整为 0%。对 30 TB 数据盘来说,默认预留比例会锁住非常可观的容量;这个盘只保存节点数据,不承担系统根文件系统的职责。
mkfs.ext4 -m 0 /dev/md0
mkdir -p /data
mount /dev/md0 /data
验证不能只看 df。至少要同时确认:
mdadm --detail /dev/md0
df -h /data
lsblk -o NAME,SIZE,TYPE,MOUNTPOINTS,MODEL
期望看到:
- RAID level 是
raid0。 - 4 块盘都是 active/working。
- failed devices 为 0。
- 阵列状态为
clean。 /data文件系统约 28 TB,来源是/dev/md0。
用两阶段 rsync 缩短停机窗口
直接停旧节点再复制 10 TB 数据,会产生很长的不可用时间。这里采用两阶段复制。
第一阶段:旧节点在线时复制大部分数据
旧 AvalancheGo 保持运行,把数据库、staking key、配置和其他节点目录复制到新机器。
在线复制 LevelDB/Pebble 数据时,文件仍在发生变化,因此第一阶段出现 file has vanished 并不意外。它说明某些文件在扫描和传输之间被数据库删除或替换,不能把这次复制直接当成最终一致副本。
第一阶段的目标只是搬走绝大多数静态数据,减少最终停机后需要传输的差异。
第二阶段:干净停止旧进程后复制增量
第一轮完成后:
- 干净停止旧 AvalancheGo,等待容器正常退出。
- 再执行一次 rsync,覆盖变化文件并删除目标端多余文件。
- 只在第二阶段返回成功后,才把新目录视为可启动副本。
这次第一阶段迁移了约 10.4 TB;旧进程停止后,最终增量仍有约 670 GB。这个数字说明:对于持续写入的大型链数据库,不能假设最后一次同步只是几分钟的小增量。
保留 NodeID 和运行配置
数据复制完成后,新容器继续挂载同一个 AvalancheGo 数据目录,并复用原来的 staking certificate/key。启动后首先确认 NodeID 与旧节点一致。
需要验证的内容包括:
- AvalancheGo 版本固定,不使用漂移的
latesttag。 - NodeID 保持一致。
- C-Chain archive、state sync 和 transaction indexing 配置没有变化。
- 数据目录、chain config 和节点 config 都从新
/data挂载。 - RPC 与 P2P 端口正在监听。
- 容器 restart count 为 0。
这次 C-Chain 的关键配置是:
{
"pruning-enabled": false,
"state-sync-enabled": false,
"trie-clean-cache": 8192,
"trie-dirty-cache": 8192,
"snapshot-cache": 2048,
"trie-prefetcher-parallelism": 48,
"metrics-expensive-enabled": false
}
没有开启 state sync,是因为该节点需要完整历史数据。state sync 可以很快跳到近期状态,但官方文档明确指出,它不适合需要历史数据的场景。
没有跳过 transaction indexing;transaction-history=0 继续使用无限历史索引语义。
迁移完成不等于立即同步完成
新节点成功读取数据库后,C-Chain 高度仍明显落后于网络。复制过来的数据库大约停留在 6900 万高度,而网络已经超过 9000 万高度,因此还需要补齐并执行约 2150 万个区块。
日志先后经历了几个阶段:
fetching blocks
compacting database before executing blocks
executing blocks
下载完缺失区块只是第一步。EVM 必须按顺序执行这些区块,重新计算状态并写入数据库。即使机器有 48 vCPU、数百 GiB 内存和 4 块 NVMe RAID0,这部分也不能简单地按 CPU 核数并行。
官方源码在普通 bootstrap 的规划逻辑里使用约 14 blocks/s 作为参考速度。按这个数量级,执行 2150 万区块可能需要数周;实际时间要根据运行数小时后的稳定吞吐重新估算。
如何理解 Generating state snapshot
启动后还会看到类似日志:
Generating state snapshot
accounts=14044
slots=799590
storage=61.57MiB
eta=1086h
这里的 snapshot 是 Coreth/EVM 内部的扁平状态快照,不是 AWS EBS snapshot。
accounts是已经扫描的账户数量。slots是已经处理的合约 storage slots。storage是当前生成的快照大小。at是 trie 遍历位置。eta是根据早期、非常不均匀的 trie 遍历速度估出来的时间。
只要 slots、storage 和遍历位置持续推进,就不是卡死。早期出现数百或上千小时 ETA 不应直接作为完成时间。
当前 snapshot-wait=false,所以状态快照在后台生成,不会等待它完成才开始区块执行。把它改成 true 反而会阻塞启动。
缓存参数并不是越大越快
公开配置的默认值大致是:
| 参数 | 默认值 | 本次配置 |
|---|---|---|
trie-clean-cache | 512 MB | 8192 MB |
trie-dirty-cache | 512 MB | 8192 MB |
snapshot-cache | 256 MB | 2048 MB |
trie-prefetcher-parallelism | 16 | 48 |
当前值已经是默认值的数倍到十几倍。运行时观察到 CPU、内存和 I/O wait 都没有达到资源上限,因此继续把 cache 翻倍,不会解决区块顺序执行这个根本瓶颈。
社区里有人尝试过增大 Pebble cache、memtable 和 compaction worker,但公开结果来自其他 Avalanche L1,而且伴随代码修改,没有形成被上游接受的 C-Chain 参数模板。这类配置更适合在确认 compaction debt 或 write stall 是瓶颈后做 A/B 测试,而不是在长时间 bootstrap 中途盲目重启。
把 Prometheus 目标切到新节点
新节点运行后,还需要迁移可观测性:
- node-exporter 暴露主机和 RAID/文件系统指标。
- AvalancheGo 暴露节点、网络、链和 bootstrap 指标。
- Prometheus 的两个 target 都从旧私网地址切到新私网地址。
这里遇到一个容易忽略的问题:Prometheus 配置以单文件 bind mount 进入容器时,host 上使用 sed -i 会创建新 inode,而容器可能仍然持有旧 inode。host 文件看起来已经更新,容器内却仍然是旧内容。
更稳妥的验证顺序是:
- 修改配置。
- 在 Prometheus 容器内部再次读取实际配置。
- 使用
promtool验证。 - reload Prometheus。
- 查询
/api/v1/targets或up指标,确认 node-exporter 和 AvalancheGo 两个 target 都为 1。
不要只因为配置文件 diff 正确,就认为监控已经切换成功。
稳定性检查
节点切换后的基础检查包括:
| 检查项 | 期望结果 |
|---|---|
| EC2 state | running |
| AvalancheGo | running,restart count 为 0 |
| node-exporter | running,restart count 为 0 |
| RAID0 | clean,4 块 active,0 failed |
/data | 容量约 28 TB,使用率正常 |
| NodeID | 与迁移前一致 |
| peers | 稳定连接数百个 peers |
| Prometheus | 主机和 AvalancheGo target 都是 up |
| bootstrap | 执行数量持续增加,没有长时间停滞 |
bootstrap 完成前,health.health 返回 healthy=false 是预期行为。重点看失败原因是否只是 subnets not bootstrapped,同时确认 database、diskspace、network 和 router 检查正常。
node is not a validator 对纯 archive/RPC/indexing 节点也不是错误。
删除旧资源意味着放弃回滚
旧 EC2、io2 volume 和 snapshot 最终被删除后,就不再存在快速回滚路径。这个动作和“流量已经切到新节点”不是同一个风险等级。
更稳妥的通用流程应该是:
- 新节点完成 bootstrap。
- RPC/indexing 查询经过一段稳定验证。
- 确认监控、告警和调用方都已切换。
- 再决定保留旧 EBS 或 snapshot 多久。
如果业务选择提前清理旧资源,就应该明确记录:这是为了立即停止旧资源费用而接受的风险,而不是一个无损、可随时撤销的动作。
这次迁移最重要的经验
- 本地 NVMe 是 SSD,但它首先是一种临时 instance storage,不是便宜版 EBS。
- 30 TB RAID0 提供容量和性能,不提供磁盘冗余。
- 大型在线数据库迁移要使用“在线全量 + 停机增量”,不能只做一次在线 rsync。
- 迁移 NodeID 需要复制 staking identity,不能只复制 chain database。
- 区块已下载不等于 bootstrap 完成;历史 EVM state transition 仍然要执行。
- snapshot ETA 和 bootstrap ETA 是两件事,而且早期 ETA 波动很大。
- 已经远高于默认值的 cache 参数,不应该在没有瓶颈证据时继续翻倍。
- 配置文件已修改不等于容器已加载,监控切换必须从 Prometheus target 反向验证。
- 一旦旧实例、volume 和 snapshot 被删除,就要把“没有回滚”当成当前运行状态的一部分。
- 对 local NVMe 节点,最重要的运行规则是:可以 reboot,绝不能 stop。
这类迁移真正困难的部分,不是创建一台更大的 EC2,而是同时处理容量供给、数据一致性、身份继承、bootstrap 语义、监控切换和不可回滚的存储边界。