K8s 高級 #5 オブザーバビリティ — Prometheus / Grafana / Loki / OpenTelemetry

K8s 高級シリーズの 5 番目の記事です。これまで扱った CNIRBAC / IRSAAdmissionCRD / Operator はすべてクラスタの動作を作るコンポーネントでした。これらすべてのコンポーネントがうまく回っているかを覗き込む 1 層がさらに必要です — オブザーバビリティ です。どの Pod がメモリをどれくらい使っているか、どの Service の latency が普段より高いか、1 つのリクエストがマイクロサービスの間をどう流れたか。この 3 つの次元がメトリクス・ログ・トレースで、その上に K8s 運用の標準スタックが位置しています。

このシリーズは K8s 高級 6 編です。

オブザーバビリティの 3 軸 #

オブザーバビリティはよく 3 種類のデータ に分けて話されます。

何か質問
Metrics時間に従う数値時系列「今何が起きているか」
Logsイベントのテキスト記録「そのイベントの詳しい事情は何か」
Traces1 つのリクエストが複数のサービスを経る経路「なぜこのリクエストが遅かったか」

この 3 つは互いに補完関係です。メトリクスで異常を発見し、ログで詳しい状況を押さえ、トレースでリクエスト経路のどの区間が問題かを絞り込む流れが日常のデバッグパターンです。運用クラスタで 3 つともそろえるのが標準で、各軸の K8s ツールはほぼ固まっています。

Metrics — Prometheus 中心の標準スタック #

K8s メトリクスの事実上の標準は Prometheus です。CNCF の卒業プロジェクトで、K8s 自体のコンポーネント(API サーバ、kubelet、controller-manager、scheduler)がすべて Prometheus が理解できる形式でメトリクスを公開します。Prometheus のモデルは単純です。

  • Pull ベース — Prometheus が定期的に各ターゲットの /metrics エンドポイントを HTTP で集めます。
  • 時系列データベース — 集めたデータはラベルが付いた時系列として保存されます。
  • PromQL — 時系列を問い合わせる独自のクエリ言語。

標準スタックのコンポーネント #

運用クラスタに Prometheus をインストールするとほぼ常に次のコンポーネントが一緒に入ります。

コンポーネント役割
Prometheus Serverメトリクス収集 + 保存 + 質疑
kube-state-metricsK8s オブジェクト(Deployment、Pod、Node など)の状態をメトリクスで公開
node-exporter各ノードのシステムメトリクス(CPU、メモリ、ディスク)公開。DaemonSet でノードごとに 1 つ。
Alertmanagerアラームのルーティング、束ね、沈黙処理
Pushgateway (オプション)短く生きる Job のメトリクスを push で受ける

この束を一度にインストールしてくれる標準マニフェストが kube-prometheus-stack Helm chart です。運用クラスタの導入の最初のステップとしてほぼ標準になっています。

ServiceMonitor / PodMonitor — Prometheus Operator の役割 #

Prometheus の scrape 対象を直接マニフェストで書く代わりに、Prometheus Operator が導入した CRD が 2 つあります。

servicemonitor-app.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: my-app
  namespace: my-app
  labels:
    release: prometheus  # kube-prometheus-stack の selector と一致
spec:
  selector:
    matchLabels:
      app: my-app
  endpoints:
    - port: metrics
      interval: 30s
      path: /metrics

この ServiceMonitor を適用すると Prometheus Operator が自動で Prometheus 設定を更新してその Service の Pod から 30 秒ごとにメトリクスを集めます。マニフェストで scrape 対象を宣言する K8s-native な方式です。

PodMonitor は Service なしに Pod に直接付ける変形です。この 2 つの CRD のおかげでアプリケーションチームは Service の隣に ServiceMonitor 1 枚を一緒に書くだけでメトリクス収集が自動で始まります。

1 行の PromQL 例 #

PromQL はそれ自体深いテーマですが、運用でもっともよく使うパターンをいくつか押さえておきます。

namespace の Pod メモリ使用量合計
sum(container_memory_usage_bytes{namespace="payments"}) by (pod)
過去 5 分間の 5xx 応答比率
sum(rate(http_requests_total{status=~"5.."}[5m]))
  / sum(rate(http_requests_total[5m]))
P95 latency (ヒストグラム)
histogram_quantile(0.95,
  sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)
)

rate() は時系列の秒あたり増加量を、histogram_quantile() はヒストグラムから特定の分位数を計算します。この 2 つの関数が PromQL 活用の 70% 程度を覆います。

Logs — Loki 中心の新しいスタック #

ログ収集の古い標準は EFK スタック(Elasticsearch + Fluentd + Kibana)でした。強力ですが重いです — Elasticsearch はすべてのログ本文をフルテキストインデックスするのでディスクとメモリを多く消費します。

Loki は Grafana Labs が作った軽い代替です。モデルが違います — ログ本文はインデックスせず、ラベルだけインデックスします。 検索時にはラベルで絞ってから本文を grep します。Prometheus と似たラベルモデルをログに持ってきたわけです。

