web3-infra
Shiden ノードのダウングレード後に起きた二重障害:RocksDB のロック競合と litep2p panic
Shiden archive RPC ノードのダウングレード事例。backup コンテナによる RocksDB ロック取得、旧 primary の litep2p panic、チェーンデータを削除しない復旧手順を整理する。
この障害は、EC2 インスタンスの通常のダウングレードから始まりました。
変更前の Shiden archive RPC ノードはチェーン先端まで同期していました。peer、RPC、コンテナ、ホストの確認にも異常はありません。ところが再起動後、二つのエラーが順番に現れました。
IO error: While lock file: /data/chains/shiden/db/full/LOCK:
Resource temporarily unavailable
ロックを解放した後には、別のエラーが発生しました。
Thread 'tokio-runtime-worker' panicked at
'SelectNextSome polled after terminated'
復旧では LOCK を削除せず、データベースを reset せず、数 TB のチェーンデータも作り直していません。実施したのはロック所有者の特定、意図せず起動した backup コンテナの停止、bind mount された datadir を残した primary コンテナの再作成です。
以下は一件の運用事例から得た記録です。すべての Astar クライアントバージョンや構成で同じ動作になるという意味ではありません。
ダウングレード前、ノードは正常だった
インスタンス停止前に、次の四つを記録しました。
- EC2 の system、instance、EBS status check は正常。
- primary コンテナの restart count は 0 で、OOM kill もない。
system_healthは peer を返し、isSyncing: false。system_syncStateのcurrentBlockとhighestBlockは一致し、ログは新しい block を import し続けている。
この基準がなければ、再起動後の LOCK エラーを「以前からデータベースが壊れていた」と誤認しやすくなります。
インスタンスのダウングレードはプロセス、Docker daemon、ネットワーク接続を中断します。bind mount 上のチェーンデータを意図的に書き換える操作ではありませんが、残っているコンテナ、restart policy、起動順序の問題を表面化させます。
一つ目の障害:backup コンテナが RocksDB のロックを取得した
primary は次のエラーで繰り返し終了していました。
Error: Service(Client(Backend(
"IO error: While lock file: /data/chains/shiden/db/full/LOCK:
Resource temporarily unavailable"
)))
RocksDB の database directory を開けるのは一つのプロセスだけです。危険な同時アクセスを防ぐため、OS のロックを取得します [1]。
調べるべきなのは「LOCK を消す方法」ではなく、「どのプロセスが LOCK を持っているか」です。
まずコンテナと collator プロセスを列挙します。
docker ps -a --format 'Name={{.Names}} Status={{.Status}}'
pgrep -af astar-collator
次に OS からロック所有者を確認します。
lsof /data/chains/shiden/db/full/LOCK
# lsof がない場合
fuser -v /data/chains/shiden/db/full/LOCK
ロックを持っていた PID は、想定していた node-primary ではなく node-backup に属していました。以前の復旧作業で残した backup でしたが、restart policy が残っていました。ホスト復帰後、このコンテナが running になり、同じ /data を先に開いていました。
Docker の restart policy は exit と daemon lifecycle に対する動作を決めます。名前に backup が含まれていても、運用上停止状態である保証にはなりません [2]。
ノードが一部正常に見えた理由
この事例では RPC が完全には停止していませんでした。
system_health は peer と isSyncing: false を返し、system_syncState もチェーン先端を示していました。backup のログでは Shiden parachain と Kusama relaychain の block import も続いていました。
しかし EVM RPC は別です。
curl -sS -H 'Content-Type: application/json' \
--data-binary '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' \
http://127.0.0.1:9944
結果は Method not found でした。
backup は Substrate RPC を提供していましたが、primary にあった --enable-evm-rpc がありません。Astar のドキュメントでも、EVM RPC はデフォルトで無効であり、明示的に flag を追加する必要があると説明されています [3]。
一つの RPC method が正常でも、同じ port で必要な namespace がすべて利用できるとは限りません。
チェーンデータに触れずロック競合を解消する
安全な順序は、backup の自動起動を無効化し、正常終了させ、ロック解放を確認することです。
docker inspect node-backup \
--format 'Running={{.State.Running}} Policy={{.HostConfig.RestartPolicy.Name}}'
docker update --restart=no node-backup
docker stop --timeout 120 node-backup
lsof /data/chains/shiden/db/full/LOCK
ロック所有者がいないことを確認するまで、同じ datadir を使う別コンテナを起動してはいけません。
LOCK ファイルを直接削除する方法は採りません。RocksDB プロセスが生きている場合、ファイルを削除しても二つの writer が安全に共存できるようにはならず、重要な診断情報を失うだけです。
二つ目の障害:ロック解放後の litep2p panic
ロックエラーは消えましたが、旧 primary は litep2p 初期化後に終了しました。
[Relaychain] Running litep2p network backend
[Relaychain] Essential task `basic-block-import-worker` failed.
Thread 'tokio-runtime-worker' panicked at
'SelectNextSome polled after terminated'
この時点で、障害を引き続き LOCK の問題として扱うのは不正確です。lsof は競合プロセスがないことを示し、元のエラー signature も消えていました。
比較条件を三つに絞りました。
- 自動 retry を無効化し、待機後に旧 primary を一度だけ cold start する。
- image、起動引数、bind mount の
/dataは変えない。 - コンテナ metadata と Docker network state だけを置き換える。
旧コンテナは待機後も panic を再現しました。同じ image、引数、datadir で新しいコンテナを作ると正常に起動しました。
ここから言えることには限界があります。証拠は旧コンテナに紐づく runtime または network state の異常を示し、再作成によってその状態は消えました。ただし、この一件だけで litep2p 内部の厳密な原因まで証明したわけではありません。
データベースではなくコンテナを再作成する
チェーンデータが container writable layer ではなく host bind mount にあったため、この境界で再作成できました。Docker の bind mount は host path をコンテナ内に公開する仕組みで、新しいコンテナから同じ永続データを再マウントできます [4]。
削除前に mount を確認します。
docker inspect node-primary --format '{{json .Mounts}}'
host /data が container /data にマウントされていることを確認してから、停止済みの旧コンテナだけを削除します。
docker rm node-primary
以下は復旧コマンドを公開用に整理した例です。96g はこの環境のダウングレード後の host に合わせた値であり、他の環境では再計算が必要です。
docker run -d \
--name node-primary \
--restart=on-failure:3 \
--memory=96g \
--memory-swap=96g \
-p 30333:30333 \
-p 9944:9944 \
--mount type=bind,src=/data,dst=/data \
staketechnologies/astar-collator:latest \
astar-collator \
--pruning archive \
--blocks-pruning archive \
--enable-evm-rpc \
--name shiden-rpc \
--chain shiden \
--base-path /data \
--rpc-external \
--rpc-methods Safe \
-- \
--database paritydb \
--sync warp \
--state-pruning 256 \
--blocks-pruning 256
新コンテナが失敗しても、rollback で /data を変更する必要はありません。新コンテナを停止し、ロック解放を確認し、最後に動作していたコンテナを起動します。
本当に復旧したことを確認する
docker start の成功ではなく、元のエラー signature を regression check にしました。
docker inspect node-primary \
--format 'Running={{.State.Running}} Restarts={{.RestartCount}} OOM={{.State.OOMKilled}}'
lsof /data/chains/shiden/db/full/LOCK
docker logs --since 10m node-primary 2>&1 | \
grep -E 'While lock file|SelectNextSome polled after terminated|Essential task.*failed'
Substrate RPC を確認します。
curl -sS -H 'Content-Type: application/json' \
--data-binary '{"jsonrpc":"2.0","id":1,"method":"system_health","params":[]}' \
http://127.0.0.1:9944
curl -sS -H 'Content-Type: application/json' \
--data-binary '{"jsonrpc":"2.0","id":1,"method":"system_syncState","params":[]}' \
http://127.0.0.1:9944
続いて EVM RPC を確認します。
curl -sS -H 'Content-Type: application/json' \
--data-binary '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' \
http://127.0.0.1:9944
curl -sS -H 'Content-Type: application/json' \
--data-binary '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' \
http://127.0.0.1:9944
最終的な green signal は次の通りでした。
- primary は running を維持し、restart count は 0、OOM kill もない。
- backup はすべて stopped で、restart policy は
no。 - RocksDB のロック所有者は primary 内の collator。
isSyncingは false で、currentBlockとhighestBlockが一致。- Shiden と Kusama のログで block import が継続。
eth_chainIdとeth_blockNumberが正常な値を返す。
再利用できるダウングレード・復旧チェックリスト
ダウングレード前
- instance type、container ID、image digest、起動引数、mount を記録する。
system_health、system_syncState、EVM RPC の結果を保存する。- primary だけでなく、
/dataを参照する全コンテナを列挙する。 - backup コンテナを
restart=noにする。 - stop/start が data volume を置換・削除しないことを確認する。
LOCK エラーが出たとき
lsofまたはfuserで実際の所有者を確認する。pgrep、docker ps -a、docker inspectを照合する。LOCKを削除せず、二つの writable datadir user を起動しない。- 意図しないプロセスを停止し、ロック解放を確認する。
primary が引き続き失敗するとき
- 新しい signature を解決済みの LOCK エラーと分けて記録する。
- 自動 retry を止め、一回だけ制御された cold start を行う。
- OOM、exit code、Docker events、最初の fatal/essential-task log を確認する。
- mount を証明できたら、チェーンデータより先にコンテナ状態の再作成を検討する。
復旧後
- Substrate RPC と EVM RPC の両方を確認する。
- 一回の高さではなく、block import の継続を観察する。
- restart count、OOM、resource、disk、ロック所有者を確認する。
- rollback 用コンテナは stopped、
restart=noのまま保持する。
この障害で見えた運用上の問題
データベースがダウングレードで壊れたわけではありません。backup コンテナを「実行されない成果物」にしていなかったことが、ホスト再起動で表面化しました。
コンテナを rename しても runtime configuration は残ります。同じ writable datadir を予期せずマウントしたプロセスは、primary の起動結果を変えます。さらに backup が一部 RPC を提供すると、監視上は正常に見えることがあります。
今後の復旧判定は四層に分けます。
- host と disk が正常。
- database lock の所有者が一つだけ。
- parachain と relaychain が進行。
- consumer が必要とする RPC namespace がすべて利用可能。
四層すべてが通って初めて復旧完了です。