Prometheus 高基数问题治理:标签设计不当会吃掉内存
Prometheus 高基数问题治理标签设计不当会吃掉内存一、一个错误的标签吃掉了 8GB 内存Prometheus 的内存大户不是 metrics 数量是时间序列基数Cardinality。每个唯一的标签组合产生一个新的时间序列而每个时间序列占用约 3-5KB 内存。你以为这些是正常的把user_id放在http_requests_total的 label 里给request_duration_seconds加了trace_id标签用pod_name做 label 而非pod_template_hash结果一个有 100 万用户的系统http_requests_total的时间序列基数 100W × endpoint 数量 × method 数量轻松突破千万级。按每条 4KB 算单这一个 metric 就 40GB。你的 Prometheus 实例直接 OOM。flowchart TD A[Metric 定义] -- B{标签组合分析} B -- C[低基数标签br/method: GET/POSTbr/status: 2xx/4xx/5xx] B -- D[中基数标签br/endpoint: 100br/service: 30] B -- E[高基数标签br/user_id: 100Wbr/trace_id: 无限br/pod_name: 动态] C -- F[安全: 时间序列 1k] D -- G[可控: 时间序列 10k] E -- H[危险: 时间序列 100k] H -- I[内存膨胀] H -- J[查询性能下降] H -- K[远程写入阻塞]二、时间序列基数的本质Prometheus 的存储引擎 TSDB 为每个 label 组合建立索引。每次写入新数据时需要查找 label 组合对应的 series ID追加 sample 数据点如果 label 组合不存在创建新 series 并分配 ID第三步的成本是 O(log N) 的倒排索引查找 内存分配。当基数达到百万级这个追加新系列的操作会明显变慢——你看到的是prometheus_tsdb_head_series_created_total疯涨。更隐蔽的问题是churn搅动pod 重启后 pod_name 变了产生新 series旧 series 被标记为 stale。TSDB 的 compaction 需要处理越来越多的 stale seriesCPU 飙升。三、高基数指标的治理策略# prometheus 配置通过 relabel_config 过滤高基数标签 scrape_configs: - job_name: kubernetes-pods kubernetes_sd_configs: - role: pod # relabel_configs 在采集阶段执行在数据进入 TSDB 之前 # 这是第一道防线——不让高基数数据进入存储 relabel_configs: # 规则1: 删除已知的高基数标签 # pod 的 uid 每次重建都变绝不能保留 - source_labels: [__meta_kubernetes_pod_uid] action: labeldrop regex: . # 规则2: 保留 pod_name 但替换为 deployment 名 # 原因pod_name 是动态的每次发布都变 # deployment 名是稳定的基数降低 100 倍 - source_labels: [__meta_kubernetes_pod_name] regex: (.)-[a-z0-9]-[a-z0-9] target_label: deployment replacement: $1 # 规则3: 只保留必要的标签 # 默认 kubernetes_sd 会携带 30 个标签 # 绝大多数都用不到保留越多基数越高 - regex: __meta_kubernetes_pod_label_(.) action: labeldrop metric_relabel_configs: # metric_relabel_configs 在 metric 级别生效 # 这是第二道防线——过滤不需要的 metric 和标签 # 规则4: 丢弃所有包含高基数标签的 metric # user_id 绝不能出现在 metric 标签中 - source_labels: [user_id] regex: . action: drop # 规则5: 保留 status_code 但只取大类 # 不做: 200, 201, 204, 301, 302, 400, 401, 403... # 做: 2xx, 3xx, 4xx, 5xx - source_labels: [status_code] regex: ([0-9]).. target_label: status_class replacement: ${1}xx应用程序侧治理package metrics import ( github.com/prometheus/client_golang/prometheus github.com/prometheus/client_golang/prometheus/promauto ) var ( // 好的设计标签固定基数可控 // endpoint method status 的组合 约 100 × 5 × 4 2000 条 httpRequestsTotal promauto.NewCounterVec( prometheus.CounterOpts{ Name: http_requests_total, Help: Total HTTP requests, }, []string{endpoint, method, status_class}, ) // 差的设计——下面这些是反面教材不要在生产中这样写 // 反模式1: user_id 做标签 → 基数 用户数 × endpoint 数 // promauto.NewCounterVec( // prometheus.CounterOpts{Name: http_requests_by_user}, // []string{user_id, endpoint}, // ) // 反模式2: trace_id 做标签 → 每个请求一个时间序列 // promauto.NewCounterVec( // prometheus.CounterOpts{Name: http_requests_by_trace}, // []string{trace_id}, // ) ) // RecordHTTPRequest 记录 HTTP 请求指标。 // 设计意图status 取百位数做归类避免固定 20 种状态码组合。 func RecordHTTPRequest(endpoint, method string, statusCode int) { statusClass : fmt.Sprintf(%dxx, statusCode/100) httpRequestsTotal.WithLabelValues(endpoint, method, statusClass).Inc() }四、如何发现高基数问题监控 Prometheus 自身的指标# 每个 job 的 series 数量趋势 sum(scrape_series_added) by (job) # head series 总量超过 1000 万需要关注 prometheus_tsdb_head_series # series churn rate每秒新建的 series 数量 rate(prometheus_tsdb_head_series_created_total[5m])快速诊断命令# 找出基数最高的 10 个 metric curl -s http://localhost:9090/api/v1/status/tsdb | \ jq .data.seriesCountByMetricName | to_entries | sort_by(.value) | reverse | .[:10]内存估算公式内存 ≈ head_series × 4KB ingested_samples_per_second × 3B这是粗略估算1 万 series 约 40MB10 万约 400MB100 万约 4GB。五、总结高基数治理的核心原则标签的值域必须是有限且可控的。判断一个标签是否安全的标准——它的取值集合是否在运行前就可以枚举。能枚举的安全method、status_class、deployment不能枚举的危险user_id、trace_id、pod_name。这个判断标准比任何规则都管用。