【Datadog】カスタムメトリクスのカーディナリティ爆発:インデックス崩壊と課金地獄を防ぐ「骨髄からの設計論」
数々の修羅場をくぐり抜けてきたオブザーバビリティ・アーキテクトなら、月末に送られてくるDatadogの請求書を見て冷や汗を流した経験が一度はあるはずだ。
その原因の9割は、開発チームが無邪気に放り込んだ「カスタムメトリクスのカーディナリティ爆発」にある。
「ユーザーごとにリクエスト数を測りたい」
「トランザクションIDをタグに持たせればデバッグが楽になる」
そんな甘い考えが、Datadogのインデックスストレージを内側から食い破り、カスタムメトリクス(Custom Metrics)の課金を天井知らずに跳ね上げる。
今回は、Datadogの内部アーキテクチャ(TSDBの仕組み)に踏み込み、カーディナリティ爆発の本質と、それを完全に無力化するための実践的な設計論、そしてAPIを使った自動防御ラインの構築手法を授けよう。
—
1. 内部アーキテクチャから暴く「なぜUUIDをタグにしてはいけないのか」
Datadogをはじめとする時系列データベース(TSDB)は、メトリクス名と「タグ(Key-Valueペア)」の組み合わせ(TimeSeries)をインデックス化して保持している。
ここで重要なのは、「一意なタグ値の数(カーディナリティ)が爆発すると、インデックスの転置インデックス(Inverted Index)のサイズが幾何級数的に肥大化する」という物理的制約だ。
炎上するアンチパターンの例
以下のコードを見てほしい。これは典型的な地雷原だ。
from datadog import initialize, statsd
import uuid
import time
options = {‘statsd_host’: ‘127.0.0.1’, ‘statsd_port’: 8125}
initialize(options)
def process_checkout(user_id: str, cart_id: str):
start_time = time.time()
# 処理…
# 【アンチパターン】UUIDやタイムスタンプをタグに含めている
transaction_uuid = str(uuid.uuid4())
statsd.increment(
‘app.checkout.requests’,
tags=[
f’user_id:{user_id}’, # ユーザー数だけ増殖
f’cart_id:{cart_id}’, # カート数だけ増殖
f’tx_uuid:{transaction_uuid}’ # リクエストごとに無限増殖(致命傷)
]
)
# 【アンチパターン】ミリ秒単位の数値をタグにしている
duration_ms = int((time.time() – start_time) 1000)
statsd.gauge(‘app.checkout.duration_ms’, duration_ms, tags=[f’exact_ms:{duration_ms}’])
何が起きるのか?
`tx_uuid` や `exact_ms` のような「二度と同じ値が現れない(あるいは値の種類が数百万・数千万を超える)フィールド」をタグに渡すと、Datadog側で「新しいTimeSeriesが生成された」と判定される。
1. メモリ圧迫とスキャン性能の劣化: DatadogエージェントおよびバックエンドのTSDBは、これら一意な組み合わせをメモリ上のインデックスに載せようとする。これがGarbage Collectionの頻発やエージェントのOOM(Out of Memory)を引き起こす。
2. Custom Metrics課金の急騰: Datadogの課金体系は「月末時点でのユニークなTimeSeries数(Custom Metrics)」に基づいている。これが数百万単位で跳ね上がり、翌月のCFOからの呼び出しが確定する。
—
2. 正しい集約設計:ディメンションの剪定とメトリクス名のリファクタリング
カーディナリティ爆発を防ぐ原則はただ一つ。
「有限の集合(Bounded Set)のみをタグにし、無限の集合(Unbounded Set)はメトリクス名に埋め込むか、ログ・トレースへ追放せよ」
改善アプローチの具体例
NGパターン
メトリクス名: api.request.latency
タグ: method:POST, endpoint:/api/v1/users, user_id:U-99821, client_ip:192.168.1.55
- `user_id` と `client_ip` が無限のカーディナリティを生む。
修正パターン A:メトリクス名への昇格(またはドメイン分割)
どうしても特定のエンティティごとのメトリクスが欲しい場合は、タグではなくメトリクス名そのものを設計し直す(ただしエンティティ数が少ない場合に限る)。
修正パターン B:バケツ化(Bucketing)とサンプリング
レイテンシや金額などの連続値は、正確な値をタグにするのではなく、ヒストグラムやパーセンタイル(Distribution Metric)を活用し、タグ側では「サービス名」「リージョン」「環境」といった静的なディメンションに絞り込む。
from datadog import statsd
def process_checkout_secure(tier: str, region: str, success: bool):
# 【正しい設計】有限かつ意味のあるタグのみを使用する
# 個別のユーザーIDやUUIDはトレース(APM)やログに任せ、メトリクスは集約値のみを送る
statsd.increment(
‘app.checkout.count’,
tags=[
f’tier:{tier}’, # free, premium, enterprise (カーディナリティ: 数種類)
f’region:{region}’, # ap-northeast-1, us-east-1 (カーディナリティ: 数種類)
f’success:{str(success).lower()}’ # true, false (カーディナリティ: 2種類)
]
)
—
3. 自動防御ライン:Datadog API & CLIを用いたインデックス・ガバナンス
人手によるコードレビューだけでは、開発者の「うっかり」を防ぐことはできない。
ここでは、CI/CDパイプラインや定期実行スクリプトで高カーディナリティメトリクスを検知・排除するための実践的なPythonスクリプトを提示する。
このスクリプトは、DatadogのMetrics APIを叩き、カーディナリティ(Active Metricのタグ数やユニーク値)が閾値を超えている違反メトリクスを自動特定するものだ。
高カーディナリティ・ハンター・スクリプト (`detect_high_cardinality.py`)
import os
import requests
from typing import Dict, Any
Datadog API Credentials
DD_API_KEY = os.getenv(“DD_API_KEY”)
DD_APP_KEY = os.getenv(“DD_APP_KEY”)
DD_SITE = os.getenv(“DD_SITE”, “datadoghq.com”) # e.g., datadoghq.eu or us3.datadoghq.com
BASE_URL = f”https://api.{DD_SITE}/api/v1″
def get_active_metrics() -> Dict[str, Any]:
“””アクティブなメトリクスの一覧を取得する”””
headers = {
“DD-API-KEY”: DD_API_KEY,
“DD-APPLICATION-KEY”: DD_APP_KEY,
“Accept”: “application/json”
}
# 直近のメトリクスリストを取得
response = requests.get(f”{BASE_URL}/metrics”, headers=headers)
response.raise_for_status()
return response.json()
def analyze_metric_metadata(metric_name: str):
“””特定のメトリクスのメタデータやタグ構成を検証する”””
headers = {
“DD-API-KEY”: DD_API_KEY,
“DD-APPLICATION-KEY”: DD_APP_KEY,
“Accept”: “application/json”
}
response = requests.get(f”{BASE_URL}/metrics/{metric_name}”, headers=headers)
if response.status_code == 200:
return response.json()
return None
def audit_metrics():
print(“=== Datadog Custom Metrics Cardinality Audit ===”)
data = get_active_metrics()
metrics = data.get(“metrics”, [])
print(f”Total active metrics found: {len(metrics)}”)
# 危険なパターンのキーワード(必要に応じて拡張)
suspicious_keywords = [“uuid”, “guid”, “token”, “email”, “ip”, “timestamp”, “session”, “tx_id”]
for metric in metrics:
metric_name = metric
# メトリクス名自体に個体識別子が含まれていないかチェック
for keyword in suspicious_keywords:
if keyword in metric_name.lower():
print(f”[WARNING] Suspicious metric name detected: ‘{metric_name}’ contains ‘{keyword}'”)
# メトリクスの詳細(タグ一覧など)を取得
meta = analyze_metric_metadata(metric_name)
if meta:
unit = meta.get(“unit”)
# 例として、カスタムメトリクスでタグの傾向を推測
# 実際にはMetrics API v2のAggregates等も組み合わせる
pass
if __name__ == “__main__”:
if not DD_API_KEY or not DD_APP_KEY:
print(“Error: DD_API_KEY and DD_APP_KEY environment variables must be set.”)
exit(1)
audit_metrics()
—
4. エキスパートの極意:Datadog Metric Exclusion Filters との二段構え
万が一、開発者が高カーディナリティなメトリクスをコードに混入させ、それが本番環境にデプロイされてしまった場合、手遅れになる前に行うべき最終防衛ラインがある。
それが Datadog Metrics Exclusion Filters(除外フィルター) だ。
メトリクス排除フィルターの設計思想
Datadogに送られてくる手前の段階(あるいはDatadogインジェスト層)で、特定のパターンを持つメトリクスを強制的にドロップ(破棄)させる。
1. Datadog Agent側 (`datadog.yaml`) でのフィルタリング
エージェントの設定で、特定のプレフィックスを持つメトリクスやタグを収集対象外にする。
# /etc/datadog-agent/datadog.yaml
# 誤って送出された高カーディナリティメトリクスをエージェントレベルで葬る
processing_rules:
- type: exclude_metrics
name: exclude_temporary_uuids
# app.tmp. のような名前空間のメトリクスは一切送信しない
metric_glob: “app.tmp.”
2. Server-side Metrics Exclusion(サーバーサイド除外)
DatadogのUIまたはAPI(`Metrics Without Limits™` 機能など)を使い、インデックスされる前に不要なタグやメトリクスを排除する。これにより、メトリクス自体はAPMやログの副産物として一時的に受けても、カスタムメトリクスの課金対象から外すことが可能になる。
—
5. 結び:オブザーバビリティとは「引き算の美学」である
監視ツールは、何でもかんでも放り込めば安心できるというものではない。
「すべてのリクエストを記録したい」というエンジニアのエゴは、往々にしてノイズの山を生み、本当に検知すべき異常(シグナル)をかき消す。さらに最悪なことに、CFOからの雷を直撃させる。
真のオブザーバビリティ・アーキテクトとは、「何を監視しないか」を定義できる者を指す。
今夜、あなたのシステムのメトリクス一覧を見返し、UUIDやタイムスタンプの匂いがするタグがないか確認してほしい。その一行を削ぎ落とすことが、システムと会社の財布を救う第一歩なのだから。