ほとんどのアーキテクチャ図はサービスのところで終わっています。テレメトリーの経路が描かれるのは後になってから、それもインシデントの最中で、トレースの半分がなぜ欠けているのか、ログの請求額がなぜ倍になったのかを調べている人が描くことになります。その時点で答えは、誰も書き残していない細部に左右されます。どのコンポーネントがサンプリングしているのか、どれが Pod 名を付けているのか、バックエンドが遅くなったら何が起きるのか、といった点です。
オブザーバビリティのアーキテクチャ図は、ログ、メトリクス、トレースが各サービスからどう出ていき、どこで付加情報を加えられ、サンプリングされ、フィルタリングされ、最終的にどこへ行き着くのかを示します。この記事では、Kubernetes 上の OpenTelemetry を中心にこの図を作り、背景にあるエージェントとゲートウェイのパターンを説明し、次の障害でも図が役立つよう何をラベルに書くべきかを挙げます。
3 つのシグナル、3 つの問い
| シグナル | 答える問い | 典型的な発生源 | 主な行き先 |
|---|---|---|---|
| メトリクス | 何かおかしいか、それはどの程度か | SDK のカウンターやヒストグラム、kubelet とノードの統計 | Prometheus 互換のストアなどの時系列データベース |
| トレース | リクエスト経路のどこがおかしいか | SDK または自動計装による Span | Jaeger や Tempo などのトレースストア |
| ログ | 正確には何が起きたか | stdout と stderr、ログファイル、SDK のログレコード | Loki や OpenSearch などのログストア |
この 3 つは、コンテキストを共有して初めて一緒に役立ちます。ログ行のトレース ID と、すべてのシグナルに共通する service.name と Pod 名があれば、アラートから遅い Span へ、さらにその原因を説明するログ行へとたどれます。アーキテクチャはこのコンテキストを崩さずに保つ必要があります。
構成要素
OTLP は、3 つのシグナルすべてに使う OpenTelemetry のプロトコルです。仕様ではデフォルトのポートが決まっており、gRPC 上の OTLP は 4317、HTTP 上の OTLP は 4318 です。
SDK と自動計装 は各サービスの中で動き、OTLP をエクスポートします。送り先はベンダーではなくローカルの Collector にしましょう。そうすれば、ルーティングを変えるたびに再デプロイする必要がなくなります。
Collector は、シグナルごとのパイプラインを持つ単一のバイナリです。レシーバーがデータを受け取り、プロセッサーが加工し、エクスポーターが先へ送ります。デプロイのドキュメントでは、主に 2 つの動かし方が説明されています。
- エージェント。 Collector をアプリケーションの隣で動かします。サイドカーとして動かすか、ノードごとに 1 つの Pod を置く DaemonSet として動かします。Kubernetes では、エージェントは Kubernetes 用コンポーネントが働く場所でもあります。Kubernetes Attributes Processor が Pod、名前空間、Deployment の名前を付け、Filelog Receiver がコンテナの stdout と stderr を追い、Kubeletstats Receiver がノードと Pod のメトリクスを取得します。
- ゲートウェイ。 1 つ以上の Collector を独立したサービスとして動かします。通常は、クラスター、データセンター、リージョンごとに 1 つのエンドポイントです。ゲートウェイは、1 か所で一度だけ決めたいことを置く場所です。サンプリング、機密性のある属性のフィルタリング、バックエンドの認証情報の保持、複数のバックエンドへの送信などです。
ヘッドサンプリングかテールサンプリングか
トレースはコストの高いシグナルなので、ほとんどのチームはサンプリングします。ヘッドサンプリング は、トレースの開始時に、たとえば 10% を残すといった判断をします。安価ですが、後で失敗したからという理由でトレースを残すことはできません。テールサンプリング は、トレースの Span をすべて、またはほとんど見てから判断するので、エラーや遅いリクエストを漏れなく残せます。
テールサンプリングには構造上のコストがあります。1 つのトレースの Span はすべて同じ Collector に届かなければなりません。ラウンドロビンのロードバランサーの後ろでは、そうはなりません。ゲートウェイのパターンはこれを 2 段構成で解決します。1 段目はトレース ID をルーティングキーにした load-balancing exporter を動かし、2 段目は tail sampling processor を動かします。図の上では 2 つの箱になり、両方を描く価値があります。
例:Kubernetes 上のショップ
このショップは、shop 名前空間で web、checkout-api、payments-worker の 3 つの Deployment を動かしており、いずれも OpenTelemetry の SDK を組み込んでいます。observability 名前空間にはパイプラインがあります。DaemonSet の otel-agent、Span をトレース ID でルーティングする Deployment の otel-lb、テールサンプリングのポリシーを適用する Deployment の otel-sampler です。メトリクス、トレース、ログはそれぞれ専用のストアに送られ、アラートのルールはメトリクスの上で動きます。
ダイアグラムの内容
- shop
- web
- checkout-api
- payments-worker
- observability
- otel-agent
- otel-lb
- otel-sampler
- Trace store
- Metrics store
- Log store
- Alerting
- OTLP/gRPC 4317
- stdout logs (filelog)
- traces
- by trace ID
- kept traces
- metrics
- logs
- alert rules
図の読み方は次のとおりです。
- 左はサービス。 各 Deployment は、トレース、メトリクス、ログを OTLP/gRPC でポート 4317 を使い、自分のノードのエージェントへエクスポートします。破線のエッジはコンテナの stdout で、エージェントはネットワーク経由で受け取るのではなく、ノードから読み取ります。
- 中央はエージェント。 エージェントはすべてに Kubernetes のメタデータを付け、メトリクスとログはそれぞれのストアへ直接送り、トレースはゲートウェイへ送ります。
- 中央はゲートウェイ。
otel-lbが各 Span をトレース ID でルーティングするため、1 つのトレースの Span はすべて同じotel-samplerの Pod に届きます。そこでエラーと遅いトレースを残し、残りはサンプリングします。 - 右はストレージとアラート。 シグナルごとに専用のバックエンドがあります。アラートのルールはメトリクスを読み、オンコール担当者を呼び出します。
- Markdown ブロック には、サンプリングのポリシーと、どのパイプラインでも守るルールを書いています。memory limiter を最初に置くことです。
ラベルに書くこと
- 各ホップのプロトコルとポート。 OTLP/gRPC 4317 か OTLP/HTTP 4318 か、そして各バックエンドへのエクスポート形式を書きます。
- 各エッジを流れるシグナル。 1 つのパイプラインが 3 つすべてを同じように運ぶことはまれです。
- サンプリングする場所とポリシー。 数値を書きましょう。「サンプリング済み」とだけ書くと誤解を招きます。
- データに情報を加える場所、伏せる場所。 Kubernetes のメタデータはエージェントで、トークンや個人データの削除はゲートウェイで行います。
- バックエンドごとの保持期間。 コストと、調査でまだ何が見つかるかを説明してくれます。
- 障害時の振る舞い。 キュー、再試行、バックエンドが落ちたときに何が捨てられるかを書きます。
よくある間違い
- SDK からベンダーへ直接エクスポートする。 ルーティングや認証情報を変えるたびに再デプロイが必要になり、データが外に出る前にフィルタリングする場所もありません。
- 普通のロードバランサーの後ろでテールサンプリングする。 1 つのトレースの Span が別々の Collector に散らばり、断片だけを見てサンプリングの判断が下されます。
- memory limiter がない。 設定のドキュメントには
memory_limiterプロセッサーがあります。各パイプラインの先頭に置けば、負荷が急増したときに Collector はメモリ不足になる代わりにデータを拒否します。 - カーディナリティの高いメトリクスのラベル。 ユーザー ID やリクエスト ID をメトリクスのラベルにすると、時系列が爆発的に増えます。Span やログに載せましょう。
- パイプライン自体を監視していない。 Collector は自身のメトリクスを、内部テレメトリーのドキュメントにあるとおり、デフォルトではポート 8888 で公開します。エクスポートの失敗にアラートを設定しないと、データの欠落に気づくのは、そのデータが必要だったインシデントの最中になります。
クラスター自体については、コントロールプレーン、ノード、ワークロードを扱う Kubernetes のアーキテクチャ図を、このパイプラインが監視するサービスについてはマイクロサービスのアーキテクチャ図を参照してください。
Archboard で作る
- Google または GitHub でサインインし、図を作成します。
- Kubernetes パックから Namespace のフレームを 2 つ、
shopとobservabilityを追加します。shopの中にはサービスごとに Deployment を置きます。 observabilityの中に、エージェント用の DaemonSet と、負荷分散とサンプリングの段のための Deployment を 2 つ追加します。- DevOps パックから、バックエンド用に メトリクス、トレース、ログ のノードを、アラート用に アラート のノードを追加します。
- それらをつなぎ、各エッジに「OTLP/gRPC 4317」のようにシグナルとプロトコルを書きます。コンテナのログのエッジは破線にします。
- サンプリングのポリシーを書いた基本パックの Markdown ブロックを追加します。
この例で使うダイアグラムノードは 13 個で、Free プランの範囲に収まります。サービスがすでに Kubernetes 上で動いているなら、Kubernetes YAML から図を作るツールでマニフェストから左半分の下書きを作れます。
よくある質問
ゲートウェイの Collector は必要ですか? エージェントだけで足りますか? 小規模な構成なら、エージェントだけで足ります。テールサンプリング、認証情報とフィルタリングを 1 か所にまとめること、複数のバックエンドが必要になったら、ゲートウェイを追加します。そのときエージェントはゲートウェイへ転送します。
ログも Collector を通すべきですか? 通せます。エージェントの Filelog Receiver はコンテナのログを読み、トレースやメトリクスと同じ Kubernetes のメタデータを付け、保存前のフィルタリングもできます。多くのチームは、トレースとメトリクスの後、最後にログを Collector へ移します。
ヘッドサンプリングで十分な場合はありますか? あります。トラフィックが少ないときや、どのトレースも似たようなときです。エラーや遅いリクエストを漏れなく残したいなら、テールサンプリングを使い、それに必要な 2 段のゲートウェイを図に描きましょう。
ダッシュボードとアラートは図のどこに置きますか? 問い合わせ先のストアの隣です。メトリクスに対するアラートのルールはアーキテクチャの一部です。誰を起こすかを決めるのはこのルールだからです。
この図を最新に保つにはどうすればよいですか? Collector の設定と同じプルリクエストで図を変更し、ランブックからリンクします。古くならないアーキテクチャ図も参照してください。