第9章: 実装

設計ドキュメントは、システムが何をすべきかを記述します。実装とは、その記述が動作するソフトウェアになるプロセスです。設計とコードの間のギャップは、微妙なバグが生まれ、パフォーマンスが勝敗を分け、システムの真の性格が現れる場所です。この章では、私たちが構築したすべてのシステムに共通するパターンと、その実装を導く原則を検討します。

実装パターン

プラネタリスケールコンピュータのすべてのサービスは同じ構造パターンに従います:インターフェースを定義する共有ライブラリ、ロジックを実装するサーバーバイナリ、そして並行リクエスト間で安全に管理される共有状態です。

共有ライブラリ(lib.rs)は、プロシージャ識別子、リクエストおよびレスポンスの構造体、そしてクライアント側のヘルパー関数を定義します。このファイルが契約です。サーバー(プロシージャを実装するため)とクライアント(それらを呼び出すため)の両方にインポートされます。ライブラリは共有されるため、変更は慎重に行う必要があります——プロシージャ識別子の変更なしにリクエスト構造体を変更すると、古いクライアントと新しいサーバー間の互換性が壊れます。

サーバーバイナリ(main.rs)は、プロシージャ識別子に基づいて受信リクエストを適切なハンドラにディスパッチするリクエストハンドラを実装します。各ハンドラはリクエストペイロードをデシリアライズし、操作を実行し、レスポンスをシリアライズします。サーバーは共有状態の初期化、ディスカバリへの登録、バックグラウンドタスクの開始も行います。

共有状態はArc<Mutex<T>>(またはリード負荷の高いワークロードの場合はArc<RwLock<T>>)でラップされ、複数のリクエストハンドラスレッドからの安全な並行アクセスを可能にします。これは非同期タスク間での共有可変状態のための標準的なRustパターンです。

バックグラウンドタスク

ほとんどのサービスは、リクエスト・レスポンスサイクルの外で行う作業が必要です。キャッシングサービスは期限切れエントリのクリーンアップのためにバックグラウンドタスクを実行します。モニタリングサービスは古いハートビートをチェックします。ディスカバリサービスは古い登録をクリーンアップします。ストレージサービスはコンパクションをトリガーします。

これらのバックグラウンドタスクは共通のパターンに従います:スリープ間隔でループする非同期タスクをスポーンし、共有状態のロックを取得し、メンテナンスを実行し、ロックを解放します。重要な制約は、バックグラウンドタスクがロックを長時間保持してはならないことです。さもなければリクエストハンドラをブロックしてしまいます。

エラー処理

私たちの実装はエラー処理に対してプラグマティックなアプローチを取ります。内部エラー(整形式の内部トラフィックでのデシリアライズ失敗など)はexpectを使用します——これらはランタイム条件ではなくバグを示します。外部エラー(他のサービスへの接続時のネットワーク障害など)は、通常クライアントへのエラーレスポンスの返却やバックオフ付きリトライによって、グレースフルに処理されます。

この区別は運用において重要です。expectからのパニックは何かが根本的に間違っていることを意味し、サービスを再起動すべきです。グレースフルなエラーは、サービスが正しく機能しているが環境で一時的な問題に遭遇したことを意味します。

テスト

インターフェースファーストの設計パターンは、自然にテストをサポートします。各サービスのインターフェースが型付き構造体として定義されているため、ユニットテストはサーバーを起動したりネットワーク呼び出しを行ったりすることなく、リクエストペイロードを構築し、ハンドラを直接呼び出し、レスポンスペイロードを検証できます。統合テストはフルサーバーを起動し、RPC呼び出しを行ってエンドツーエンドの振る舞いを検証します。

分散システムにとって最も価値のあるテストは、ユニットテストや統合テストではなく障害注入テストです:ディスカバリサービスが利用不可の場合はどうなるか?ストレージの書き込みが失敗した場合はどうなるか?コンセンサスメンバーがレプリケーション中にクラッシュした場合はどうなるか?これらのテストはシステムのレジリエンスを検証します。プラネタリスケールにおいて最終的に重要なのはレジリエンスです。