0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

監視と可観測性を一から学ぶ① メトリクスと外形監視【図解】

0
Posted at

研修で「監視と可観測性(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 ステータスコードはラベルに入れない

外部呼び出しの statussuccess/error2値 にとどめ、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) を学んでいきます。

最後まで読んでいただきありがとうございました 🙌

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?