第5章: ディスカバリ

プラネタリスケールコンピュータでは、サーバーは一時的なものです。起動、停止、マシン間の移動、負荷に応じたスケールアップとスケールダウンが行われます。依存するサーバーのアドレスをハードコードするクライアントはそのサーバーが移動した瞬間に壊れます。ディスカバリサービスはどのサーバーが利用可能でどこに見つけられるかの動的レジストリを維持することでこの問題を解決します。

第1章でディスカバリを簡単に紹介しました。ここでは実装を詳しく見ていきます:レジストリのデータ構造、最新の状態を保つためのメカニズム、ディスカバリをすべてのサービス通信の基盤として十分に信頼性の高いものにする設計上の決定です。

インターフェース

discovery/src/lib.rs ディスカバリのインターフェースは意図的にミニマルです:2つのプロシージャ。registerはサーバーが自身をアナウンスしqueryはクライアントがサーバーを見つけることを可能にします。このシンプルさは意図的なもので——複雑なディスカバリサービスは複雑な方法で障害を起こします:

pub const REGISTER_PROCEDURE: ProcedureId = 1;
pub const QUERY_PROCEDURE: ProcedureId = 2;

#[derive(Debug, Serializable, Deserializable)]
pub struct RegisterArgs {
    pub name: String,
    pub address: String,
}

#[derive(Debug, Serializable, Deserializable)]
pub struct QueryArgs {
    pub name: String,
}

#[derive(Debug, Serializable, Deserializable)]
pub struct QueryResult {
    pub address: String,
}

レジストリ

discovery/src/main.rs ディスカバリサービスの心臓部はRegistryデータ構造です。2つのマップを維持します:システム名からサーバーアドレスのリストへのマップと各アドレスが最後にチェックインした時刻を追跡するマップです。このデュアルマップ設計は論理的な関心事(どのサーバーがどのシステムを実装するか)と運用上の関心事(どのサーバーがまだ生きているか)を分離します:

#[derive(Default)]
pub struct Registry {
    registry: HashMap<Name, Vec<Address>>,
    last_ping: HashMap<Address, Instant>,
}

impl Registry {
    fn register(&mut self, name: Name, address: Address) {
        if let Some(time) = self.last_ping.get_mut(&address) {
            *time = Instant::now();
        } else {
            self.registry.entry(name)
                .or_insert_with(Vec::new)
                .push(address.clone());
            self.last_ping.insert(address, Instant::now());
        }
    }

    fn get_address(&self, name: &Name) -> Option<&Address> {
        self.registry.get(name)?.choose(&mut rand::thread_rng())
    }
}

registerメソッドは初回登録と再登録の両方を処理します。既知のアドレスが再登録するとタイムスタンプのみが更新され——レジストリにアドレスが重複することはありません。この冪等性はサーバーが定期的に登録するため重要で、エントリの重複はget_addressのランダム選択にバイアスをかけてしまいます。

get_addressメソッドはランダム選択を使用してシステムを実装するすべてのサーバーに負荷を分散します。3つのサーバーがechoシステムを実装している場合各クエリはほぼ3分の1の確率で各サーバーのアドレスを返します。

古いエントリのクリーンアップ

サーバーは登録解除なしに障害を起こす可能性があります——クラッシュ、ネットワーク分断、またはハードウェア障害によりレジストリに古いエントリが残ります。クリーンアップメカニズムは設定可能な期間内にハートビートを送信していないアドレスを削除します。クリーンアップはバックグラウンドタスクとして定期的にレジストリをスキャンします。

エクスポネンシャルバックオフ付き登録

ディスカバリのクライアント側も同様に重要です。各サーバーは古いものとしてクリーンアップされないように継続的に再登録する必要があります。ディスカバリサービスが利用不可能な場合、登録はエクスポネンシャルバックオフを使用します。これにより、ディスカバリサービスが障害後に再起動した際の再登録試行のサンダリングハードを防ぎます。

設計の議論

重要な問題はサービスがディスカバリサービス自体をどのように見つけるかです。私たちの実装は既知のアドレス(127.0.0.1:10200)を使用します。本番環境ではいくつかの代替手段があります。DNSは既知のホスト名をディスカバリサービスの現在のアドレスにマップできます。マルチキャストまたはブロードキャストプロトコルはローカルネットワーク上でディスカバリサービスをアナウンスできます。

get_addressのランダム選択は基本的なロードバランシングを提供しますがサーバーのヘルスや負荷を認識しません。ルーティングサービスはディスカバリの上に構築してヘルス対応のルーティングとコネクションプーリングを追加します。この階層化——ディスカバリはロケーションを処理しルーティングはヘルスと効率を処理する——により各サービスが焦点を絞りシンプルに保てます。

より大規模なデプロイメントではディスカバリサービス自体を可用性のためにレプリケートする必要があります。ディスカバリは本質的にレプリカ間で一貫している必要のあるキーバリューペアのレジストリであるため、前章で見たコンセンサスプロトコルの自然な候補です。