3. 日志标签与流设计:标签(Labels)的最佳实践、基数(Cardinality)控制、如何避免高基数导致的性能问题

在 Loki 中,标签(Labels)是日志流的唯一标识符,决定了日志如何被索引、存储和查询。错误的标签设计——尤其是高基数(High Cardinality)——是导致 Loki 性能退化、存储膨胀和查询超时的首要原因。本章将深入剖析标签设计原则、基数控制策略以及生产级避坑指南。

3.1 标签与日志流的核心关系

Loki 的每个日志流由一组键值对标签唯一标识。当日志写入时,具有相同标签组合的日志会被聚合到同一个流中。流是 Loki 存储和查询的最小逻辑单元。

核心公式: 日志流数量 = 标签组合的笛卡尔积。每增加一个高基数标签,流数量呈指数级增长。
概念 说明 示例
日志流 具有相同标签集合的日志序列 {job="nginx", env="prod", host="web-01"}
标签键 标签的名称,如 job、env job、instance
标签值 标签的具体取值,如 nginx、prod web-01、api-v2
基数 某个标签键下不同取值的数量 host 标签有 1000 个不同主机名 → 基数 1000
标签组合与日志流映射关系 标签组合 → 日志流映射 job=nginx env=prod host=web-01 host=web-02 流 1: {job, env, host=web-01} 日志内容 A 流 2: {job, env, host=web-02} 日志内容 B 流 N: 其他组合 日志内容 N ⚠ 高基数风险 host 标签有 1000 个值 → 产生 1000 个独立流 → 索引膨胀、查询变慢

3.2 标签最佳实践

3.2.1 应该作为标签的字段

  • 服务/应用标识:job、service、app —— 基数通常 < 50
  • 部署环境:env、cluster —— 基数通常 < 10
  • 固定拓扑维度:namespace、datacenter、region —— 基数通常 < 100
  • 日志级别:level —— 基数固定为 5~8

3.2.2 不应该作为标签的字段

高危操作: 将高基数变量直接作为标签。以下字段应放入日志行内容中,而非标签。
字段 典型基数 推荐处理方式
user_id、request_id 百万级 保留在日志行内,使用 |~ 或 json 解析器查询
ip、client_ip 万级 保留在日志行内,使用 regexp 提取
timestamp、duration_ms 无限 保留在日志行内,使用 unwrap 进行指标计算
host、instance(动态扩缩容场景) 千级 如果必须使用,确保基数 < 1000;否则使用 job + namespace 聚合

3.3 基数控制策略

3.3.1 基数估算公式

总流数 = ∏ (每个标签键的基数)
示例:
  job: 5 个值 × env: 3 个值 × host: 200 个值 = 3,000 个流  ✅ 可接受
  job: 5 个值 × env: 3 个值 × user_id: 100,000 个值 = 1,500,000 个流  ❌ 灾难

3.3.2 生产级基数控制清单

  1. 限制标签总数:每个日志流不超过 5~8 个标签键。
  2. 限制标签值长度:标签值建议不超过 64 字节,避免长字符串。
  3. 使用 chunk_target_size:配置目标块大小(如 1.5MB),避免小流产生过多块。
  4. 启用 cardinality_limit:在 Loki 配置中设置每租户最大流数。
  5. 定期审计:使用 loki-canary 或 Grafana 的 loki_boltdb_shipper_streams 指标监控流数量。
SRE 建议: 在 Promtail 或 Alloy 的 pipeline 中,使用 drop 阶段丢弃高基数标签,或使用 template 阶段将高基数字段映射到低基数范围(如将 user_id 哈希到 10 个桶)。

3.4 避免高基数的架构设计

下图展示了从日志采集到存储的完整链路中,如何在每个环节控制基数。

日志采集链路基数控制架构 日志采集链路基数控制架构 应用日志 原始高基数字段 Promtail / Alloy Pipeline 处理 Loki Distributor 流哈希 & 限流 Ingester 块写入 各环节基数控制措施 Pipeline 阶段 drop / template / regex 移除或映射高基数标签 Distributor 限流 per_stream_rate_limit per_stream_rate_limit_burst Ingester 配置 chunk_target_size max_chunk_age

3.5 实战:Promtail 配置示例

以下配置展示了如何在 Promtail 中移除高基数标签,并将高基数字段保留在日志行内。

scrape_configs:
  - job_name: kubernetes-pods
    kubernetes_sd_configs:
      - role: pod
    pipeline_stages:
      # 1. 解析 JSON 日志
      - json:
          expressions:
            user_id: user_id
            request_id: request_id
            level: level
            msg: message
      # 2. 丢弃高基数标签(不将其作为 Loki 标签)
      - drop:
          source: user_id
      - drop:
          source: request_id
      # 3. 将日志级别映射为标签(低基数)
      - labels:
          level: level
      # 4. 重新组合日志行内容(保留高基数字段供查询)
      - template:
          source: message
          template: "{{.msg}} | user={{.user_id}} | req={{.request_id}}"
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_label_app]
        target_label: job
      - source_labels: [__meta_kubernetes_namespace]
        target_label: namespace

3.6 监控与告警:基数异常检测

使用以下 PromQL 查询监控 Loki 的流数量,并设置告警阈值。

# 每个租户的活跃流数量
sum by (tenant_id) (loki_ingester_streams_count)

# 流数量变化率(5分钟)
rate(loki_ingester_streams_count[5m])

# 高基数标签检测(需要开启 -log.level=debug)
sum by (label_name) (loki_distributor_label_cardinality)
告警阈值建议:
  • 单租户流数量 > 10,000 → Warning
  • 单租户流数量 > 50,000 → Critical
  • 流数量 5 分钟增长率 > 20% → 立即排查

3.7 常见误区与避坑

误区 错误做法 正确做法
标签越多越好 将日志中所有字段都设为标签 只保留 < 8 个低基数标签,其余放日志行
动态标签无影响 使用 pod 或 container_id 作为标签 使用 job + namespace 聚合,避免动态标签
忽略 chunk_target_size 使用默认值,小流产生大量小块 设置 chunk_target_size: 1536000(1.5MB)
不监控流数量 直到性能问题爆发才排查 配置 Grafana 仪表盘实时监控流数量
Golden Signals 关联: 标签基数直接影响 Loki 的 延迟(Latency)和错误(Errors)。高基数导致索引写入变慢、查询超时,进而影响 SLO。控制好标签基数,是保障 Loki 可靠性的第一道防线。