Loki スタックのコンポーネント #

コンポーネント役割
Lokiログ保存 + 質疑
Promtail (または Fluent Bit)各ノードからログを読んで Loki に送る。DaemonSet。
GrafanaLogQL クエリ + 可視化

Promtail はノードの /var/log/containers/ を読んで K8s メタデータ(Pod 名、namespace、コンテナ名、ラベル)を自動でラベルとして付けて Loki に送信します。別途のアプリケーション変更なしですべてのコンテナの stdout/stderr がそのまま収集されます。

LogQL — Loki のクエリ言語 #

PromQL と似た肌理です。

payments namespace の ERROR ログ
{namespace="payments"} |= "ERROR"
特定 Pod のエラー比率 (メトリクスに変換)
sum(rate({pod="checkout-abc123"} |= "ERROR" [5m]))

{...} でラベルフィルター、|= で本文 substring マッチ、|~ で正規表現マッチ。rate() でメトリクスのように扱えるので Grafana ダッシュボードでメトリクスチャートと一緒に描けます。

Loki vs EFK — 選択の肌理 #

次元LokiEFK
インデックスラベルのみ全本文
ディスクコスト
フルテキスト検索grep (遅い)早い
運用負担高 (Elasticsearch クラスタ運用)
Grafana 統合1 級可能

新規導入は Loki が標準に近いです。フルテキスト検索が核心要求なら EFK または OpenSearch がより適合しますが、K8s 運用の日常的なデバッグには Loki のラベル + grep モデルで十分です。

Traces — OpenTelemetry 中心の統合 #

分散トレーシングの古い標準は 2 つに分かれていました — OpenTracingOpenCensus。2 プロジェクトが合わさって作られたものが OpenTelemetry(OTel) です。今は分散トレーシング・メトリクス・ログを一緒に扱う単一の標準で、CNCF でももっとも活発なプロジェクトの 1 つです。

OpenTelemetry の核心概念は 3 つです。

  • Instrumentation ライブラリ — 各言語別 SDK がアプリケーションコードに挿入されて trace を作ります。自動 instrumentation ツール(Java agent など)がコード変更なしに付くケースもよくあります。
  • OpenTelemetry Collector — アプリケーションが送るデータを受けて処理・ルーティング。一般的に K8s に DaemonSet または Deployment として立てます。
  • バックエンド — 実際の trace を保存して可視化。Jaeger、Tempo、Datadog、Honeycomb など。

Trace のモデル — Span のツリー #

分散トレーシングの単位データは Span です。1 つのリクエストが複数のサービスを経る間、各段階ごとに span 1 つが作られて、親子関係で結ばれてツリーをなします。

1 つのリクエストの span ツリー例
[gateway] /api/orders POST  (200ms)
 ├─ [orders-service] create order   (180ms)
 │   ├─ [postgres] INSERT orders    (15ms)
 │   ├─ [postgres] INSERT items     (12ms)
 │   └─ [kafka] publish order.created (45ms)
 └─ [auth-service] verify token     (10ms)

このツリーを見れば 200ms のうちどの区間で時間がもっとも多くかかったかが一目でわかります。P99 latency が普段より高いときにどのサービスが原因かを絞り込むのに trace が決定的です。

Tempo — Loki と同じ肌理の trace 保存所 #

Grafana Labs は trace 保存所も軽いモデルで作りました — Tempo です。Loki がログにしたことを trace にしたわけです。インデックスを最小化してオブジェクトストレージ(S3 / GCS)に trace 本文を保存します。trace ID で直接照会するのに最適化されていて、Loki・Prometheus と一緒に使うときに Grafana でメトリクス → ログ → trace へ自然に流れる流れが作られます。

Grafana — 可視化の標準 #

3 軸のデータを 1 か所で覗き込むツールが Grafana です。Prometheus、Loki、Tempo、Elasticsearch、CloudWatch などほぼすべてのデータソースを 1 つのダッシュボードに束ねられて、各パネルが自分のクエリ言語でデータを取ってきます。

運用クラスタの標準ダッシュボードセットは通常次の程度で構成されます。

  • クラスタ概要 — ノード別 CPU・メモリ・ディスク、Pod 個数、namespace 別リソース使用
  • ワークロード概要 — Deployment / StatefulSet 別の replica 状態、再起動回数、OOMKilled
  • API サーバヘルス — request rate、error rate、P99 latency、etcd lag
  • 各アプリケーション — ビジネスメトリクス + 4 golden signals (latency, traffic, errors, saturation)

kube-prometheus-stack を導入するとクラスタ・ワークロード・API サーバダッシュボードは事前構成された状態で一緒に入ります。アプリケーションダッシュボードだけドメインに合わせて新しく作ればよいです。

Alerting — Alertmanager の役割 #

メトリクスがある条件を満たすときにアラームを送ることは Prometheus 自体ではなく Alertmanager が担当します。Prometheus がアラームルールを評価して発生したアラームを Alertmanager に送り、Alertmanager がルーティング・束ね・沈黙・反復処理をします。

