Article

システム監視ツールの選定と活用法

高田晃太郎
システム監視ツールの選定と活用法

稼働率99.9%は年間約8.76時間の停止を意味し、99.99%でも約52分の停止が残ります¹。売上に直結するプロダクトでは、1分のダウンタイムが数十万円規模の機会損失になる計算は珍しくありません²。SREの公開知見が示す通り、可用性は設計だけでなく監視と運用の質で決まり、検知の遅延や誤検知はそのままMTTR(平均復旧時間)の悪化に跳ね返ります。公開資料やOSSの実装特性を俯瞰すると、システム監視ツールの選定で重要なのは、SLO(サービス目標)に直結する信号を、適切なコストで、運用が回る形で取得できるかに尽きます。

本稿では、CTOやエンジニアリングマネージャが意思決定できるレベルに論点を集約します。まず監視の目的と評価軸を明確にし、次に代表的なアーキテクチャの比較観点を俯瞰します。その上で、実際に現場へ落とすためのコード例と運用のベストプラクティスを提示し、最後にROIとスケール戦略の考え方を整理します。

監視の目的と選定基準をSLOから逆算する

監視は「何でも見る」活動ではありません。到達すべきサービス目標値(SLO)から逆算し、必要最小限の信号(SLI: サービス指標)を測定しながら、検知から復旧までの一連の流れを縮めることが本質です。SLOで可用性を定義し、エラーバジェット(許容できる失敗の余地)を運用判断に落とし込むと、どのシステム監視ツールが適しているかは自ずと絞れてきます¹。例えばAPIのレイテンシSLOをp95 300ms(95%のリクエストが300ms以下)に置くなら、分布を潰して平均だけを見る監視はミスリードになります。ヒストグラムや概要統計を扱えるメトリクスバックエンドを選ぶ、あるいは分散トレースで遅延のボトルネックを特定できる経路を確保する、といった要件が選定基準になります⁴⁵。

また、運用可能性は机上で軽視されがちですが、現場では最上位の判断軸です。誤検知を抑えるための抑止策、当番が運べるページャー連携、オンコール後の事後検証の容易さなど、毎日の回しやすさが継続性を左右します。検知の遅延、アラートの精度、計測対象のカーディナリティ(ラベル種類の多さ)、データ保持期間、運用の自動化余地、総保有コスト(TCO)を、候補のシステム監視ツールごとに実データで評価するのが効果的です。エージェントやSDK追加で発生するアプリ側のオーバーヘッドも無視できません。サンプリング、バッチサイズ、エクスポート間隔などのチューニング余地があるかは、将来のスケール時に効いてきます⁵。

最後に、組織の成熟度との適合性を見ます。自前運用の自由度を取るのか、マネージドの応答性と統合性を取るのかは、チームのSRE/プラットフォーム機能の厚みに強く依存します。単一の正解はなく、サービス規模や変更速度に合わせて段階的にハイブリッド化するのが現実解です。

主要アーキテクチャの比較視点と導入シナリオ

現代の監視は、おおまかにメトリクス中心の堅牢な監視と、トレース中心のボトルネック特定、そしてログによる根拠づけの三位一体で設計します³。メトリクスではPrometheus系が広く使われ、プル型スクレイプ、アラートルール、リラベリング、リモートストレージでの拡張性が魅力です⁴。SaaSではDatadogやNew Relicのように、収集からダッシュボード、AIOpsまで一体化された体験を強みにします⁷。トレースはOpenTelemetryがデファクトで、ベンダーロックインを避けながら、OTLPでCollectorに中継し、最終的な後段をSaaSや自前のTempoやJaegerに切り替える構成が増えています⁵。ログは構造化が前提で、クラウドのマネージドやVector/Fluent Bitで取り回しつつ、保管はコストダウンの工夫が不可欠です。

