Back home

Rust/Wasm ランタイムの信頼性を確保するには、パニックとリカバリの中止の両方を処理する必要があります

共有 Wasm インスタンスが長時間呼び出しを受け付け始めると、クラッシュは単一の障害から状態回復と障害分離の問題にまで拡大します。

Wasm は、最初は移植レイヤーとして簡単に考えることができます。コードはプログラムでき、ページは実行でき、パフォーマンスは問題なく、物事はほぼ同じであるように見えます。実際に難しくなり始めるのは、通常はデモが終わった後です。エディター、レンダラー、ドキュメント パーサーなどのモジュールが単一ページの実験から長期常駐ランタイムに移行すると、障害モデルはすぐに変更されます。

現時点では、パニックと中止は言語層の例外ブランチではなくなりました。彼らが決定するのは、このインスタンスが後続の作業を受信し続けることができるかどうか、メモリ内の状態が汚染されているかどうか、ホスト層がインスタンスを直ちに破棄する必要があるかどうか、インスタンス プールを埋める必要があるかどうかです。モバイル チームがネイティブ コンテナで長期間実行されていたカーネルを Web に移行する場合、最も過小評価されやすいのはこの変更層です。

デモが完了すると、障害モデルが開始されたばかりです。

1 回の呼び出しでのクラッシュを理解するのは難しくありません。ボタンをクリックすると Wasm 呼び出しがトリガーされます。失敗した場合は、操作に関するエラーが報告されます。ページを更新して、もう一度お試しください。コストはまだコントロール可能です。

この問題は、ランタイムがインスタンスの再利用を開始した後に発生します。同じ Wasm インスタンスが継続的に複数のドキュメントを開き、複数ラウンドの入力イベントを受信し、複数の JS ブリッジ呼び出しを通過する場合、パニックとアボートの影響範囲は現在のアクションにとどまりません。不完全な失敗は、後続のリクエストに影響を与える可能性があります。

このようなリスクは、初日には気づかれないことがよくあります。最初の段階では、通常、散在するエラー レポートのみが表示されます。たとえば、時折レンダリングが失敗する、特定のエクスポートが停止する、特定のドキュメントを閉じて再度開いた後に不正な状態になるなどです。さらに確認していくと、手がかりは徐々に同じ現象に収束していきます。つまり、コール チェーンで障害が発生しているにもかかわらず、共有インスタンスには損傷が残っているということです。

この時点で、議論の焦点はもはや「Rust コードがパニックになるかどうか」ではなく、「このランタイムがパニック後に次の呼び出しを処理し続ける資格があるかどうか」になります。

パニックは捕捉できますが、中止はインスタンスの変更のみ可能です。

Rust/Wasm で分離すべき最も重要なことは、パニックと中止の 2 つの失敗セマンティクスです。

パニックには、確立された境界線に沿ってリラックスする機会もあります。バインディング層とホスト層が事前に回復方法に同意している限り、現在の呼び出しは失敗する可能性がありますが、インスタンス内の他の状態も維持できます。中絶は決して良い方法ではありません。これは、現在の実行が回復不可能な状態に達したことを意味します。同じインスタンスを使用してリクエストを受信し続ける場合は、基本的に、メモリとリソースが途中で破損しないことに賭けることになります。

実行時にこの 2 つが混在すると、後続の処理で確実に問題が発生します。

  • 中止は通常の例外として受け入れられ、インスタンス プールは信頼性を失ったオブジェクトを再利用し続けます。
  • すべてのパニックをインスタンスを破棄する必要があるかのように扱うと、スループットが不必要に低下します。
  • JS ホストは「呼び出しが失敗した」ことだけを知っていますが、再試行するか、インスタンスを失うか、現在のセッションを切断するかはわかりません。

これは、Wasm ランタイムの信頼性に関して最も現実的なことでもあります。回復セマンティクスは、その後の分離とスケジューリングを実装する前に、最初に定義する必要があります。

バインディング層が回復セマンティクスを提供しない場合、ホスト層は不良状態となり、作業を受け入れ続けます。

この種の問題で最も危険な場所は、ビジネスコードではなく、「すでに対処されている」ように見えるバインディング層です。多くの場合、ホスト層はスローされたエラー オブジェクトのみを認識し、それを通常の呼び出し失敗として記録します。ログは存在し、ページはすぐにはクラッシュしませんが、システムがインスタンス内に悪い状態を残した可能性があります。