PrometheusRule — アラーム定義の CRD #

prometheusrule-high-error-rate.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: app-alerts
  namespace: my-app
  labels:
    release: prometheus
spec:
  groups:
    - name: my-app
      rules:
        - alert: HighErrorRate
          expr: |
            sum(rate(http_requests_total{status=~"5..", app="my-app"}[5m]))
              / sum(rate(http_requests_total{app="my-app"}[5m])) > 0.05
          for: 5m
          labels:
            severity: warning
            team: payments
          annotations:
            summary: "High 5xx rate on my-app ({{ $value | humanizePercentage }})"
            description: "5xx rate over the last 5 minutes is above 5%."

expr が Prometheus で評価される条件、for: 5m が「この条件が 5 分連続真でなければアラーム発生」という意味です。labelsseverityteam が Alertmanager でルーティングキーとして使われます。

Alertmanager のルーティング #

Alertmanager の設定ではラベルベースでアラームをどこに送るかを定めます。

alertmanager.yaml — 単純化
route:
  receiver: default
  group_by: ['alertname', 'team']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - match:
        severity: critical
      receiver: pagerduty
    - match:
        team: payments
      receiver: payments-slack

receivers:
  - name: default
    slack_configs:
      - channel: '#alerts'
  - name: pagerduty
    pagerduty_configs:
      - service_key: ...
  - name: payments-slack
    slack_configs:
      - channel: '#payments-alerts'

このモデルのおかげでアラームのルーティングがコード(マニフェスト)で管理され、Slack / PagerDuty / Email のようなチャンネルに分岐します。

運用時に押さえておく原則 #

オブザーバビリティスタックは一度よく備えておけばクラスタの視野が一気に深くなりますが、誤って運用するとクラスタリソースの大部分を消費するツールになります。次の 4 つを押さえておくと良いです。

1. メトリクスのカーディナリティ爆発に注意 #

Prometheus はラベル組み合わせ 1 つあたり別途の時系列を作ります。ラベルに user ID、request ID、UUID のような高カーディナリティ値を入れると時系列数が爆発し、Prometheus がメモリを早く使い切ります。運用ガイドの最初の原則は ラベルに高カーディナリティ値を入れない です。

カーディナリティ点検 — 時系列がもっとも多いメトリクス
topk(10, count by (__name__)({__name__=~".+"}))

このクエリで時系列数が多いメトリクスを定期的に点検するのが運用の一部です。

2. 保存期間と remote ストレージ #

Prometheus のローカルストレージはデフォルト 15 日保存です。それ以上保管するには remote ストレージ(Thanos、Cortex、Mimir、VictoriaMetrics)に一緒に送らなければなりません。同様に Loki と Tempo もオブジェクトストレージ(S3 / GCS)に long-term storage を置くのが標準です。

保存期間はコストと直結します。メトリクス 6 か月、ログ 30 日、trace 7 日程度が一般的な出発点で、各軸の保存を別々に決められます。

3. アラームの SNR — 多すぎるアラームは無アラームと同じ #

アラームを多く作りすぎると運用者がアラームを無視し始め、本当に重要なアラームも見逃すようになります。アラーム設計の標準原則は 「アラーム 1 件 = 人の即時対応 1 回」 です。

  • Symptom-based — 原因ではなく症状にアラーム。「DB 接続プールが 80% 満たされている」ではなく「API の 5xx 比率が 5% を超える」。
  • for 期間でノイズ除去 — 短いスパイクにアラームが鳴らないように。
  • severity の分岐critical は起こすべきもの、warning は明日朝見ればよいもの。分岐がぼやけると無視され始めます。

4. golden signals を標準に #

Google SRE 文化から出発した 4 golden signals はほぼすべてのワークロードモニタリングの出発点です。

シグナル意味
Latencyリクエスト処理時間 (P50 / P95 / P99)
Traffic秒間リクエスト数
Errors失敗比率
Saturationリソース飽和度 (CPU、メモリ、キュー長)

この 4 つをすべてのサービスに同じ形態で公開しておけばダッシュボードもアラームも標準化されます。ドメインメトリクスはその上に乗せます。

締めくくり #

K8s 運用のオブザーバビリティスタックを 1 サイクルで整理しました。メトリクスの Prometheus + kube-state-metrics + node-exporter、ログの Loki(または EFK)、トレースの OpenTelemetry + Tempo、可視化の Grafana、アラームの Alertmanager までがほぼ固まった標準の束で、kube-prometheus-stack と Loki / Tempo Helm chart が導入の最初のステップです。カーディナリティ / 保存 / アラーム SNR / golden signals の 4 原則を運用段階のガードレールとして押さえておけば、スタックがクラスタのリソースを過剰に消費することなく、観測の視野だけを深めることができます。次の記事であり K8s 高級シリーズの最後の記事ではマニフェストの source of truth を git に置く運用モデル — ArgoCD と Flux ベースの GitOps を扱います。

X