見出し画像

02-06-24 Solanaメインネットベータ障害報告

原題:https://solana.com/news/02-06-24-solana-mainnet-beta-outage-report

9 February 2024, by Anza

このリポートは Anzaによって発表されたものである。

タイムライン

2024-02-06 09:53 UTCにSolanaメインネットベータのブロックファイナライズが停止した。様々なエコシステムチームのエンジニアが直ちに状況のトリアージを開始し、原因は最近のDevnet停止の調査中に特定されたバグと一致すると判断し、パッチが間もなくデプロイされることになりました。このパッチはクラスタの再起動時に即座に適用されるように若干修正され、v1.17.20リリースにはこの変更が含まれていた。同時にバリデータオペレータは再起動の指示を調整し、246,464,040をクラスタを開始するために利用可能な最も高い楽観的に確認されたスロットとして決定し、その時点から適切なスナップショットを準備していた。コンセンサスの進行は14:55 UTCに再開され、インシデントの総時間は約5時間となった。

根本原因分析

予備知識

Solana Labsのバリデータ実装では、プログラムを参照するトランザクションを実行する前に、すべてのプログラムをJITコンパイルする。過剰な再コンパイルを避けるため、頻繁に使用されるプログラムのJIT出力はキャッシュされる。

歴史的に、このキャッシュはExecutorsCache を介して実装されており、その構造は親ブロックから新しいブロックにコピーされ、アカウンティング情報が重複し、分岐イベントの幅のために追加の再コンパイルが必要でした。v1.16 リリース・ブランチでは、ExecutorsCacheはLoadedPrograms と呼ばれる新しい実装に置き換えられました。

LoadedProgramsの関連する目的は、キャッシュされたプログラムビューをグローバルにし、フォークアウェアにすることで、会計情報の重複を減らし、トランザクション実行スレッドが新しいプログラムを協調してロードできるようにすることで、スレッドが互いの進捗をブロックする原因となるJITコンパイルの競合を防ぐことである。フォーク・アウェアネス実装の一部は、キャッシュ・エントリが置換されるオンチェーン・プログラム・データによって無効になるタイミングを検出するために、各プログラム・デプロイメントの有効スロット高さ(プログラムがアクティブになるスロット)を追跡することである。協調ローディング・ストラテジーは、他のプログラムによって参照された各プログラムの使用統計を保持します。これには、JIT出力が立ち退きや無効化によってアンローディングされたプログラムも含まれ、立ち退きパフォーマンスを向上させます。

バグ

最新のローダーでデプロイされたプログラムの場合、LoadedProgramsは、プログラムのオンチェーンアカウントに保存されている会計情報を使用して、最新のデプロイメントスロットを検索し、これを使用して有効スロットの高さを計算することができます。ただし、レガシー・ローダーでデプロイされたプログラムの場合、デプロイメント・スロットはアカウントに保持されないため、LoadedProgramsは レガシー・ローダー・プログラムに遭遇するたびに、センチネルの有効スロットの高さを ゼロとして使用します。プログラムのバイトコードが置き換えられたことを示す、実際のデプロイ命令が観測された場合は、このルールに例外があります。この場合、LoadedProgramsは、どのローダーがプログラムをデプロイするために使用されるかに関係なく、真の実効スロット高さを持つ対応するエントリをそのアカウンティング・テーブルに挿入します。しかし、このエントリは、トランザクションによって参照されたことがないため、消去される可能性が非常に高い。これが発生すると、JIT出力は破棄され、プログラムのアカウンティング・エントリは、そのステータスがアンロードされたことを示し、有効 スロットの高さを保持 するものに置き換えられる。

次にトランザクションがこのプログラムを参照するとき、LoadedProgramsは正しく、アンロードされたステータスのために再コンパイルされることを要求する。コンパイルが完了すると、プログラムの有効スロットの高さに新しい会計項目が挿入される。次のLoadedProgramsのメインループの繰り返しで、新しくロードされたプログラムが表示され、トランザクション実行のために戻される。しかし、レガシー・ローダー・プログラムの場合、新しいJIT出力は、センチネル有効スロットの高さゼロに挿入される。これにより、新しいエントリーはロードされていないエントリーの後ろに配置されるため、LoadedProgramsからは 実質的に見えなくなります。そのため、mainloopを繰り返し実行するたびに、同じプログラムの再コンパイルが発生する。これが古典的な無限ループの原因だ。

これだけでは、影響を受けるプログラムを参照するトランザクションを実行しようとするリーダーをストールさせるのに十分なだけである。対応するブロックがブロードキャストされることはなく、トリガーとなったトランザクションがクラスタの残りの部分に伝搬されることもない。しかしv1.16のLoadedProgramsには 協調ローディング機能が実装されていなかったため、縮退ケースに対する脆弱性はなかった。このため、トリガーとなるトランザクションをブロックに格納し、そのブロックを残りのバリデータに配布して、リプレイ中に無限ループに突入させることができる。障害発生時、クラスタの95%以上のステークが1.17を実行していたため、ほぼすべてのバリデータがこのブロックで停滞していた。その結果、コンセンサスは回復不能に停止した。

修正

このバグは、前週に発生したDevnet停止の原因として特定されていた。このバグを引き起こす可能性のある2つのレガシーローダーのうち、1つ(「v1」)はすでにデプロイ不能になっており、もう1つ(「v2」)は非推奨でv1.18リリースサイクル中にデプロイ不能になる予定だった。選択された緩和策は、v2のデプロイ無効化の変更をv1.17にバックポートし、機能ゲートを削除して、クラスタの再起動時に "v2 "を直ちにデプロイ無効化することでした。この修正により、バグのトリガーに必要な前提条件を作成する機能がなくなり、よりシンプルな解決策となりました。より完全な修正は、LoadedProgramsのさらなる改善とともに含まれ、通常のリリース・サイクルで安定するようになります。

tl;dr

レガシー・ローダー・プログラムのデプロイ・エビクト・リクエスト・サイクルが、JITキャッシュの無限リコンパイル・ループを引き起こした。

この記事が気に入ったらサポートをしてみませんか?