本文へスキップ
Starone

運用

分散システムのオブザーバビリティ

メトリクス、ログ、トレースの実践ガイドです。それぞれが何に答えるのか、カーディナリティがどのようにコストを左右するのか、そしてSLOがテレメトリーをどのようにして起こされる価値のあるアラートへ変えるのかを解説します。

· 5分で読めます · Starone エンジニアリング

モニタリングは、既知の障害が発生したことを知らせます。オブザーバビリティとは、誰も予測していなかった障害を、システムがすでに出力しているテレメトリーだけを使って説明できる能力です。分散システムではこの違いが重要になります。注目すべき障害は、単体ではどれも正常に見えるサービス同士の相互作用として起こるからです。

私たちは、オブザーバビリティはユーザー体験から逆算して設計すべきだと考えています。サービスレベル目標(SLO)から始め、1つのリクエストをエンドツーエンドで追跡できるようにリクエスト経路を計装し、それ以外のデータは、誰かが実際に尋ねる問いに答えるものだけを収集します。取得可能なテレメトリーをすべて集め、いつか役に立つことを期待するやり方は、高額な請求と時間のかかる調査をもたらすだけです。

3つのシグナル、3つの異なる問い

メトリクス、ログ、トレースは互いに代替できるものではありません。それぞれ、ある種の問いには安価に答えられますが、それ以外の問いに対しては高くつくか、役に立ちません。

  • メトリクスは、時間軸に沿って事前集計された数値です。何かがおかしいのか、どの程度深刻なのかという問いに、安価に、かつ長期間にわたって答えます。
  • トレースは、単一のリクエストが複数のサービスをまたいでたどった経路を記録します。時間がどこで費やされたのか、どの依存先が失敗したのかという問いに答えます。
  • ログは、詳細を伴う個別のイベントです。見るべき場所がわかった後に、ある時点で正確に何が起きたのかという問いに答えます。

カーディナリティがコストモデルである

メトリクスは、ラベル値の一意な組み合わせごとに1本の時系列として保存されます。サービス、エンドポイント、ステータスコードをラベルに持つリクエストカウンターであれば、数百本の時系列で収まります。そこにユーザーIDのラベルを加えると、ユーザーごと、エンドポイントごと、ステータスごとに1本の時系列となり、Prometheus系のストアは役に立つようになるはるか手前でメモリを使い果たします。

私たちが適用するルールは、メトリクスのラベルは有界で小さくなければならない、というものです。生のURLではなくルートテンプレートを、エラーメッセージではなくステータスクラスを使います。ユーザー、テナント、注文、リクエストIDといった高カーディナリティの識別子は、トレースと構造化ログに置きます。そこでは各イベントが個別に保存され、コストは次元の積ではなくデータ量に比例します。エグゼンプラーはこの2つを橋渡しする仕組みで、メトリクスのバケットにサンプルのトレースIDを付与します。これにより、レイテンシグラフ上のスパイクから、それを実際に経験したリクエストへ直接たどれます。

サービスにはRED、リソースにはUSE

2つの小さなフレームワークで、ほとんどのダッシュボードをカバーできます。REDはリクエストを処理するものすべてに適用でき、レート、エラー、所要時間(Duration)を見ます。USEは枯渇しうるものすべてに適用でき、CPU、ディスク、コネクションプール、スレッドプール、キューについて、使用率、飽和度(Saturation)、エラーを見ます。

所要時間は平均ではなく、ヒストグラムとして記録しなければなりません。平均は、ユーザーが不満を訴える遅いリクエストを覆い隠します。また、インスタンスごとに算出したパーセンタイルは後から合成できません。適切なバケット境界を持つヒストグラムであれば、インスタンスをまたいで集約したうえでp95やp99を問い合わせることができます。最も欠落しがちなシグナルは飽和度です。使用率が上限に達していても待ちが発生していないプールは問題ありませんが、同じプールで待ち行列が伸び続けているなら、それはまもなく起こるインシデントです。

何が重要かを決めるのはSLOとエラーバジェット

