LabHub

ブログ

Observabilityデータパイプラインコスト最適化:サンプリング・フィルタリング・ティアリング戦略

한국어English日本語

Observability Cost Optimization

はじめに

Observabilityコストがクラウドインフラ支出の上位項目として浮上している。2025年のグローバルObservability市場規模は285億ドルを突破し、2026年末までに341億ドルに達する見通しだ。Elasticの2026 Observability Surveyによると、IT意思決定者の54%が、経営陣からObservability支出に対する正当性の要求が増えたと報告している。

コスト急増の根本原因はデータボリュームにある。マイクロサービスアーキテクチャが広がるにつれ、トレースがObservabilityコスト全体の60~70%を占め、ログが20~30%を占める。数百のサービスが毎秒数百万件のスパンとログを生成すると、ストレージとインデックスのコストは線形ではなく指数関数的に増加する。

しかし、単にデータを減らすことが答えではない。Elasticの同じ調査では、70%の組織がデータ削減ではなく既存支出の最適化を優先すると回答した。核心は シグナルとノイズを分離 し、コストを削減しながらも可観測性の品質を維持することだ。

この記事では、OpenTelemetry Collectorを中心としたテレメトリパイプラインのコスト最適化戦略を扱う。Head SamplingとTail Samplingの違いとポリシー設計、ログフィルタリングパイプラインの構築、メトリクスのカーディナリティ爆発の管理、そしてHot/Warm/Coldストレージティアリングアーキテクチャを、実践的な設定とコード例を中心に説明する。最後に障害事例と復旧手順、コスト最適化チェックリストまで含め、プラットフォームエンジニアがすぐに適用できるガイドを提供する。

コスト構造分析:どこでコストが発生するか

Observabilityコストを最適化するには、まずコストがどこでどのように発生するのかを正確に理解する必要がある。テレメトリパイプラインのコストは大きく四つの軸に分類できる。

シグナル別のコスト比重

シグナルタイプコスト比重主なコスト要因最適化の難易度
Traces60~70%高いカーディナリティ、大容量ペイロード、スパン数の爆発高い
Logs20~30%非構造化データ、高いボリューム、フルテキストインデックス中程度
Metrics5~15%時系列カーディナリティ、ラベル組み合わせの爆発中程度
Profiles1~5%CPU/メモリプロファイルデータのサイズ低い

コスト発生段階別の分析

テレメトリデータのライフサイクルに沿って、コストが発生する地点を細分化すると以下の通りだ。

[生成] --> [収集/転送] --> [処理/加工] --> [インデックス] --> [保存] --> [クエリ]
  |             |               |                |              |           |
 SDK       ネットワーク     Collector       バックエンド    ストレージ コンピュート
オーバーヘッド 帯域幅      CPU/メモリ     インデックスI/O   ディスク/S3  クエリコスト

コスト最適化の核心原則は「パイプラインのできるだけ前段で不要なデータを除去せよ」ということだ。生成段階で除去すればそれ以降のすべてのコストが節約できるが、保存段階で除去しても、その前の段階のコストはすでに支払っている。これを First-Mile Processing または テレメトリ最適化 と呼ぶ。

Observabilityコストの計算式

月間のObservabilityコストを大まかに算出する計算式を整理すると以下の通りだ。

月間コスト = (インジェストボリューム x インジェスト単価) + (保存ボリューム x ストレージ単価) + (クエリ数 x クエリ単価)

インジェストボリューム = サービス数 x サービスあたりRPS x スパン/ログ比率 x 平均ペイロードサイズ x 保持期間

大規模プラットフォームで一日に数十億件のデータポイントを処理するとカーディナリティが急速に増加し、これがObservabilityスタック全体で最大のコスト要因になる。インデックスが数百万個のシリーズを含むとRAMとディスクの使用量が急増し、インジェスト遅延とデータのドロップが発生する。

サンプリング戦略:Head vs Tail vs Adaptive

サンプリングはObservabilityのコスト削減で最も即効性のある戦略だ。すべてのトレースを100%収集する代わりに、意味のあるデータだけを選別して保存することで、ボリュームを劇的に減らせる。

サンプリング戦略の比較

