- はじめに
- コスト構造分析:どこでコストが発生するか
- サンプリング戦略:Head vs Tail vs Adaptive
- ログフィルタリングパイプライン
- メトリクスのカーディナリティ管理
- ストレージティアリングアーキテクチャ
- 統合コスト最適化アーキテクチャ
- 障害事例と復旧手順
- 運用上の注意事項
- コスト最適化チェックリスト
- トラブルシューティングガイド
- おわりに
- 参考資料

はじめに
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コストを最適化するには、まずコストがどこでどのように発生するのかを正確に理解する必要がある。テレメトリパイプラインのコストは大きく四つの軸に分類できる。
シグナル別のコスト比重
| シグナルタイプ | コスト比重 | 主なコスト要因 | 最適化の難易度 |
|---|---|---|---|
| Traces | 60~70% | 高いカーディナリティ、大容量ペイロード、スパン数の爆発 | 高い |
| Logs | 20~30% | 非構造化データ、高いボリューム、フルテキストインデックス | 中程度 |
| Metrics | 5~15% | 時系列カーディナリティ、ラベル組み合わせの爆発 | 中程度 |
| Profiles | 1~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 Sampling | Tail Sampling | Adaptive 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 Tier | Warm Tier | Cold Tier | Archive |
|---|---|---|---|---|
| 保持期間 | 0~7日 | 7~30日 | 30~90日 | 90日~1年+ |
| ストレージ種別 | NVMe SSD / EBS gp3 | EBS st1 / S3 Standard | S3 Infrequent Access | S3 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 Sampling | 10%の確率サンプリング | トレース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_traces と decision_wait の設定がメモリ容量を超える。例えば decision_wait: 60s で毎秒10,000件の新規トレースが流入すると、最大600,000件のトレースをメモリに保持しなければならない。
復旧手順:
decision_waitを30s以下に短縮する(トレース完了率とのトレードオフ)。num_tracesをメモリ容量に合わせて調整する。経験的にトレースあたり約1~5KBと見積もって計算する。memory_limiterプロセッサをTail Samplingの前に配置し、メモリ閾値を超えたらデータをドロップさせる。- 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: 過度なフィルタリングによるブラインドスポット
症状: 本番で障害が発生したのに関連するログとトレースがなく、原因を分析できない。フィルタリングポリシーが攻撃的すぎる設定になっており、中核となるデータまでドロップされていた。
原因: ログフィルタが正規表現パターンを広く取りすぎて関連するログまでマッチさせたり、メトリクスのラベル除去が過度で特定インスタンスの異常を識別できなくなったりした場合に発生する。
復旧手順:
- フィルタリングポリシーを変更する際は必ず Shadow Mode を先に適用する。実際にはドロップせず、ドロップされるデータのカウントだけを記録して影響度を事前に評価する。
- 中核サービス(決済、認証、注文)に対しては、フィルタリングの例外ルールを適用する。
error_mode: ignoreを活用し、条件評価に失敗した場合でもデータをドロップしないようにする。
障害事例3: カーディナリティ爆発によるMimir/Prometheusのインジェスト失敗
症状: PrometheusまたはMimirで too many active series エラーが発生し、メトリクスのインジェストが拒否される。Grafanaダッシュボードに空のパネルが現れる。
原因: 新しいサービスをデプロイする際にラベル値へ固有識別子(UUID、タイムスタンプ、Pod名など)が含まれ、シリーズ数が爆発的に増加する。
復旧手順:
- ただちにCollectorの
metricstransformプロセッサで問題のラベルをドロップする。 - Mimirの
max_global_series_per_userの上限を一時的に引き上げる。 - 原因となったサービスのSDK計装コードを修正し、高カーディナリティ属性を除去する。
- 長期的には、CI/CDパイプラインにカーディナリティ検証ステップを追加する。
障害事例4: ティアリング移行中のクエリ失敗
症状: ILMポリシーによってインデックスがWarmやColdティアへ移行している間、その時間帯のログクエリが失敗またはタイムアウトする。
原因: ティア移行中のシャード再配置、フォースマージ、スナップショット作成などの作業がクラスタリソースを占有し、クエリ性能が低下する。
復旧手順:
- ILMの移行作業を、トラフィックの少ない時間帯(深夜2~5時)にスケジュールする。
- 移行中でもクエリが可能なように
searchable_snapshotを活用する。 - Warm/Coldノードのリソースを十分に確保し、移行作業が速やかに完了するようにする。
運用上の注意事項
コスト最適化戦略を適用する際に必ず留意すべき事項を整理する。
サンプリング関連
- Tail Samplingの
decision_waitを短くしすぎると 遅れて到着するスパンが欠落し、不完全なトレースが保存される。一般的にサービス間の最大遅延の2倍以上に設定する。 - Head SamplingとTail Samplingを同時に適用すると SDKですでにドロップしたトレースはTail Samplingで復元できない。二つの戦略を併用する場合は、Head Samplingの比率を保守的に(50%以上)設定する。
- Tail Sampling Gatewayをスケールアウトする際は 必ずトレースID基準のルーティング(LoadBalancing ExporterまたはConsistent Hashing)を維持しなければならない。誤ったルーティングは同一トレースのスパンを別インスタンスへ分散させ、サンプリング決定を歪める。
フィルタリング関連
- フィルタリングポリシーを変更する際は必ず段階的に適用する。カナリアパイプラインでまずテストし、ドロップされるデータの特性を検証してから全体へ展開する。
- エラーレベルのログは絶対にフィルタリングしない。ERROR、FATALレベルのログをドロップすると、障害対応時に致命的なブラインドスポットが発生する。
- Collector自体のメトリクス(
otelcol_processor_dropped_*)を必ず監視する。ドロップ率が想定範囲を外れたら、ただちに通知が発生するよう設定する。
カーディナリティ関連
- 新しいサービスをデプロイする前にカーディナリティ影響分析を実施する。CI/CDパイプラインに、メトリクスラベルの想定カーディナリティを検証するステップを追加する。
- Recording Ruleを活用して、頻繁に使う高カーディナリティのクエリを事前集約する。こうすればクエリ時点の演算コストと時間を減らせる。
ティアリング関連
- Cold Tierからデータを照会するとコストが発生する。S3 Glacierからの復元リクエストは件数に応じて課金されるため、頻繁なCold照会が想定される場合はS3 Intelligent-TieringのArchive Instant Accessティアを活用する。
- 保持期間ポリシーは法的/規制要件を必ず確認してから設定する。金融、医療などの規制産業では、特定期間のデータ保存が法的に義務づけられている。
コスト最適化チェックリスト
以下のチェックリストを活用して、現在のObservabilityパイプラインのコスト最適化レベルを点検できる。
Phase 1: すぐに適用できるQuick Wins(1~2週間)
- 本番環境でDEBUG/TRACEレベルのログをCollectorでドロップしているか?
- ヘルスチェック、readiness probe関連のログとトレースをフィルタリングしているか?
- メトリクスラベルにユーザーID、リクエストIDなどの固有識別子が含まれていないか?
- URLパスのラベルが正規化されているか?(
/api/users/123の代わりに/api/users/:id) - Collectorに
memory_limiterプロセッサが設定されているか? - Collector内部メトリクス(ドロップ率、キューサイズ、メモリ使用量)を監視しているか?
Phase 2: 中期の最適化(2~4週間)
- Tail Samplingポリシーがエラー/高遅延トレースを100%保存しつつ、正常トラフィックを5~10%に制限しているか?
- 2-Tier Collectorアーキテクチャ(Agent + Gateway)が構成されているか?
- トレースID基準のルーティング(LoadBalancing Exporter)が設定されているか?
- ログ属性から不要なフィールドを除去しているか?(ファイルパス、ストリーム種別など)
- サービス別のObservability Budgetが定義されているか?
- カーディナリティ監視ダッシュボードが構築されているか?
Phase 3: 長期のインフラ最適化(1~3か月)
- Hot/Warm/Coldストレージティアリングが構成されているか?
- S3 Intelligent-Tieringまたは同等の自動ティアリングが有効になっているか?
- メトリクスのダウンサンプリングポリシーが適用されているか?(7日以降は5分、30日以降は1時間の解像度)
- ILM/保持ポリシーが規制要件を満たしているか?
- CI/CDパイプラインにカーディナリティ検証ゲートが含まれているか?
- コスト監視ダッシュボードで、シグナル別・サービス別のコスト推移を追跡しているか?
- Adaptive Samplingへの移行を検討したか?
トラブルシューティングガイド
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
フィルタリングポリシーが想定どおりに動作しない場合
error_modeをpropagateに変更して、エラーをログへ出力する。debugexporter をパイプラインに追加し、フィルタリング後に残ったデータを確認する。- OTTL(OpenTelemetry Transformation Language)式の文法を再確認する。特に
bodyとattributesのアクセス方法の違いに注意する。
# デバッグ用のパイプライン設定
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のコスト最適化は単にデータを減らすことではなく、シグナルとノイズを精密に分離し、可観測性の品質を維持しながらコスト効率を最大化するエンジニアリング活動だ。
この記事で扱った戦略をまとめると以下の通りだ。
- サンプリング: Head Samplingでネットワークコストまで削減しつつ、中核サービスにはTail Samplingを適用してエラーと高遅延のトレースを100%保存する。
- フィルタリング: DEBUG/ヘルスチェックのログをCollector側でドロップし、属性の整理でペイロードサイズを減らす。
- カーディナリティ管理: 高カーディナリティのラベルを除去し、URLパスを正規化し、Observability Budgetでサービス別の使用量を統制する。
- ストレージティアリング: Hot/Warm/Coldアーキテクチャで、データの古さに応じて低コストストレージへ自動的に移す。
これらの戦略を体系的に適用すれば、Observabilityコスト全体を60~80%削減しながらも、障害対応に必要なデータは完全に保存できる。コスト最適化は一回きりの作業ではなく、サービスの成長とともに継続的に調整すべき運用プロセスだ。チェックリストを定期的に点検し、Collector内部メトリクスとコスト推移を継続的に監視して最適なバランスを維持することが核心だ。
参考資料
- OpenTelemetry Sampling 公式ドキュメント
- OpenTelemetry Collector Contrib - Tail Sampling Processor
- OpenTelemetry Collector Contrib - Filter Processor
- OpenTelemetry Sampling Milestones (2025)
- ClickHouse - A Practical Guide to Observability TCO and Cost Reduction
- Netdata - Metric Cardinality in Observability Platforms
- Logz.io - How to Optimize Your Observability Spend
- Amazon S3 Intelligent-Tiering Storage Class
- Grafana Cloud - Reduce Application Observability Costs
- OpenTelemetry Collector Configuration