選定時の見落としとして、カーディナリティ爆発が挙げられます。ユーザーIDなど高変動のラベルを無邪気にメトリクスへ載せると、TSDB(時系列データベース)が膨れ上がり、クエリも破綻します。高カーディナリティはトレースやログで扱い、メトリクスは集約に徹するという原則を堅持すると事故を大きく避けられます⁶。アラート設計では、静的しきい値だけでなく、レイテンシの外れ値やエラーバジェット消費率を監視するほうが、ユーザー影響に近い信号を拾えます¹。クラウドネイティブ環境では、HPA/Cluster-autoscalerの挙動を合わせて可視化し、インフラとアプリの責任境界を横断する視点も重要です。なお、クラウドのマネージド監視(CloudWatch、Cloud Monitoring)は導入の速さが光りますが、詳細な分布統計やラベルの自由度、コストの粒度でトレードオフが出ます。混在環境では、OpenTelemetry Collectorをハブとして据えると、吸い込みと出し分けの自由度が増し、システム監視ツールの変更コストを抑えられます⁵。

移行戦略は一気通貫よりも、サービス単位でSLOの厳しい領域から先行するのが現実的です。たとえばユーザー向けAPIのp95レイテンシ監視とアラートを先に整え、その知見をバッチや管理画面へ展開します¹。ダッシュボードは責務別に薄く作り、深掘りはトレースへ明確に誘導します。LookMLやSQLベースのダッシュボード文化が強い組織では、メトリクスをセマンティクスレイヤに橋渡しする仕立てにすると、意思決定が早まります。

実装と運用のベストプラクティス(コード例付き)

実装の勘所は、収集のオーバーヘッドを抑えつつ、運用に効く信号を最短で出すことです。以下では、メトリクス、トレース、ヘルスチェックをアプリから発火させ、CollectorやPrometheusに運ぶ代表例を示します。いずれも本番想定でエラーハンドリングやシャットダウン手続きを含めています(まずはローカル実行で動作確認し、Prometheusの/targetsやcurlで健全性を確かめるのが良い出発点です)。

Python(Prometheusエクスポーター)

import time
import signal
import sys
from prometheus_client import start_http_server, Counter, Summary

REQUESTS = Counter('app_requests_total', 'Total requests', ['endpoint'])
LATENCY = Summary('app_request_latency_seconds', 'Request latency', ['endpoint'])

running = True

def handle_sigterm(signum, frame):
    global running
    running = False

signal.signal(signal.SIGTERM, handle_sigterm)
signal.signal(signal.SIGINT, handle_sigterm)

if __name__ == '__main__':
    try:
        start_http_server(9100)
        while running:
            endpoint = '/v1/orders'
            REQUESTS.labels(endpoint).inc()
            with LATENCY.labels(endpoint).time():
                time.sleep(0.03)
        sys.exit(0)
    except Exception as e:
        print(f"exporter error: {e}", file=sys.stderr)
        sys.exit(1)

ローカル9100番でメトリクスを公開し、Prometheusがプルします。curlでhttp://localhost:9100/metrics を確認できます。ラベルは少数に抑え、ユーザーIDなどは含めないのが原則です。Summaryを使う場合はクエリ特性と保持コストを把握し、用途に応じてヒストグラムへ切り替えます。

Go(OpenTelemetry Metrics → OTLP)

package main

import (
    "context"
    "log"
    "math/rand"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
    "go.opentelemetry.io/otel/sdk/metric"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    exp, err := otlpmetricgrpc.New(ctx)
    if err != nil { log.Fatalf("otlp exp: %v", err) }
    mp := metric.NewMeterProvider(metric.WithReader(metric.NewPeriodicReader(exp, metric.WithInterval(5*time.Second))))
    otel.SetMeterProvider(mp)
    meter := otel.Meter("orders")

    latency, err := meter.Float64Histogram("http.server.duration")
    if err != nil { log.Fatalf("hist: %v", err) }

    mux := http.NewServeMux()
    mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })
    srv := &http.Server{ Addr: ":8080", Handler: mux }

    go func() {
        for {
            time.Sleep(200 * time.Millisecond)
            latency.Record(ctx, rand.Float64()*0.25, 
                // semantic attributes例。高カーディナリティを避ける。
            )
        }
    }()

    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("http: %v", err)
        }
    }()

    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
    <-sig
    _ = srv.Close()
    ctx2, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel2()
    if err := mp.Shutdown(ctx2); err != nil { log.Printf("mp shutdown: %v", err) }
}