項目Head SamplingTail SamplingAdaptive Sampling
決定タイミングトレース開始時(SDKレベル)トレース完了後(Collectorレベル)リアルタイムのトラフィックパターンに基づく動的
決定根拠確率またはトレースID全スパンの分析(遅延、エラー、属性)サービス別のトラフィック量とエラー率
コスト削減率高い(ネットワーク帯域まで節約)中程度(Collectorまでは全量転送)中~高
データ品質低い(エラートレースの欠落あり)高い(エラー/高遅延トレースを100%保存)高い
実装の複雑さ低い高い(メモリとルーティングが必要)非常に高い
メモリ使用最小高い(decision_wait の間すべてのスパンをバッファ)中程度
適したシナリオトラフィックが極めて多い非中核サービス本番の中核サービストラフィック変動の大きい大規模プラットフォーム

Head Samplingの設定

Head SamplingはSDKレベルでトレースを生成するかどうかを決定する。最も単純でネットワーク帯域まで節約できるが、エラーや高遅延のトレースを取りこぼす可能性がある。Consistent Probability Samplingは、同じトレースIDに対してすべてのサービスが同じサンプリング決定を下すことを保証する。

# OpenTelemetry Collector - Probabilistic Sampler Processor
processors:
  probabilistic_sampler:
    # 全トレースの10%のみをサンプリング
    sampling_percentage: 10
    # hash_seed: トレースIDのハッシュシード(複数のCollectorインスタンスで同じ決定になるよう固定)
    hash_seed: 22

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [probabilistic_sampler, batch]
      exporters: [otlp/tempo]

Python SDKでHead Samplingを設定する方法は以下の通りだ。

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased, ParentBasedTraceIdRatio
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

# 10% Head Sampling - ParentBasedは親スパンの決定に従う
sampler = ParentBasedTraceIdRatio(0.1)

provider = TracerProvider(sampler=sampler)

# Batch processorで効率的に転送
provider.add_span_processor(
    BatchSpanProcessor(
        OTLPSpanExporter(endpoint="http://otel-collector:4317"),
        max_queue_size=2048,
        max_export_batch_size=512,
        schedule_delay_millis=5000,
    )
)
trace.set_tracer_provider(provider)

Tail Samplingの設定

Tail Samplingは、トレースのすべてのスパンが収集された後にサンプリング決定を下す。エラーが発生したトレースや遅延が大きいトレースを100%保存しつつ、正常なトレースだけを低い比率でサンプリングできるため、データ品質を損なわずにコストを削減できる。

注意点は、Tail Samplingが動作するには同一トレースのすべてのスパンが 同じCollectorインスタンス に到着しなければならないことだ。複数Collector環境では、トレースID基準のロードバランサーまたは groupbytrace プロセッサが不可欠だ。

# OpenTelemetry Collector - Tail Sampling Processor
# 必ずGatewayモードのCollectorで使用すること
processors:
  tail_sampling:
    # 最初のスパンを受信してから決定までの待機時間(トレース完了を待つ)
    decision_wait: 30s
    # メモリに保持する最大トレース数
    num_traces: 100000
    # 秒あたりの想定新規トレース数(内部メモリ割り当ての最適化)
    expected_new_traces_per_sec: 1000
    policies:
      # ポリシー1: エラーを含むトレースは100%保存
      - name: error-policy
        type: status_code
        status_code:
          status_codes:
            - ERROR

      # ポリシー2: 500ms以上遅延したトレースは100%保存
      - name: latency-policy
        type: latency
        latency:
          threshold_ms: 500

      # ポリシー3: 特定サービスのトレースを100%保存
      - name: critical-service-policy
        type: string_attribute
        string_attribute:
          key: service.name
          values:
            - payment-service
            - auth-service
            - order-service

      # ポリシー4: 残りの正常なトレースは5%だけサンプリング
      - name: normal-traffic-policy
        type: probabilistic
        probabilistic:
          sampling_percentage: 5

      # ポリシー5: 複合ポリシー - AND条件
      - name: composite-policy
        type: and
        and:
          and_sub_policy:
            - name: is-health-check
              type: string_attribute
              string_attribute:
                key: http.route
                values:
                  - /healthz
                  - /readyz
                  - /livez
            - name: drop-most
              type: probabilistic
              probabilistic:
                sampling_percentage: 0.1

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, tail_sampling, batch]
      exporters: [otlp/tempo]

Tail Samplingアーキテクチャ:2-Tier Collectorパターン

本番環境でTail Samplingを安定して運用するには、Agent CollectorとGateway Collectorを分離する2-Tierアーキテクチャが不可欠だ。

