第14章: モニタリング

観測できない分散システムは、運用できない分散システムです。何かがうまくいかないとき——プラネタリスケールコンピュータでは、どこかで常に何かがうまくいっていません——運用者は何が起きているのか、どこで起きているのか、そしてできればなぜ起きているのかを知る必要があります。モニタリングサービスは、システム内のすべてのサービスからヘルスとパフォーマンスのデータを収集、保存、公開します。

モニタリングは2つの対象に奉仕します。人間に対しては、システムの振る舞いを理解しインシデントに対応するためのダッシュボード、アラート、診断データを提供します。マシンに対しては、ロードシェディング、フェイルオーバー、オートスケーリングなどの自動化アクションを可能にするヘルスシグナルを提供します。例えばルーティングサービスは、モニタリングからのヘルスシグナルを使用して、不健全なサーバーへのトラフィック送信を回避できます。

インターフェース

monitoring/src/lib.rs モニタリングサービスは4つのプロシージャを公開します。reportプロシージャはサービスからメトリクスデータポイントを受け取ります。heartbeatプロシージャはヘルスステータスの更新を受け取ります。queryプロシージャはメトリクスの時系列を取得し、healthプロシージャはすべての既知のサービスのヘルスステータスを返します。

pub const REPORT_PROCEDURE: ProcedureId = 1;
pub const HEARTBEAT_PROCEDURE: ProcedureId = 2;
pub const QUERY_PROCEDURE: ProcedureId = 3;
pub const HEALTH_PROCEDURE: ProcedureId = 4;

#[derive(Debug, Serializable, Deserializable)]
pub struct ReportArgs {
    pub service: String,
    pub metric: String,
    pub value: i32,
}

#[derive(Debug, Serializable, Deserializable)]
pub struct HeartbeatArgs {
    pub service: String,
    pub status: String,
}

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

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

#[derive(Debug, Serializable, Deserializable)]
pub struct HealthArgs {
    pub placeholder: i32,
}

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

ReportArgs構造体は、何を計測しているかを識別するために汎用的なservicemetricのペアを使用し、計測値として整数valueを持ちます。このシンプルなスキーマは、リクエスト数、レイテンシー、キュー深度、キャッシュヒット率など、幅広い種類のメトリクスを表現できます。

実装

monitoring/src/main.rs モニタリングサーバーは2つのデータ構造を維持します:各サービスのステータスと最終ハートビート時刻を追跡するヘルスレジストリと、報告された値のローリングウィンドウを保持するメトリクスストアです。

const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(30);
const MAX_METRIC_WINDOW: usize = 100;

struct ServiceHealth {
    status: String,
    last_heartbeat: Instant,
}

struct MonitoringState {
    health: HashMap<String, ServiceHealth>,
    metrics: HashMap<String, Vec<i32>>,
}

ハートビートハンドラはサービスのヘルスステータスとタイムスタンプを更新します。サービスは定期的にハートビートを送信することが期待されています。サービスがハートビートウィンドウを逃した場合、モニタリングシステムはそれを不健全としてマークします:

pub async fn heartbeat(
    payload: &str,
    state: &mut MonitoringState,
) -> Response {
    let args = HeartbeatArgs::deserialize(payload)
        .expect("Failed to deserialize payload");
    let health = state.health.entry(args.service.clone())
        .or_insert(ServiceHealth {
            status: args.status.clone(),
            last_heartbeat: Instant::now(),
        });
    health.status = args.status;
    health.last_heartbeat = Instant::now();
    Response { payload: "OK".to_string() }
}

レポートハンドラはメトリクス値をローリングウィンドウに保存します。各メトリクスキーはサービス名とメトリクス名の組み合わせ(例:storage:latency)で形成されます。ウィンドウは最新の100件の値を保持し、メモリ使用量を制限しつつ、平均やパーセンタイルなどの統計を計算するのに十分なデータを提供します:

pub async fn report(
    payload: &str,
    state: &mut MonitoringState,
) -> Response {
    let args = ReportArgs::deserialize(payload)
        .expect("Failed to deserialize payload");
    let key = format!("{}:{}", args.service, args.metric);
    let values = state.metrics.entry(key).or_insert_with(Vec::new);
    values.push(args.value);
    if values.len() > MAX_METRIC_WINDOW {
        values.remove(0);
    }
    Response { payload: "OK".to_string() }
}

バックグラウンドタスクが定期的にタイムアウト期間内にハートビートを送信していない古いサービスをチェックし、不健全としてマークします。これがモニタリングシステムの障害検出における主要なメカニズムです:

fn check_stale_services(&mut self) {
    let now = Instant::now();
    for (service, health) in self.health.iter_mut() {
        if now.duration_since(health.last_heartbeat) > HEARTBEAT_TIMEOUT {
            if health.status != "unhealthy" {
                println!("Service {} marked unhealthy (heartbeat timeout)",
                    service);
                health.status = "unhealthy".to_string();
            }
        }
    }
}

設計の議論

ハートビートパターンは、サービス障害を検出するためのシンプルで効果的な方法です。各サービスは定期的にモニタリングシステムに「生きています」というメッセージを送信します。モニタリングシステムがタイムアウト期間内にサービスからの連絡を受けなかった場合、サービスが障害を起こしたと判断します。タイムアウトは慎重に調整する必要があります:短すぎると一時的なネットワーク遅延により健全なサービスが不健全とマークされる可能性があり、長すぎると実際の障害の検出に時間がかかりすぎます。

ローリングメトリクスウィンドウは、メモリ効率とデータ保持のバランスです。100件の固定ウィンドウは、メモリ使用量を制限しつつ基本的な統計に十分なデータを提供します。Prometheusのような本番モニタリングシステムは、設定可能な保持期間と古いデータのダウンサンプリングを備えた、より洗練されたストレージを使用します。

重要なアーキテクチャ原則は、モニタリングはプル型かプッシュ型のいずれかであるべきで、両方ではないということです。私たちの実装はプッシュモデルを使用しています:サービスがメトリクスとハートビートをモニタリングシステムに送信します。代替手段であるプルモデル(Prometheusが使用)は、モニタリングシステムが各サービスから能動的にメトリクスをスクレイプします。プッシュはサービスにとってよりシンプルですが、サービスが完全に消失した場合の検出が困難です。プルは障害検出を自動化しますが、モニタリングシステムがすべてのサービスを事前に知っている必要があります。

healthプロシージャはすべてのサービスのステータスを単一のレスポンスで返すため、他のシステム(フロントエンドダッシュボードなど)がシステムヘルスの包括的なビューを表示しやすくなります。この集約はモニタリングシステムの一般的なパターンであり、ステータスページや運用ダッシュボードの基盤を形成します。