本当に修正する必要があるのは、try/catch だけではなく、失敗後の処理アクションです。次のようなロジックが信頼性設計に入り始めたばかりです。

async function runWithRecovery(instance, input) {
  try {
    return await instance.exports.handle(input)
  } catch (error) {
    if (isAbort(error)) {
      pool.replace(instance.id)
    }
    throw error
  }
}

このコードの焦点は構文ではなく、現在の障害によってこのインスタンスが信頼できないオブジェクトとしてマークされているかどうかという単純な判断にあります。答えが「はい」の場合、回復アクションはエラーのスローにとどまらず、インスタンスの削除、リソースの再構築、リクエスト フローの切断まで継続する必要があります。

この層が明確に定義されていない限り、システムはエラーを処理しているように見えますが、実際に行っているのは、破損した可能性のあるランタイムを運用パスに戻すことです。

共有インスタンスは、リカバリの問題をプーリング戦略の問題に拡大します

Wasm を実際の製品に導入した後、「ページが閉じるまで 1 回のインスタンス」しか発生しないことはほとんどありません。より一般的なのは、一連のランタイム リソースを共有するインスタンス プール、ワーカー プール、またはフォアグラウンド ドキュメントとバックグラウンド タスクです。この段階では、パニックと中止による回復コストがプーリング戦略を直接書き換えることになります。

インスタンスの初期化にコストがかかる場合、システムは当然、それを可能な限り再利用する傾向があります。ただし、再利用が確立されたら、障害分離も同時にアップグレードする必要があります。

  • どの状態が 1 回の通話でのみハングアップされ、失敗後に通話とともに破棄されるのか
  • どのキャッシュを呼び出し間で保持できるか、また、アボートが発生したらどのキャッシュを完全に無効にする必要があるか
  • インスタンスが置き換えられた後、キューに入れられたタスクはどのように移行されますか?再試行すると副作用が 2 回発生しますか?

これらは、言語層が自動的に送信する回答ではありません。これらはランタイム設計です。

このため、Rust/Wasm の信頼性に関する議論が「パニックに陥る可能性があるかどうか」という点にとどまると、問題を過小評価してしまいがちです。メンテナンス コストのギャップを実際に拡大するのは、障害発生後にインスタンス プールが明確な信頼境界を維持できるかどうかです。

適用される境界はライフサイクルと強く関係します

この修復デザインのセットは、すべての Wasm プロジェクトに必要なわけではありません。

モジュールが 1 回限りのオフライン ツールである場合、またはページが破棄されたときにインスタンス全体がリサイクルされる場合、パニックと中止の違いは依然として存在しますが、回復の利点ははるかに小さくなります。多くの場合、ページを直接更新してタスクを直接再実行するだけで十分です。

システムが次の特性を持つと、回復セマンティクスは「最適化項目」から「インフラストラクチャ項目」にすぐに変わります。

  • インスタンスは長期間存在し、単一ページのライフサイクルとともに破棄されません。
  • 同じインスタンスが複数ラウンドの呼び出しを継続的に実行します。
  • ホスティング層は起動時間とスループットと引き換えにプーリングを使用する必要がある
  • 失敗後にセッション状態、キャッシュ状態、キューに入れられたタスクを保護する

モバイル チームがネイティブ機能を Web に移行する場合、この境界に遭遇する可能性が最も高くなります。アプリケーション プロセスで元々デフォルトで確立されていた分離関係は、多くの場合、JS/Wasm ホスト境界に達した後に再度埋める必要がありました。

Wasm を使用すると、ネイティブ コードがブラウザに簡単に入力できるようになりますが、ランタイム回復セマンティクスは組み込まれません。システムがインスタンスの共有、状態の再利用、および長期呼び出しの受け入れを開始するとすぐに、パニックと中止を 2 つの異なるランタイム イベントとして扱う必要があります。前者は現在の呼び出しを終了する方法を考慮し、後者はこのインスタンスがプール内で存続できるかどうかを考慮します。この判断を先に行わないと、コード移植が成功すればするほど、その後の失敗への対処が難しくなります。