[サービスA] --OTLP--> [Agent Collector (DaemonSet)]
[サービスB] --OTLP--> [Agent Collector (DaemonSet)]  --トレースID基準のルーティング-->
[サービスC] --OTLP--> [Agent Collector (DaemonSet)]

                                                     [Gateway Collector #1] --> [Tempo]
                                                     [Gateway Collector #2] --> [Tempo]
                                                     [Gateway Collector #3] --> [Tempo]
                                                     (Tail Samplingを実行)

Agent Collectorでは loadbalancing exporter を使用して、同一トレースIDのスパンを同じGatewayへルーティングする。

# Agent Collectorの設定 - LoadBalancing Exporter
exporters:
  loadbalancing:
    protocol:
      otlp:
        tls:
          insecure: true
    resolver:
      dns:
        hostname: otel-gateway-headless.observability.svc.cluster.local
        port: 4317

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [loadbalancing]

ログフィルタリングパイプライン

ログはObservabilityコストで二番目に大きな比重を占める。特にデバッグログ、ヘルスチェックログ、繰り返し発生するインフラログが全体ボリュームの大半を占めながら、実質的な分析価値は低い場合が多い。OpenTelemetry CollectorのFilter ProcessorとTransform Processorを活用すれば、パイプラインレベルで不要なログを除去し、必要な情報だけをバックエンドへ転送できる。

ログレベルに基づくフィルタリング

本番環境でDEBUG、TRACEレベルのログをCollector側でドロップすると、一般的に30~50%のログボリュームを削減できる。

# OpenTelemetry Collector - Log Level Filtering
processors:
  # DEBUG/TRACEログをドロップ
  filter/drop-debug:
    error_mode: ignore
    logs:
      log_record:
        - 'severity_number < SEVERITY_NUMBER_INFO'

  # 特定のログ本文パターンをドロップ(ヘルスチェック、readiness probe)
  filter/drop-healthcheck:
    error_mode: ignore
    logs:
      log_record:
        - 'IsMatch(body, ".*GET /health.*")'
        - 'IsMatch(body, ".*GET /readyz.*")'
        - 'IsMatch(body, ".*GET /livez.*")'
        - 'IsMatch(body, ".*kube-probe.*")'

  # 特定ネームスペースのログのみ保存
  filter/namespace:
    error_mode: ignore
    logs:
      log_record:
        - 'resource.attributes["k8s.namespace.name"] == "kube-system"'

  # 属性変換 - 不要なフィールドを除去してペイロードを縮小
  transform/reduce-attributes:
    error_mode: ignore
    log_statements:
      - context: log
        statements:
          - delete_key(attributes, "log.file.path")
          - delete_key(attributes, "log.iostream")
          - truncate_all(attributes, 256)
          - limit(attributes, 20)

service:
  pipelines:
    logs:
      receivers: [otlp, filelog]
      processors:
        - memory_limiter
        - filter/drop-debug
        - filter/drop-healthcheck
        - filter/namespace
        - transform/reduce-attributes
        - batch
      exporters: [otlp/loki]

ログのサンプリングと集約

すべてのログを個別に保存する代わりに、繰り返されるログを集約してカウントだけを記録する戦略も効果的だ。例えば、同じエラーメッセージが毎秒1,000件発生する場合、1件だけ保存してカウント属性を追加する方式だ。

# OpenTelemetry Collector - Group By Attributesでログを集約
processors:
  groupbyattrs:
    keys:
      - service.name
      - severity_text
      - log.template

  # 重複ログを集約した後にカウント属性を追加
  transform/aggregate-count:
    error_mode: ignore
    log_statements:
      - context: log
        statements:
          - set(attributes["log.dedup_count"], 1) where attributes["log.dedup_count"] == nil

ログフィルタリング効果の測定

フィルタリングポリシーを適用した後は、必ず効果を測定しなければならない。Collector自体のメトリクスを活用すれば、フィルタリング前後のボリューム変化を定量的に追跡できる。

#!/bin/bash
# Collector内部メトリクスでフィルタリング効果を測定
# Prometheusエンドポイントから収集

# フィルタリング前に受信したログ数
RECEIVED=$(curl -s http://localhost:8888/metrics | \
  grep 'otelcol_receiver_accepted_log_records' | \
  awk '{sum += $2} END {print sum}')

# フィルタリング後に転送したログ数
EXPORTED=$(curl -s http://localhost:8888/metrics | \
  grep 'otelcol_exporter_sent_log_records' | \
  awk '{sum += $2} END {print sum}')

# ドロップ率の計算
if [ "$RECEIVED" -gt 0 ]; then
  DROP_RATE=$(echo "scale=2; (1 - $EXPORTED / $RECEIVED) * 100" | bc)
  echo "受信ログ: $RECEIVED"
  echo "転送ログ: $EXPORTED"
  echo "ドロップ率: ${DROP_RATE}%"
  echo "想定コスト削減: 月間 $(echo "scale=0; $DROP_RATE * 150 / 100" | bc) USD (基準: 150 USD/月)"
fi

# プロセッサ別のドロップ数を確認
echo ""
echo "=== プロセッサ別のドロップ状況 ==="
curl -s http://localhost:8888/metrics | \
  grep 'otelcol_processor_dropped_log_records' | \
  sort -t' ' -k2 -rn

メトリクスのカーディナリティ管理

メトリクスのカーディナリティ爆発(Cardinality Explosion)は、Observabilityコストを予測不可能にする主要な原因だ。すべての固有な時系列はデータベースインデックスに個別のエントリを要求し、数百万個のシリーズが生成されるとRAMとディスクの使用量が急増し、インジェスト遅延とクエリ性能の低下が発生する。

カーディナリティ爆発の原因

カーディナリティ爆発は一般的に、以下のようなラベル使用パターンで発生する。

危険なパターンカーディナリティの増加解決方法
ユーザーIDをラベルにuser_id="u12345"ユーザー数だけシリーズ増加ラベルを除去し、ログ/トレースへ移動
リクエストパスの原文path="/api/users/12345"無限に増加パスを正規化 path="/api/users/:id"
Pod名pod="web-7f8c9-xk2m"デプロイのたびに増加Deployment名のみ使用
エラーメッセージ全文error="Connection refused: 10.0.1.42:3306"IP別にシリーズ生成エラーコードで分類
タイムスタンプのラベルrequest_time="1709856000"毎秒シリーズ生成ラベルから除去

Collectorレベルのカーディナリティ制御

OpenTelemetry Collectorでメトリクスのラベルを整理し、カーディナリティを制御する設定は以下の通りだ。

# OpenTelemetry Collector - メトリクスのカーディナリティ管理
processors:
  # 高カーディナリティ属性を除去
  metricstransform/drop-high-cardinality:
    transforms:
      - include: http.server.request.duration
        action: update
        operations:
          # user_id、session_idなど高カーディナリティのラベルを除去
          - action: delete_label_value
            label: user_id
          - action: delete_label_value
            label: session_id

  # URLパスの正規化
  transform/normalize-paths:
    error_mode: ignore
    metric_statements:
      - context: datapoint
        statements:
          - replace_pattern(attributes["url.path"], "^/api/users/[0-9]+", "/api/users/:id")
          - replace_pattern(attributes["url.path"], "^/api/orders/[0-9]+", "/api/orders/:id")
          - replace_pattern(attributes["url.path"], "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", ":uuid")

  # 属性数の制限
  attributes/limit:
    actions:
      - key: http.request.header.x-request-id
        action: delete
      - key: http.request.header.authorization
        action: delete

  # メトリクス集約 - 細かいラベルを除去し集約済みメトリクスのみ転送
  filter/metrics-allowlist:
    error_mode: ignore
    metrics:
      metric:
        # 特定のメトリクスのみ許可(ホワイトリスト方式)
        - 'name == "http.server.request.duration"'
        - 'name == "http.server.active_requests"'
        - 'name == "process.runtime.go.goroutines"'
        - 'IsMatch(name, "^system\\..*")'

service:
  pipelines:
    metrics:
      receivers: [otlp, prometheus]
      processors:
        - memory_limiter
        - metricstransform/drop-high-cardinality
        - transform/normalize-paths
        - attributes/limit
        - filter/metrics-allowlist
        - batch
      exporters: [prometheusremotewrite/mimir]

Observability Budgetパターン

最近、大規模組織で導入されているパターンが Observability Budget だ。各サービスが放出できるテレメトリデータの最大量を事前に定義し、予算を超過するとCollectorが自動的にデータをドロップする方式だ。

#!/usr/bin/env python3
"""
Observability Budget Enforcer
サービス別のテレメトリ予算を監視し、警告を発生させるスクリプト
"""

import requests
import yaml
import sys
from datetime import datetime

# サービス別Observability Budgetの定義(日次基準)
BUDGETS = {
    "payment-service": {
        "traces_per_day": 5_000_000,
        "logs_per_day": 10_000_000,
        "metrics_series": 50_000,
        "alert_threshold": 0.8,  # 80%到達時に警告
    },
    "user-service": {
        "traces_per_day": 2_000_000,
        "logs_per_day": 5_000_000,
        "metrics_series": 30_000,
        "alert_threshold": 0.8,
    },
    "default": {
        "traces_per_day": 1_000_000,
        "logs_per_day": 3_000_000,
        "metrics_series": 20_000,
        "alert_threshold": 0.8,
    },
}

PROMETHEUS_URL = "http://prometheus:9090"


def query_prometheus(query: str) -> float:
    """Prometheusから現在の値を取得する。"""
    resp = requests.get(
        f"{PROMETHEUS_URL}/api/v1/query",
        params={"query": query},
    )
    result = resp.json().get("data", {}).get("result", [])
    if result:
        return float(result[0]["value"][1])
    return 0.0


def check_budget(service_name: str) -> dict:
    """サービスの現在のテレメトリ使用量を予算と比較して確認する。"""
    budget = BUDGETS.get(service_name, BUDGETS["default"])
    today = datetime.now().strftime("%Y-%m-%d")

    # 今日生成されたトレース数
    traces_today = query_prometheus(
        f'sum(increase(traces_spanmetrics_calls_total{{service_name="{service_name}"}}[24h]))'
    )

    # 今日生成されたログ数
    logs_today = query_prometheus(
        f'sum(increase(loki_distributor_lines_received_total{{service="{service_name}"}}[24h]))'
    )

    # 現在のアクティブなメトリクスシリーズ数
    active_series = query_prometheus(
        f'count({{service_name="{service_name}"}})'
    )

    usage = {
        "service": service_name,
        "date": today,
        "traces": {
            "current": int(traces_today),
            "budget": budget["traces_per_day"],
            "usage_pct": round(traces_today / budget["traces_per_day"] * 100, 1),
        },
        "logs": {
            "current": int(logs_today),
            "budget": budget["logs_per_day"],
            "usage_pct": round(logs_today / budget["logs_per_day"] * 100, 1),
        },
        "metrics_series": {
            "current": int(active_series),
            "budget": budget["metrics_series"],
            "usage_pct": round(active_series / budget["metrics_series"] * 100, 1),
        },
    }

    # 予算超過の確認
    for signal_type, data in usage.items():
        if isinstance(data, dict) and "usage_pct" in data:
            if data["usage_pct"] >= budget["alert_threshold"] * 100:
                print(
                    f"[WARNING] {service_name}: {signal_type} 予算 "
                    f"{data['usage_pct']}% 使用 ({data['current']:,} / {data['budget']:,})"
                )

    return usage


if __name__ == "__main__":
    services = sys.argv[1:] if len(sys.argv) > 1 else list(BUDGETS.keys())
    for svc in services:
        if svc == "default":
            continue
        result = check_budget(svc)
        print(f"\n=== {svc} Observability Budget ===")
        for key, val in result.items():
            if isinstance(val, dict):
                print(f"  {key}: {val['current']:,} / {val['budget']:,} ({val['usage_pct']}%)")

ストレージティアリングアーキテクチャ

すべてのテレメトリデータを同じストレージ階層に保存するのは、コストの観点から非効率だ。直近7日間のデータは高速なクエリのために高性能ストレージへ、それ以降のデータは低コストストレージへ移す Hot/Warm/Coldティアリング がコスト最適化の核心だ。

ストレージティアの比較

項目Hot TierWarm TierCold TierArchive
保持期間0~7日7~30日30~90日90日~1年+
ストレージ種別NVMe SSD / EBS gp3EBS st1 / S3 StandardS3 Infrequent AccessS3 Glacier
GBあたり月額コスト$0.10~0.16$0.025~0.045$0.0125$0.004
クエリ遅延ms単位秒単位分単位時間単位
適したデータリアルタイム監視、通知直近の障害分析、トレンド監査、コンプライアンス法的保存義務
コスト削減率 (vs Hot)基準70~75%87~92%97%

Elasticsearch Hot-Warm-Coldアーキテクチャ

ログバックエンドとしてElasticsearchを使用する場合、ILM(Index Lifecycle Management)によって自動ティアリングを構成できる。

# Elasticsearch ILM Policy - ログのティアリング
# PUT _ilm/policy/observability-logs-policy
{
  'policy':
    {
      'phases':
        {
          'hot':
            {
              'min_age': '0ms',
              'actions':
                {
                  'rollover': { 'max_primary_shard_size': '50gb', 'max_age': '1d' },
                  'set_priority': { 'priority': 100 },
                },
            },
          'warm':
            {
              'min_age': '7d',
              'actions':
                {
                  'shrink': { 'number_of_shards': 1 },
                  'forcemerge': { 'max_num_segments': 1 },
                  'allocate': { 'require': { 'data': 'warm' } },
                  'set_priority': { 'priority': 50 },
                },
            },
          'cold':
            {
              'min_age': '30d',
              'actions':
                {
                  'searchable_snapshot':
                    { 'snapshot_repository': 's3-repo', 'force_merge_index': true },
                  'allocate': { 'require': { 'data': 'cold' } },
                  'set_priority': { 'priority': 0 },
                },
            },
          'delete': { 'min_age': '365d', 'actions': { 'delete': {} } },
        },
    },
}

Grafana Tempo + S3ティアリング

トレースバックエンドとしてGrafana Tempoを使用する場合、ブロックストレージベースのティアリングを構成できる。Tempoは基本的にオブジェクトストレージ(S3)をバックエンドとして使用するため、S3 Intelligent-Tieringを活用すれば自動的にコストを最適化できる。

S3 Intelligent-Tieringはアクセスパターンに応じて自動的にデータを移動する。30日間アクセスがなければInfrequent Accessティアへ移動して40%削減、90日間アクセスがなければArchive Instant Accessティアへ移動して68%削減を提供する。T-Mobileは1.87PBのデータレイクにこの戦略を適用し、S3コストを40%削減した。

Grafana Mimirのメトリクスティアリング

メトリクスバックエンドとしてGrafana Mimirを使用する場合、compactorとstore-gatewayの設定でデータ保持とダウンサンプリングを組み合わせる。

# Mimir設定 - メトリクスの保持およびダウンサンプリング
limits:
  # 元の解像度のメトリクスは14日間保持
  compactor_blocks_retention_period: 14d

compactor:
  # ダウンサンプリングを有効化
  downsample:
    # 7日以降は5分解像度へダウンサンプル
    - resolution: 5m
      retention: 7d
    # 30日以降は1時間解像度へダウンサンプル
    - resolution: 1h
      retention: 30d

store_gateway:
  sharding_ring:
    replication_factor: 3

# S3バックエンドの設定
blocks_storage:
  backend: s3
  s3:
    bucket_name: mimir-metrics
    endpoint: s3.ap-northeast-2.amazonaws.com
    storage_class: INTELLIGENT_TIERING
  bucket_store:
    sync_dir: /data/mimir-sync
    # インデックスキャッシュでクエリ性能を維持
    index_cache:
      backend: memcached
      memcached:
        addresses: dns+memcached.observability.svc:11211

統合コスト最適化アーキテクチャ

ここまで説明したサンプリング、フィルタリング、カーディナリティ管理、ストレージティアリングを統合した全体アーキテクチャを図示すると以下の通りだ。

                    [マイクロサービスクラスタ]
                    (SDK Head Sampling 10%)
                            |
                            v
            +-------------------------------+
            |   Agent Collector (DaemonSet)  |
            |   - Memory Limiter            |
            |   - Filter/drop-debug         |
            |   - Filter/drop-healthcheck   |
            |   - Attributes/limit          |
            +-------------------------------+
                     |          |          |
                  Traces     Logs      Metrics
                     |          |          |
                     v          v          v
     +---------------+    +---------+  +-----------+
     | Gateway        |    | Gateway |  | Gateway   |
     | Collector      |    | (Logs)  |  | (Metrics) |
     | (Traces)       |    |         |  |           |
     | - Tail Sampling|    | - Group |  | - Cardina |
     | - LoadBalance  |    |   ByAttr|  |   lity    |
     +-------+--------+    +----+----+  +-----+-----+
             |                   |              |
             v                   v              v
     +-------+--------+  +------+------+  +----+------+
     | Tempo          |  | Loki        |  | Mimir     |
     | (S3 + Intelli- |  | (S3 + ILM)  |  | (S3 +     |
     |  gent Tiering) |  |             |  | Downsample|
     +----------------+  +-------------+  +-----------+
             |                   |              |
             v                   v              v
     +--------------------------------------------------+
     |              Grafana Dashboard                    |
     |  - リアルタイム監視 (Hot Tier)                   |
     |  - 障害分析 (Warm Tier)                          |
     |  - コンプライアンス監査 (Cold/Archive Tier)      |
     +--------------------------------------------------+

このアーキテクチャを適用すると、以下のようなコスト削減効果が期待できる。

最適化レイヤー適用手法想定ボリューム削減コスト削減効果
SDK Head Sampling10%の確率サンプリングトレース90%削減ネットワーク + ストレージコストを大幅削減
AgentログフィルタリングDEBUG/ヘルスチェックをドロップログ30~50%削減インジェスト + ストレージコスト削減
Gateway Tail Samplingエラー/高遅延を保存 + 正常5%残りのトレース95%削減最終ストレージコスト削減
カーディナリティ制御高カーディナリティラベルを除去シリーズ数50~70%削減インデックス + クエリコスト削減
ストレージティアリングHot/Warm/Cold + S3 ITなし(同一データ、低コスト保存)ストレージコスト40~68%削減

総合的にこれらの戦略をすべて適用すれば、 Observabilityコスト全体を60~80%削減 しながらも、エラートレースと異常の兆候に対する可観測性は100%維持できる。

障害事例と復旧手順

コスト最適化戦略を適用する際に発生しうる障害シナリオと、その復旧方法を事前に把握しておく必要がある。

障害事例1: Tail SamplingのOOM (Out of Memory)

症状: Gateway Collectorが繰り返しOOMで再起動する。decision_wait の間にメモリへ蓄積されるスパン数が num_traces の上限を超え、メモリが急増する。

原因: トラフィック急増時に num_tracesdecision_wait の設定がメモリ容量を超える。例えば decision_wait: 60s で毎秒10,000件の新規トレースが流入すると、最大600,000件のトレースをメモリに保持しなければならない。

復旧手順:

  1. decision_wait を30s以下に短縮する(トレース完了率とのトレードオフ)。
  2. num_traces をメモリ容量に合わせて調整する。経験的にトレースあたり約1~5KBと見積もって計算する。
  3. memory_limiter プロセッサをTail Samplingの前に配置し、メモリ閾値を超えたらデータをドロップさせる。
  4. Gateway Collectorのリソースを垂直スケールするか、インスタンス数を水平スケールする。
# OOM防止のためのMemory Limiter + Tail Samplingの組み合わせ
processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 3800 # 4GBコンテナで200MBの余裕
    spike_limit_mib: 800 # 一時的なバーストを許容

  tail_sampling:
    decision_wait: 20s # 30sから20sへ短縮
    num_traces: 50000 # メモリを基準に算出
    expected_new_traces_per_sec: 2500
    policies:
      - name: error-always
        type: status_code
        status_code:
          status_codes: [ERROR]
      - name: latency-always
        type: latency
        latency:
          threshold_ms: 500
      - name: probabilistic-default
        type: probabilistic
        probabilistic:
          sampling_percentage: 5

service:
  pipelines:
    traces:
      receivers: [otlp]
      # memory_limiterが必ず最初
      processors: [memory_limiter, tail_sampling, batch]
      exporters: [otlp/tempo]

障害事例2: 過度なフィルタリングによるブラインドスポット

症状: 本番で障害が発生したのに関連するログとトレースがなく、原因を分析できない。フィルタリングポリシーが攻撃的すぎる設定になっており、中核となるデータまでドロップされていた。

原因: ログフィルタが正規表現パターンを広く取りすぎて関連するログまでマッチさせたり、メトリクスのラベル除去が過度で特定インスタンスの異常を識別できなくなったりした場合に発生する。

復旧手順:

  1. フィルタリングポリシーを変更する際は必ず Shadow Mode を先に適用する。実際にはドロップせず、ドロップされるデータのカウントだけを記録して影響度を事前に評価する。
  2. 中核サービス(決済、認証、注文)に対しては、フィルタリングの例外ルールを適用する。
  3. error_mode: ignore を活用し、条件評価に失敗した場合でもデータをドロップしないようにする。

障害事例3: カーディナリティ爆発によるMimir/Prometheusのインジェスト失敗

症状: PrometheusまたはMimirで too many active series エラーが発生し、メトリクスのインジェストが拒否される。Grafanaダッシュボードに空のパネルが現れる。

原因: 新しいサービスをデプロイする際にラベル値へ固有識別子(UUID、タイムスタンプ、Pod名など)が含まれ、シリーズ数が爆発的に増加する。

復旧手順:

  1. ただちにCollectorの metricstransform プロセッサで問題のラベルをドロップする。
  2. Mimirの max_global_series_per_user の上限を一時的に引き上げる。
  3. 原因となったサービスのSDK計装コードを修正し、高カーディナリティ属性を除去する。
  4. 長期的には、CI/CDパイプラインにカーディナリティ検証ステップを追加する。

障害事例4: ティアリング移行中のクエリ失敗

症状: ILMポリシーによってインデックスがWarmやColdティアへ移行している間、その時間帯のログクエリが失敗またはタイムアウトする。

原因: ティア移行中のシャード再配置、フォースマージ、スナップショット作成などの作業がクラスタリソースを占有し、クエリ性能が低下する。

復旧手順:

  1. ILMの移行作業を、トラフィックの少ない時間帯(深夜2~5時)にスケジュールする。
  2. 移行中でもクエリが可能なように searchable_snapshot を活用する。
  3. Warm/Coldノードのリソースを十分に確保し、移行作業が速やかに完了するようにする。

運用上の注意事項

コスト最適化戦略を適用する際に必ず留意すべき事項を整理する。

サンプリング関連

フィルタリング関連

カーディナリティ関連

ティアリング関連

コスト最適化チェックリスト

以下のチェックリストを活用して、現在のObservabilityパイプラインのコスト最適化レベルを点検できる。

Phase 1: すぐに適用できるQuick Wins(1~2週間)

Phase 2: 中期の最適化(2~4週間)

Phase 3: 長期のインフラ最適化(1~3か月)

トラブルシューティングガイド

Collectorのメモリ使用量が増え続ける場合

# 1. Collectorのメモリ使用量を確認
kubectl top pods -n observability -l app=otel-gateway

# 2. pprofでメモリプロファイリング(Extensionの有効化が必要)
curl -s http://localhost:1777/debug/pprof/heap > heap.prof
go tool pprof -top heap.prof

# 3. zPagesでパイプラインの状態を確認
curl -s http://localhost:55679/debug/tracez | jq .

# 4. Tail Samplingのキュー状態を確認
curl -s http://localhost:8888/metrics | grep tail_sampling

フィルタリングポリシーが想定どおりに動作しない場合

  1. error_modepropagate に変更して、エラーをログへ出力する。
  2. debug exporter をパイプラインに追加し、フィルタリング後に残ったデータを確認する。
  3. OTTL(OpenTelemetry Transformation Language)式の文法を再確認する。特に bodyattributes のアクセス方法の違いに注意する。
# デバッグ用のパイプライン設定
exporters:
  debug:
    verbosity: detailed
    sampling_initial: 5
    sampling_thereafter: 200

service:
  pipelines:
    logs/debug:
      receivers: [otlp]
      processors: [filter/drop-debug]
      exporters: [debug]

カーディナリティ爆発の原因サービスを特定する方法

# Prometheusでカーディナリティ上位のメトリクスを確認
curl -s http://prometheus:9090/api/v1/status/tsdb | \
  jq '.data.seriesCountByMetricName | sort_by(-.value) | .[0:10]'

# 特定メトリクスのラベルカーディナリティを分析
curl -s 'http://prometheus:9090/api/v1/query?query=count(http_server_request_duration_seconds_bucket) by (service_name)' | \
  jq '.data.result | sort_by(-.value[1] | tonumber) | .[0:10]'

# Mimirでテナント別のアクティブシリーズ数を確認
curl -s http://mimir:8080/api/v1/cardinality/active_series | \
  jq '.data | sort_by(-.active_series) | .[0:10]'

おわりに

Observabilityのコスト最適化は単にデータを減らすことではなく、シグナルとノイズを精密に分離し、可観測性の品質を維持しながらコスト効率を最大化するエンジニアリング活動だ。

この記事で扱った戦略をまとめると以下の通りだ。

  1. サンプリング: Head Samplingでネットワークコストまで削減しつつ、中核サービスにはTail Samplingを適用してエラーと高遅延のトレースを100%保存する。
  2. フィルタリング: DEBUG/ヘルスチェックのログをCollector側でドロップし、属性の整理でペイロードサイズを減らす。
  3. カーディナリティ管理: 高カーディナリティのラベルを除去し、URLパスを正規化し、Observability Budgetでサービス別の使用量を統制する。
  4. ストレージティアリング: Hot/Warm/Coldアーキテクチャで、データの古さに応じて低コストストレージへ自動的に移す。

これらの戦略を体系的に適用すれば、Observabilityコスト全体を60~80%削減しながらも、障害対応に必要なデータは完全に保存できる。コスト最適化は一回きりの作業ではなく、サービスの成長とともに継続的に調整すべき運用プロセスだ。チェックリストを定期的に点検し、Collector内部メトリクスとコスト推移を継続的に監視して最適なバランスを維持することが核心だ。

参考資料

コメント

まだコメントはありません。

ログインするとコメントできます