研修で「監視と可観測性(Observability)」を一から学ぶ機会があり、その学習記録をまとめます。
可観測性の3本柱(メトリクス・ログ・トレース)のうち、この記事では メトリクス と 外形監視 を、図とたとえ話多めで初心者向けに整理します。
題材にした実装の固有名(サービス名・組織名・URL 等)はすべて伏せ、汎用化した「学び」だけを書いています。
そもそも、なぜ可観測性が要るのか
アプリは動いている。でも「中で何が起きているか」を覗く窓が無いと、こうなります。
ユーザー 運用者 (´・ω・`)?
│ リクエスト │ 中で何が起きてるか
▼ │ 全く見えない
┌───────────────────┐ │
│ アプリ │ ◀─────────┘
│ ┌──────────────┐ │ ← スレッド何本詰まってる? 🤷
│ │ 内部状態 │ │ ← コネクション枯渇してない? 🤷
│ └──────────────┘ │ ← 外部呼び出し遅くない? 🤷
└───────────────────┘
箱の中=ブラックボックス
「たまに500が出る」と報告されても、窓が無ければ 再起動して祈る しかできません。
可観測性の目的は 「箱に窓を開けて、出力からシステムの内部状態を推測できるようにする」 ことです。
3本柱の役割分担はこう整理できます。
| 柱 | 何を見るか | たとえ |
|---|---|---|
| 📊 メトリクス | 時系列の「数値」 | 車のダッシュボード(針)「今ヤバい」が一目で分かる |
| 📜 ログ | 「いつ・何が起きたか」のテキスト | ドラレコ「何が起きたか」を後から再生 |
| 🧵 トレース | 呼び出しの因果関係 | 同上、経路まで追える |
メトリクスで「ヤバい」に気づき、ログ/トレースで「なぜ」を掘る。この記事はまず1本目の メトリクス から。
📊 メトリクスとは
メトリクスは 「時間とともに変化する“数値”を、定期的にサンプリングしたもの」 です。
時刻 → 10:00 10:15 10:30 10:45 11:00
busy_threads: 2 3 5 ⚠️ 5 ⚠️ 1
↑ここでスレッド枯渇してた!
数字の折れ線なので軽く・安く・貯めておける。だから「異常をいち早く検知する」のに最も向いており、最初に手を付ける柱になりやすいです。
メトリクスの4タイプ
Prometheus が扱うメトリクスには型があります。
| タイプ | イメージ | 例 |
|---|---|---|
| Counter | 📈 増えるだけ(単調増加) | 累計リクエスト数 |
| Gauge | 📊 上下する瞬間値 | 今ビジーなスレッド数 |
| Histogram | 📐 値の分布 | レスポンス時間のばらつき |
| Summary | 📐 分布(分位数を計算) | (Prometheus 文化では Histogram が好まれがち) |
迷ったら 「増えるだけ → Counter / 今いくつ → Gauge / 速さや大きさのバラつき → Histogram」。
なぜ Histogram が要るのか:平均は嘘をつく
平均だけ見ると… avg = 0.2秒 → 「速いね!」
でも実際は…
900件: 0.05秒 (爆速)
100件: 1.5秒 (激遅) ← この100人は怒ってる
平均に埋もれて「遅い人がいる」事実が見えない!
Histogram は値を「バケツ」に振り分けて分布で見ます。これで p95/p99(遅い方の体感) を取れ、「平均は速いのに一部だけ激遅」を炙り出せます。
≤0.1秒 |████████████████████ 850件
≤0.5秒 |███ 90件
≤1.0秒 |█ 30件
≤5.0秒 |██ 80件 ← 1秒超えが80件もいる!⚠️
🔧 メトリクスをアプリから出す(計装)
メトリクスを出すには本来「クライアント初期化 → 各所にカウンタ仕込み → 処理時間計測…」と手間がかかります。多くの言語/フレームワークには、これを肩代わりする メトリクス抽象化ライブラリ があり、「定番メトリクスのセットをプラグインで合体させる」だけで揃います。
┌─────────── 計装ライブラリ(土台)───────────┐
│ + HTTP : 累計リクエスト / エラー / 応答時間 │ ← 全体の症状
│ + ジョブ : queue / 処理 / 失敗 │ ← ジョブ詰まり
│ + Webサーバ: busyスレッド / backlog │ ← スレッド枯渇
│ + エクスポータ : /metrics で吐き出す │ ← 窓口係
└──────────────────────────────────────────┘
定番を入れるだけで requests_total(Counter)や response_duration_seconds(Histogram)が自動で取れます。
カスタムメトリクスと tags(ラベル)
アプリ固有の関心事(例:「外部依存への呼び出しが遅くないか」)は定番に無いので、自分で定義します。ここで最重要なのが タグ(ラベル) です。1つの数値に「切り口」を持たせます。
タグ無し … 「外部呼び出しは合計1000回」だけ。粗い
タグ付き … 同じ数値を切り口で分解できる \(^o^)/
┌────────────┬─────────┬──────┐
│ dependency │ status │ 回数 │
├────────────┼─────────┼──────┤
│ depA │ success │ 700 │
│ depA │ error │ 250 │ ← depAだけ失敗多い!
│ depB │ success │ 48 │
│ depB │ error │ 2 │
└────────────┴─────────┴──────┘
「ディメンション(キーの数)とカーディナリティ(値の種類)が高いほど、探索が精密になる」——これがタグの効能です。
設計の勘所:HTTP ステータスコードはラベルに入れない
外部呼び出しの status を success/error の 2値 にとどめ、HTTP コード(200/404/500…)をラベルにしないのが定石です。理由は2つ。
① カーディナリティ爆発を防ぐ
status=200,301,400,404,429,500,502... × dependency × ...
→ 時系列の組み合わせが爆発する(Prometheusは組合せ1個ごとに別系列を保持)
② 「通信が完遂したか」≠「業務的に成功か」は別レイヤー
例: 即200で「在庫切れ」を返す → 通信は success だが業務は失敗
メトリクスが見たいのは「依存先インフラが生きてるか・遅くないか」
→ 通信できた=success / 例外=error で十分。業務的成否は呼び出し側で判断
計装ラッパー:既存コードを「包む」だけ
外部呼び出しを計測するには、既存コードをブロックで包む共通ヘルパーが便利です(擬似コード)。
measure(dependency: "depA") {
# 既存のHTTP呼び出し(中身は一切変えない)
}
中の流れはこうです。
┌─ measure ─────────────────────────────┐
│ ⏱️start │
│ ▼ 中身を実行 │
│ ├─ 正常に戻った → record(success) ─┐ │
│ └─ 例外 → record(error)──┐ │ │
│ ⏱️終了→所要時間を Histogram に ◀─┴─┘ │
│ 回数を Counter に +1 │
│ ※例外は握りつぶさず再送出(透過性) │
└─────────────────────────────────────────┘
ここで効く2つの鉄則:
- 例外を握りつぶさない:計器を付けてもアプリの挙動は変えない(透過)。
- 記録処理自体も fail-open:メトリクス記録がコケても warn ログだけ出して業務は続行。「計器が壊れてアプリが落ちる」本末転倒を防ぐ。
観測コードはアプリより偉くない。
「依存先の障害」を観測可能にする2層構造
外部依存(例:機能フラグ配信サービスのような毎リクエスト問い合わせる先)が落ちると、素朴な実装では 応答待ちで自分のリクエストまで道連れに固まる(遅延伝染)。これを防ぎつつ「障害」を観測するには、役割を層で分けます。
┌─ 外側(安全弁)──────────────────────────────────┐
│ measure(dependency:"depA") { │
│ ┌─ 内側(HTTPだけ)────────────────────────┐ │
│ │ HTTP呼ぶ → 非200なら「失敗だ!」と叫ぶ(raise) │ │ ← ③変換
│ └────────────────────────────────────────┘ │
│ } ← 叫びを受けて success/error を記録 │ ← ②観測
│ rescue → warnログ + fail-open(止めない) │ ← ①安全
└────────────────────────────────────────────────┘
| 担当 | 仕事 | 守るもの |
|---|---|---|
| 内側 | HTTPを叩く。非200を「例外」に翻訳 | 正確さ(誤記録防止) |
| measure | 例外の有無で success/error を記録 | 観測(メトリクス) |
| 外側 | 例外を受け止め warn + fail-open | 安全(遅延伝染防止) |
結果、依存先が落ちると メトリクス(error増)・ログ(warn)・挙動(止まらない) の3つにキレイに反映されます。「気づける」と「壊れない」の両立です。
📡 Prometheus は「取りに来る」(pull 型)と scrape
アプリが出す /metrics は、ただのテキストページです。
# TYPE puma_busy_threads gauge
puma_busy_threads 3
# TYPE external_requests_total counter
external_requests_total{dependency="depA",status="success"} 700
external_requests_total{dependency="depA",status="error"} 250
驚きポイントは方向。アプリが送る(push)のではなく、Prometheus が取りに行く(pull) です。
✅ pull型:
Prometheus ──「数値ちょうだい」定期GET──▶ アプリ:/metrics
(例: 15秒ごと)
この「1回の取得動作」を scrape(スクレイプ=こすり取る) と呼びます。pull の利点:
- 取りに行って応答が無ければ Down と分かる(死活も兼ねる)
- 対象が増えても Prometheus が「行き先リスト」を持つだけ。アプリは送信先を知らなくてよい(疎結合)
up メトリクス:計装ゼロでつく死活監視
scrape 成否を Prometheus が自動でメトリクス化してくれます。
up{job="myapp"} = 1 ← scrape成功(生きてる)
up{job="myapp"} = 0 ← scrape失敗(落ちてる)⚠️
→ 「up == 0 が1分続いたらアラート」で死活監視が即作れる
別ポートで /metrics を出す(多層防御)
/metrics にはスレッド数・エラー率・依存先の状態など内部情報が丸見えです。アプリ本体のポート(公開用)とは 別ポート に分け、外部公開の経路に乗せないのが定石です。
外の世界 🌍
│ 公開経路は本体ポートの特定パスしか通さない
▼
┌──────── Pod ────────┐
│ :app /(公開) │
│ :metrics /metrics │ ← 外部に口を作らない=内部の収集系だけ到達
└────────────────────────┘
設定ミスで /metrics が全世界公開、という事故を 構造的に 防げます。
k8s での「動く的」問題と ServiceMonitor
k8s では Pod が増減し IP も変わります。pull の行き先を手書きすると破綻するので、ラベルで宣言して探させる 方式(Prometheus Operator の ServiceMonitor)を使います。
┌─ ServiceMonitor ─┐ selector: app=myapp / port: metrics / interval:15s
└────────┬─────────┘
▼ ラベル一致
┌─ Service (app=myapp) ─┐ port: metrics → :metrics
└────────┬───────────────┘
▼ 束ねる
┌─ Pod :metrics/metrics ─┐
└─────────────────────────┘
Operator が ServiceMonitor を監視 → scrape設定に自動反映
→ Podが増減してもServiceが追従。行き先を手で書き直さなくていい
あなたは「ラベルを付けて宣言を置く」だけ。動く的の追跡は k8s/Operator に任せる——これが 宣言的(declarative) という考え方です。
Prometheus 自身はどこに・どう立てる?
-
クラスタ内:pull で各 Pod の
/metricsを叩くので、ターゲットの隣にいるのが自然。 - StatefulSet + PVC:集めた数値を時系列DB(TSDB)としてディスクに貯める=状態を持つ。だからステートフル。
-
Operator に任せる:
Prometheus(CRD)で「retention 何日・PVC 何 Gi」を宣言すると、Operator が StatefulSet も PVC も自動生成。ServiceMonitor を拾うのも Operator。
共有基盤として既に Prometheus/Grafana が立っている環境では、利用側は ServiceMonitor で「scrape して」と宣言するだけで、本体のデプロイや貯蔵の心配は不要、という構図になります。
🔍 PromQL:集めた数値を読む
選ぶ・絞る
external_requests_total … 全時系列
external_requests_total{dependency="depA"} … depAだけに絞る(SQLのWHERE風)
rate():Counter を「勢い」に変える
Counter(累計)をそのまま描くと右肩上がりで役に立ちません。車のオドメーター(累計)→ スピードメーター(今の勢い) に変換するのが rate()。
rate(external_requests_total{dependency="depA",status="error"}[5m])
↑ 直近5分を見て「1秒あたり何件errorが増えたか」
累計(役立たず) rate()(欲しいやつ)
error累計 error/秒
250┤ ╱── 5┤ ╱╲ ← いつ急増したかが見える⚠️
50┤╱─ 1┤─────╱ ╲──
Counter は再起動で0に戻っても、
rate()が「急減=リセット」と検知して正しく計算する。だからアプリ側は「ひたすら足すだけ」の単純実装でよい(賢さは PromQL 側)。
sum / sum by:まとめる
sum(rate(... [5m])) … 全部1本に合計
sum by(dependency)(rate(... [5m])) … 依存先ごとにまとめる(SQLのGROUP BY風)
エラー率と RED
部品を組み合わせると、SREの定番「エラー率」が書けます。
sum(rate(external_requests_total{status="error"}[5m]))
─────────────────────────────────────────────────── = 失敗の割合/秒
sum(rate(external_requests_total[5m]))
これが RED の E。Rate(量)・Error(率)・Duration(応答時間=Histogram)が揃うと、サービスの健康状態をほぼ掴めます。
🛰️ 外形監視(Blackbox):up との違い
up は「クラスタ内から Pod に届くか」しか見ていません。でもユーザーは外から、DNS・TLS証明書・Ingress・認証…という長い道のりを通ってきます。
🌍ユーザー → DNS → 🔒TLS → 🚪Ingress → 🔑認証 → Service → Pod
▲
up はここしか見てない ─────────────────────────────┘
外形監視はこの道のり全部を「外から」通しで確認 ◀────
up == 1 … Podは生きてる。でもDNS/証明書/Ingressが壊れてたら
ユーザーは繋がらないのに up は 1 のまま(嘘をつく)
probe_success … 実際に外から公開URLを叩いて2xxが返った
= ユーザー目線で本当に使える状態の保証
中を見ず「外から叩いて結果だけ」見るので black-box。専用の Blackbox Exporter にURLを叩かせ、probe_success(届いたか)/probe_duration(何秒か)を Prometheus が scrape します。
設計の勘所①:follow_redirects: false
「繋がったか」だけでなく 「正しいものが返ってきたか」 を確かめたい。
もし監視対象パスが誤って認証必須に戻ると、認証ページへ302リダイレクトされる。
リダイレクトを追うと「ログインページに繋がった=成功」と誤判定 ❌
追わなければ「302が返った=設定が壊れてる」と検知できる ✅
設計の勘所②:限定公開(satisfy: any + IP whitelist)
外形監視は「外の公開URL」を叩くので、監視用パスは外から到達可能にする必要があります。でも全世界に無認証公開はしたくない。このジレンマを解きます(nginx ingress の例)。
satisfy: any = 「次のどちらかを満たせば通す」
条件A: 送信元IPが許可IP(監視の出口IP)か?
条件B: 認証を通ったか?
🛰️監視(許可IP) → 条件A○ → 認証なしで通す
🌍他人(別IP) → 条件A× → 条件Bで認証要求
👤人間(ブラウザ) → 条件A× → 通常ログイン
⚠️ 落とし穴:IP whitelist の行を書き忘れて satisfy: any だけ適用すると、「条件Aが常に満たされる」状態になり 全世界に無認証公開 されてしまう。Ingress は「外の世界との境界」なので、1行のミスが即・全世界に波及します。Ingress 変更は必ずダブルチェック を。
まとめ
┌──────────────────────────────────────────────────┐
│ 📊 メトリクス │
│ 型: Counter(増加) / Gauge(瞬間値) / Histogram(分布) │
│ tags = 切り口(カーディナリティ=探索性) │
│ 計装は「包む」/ fail-open / 観測コードは偉くない │
│ 依存先障害は2層構造で「気づける×壊れない」 │
│ │
│ 📡 収集 │
│ pull型 / scrape / up(死活オマケ) │
│ /metricsは別ポート(多層防御) │
│ ServiceMonitorで宣言的scrape / 本体はStatefulSet+Operator│
│ │
│ 🔍 PromQL │
│ {label}絞り込み / rate(勢い) / sum・sum by │
│ エラー率 = sum(rate(error))/sum(rate(all)) / RED │
│ │
│ 🛰️ 外形監視 │
│ up(内側) vs probe_success(外側・black-box) │
│ follow_redirects:false / satisfy any + IP制限 │
└──────────────────────────────────────────────────┘
次は残る2本柱、ログ(構造化/Loki) と トレース(OpenTelemetry/Tempo) を学んでいきます。
最後まで読んでいただきありがとうございました 🙌