web3-infra

Shiden ノードのダウングレード後に起きた二重障害:RocksDB のロック競合と litep2p panic

Shiden archive RPC ノードのダウングレード事例。backup コンテナによる RocksDB ロック取得、旧 primary の litep2p panic、チェーンデータを削除しない復旧手順を整理する。

Jul 15, 2026
ShidenSubstrateDockerRocksDB障害復旧

この障害は、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_syncStatecurrentBlockhighestBlock は一致し、ログは新しい 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 も消えていました。

比較条件を三つに絞りました。

  1. 自動 retry を無効化し、待機後に旧 primary を一度だけ cold start する。
  2. image、起動引数、bind mount の /data は変えない。
  3. コンテナ 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 で、currentBlockhighestBlock が一致。
  • Shiden と Kusama のログで block import が継続。
  • eth_chainIdeth_blockNumber が正常な値を返す。

再利用できるダウングレード・復旧チェックリスト

ダウングレード前

  • instance type、container ID、image digest、起動引数、mount を記録する。
  • system_healthsystem_syncState、EVM RPC の結果を保存する。
  • primary だけでなく、/data を参照する全コンテナを列挙する。
  • backup コンテナを restart=no にする。
  • stop/start が data volume を置換・削除しないことを確認する。

LOCK エラーが出たとき

  • lsof または fuser で実際の所有者を確認する。
  • pgrepdocker ps -adocker 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 を提供すると、監視上は正常に見えることがあります。

今後の復旧判定は四層に分けます。

  1. host と disk が正常。
  2. database lock の所有者が一つだけ。
  3. parachain と relaychain が進行。
  4. consumer が必要とする RPC namespace がすべて利用可能。

四層すべてが通って初めて復旧完了です。

参考資料

  1. RocksDB Wiki: Basic Operations
  2. Docker Docs: docker container run — Restart policies
  3. Astar Docs: Node CommandsYour Own RPC Server
  4. Docker Docs: Bind mounts