CollectorへOTLP/gRPCで送信します。エクスポート間隔やバッチサイズはCollector側も含めて全体最適を図ると、CPUとネットワークのオーバーヘッドを制御できます⁵。最初はCollectorの受け口(4317/4318)到達性をnetcatやcurlで確認するとトラブルシュートが容易です。

Node.js(OpenTelemetry SDK → OTLP)

import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import http from 'node:http';

const exporter = new OTLPMetricExporter({ url: 'http://otel-collector:4318/v1/metrics' });
const sdk = new NodeSDK({
  metricReader: new PeriodicExportingMetricReader({ exporter, exportIntervalMillis: 5000 })
});

process.on('SIGTERM', async () => { await sdk.shutdown(); process.exit(0); });

await sdk.start();

let ok = true;
setInterval(() => { ok = !ok; }, 120000);

http.createServer((req, res) => {
  if (req.url === '/healthz') { res.writeHead(ok ? 200 : 500); res.end(); return; }
  res.writeHead(200); res.end('ok');
}).listen(3000);

HTTPエクスポーターでCollectorへ送ります。Nodeのイベントループ特性上、重い同期処理と計測処理が競合しないよう、メトリクス生成は軽量に抑えます。Docker ComposeなどでCollectorと同一ネットワークに置くと疎通確認が容易です。

Java(Micrometer + Prometheus)

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.prometheus.PrometheusMeterRegistry;
import io.micrometer.prometheus.PrometheusConfig;
import spark.Spark;

public class App {
  public static void main(String[] args) {
    PrometheusMeterRegistry reg = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
    Counter c = Counter.builder("orders_created_total").description("created").register(reg);

    Spark.get("/metrics", (req, res) -> { res.type("text/plain"); return reg.scrape(); });
    Spark.post("/orders", (req, res) -> { c.increment(); res.status(201); return ""; });
    Spark.get("/healthz", (req, res) -> "ok");
  }
}

Micrometerで一貫したメトリクス命名を行い、/metricsをPrometheusにスクレイプさせます。後段をSaaSに切り替える場合もレイヤ分離が効きます。まずはhttp://localhost:8080/metrics にアクセスし、メトリクス露出を確認します。

Rust(OpenTelemetry Metrics)

use opentelemetry::metrics::MeterProvider as _;
use opentelemetry_sdk::metrics::{MeterProvider, PeriodicReader};
use opentelemetry_otlp::WithExportConfig;
use std::time::Duration;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let exporter = opentelemetry_otlp::new_exporter().tonic();
    let reader = PeriodicReader::builder(exporter).with_interval(Duration::from_secs(5)).build();
    let provider = MeterProvider::builder().with_reader(reader).build();
    let meter = provider.meter("inventory");
    let gauge = meter.i64_observable_gauge("stock_level").with_description("stock").init();

    let _obs = meter.register_callback(move |cx| {
        let val = 100; // 実装ではDBから取得
        gauge.observe(cx, val, &[]);
    })?;

    std::thread::sleep(Duration::from_secs(30));
    provider.shutdown()?;
    Ok(())
}

RustでもOTLPに統一でき、Collectorを介して後段を差し替え可能です。高スループットなサービスでランタイムオーバーヘッドを抑えたい場合に有効です。

これらアプリの前段にはCollectorやPrometheusサーバを置きます。Prometheusのスクレイプ設定例は以下の通りで、ジョブ単位でタイムアウトやリラベリングを適用します。

global:
  scrape_interval: 15s
  scrape_timeout: 10s
scrape_configs:
  - job_name: 'orders'
    static_configs:
      - targets: ['app:9100']

アラートはユーザー影響に近い信号から作ります。p95レイテンシの連続悪化、エラーバジェットの消費速度、依存先のSLA違反など、SLOに紐づく条件を先に定義し、CPUやメモリはトリアージの補助に留めると誤検知が減ります¹。オンコール負担はPagerDutyなどで勤務体系と連動させ、サーキットブレーカーや自動ロールバックと組み合わせると、検知から緩和までの時間を短縮できます。事後はプレイブック更新、タグやトレースのアノテーション付与で学習を資産化します。