サービスレベル指標(SLI)は、ユーザーから見える振る舞いの測定値です。たとえば、チェックアウトのリクエストのうち500 ms以内に成功した割合がそれにあたります。サービスレベル目標(SLO)は、ある期間におけるその指標の目標値で、たとえば30日間で99.9パーセントといった形で定めます。目標値と完璧との差がエラーバジェット、つまりサービスに許容される失敗の量です。

このバジェットがあると、2つの判断から社内政治の色合いが薄れます。バジェットの消費が速ければ、機能開発より信頼性の作業を優先します。ほとんど消費されていなければ、チームはより速くリリースするか、より多くのリスクを取ることができます。さらに、アラートにも原則に基づいた根拠が与えられます。私たちはバーンレートに対してアラートを設定します。短いウィンドウでの速い消費は即座に担当者を呼び出し、長いウィンドウでの緩やかな消費はチケットを起票します。各アラートで短いウィンドウと長いウィンドウを組み合わせることで、一瞬の乱れで呼び出しがかかることも、復旧済みのインシデントが鳴り続けることも防げます。

トレードオフは労力です。良い指標を定義するには、ユーザーが何を重視しているかについての合意が必要であり、選び方を誤った指標は根拠のない安心感を与えます。対話的なユーザーがいない社内のバッチシステムであれば、より単純な鮮度チェックや完了チェックでたいていは十分です。

トレースコンテキストはすべてのホップを生き延びなければならない

トレースは、完全であって初めて役に立ちます。各サービスは受信したトレースコンテキストを読み取り、自身のスパンをその子として作成し、呼び出すすべての先へコンテキストを渡さなければなりません。W3C Trace Context標準はそのためのtraceparentヘッダーを定義しており、OpenTelemetryの計装は一般的なHTTPおよびgRPCライブラリについてこれを処理します。

途切れが発生するのは、自動計装がカバーしない境界です。ヘッダーにコンテキストを持たないままKafkaにパブリッシュされたメッセージ、スレッドプールに渡されたジョブ、スケジュールされたタスク、未知のヘッダーを取り除くプロキシを経由する呼び出しなどが該当します。そのどれもが、トレースを互いに無関係な断片へと分断します。私たちは非同期の境界をまたぐ伝播を明示的な作業として扱い、構造化ログのすべての行にトレースIDを含めて、ログとトレースを結合できるようにします。

もう1つの判断がサンプリングです。ヘッドサンプリングはリクエストの開始時点で採否を決めます。安価ですが、通常のトラフィックと一緒にエラーの大半も捨ててしまいます。テールサンプリングはトレースが完了してから判断するため、すべてのエラーとすべての遅いリクエストを残せますが、コレクターでスパンをバッファリングするコストがかかります。障害がまれで、調査する価値が高いシステムでは、テールサンプリングはコンポーネントを追加するだけの価値があります。

アラート疲れは設計上の欠陥

対応の必要がないことで呼び出されるオンコールエンジニアは、呼び出しを無視することを覚えます。原因はほぼ例外なく、症状ではなく原因に対してアラートを設定していることです。閾値を超えたCPU、再起動した1つのPod、一時的にいつもより長くなったキューなどがその例です。これらはグラフにする価値はあっても、人を起こす価値があることはまれです。

  • 呼び出しはユーザーから見える症状に限定し、SLOのバーンレートで表現する
  • すべての呼び出しにランブックへのリンクと、人が最初に取れるアクションを用意する
  • 原因ベースのシグナルは電話ではなく、ダッシュボードとチケットに送る
  • 対応を必要とせずに発報したアラートはレビューし、修正するか削除する

少ないほうが適量である場合

分散トレーシング、メトリクスパイプライン、ログ基盤には、現実の運用コストがかかります。データベースが1つのモジュラーモノリスにトレーシングのバックエンドは必要ありません。リクエストIDを含む構造化ログ、エッジでのREDメトリクス、データベースクエリの所要時間があれば、ほとんどの問題は説明できます。私たちがトレーシングを追加するのは、1つのリクエストが日常的に数個を超えるプロセス境界をまたぐようになったときです。そこが、ログだけでは全体像を語れなくなる分岐点だからです。

同様の課題に取り組んでいる方へ

ぜひ意見を交換させてください。この記事を書いたエンジニアが直接お話しします。