性能面では、メトリクスの記録がリクエストに与える影響を計測し、目安としてレイテンシへの上乗せをパーセンタイルで可視化しておくと安心です。シンプルなベンチマークとして、ワーカプロセス毎にQPSを一定に保った状態でメトリクスのオン/オフを切り替え、p95とp99の差分、CPU比率、ネットワーク送信量を比較すると改善余地が見えます。Collectorのバッチと再試行設定を合わせて最適化すると、ネットワーク回数の削減とSLOの両立が進みます。

ROIの測り方とスケール戦略

監視の投資対効果は、導入コストと運用コストに対し、MTTR短縮と障害回避による損失回避額で測ります¹。例えば平均売上10万円/分のサービスで、重大障害が四半期に3回、平均20分の復旧だったと仮定します。SLOに直結するアラート再設計とオンコール体制の改善でMTTRを20分から12分へ短縮できれば、四半期で24分の停止削減、理論上は約240万円の機会損失を回避した計算になります。SaaS監視費用やエンジニア工数を差し引いても、プラスであれば投資継続の判断ができます。加えて、小規模障害の早期検知により拡大前に抑制できる効果はモデル化しづらいものの、アラート疲れの解消や深夜対応の削減など、組織的な健全性にも波及します。

スケール時は、カーディナリティ制御とデータ保持戦略がボトルネックになります。高変動ラベルの抑制、サマリやスケッチを活用した近似集計、低頻度メトリクスのダウンサンプリング、ログのホット/コールド分離やオブジェクトストレージ退避といった層別化が効いてきます⁶。トレースはルートスパンのサンプリングをデフォルトで抑え、SLO違反時のみヘッドベースサンプリングを引き上げる方式が現実的です⁵。ベンダーロックインを避けるには、収集SDKをOpenTelemetryで統一し、後段の保存・可視化・分析を疎結合にしておくと、コスト変動や要件変更にも耐えやすくなります⁵。ダッシュボードは狭く深くを意識し、プロダクト、SRE、ビジネスの三者が使い続ける画面を厳選すると、運用の摩擦が減ります。

まとめ:SLO起点で、軽く、深く、続けられる監視へ

監視は導入ではなく運用が勝負です。SLOから逆算して信号を選び、少ない誤検知で迅速に動ける体制を整え、システム監視ツールは将来の変更に耐える構成を意識して選択します。メトリクスは集約に徹し、深掘りはトレースへ寄せ、判断の根拠はログで補完する三位一体の設計が、変化の速いプロダクトでも安定をもたらします³。目標はダッシュボードを作ることではなく、ユーザー影響を最小化することであり、そのための手段としてのツールに過度な期待を載せない姿勢が、結果的に最短距離になります。

次の一歩として、今のSLOとアラートがユーザー影響を適切に反映しているかを棚卸しし、最も重要な1シグナルを改善対象に選んでみてください。既存の監視にOpenTelemetry Collectorを挟み、収集系の分離と将来の出し分け余地を確保するのも良い出発点です⁵。社内の合意形成が必要であれば、本稿と合わせて前述の関連記事を関係者に共有し、半年スパンの改善ロードマップを軽量に描くことから始めましょう。

参考文献

  1. Google Cloud Japan. SRE と SLO の基本(SRE Workbook 概要). https://medium.com/google-cloud-jp/sre-slo-d7c6aee1fb0e
  2. Gatling. The cost of downtime — $9,000 per minute: That’s the average cost of downtime. https://gatling.io/blog/the-cost-of-downtime
  3. TechTarget. The 3 pillars of observability: Logs, metrics and traces. https://www.techtarget.com/searchitoperations/tip/The-3-pillars-of-observability-Logs-metrics-and-traces
  4. CNCF. Prometheus Project Journey Report. https://www.cncf.io/reports/prometheus-project-journey-report
  5. CNCF. OpenTelemetry Project Journey Report. https://www.cncf.io/reports/opentelemetry-project-journey-report
  6. Grafana Labs. How to manage high-cardinality metrics in Prometheus and Kubernetes. https://grafana.com/blog/2022/10/20/how-to-manage-high-cardinality-metrics-in-prometheus-and-kubernetes
  7. Datadog. Observability Platform — A unified cloud observability platform. https://www.datadoghq.com/observability-platform
  8. Mdpi (Future Internet). A survey/review on observability/logs/metrics/traces integration. https://www.mdpi.com/1999-5903/14/10/274