可观测性层

本页讲解湖仓一体架构的可观测性建设:以 Prometheus 为核心的指标采集与告警、以 EFK 为核心的日志链路、以 Grafana 为核心的分层看板。学完本页,你将能够按"基础设施 → 组件 → 作业 → 业务"四层设计湖仓指标体系,落地告警规则与日志检索,并掌握小文件增长、Compaction 积压、快照堆积等湖仓特有监控点。
看板层:Grafana(分层大盘 · 变量下钻 · 告警视图)
指标层:Prometheus + Alertmanager(指标采集 · PromQL · 分组抑制告警)
日志层:EFK(Filebeat → Kafka → Elasticsearch → Kibana)
被观测对象:基础设施 · 组件(YARN/HDFS/Kafka)· 作业(Spark/Flink)· 业务(入湖延迟/新鲜度)

可观测性的三大支柱各司其职:指标(Metrics)回答"现在怎么样"——低成本的数值时序,适合告警与趋势判断;日志(Logs)回答"为什么这样"——故障时刻的现场细节,适合根因定位;看板(Dashboards)把两者组织成"从全局到单点"的下钻路径,是值班同学的第一入口。湖仓链路长(采集 → 入湖 → 清洗 → 聚合 → 服务),任何一环都可能成为故障源,因此观测体系必须分层设计、逐层下钻,而不是堆一堆零散面板。

排查路径记忆法:业务异常 → 看业务层指标(新鲜度/延迟)定位到链路环节 → 看作业层(时长/失败率)定位到具体作业 → 看组件层(队列/Kafka/NN)定位到资源瓶颈 → 用 Kibana 检索该作业该时段的日志确认根因。
Grafana 看板 分层大盘 · 变量下钻 · 告警视图 Prometheus 指标体系 exporter 生态 · PromQL · Alertmanager 分组抑制 EFK 日志链路 Filebeat → Kafka → Elasticsearch → Kibana 被观测对象 基础设施 · 组件(YARN / HDFS / Kafka)· 作业(Spark / Flink)· 业务(入湖延迟 / 数据新鲜度)

指标体系设计

指标不是越多越好,而是每个告警都必须有人在等。湖仓指标体系建议按四层组织:上层回答业务问题,下层支撑技术归因,下层异常要通过上层体现价值,上层异常要能下钻到下层定位根因。

层级 代表指标 采集来源 建议告警阈值
基础设施层 CPU 使用率 node_exporter(node_cpu_seconds_total) > 85% 持续 5 分钟
磁盘 IO 利用率 node_exporter(node_disk_io_time_seconds_total) > 90% 持续 10 分钟
磁盘空间使用率 node_exporter(node_filesystem_avail_bytes) > 80% 警告 / > 90% 严重
网络带宽利用率 node_exporter(node_network_*_bytes_total) > 链路带宽 80% 持续 5 分钟
组件层 YARN 队列使用率 RM JMX(QueueMetrics) > 90% 持续 15 分钟(触发容量评估)
Kafka 消费积压(lag) kafka_exporter > 10 万条,或持续增长 10 分钟
HDFS NN GC 耗时 jmx_exporter(jvm_gc_collection_seconds_sum) 5 分钟累计 > 3s
HDFS NN RPC 队列/堆使用 NN JvmMetrics / RpcMetrics 堆 > 75% 警告 / RPC 队列 > 100 持续 5 分钟
作业层 作业时长 DS API → PushGateway(每作业回写) > 同任务历史 P50 × 1.5
作业失败率 DS 任务计数(ds_task_failed_total) 30 分钟窗口 > 5%
Checkpoint 时长 / 大小 Flink Prometheus Reporter 时长 > 60s;大小环比 +50%
业务层 入湖延迟 自定义 exporter(源端事件时间 − 入湖完成时间) > 15 分钟警告 / > 30 分钟严重
数据新鲜度 最新分区距当前时间(SQL exporter 定时查询) > 2 小时警告 / > 4 小时严重
数据质量得分 质量校验作业回写(规则通过率加权) < 95 分警告 / < 90 分严重
指标设计原则:组件层用 USE 方法(Utilization / Saturation / Errors)扫盲;作业与业务层用 SLI 思维——先定义"用户可感知的健康"(新鲜度、延迟、得分),再反推需要哪些技术指标支撑。
阈值不是抄来的:上表阈值是多数湖仓集群的起步参考值,上线后应结合两周的真实分布(P50/P95)校准;只告警"持续异常"(带 for 持续时间),避免瞬时毛刺引发告警疲劳。

Prometheus

1. 架构与 exporter 生态

Prometheus 采用拉取(Pull)模型:Server 周期性抓取各 exporter 暴露的 /metrics 端点,本地 TSDB 存储,经 PromQL 查询;告警规则在 Server 端评估,触发后推给 Alertmanager 分发。湖仓集群的 exporter 布局:

exporter / 指标源 部署位置 覆盖对象
node_exporter 每台机器(物理机/容器宿主机) CPU、内存、磁盘 IO、网络、文件系统
jmx_exporter(javaagent) HDFS NN/DN、YARN RM/NM、Kafka Broker、Trino 进程侧 JVM(堆/GC)、组件内部指标(队列、RPC、分区)
kafka_exporter 任意节点连接 Kafka topic/分区 offset、消费组 lag
Flink Prometheus Reporter JobManager / TaskManager 作业运行时、Checkpoint、背压、算子水位
PushGateway 中心化部署 短生命周期批作业(Spark/DS 任务结束时 push 自定义指标)
自定义 exporter / SQL exporter 中心化部署 湖仓表健康度:小文件数、快照数、新鲜度、质量得分
PushGateway 的陈旧问题:PushGateway 中的指标不会自动过期,作业失败导致"最后一次成功"的数值会一直驻留。务必在抓取配置中对 pushgateway 任务设置 honor_timestamps,并在每次作业开始时先写 0 或用 push_all 全量覆盖,保证"看不到"等于"没跑"。

2. PromQL 常用查询

# 1) Kafka 消费积压:分区最新 offset − 消费组已提交 offset
sum by (consumergroup, topic) (kafka_consumergroup_group_lag)

# 2) 消费速率(条/秒),用于估算积压追平时间 = lag ÷ rate
sum by (topic) (rate(kafka_consumergroup_group_current_offset[5m]))

# 3) 调度作业失败率:30 分钟窗口失败任务占比
sum(rate(ds_task_failed_total[30m])) / clamp_min(sum(rate(ds_task_total[30m])), 1)

# 4) YARN 离线队列内存使用率(RM QueueMetrics 经 JMX 暴露,指标名以实际输出为准)
yarn_queue_allocated_mb{queue="offline"} / yarn_queue_total_mb{queue="offline"}

# 5) HDFS NameNode 5 分钟内 Full GC 累计秒数
increase(jvm_gc_collection_seconds_sum{role="namenode", gc="G1 Old Generation"}[5m])

# 6) 节点磁盘 IO 利用率(device 正则按实际盘名调整)
rate(node_disk_io_time_seconds_total{device=~"sd.*|nvme.*"}[5m])

3. Alertmanager:分组、抑制、静默

告警规则(Prometheus 侧)示例——覆盖组件层与作业/业务层两个规则组:

# prometheus/rules/lakehouse-alerts.yml
groups:
  - name: lakehouse.component          # 组件层规则组
    rules:
      - alert: NodeCpuHigh
        expr: (100 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
        for: 5m
        labels:
          severity: warning
          layer: infra
        annotations:
          summary: "节点 CPU 持续偏高:{{ $labels.instance }}"
          runbook: "http://wiki.example/runbook/node-cpu"

      - alert: KafkaConsumerLagHigh
        expr: sum by (consumergroup, topic) (kafka_consumergroup_group_lag) > 100000
        for: 10m
        labels:
          severity: warning
          layer: component
        annotations:
          summary: "消费组 {{ $labels.consumergroup }} 在 {{ $labels.topic }} 积压 {{ $value }} 条"

      - alert: NameNodeGcPause
        expr: increase(jvm_gc_collection_seconds_sum{role="namenode", gc="G1 Old Generation"}[5m]) > 3
        labels:
          severity: critical
          layer: component
        annotations:
          summary: "HDFS NameNode 5 分钟 Full GC 累计 {{ $value }}s,存在全局卡顿风险"

  - name: lakehouse.job-biz            # 作业与业务层规则组
    rules:
      - alert: BatchJobFailureRate
        expr: |
          sum(rate(ds_task_failed_total[30m]))
            / clamp_min(sum(rate(ds_task_total[30m])), 1) > 0.05
        for: 15m
        labels:
          severity: critical
          layer: job
        annotations:
          summary: "调度作业 30 分钟失败率超过 5%"

      - alert: IngestDelayHigh
        expr: max(lakehouse_ingest_lag_seconds) > 900
        for: 10m
        labels:
          severity: warning
          layer: business
        annotations:
          summary: "入湖延迟超过 15 分钟,请下钻查看同步作业状态"

Alertmanager 侧路由配置——分组、抑制、静默三件套:

# alertmanager.yml
route:
  receiver: team-default
  group_by: ['alertname', 'cluster']     # 同名告警打包发送,减少轰炸
  group_wait: 30s                        # 组内等待归并的时间窗
  group_interval: 5m                     # 同组两次通知的最小间隔
  repeat_interval: 4h                    # 未恢复时的重复提醒周期
  routes:
    - matchers: ['severity="critical"']  # 严重级:升级到值班电话
      receiver: oncall-phone
      continue: true
    - matchers: ['team="stream"']        # 流团队告警路由到专属群
      receiver: stream-wecom

inhibit_rules:                           # 抑制:critical 触发时压掉同源 warning
  - source_matchers: ['severity="critical"']
    target_matchers: ['severity="warning"']
    equal: ['cluster', 'alertname']

receivers:
  - name: team-default
    webhook_configs:
      - url: 'http://wecom-proxy/alert/default'
  - name: oncall-phone
    webhook_configs:
      - url: 'http://pager-adapter/critical'
  - name: stream-wecom
    webhook_configs:
      - url: 'http://wecom-proxy/alert/stream'
# 静默(Silence):维护窗口内按匹配器屏蔽告警,事由可审计
amtool silence add \
  --comment "node-03 换盘维护,CPU/磁盘告警预期内" \
  --duration 4h \
  alertname=~"NodeCpu.*|NodeDisk.*" instance=node-03

# 查看与过期静默规则
amtool silence ls
amtool silence expire <silence-id>
告警分级实践:critical 打电话(15 分钟内必须响应)、warning 进群消息(当天处理)、info 仅看板展示不推送。每周回顾一次告警量与响应记录:无人响应的告警要么降级要么删除。

Grafana

1. 看板分层设计

湖仓看板的最大反模式是"一个大盘塞进一切"。正确姿势是三层漏斗,上层回答"有没有事",下层回答"是哪里的事":

L1 集群概览:全局状态灯(CPU/存储水位、YARN 总量与 pending、Kafka 总 lag、NN GC、今日作业失败数)
L2 队列 / 主题下钻:单队列资源走势、单 topic 分区 lag 明细、单组件 JVM 与 RPC
L3 单作业下钻:Spark Stage 与 Executor、Flink Checkpoint 曲线与背压,并携带跳转 Kibana 的日志链接

层与层之间用面板 Data Link 串联:L1 的队列面板点击跳转到 L2 大盘并自动带入 $queue 变量;L2 的作业面板跳转 L3 并带入 $job_id;L3 面板携带时间范围直链 Kibana Discover。值班同学从收到告警到看到日志现场,全程不需要手动改查询条件。

2. 变量与面板设计

变量 类型 取值 Query 示例 用途
$cluster Query label_values(up, cluster) 环境切换(prod / staging)
$queue Query label_values(yarn_queue_allocated_mb, queue) YARN 队列下钻
$topic Query label_values(kafka_consumergroup_group_lag, topic) Kafka 主题下钻
$job_id Query label_values(flink_jobmanager_job_lastCheckpointDuration, job_id) 单作业实例下钻
面板类型 适用内容 湖仓示例
Stat(大数/状态灯) 单值 + 阈值着色 集群可用率、今日失败作业数、最大 lag
Time series 走势与同环比 队列内存使用率、作业时长 P50/P95 曲线
Heatmap 分布型指标 查询延迟分布、Checkpoint 耗时分布
Table 明细列表 + 可复制字段 慢查询清单、失败任务清单(带日志链接)
看板所见 = 告警所判:面板阈值线必须与告警规则表达式保持一致(面板上画 85% 的红线,告警也应在 85% 触发),否则会出现"看板一切正常却半夜被告警叫醒"的信任危机。每个告警面板的描述中附上 runbook 链接。

EFK 日志链路

1. 全链路拓扑

EFK = Filebeat(采集)+ Elasticsearch(存储与检索)+ Kibana(查询与可视化)。湖仓集群日志量往往在百 GB/天量级,直接把 Filebeat 打到 ES 会在重写/恢复期形成反压,因此推荐引入 Kafka 作为缓冲层削峰:

Filebeat 多来源采集 · 打标 Kafka 缓冲削峰(可选) Elasticsearch 存储 · 全文检索 · ILM Kibana Discover 检索 · 看板 小规模可跳过 Kafka 直写 ES(吞吐 < 5 万行/秒)

2. filebeat.yml 完整配置

三个关键设计:多来源分 input(每类组件一个 input,便于差异化处理);multiline 合并 Java stacktrace(不以时间戳开头的行为同一条日志);fields 打标(cluster/service/role 贯穿到 Kibana 检索与 ILM)。

# ---------- 1) HDFS NameNode:JVM 长堆栈需要 multiline 合并 ----------
filebeat.inputs:
  - type: log
    enabled: true
    paths:
      - /var/log/hadoop/*namenode*.log
    fields:
      cluster: prod-lakehouse
      service: hdfs
      role: namenode
    fields_under_root: true        # 字段平铺到根,KQL 直接用 service: hdfs
    multiline.pattern: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'   # 以时间戳开头才算新日志行
    multiline.negate: true
    multiline.match: after
    multiline.max_lines: 200       # 单条日志合并上限,防畸形日志撑爆事件
    tags: ["hdfs"]

  # ---------- 2) Flink TaskManager:同样需要 stacktrace 合并 ----------
  - type: log
    enabled: true
    paths:
      - /var/log/flink/taskmanager*.log
    fields:
      cluster: prod-lakehouse
      service: flink
      role: taskmanager
    fields_under_root: true
    multiline.pattern: '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'
    multiline.negate: true
    multiline.match: after
    tags: ["flink"]

  # ---------- 3) 调度与同步组件日志 ----------
  - type: log
    enabled: true
    paths:
      - /var/log/dolphinscheduler/*.log
      - /var/log/datax/*.log
    fields:
      cluster: prod-lakehouse
      service: scheduler
      role: worker
    fields_under_root: true
    tags: ["scheduler"]

processors:
  - add_host_metadata: ~          # 自动补充 host.name / host.ip
  - add_fields:
      target: ''
      fields:
        env: prod

# ---------- 输出:大规模走 Kafka 缓冲 ----------
output.kafka:
  hosts: ["kafka1:9092", "kafka2:9092", "kafka3:9092"]
  topic: "log_lakehouse_all"
  required_acks: 1                 # 性能与可靠性的折中
  compression: gzip
  worker: 2

# ---------- 小规模直写 ES:注释掉 Kafka 输出后启用 ----------
# output.elasticsearch:
#   hosts: ["http://es-lb:9200"]
#   index: "logs-lakehouse-%{+yyyy.MM.dd}"
#   bulk_max_size: 4096

3. ES ILM 索引生命周期

湖仓日志的访问特征是前 3 天高频检索、之后几乎不再访问,用 ILM 按热/温/冷/删除四阶段滚动,比手动 curator 定时任务可靠得多:

PUT _ilm/policy/lakehouse-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": "3d",
        "actions": {
          "shrink": { "number_of_shards": 1 },
          "forcemerge": { "max_num_segments": 1 },          // 合并段,检索量骤降后压缩存储
          "allocate": { "number_of_replicas": 0,
                        "require": { "data": "warm" } }
        }
      },
      "cold": {
        "min_age": "15d",
        "actions": {
          "set_priority": { "priority": 50 },
          "allocate": { "require": { "data": "cold" } }      // 开源版回退方案:挪到冷存储节点
        }
      },
      "delete": {
        "min_age": "30d",
        "actions": { "delete": {} }                        // 保留 30 天,审计诉求可延长 cold 阶段
      }
    }
  }
}

# 绑定到索引模板(新建索引自动应用)
PUT _index_template/logs-lakehouse
{
  "index_patterns": ["logs-lakehouse-*"],
  "template": {
    "settings": {
      "index.lifecycle.name": "lakehouse-logs-policy",
      "index.lifecycle.rollover_alias": "logs-lakehouse",   // rollover 需要别名配合
      "number_of_shards": 3,
      "number_of_replicas": 1
    }
  }
}

4. Kibana 检索用法

Kibana Discover 使用 KQL(Kibana Query Language)做前缀式检索,配合 filebeat 打的 fields,常用姿势:

# 1) 定位 Flink Checkpoint 相关的 ERROR 日志
service: "flink" and role: "taskmanager" and level: "ERROR" and message: *checkpoint*

# 2) 查被 YARN 杀掉的 Container(定位内存超限)
service: "yarn" and message: "Container killed on request" and cluster: "prod-lakehouse"

# 3) 按服务聚合 ERROR 数量:KQL 过滤后,用 Visualize
#    按 service 做 Terms 聚合、按 @timestamp 做 Date histogram(每 5 分钟)
service: "spark" and level: "ERROR"
检索效率提醒:message 上的通配符(*keyword*)会退化成 term 全扫,先用 service / level / host.name 等结构化字段把范围收窄到分钟级 + 单服务,再对 message 做模糊——这是 Kibana 检索不卡的第一原则。

湖仓特有监控点

通用大数据监控覆盖不到湖仓的"慢性病":小文件、快照、Compaction 这些问题不报警、也不崩溃,只会让性能一寸寸劣化。以下五个监控点是湖仓集群的"体检必查项":

监控点 采集方式 Warning 阈值 Critical 阈值
小文件增长速率 SQL exporter 定时查 Iceberg files / Hudi 文件数元数据 单表日增 > 500 文件 单表日增 > 2000 或总量 > 5 万
Compaction 积压 调度系统回写"上次成功 Compaction 距今时长" > 24 小时 > 48 小时或连续 3 次失败
Iceberg 快照数量 SQL exporter 定时 count(*) 表的 snapshots 元数据表 > 500 > 2000(立即触发 expire)
Flink Checkpoint Flink Prometheus Reporter(时长、失败计数) 时长 > 60s 或大小环比 +50% 连续失败 ≥ 2 次立即告警
Trino 慢查询 Trino JMX(query execution 统计) P95 > 30s 单查询 > 5 分钟或被 limit 杀掉的查询突增

湖仓表健康度类指标没有现成 exporter,用 SQL exporter 思路——定时执行元数据查询,把结果暴露为 Prometheus 指标:

-- SQL exporter 定时执行:把湖仓表健康度变成指标
-- 1) 快照堆积(对应 iceberg_snapshot_total 指标)
SELECT count(*) AS iceberg_snapshot_total FROM lakehouse.dws_orders_day.snapshots;

-- 2) 数据文件数(小文件规模,对应 iceberg_file_num)
SELECT count(*) AS iceberg_file_num FROM lakehouse.dws_orders_day.files;

-- 3) 数据新鲜度:最新分区距当前的小时数(对应 lakehouse_freshness_hours)
SELECT round((unix_timestamp() - unix_timestamp(max(dt), 'yyyy-MM-dd')) / 3600)
       AS lakehouse_freshness_hours
FROM lakehouse.dws_orders_day;
# Flink Checkpoint 告警规则(Prometheus 规则片段)
# 时长超过 60s 预示背压或状态膨胀(间隔按流作业实际 checkpoint 配置调整)
flink_jobmanager_job_lastCheckpointDuration > 60000

# 连续失败:5 分钟窗口内失败检查点增量 ≥ 2 次即为 Critical
increase(flink_jobmanager_job_numberOfFailedCheckpoints[5m]) >= 2
为什么要监控快照数量:Iceberg 的每个快照都持有数据文件引用,快照堆积不仅拖慢查询计划(manifest 扫描变重),还会让 expire_snapshots 的清理时间随快照数线性增长。超过 2000 个快照的表,往往还伴随"过期清理作业超时失败"的连锁反应——发现即处理。

调优要点与边界

1. 采样率与基数爆炸

Prometheus 最常见的死法不是抓不动,而是时间序列基数爆炸——Spark/Flink 每个作业、每个 executor、每个算子都会生成带唯一标签的序列,高峰期序列数轻松破千万。治理手段:

# 采集频率分级:基础设施 30s,组件 60s,业务指标 60s
global:
  scrape_interval: 30s
  scrape_timeout: 10s

scrape_configs:
  - job_name: 'spark-pushgateway'
    honor_labels: true
    scrape_interval: 60s
    metric_relabel_configs:
      # 1) 按 metric 名丢弃高基数序列(流式 gauge 常见每秒一变)
      - source_labels: [__name__]
        regex: 'spark_.+_gauges_.+|spark_.+_live_.+'
        action: drop
      # 2) 剔除高基数标签,保留聚合维度
      - action: labeldrop
        regex: '(executor_id|attempt_id|application_id)'

2. 日志分级采集

照单全收的日志采集会把 70% 的 ES 存储浪费在没人看的 INFO 上。在 Filebeat 端(越早丢弃成本越低)分级:

# 日志分级:ERROR/FATAL 全量,DEBUG 直接丢弃,高噪服务的 INFO 也裁掉
processors:
  - drop_event.when:
      equals:
        level: "DEBUG"
  - drop_event.when:
      and:
        - equals:
            level: "INFO"
        - equals:
            service: "yarn-nodemanager"     # 心跳类 INFO 日志量大且无价值

3. 何时用 Loki 替代 ES

改用 Loki 的信号:日志检索模式固定为"按标签过滤 + 看原文"(服务/级别/host),不需要全文倒排与复杂聚合;日志量超过 1TB/天,ES 集群成本失控;团队已有对象存储,希望日志存储成本降一个数量级(Loki 只索引标签,压缩比远高于 ES)。
保留 ES 的理由:需要 KQL 式全文检索与多层聚合分析(安全审计、错误模式挖掘);日志即数据(合规留档需要逐条检索);EFK 生态已运转成熟,迁移收益不明显。湖仓生产常见组合是:基础设施/组件日志进 Loki,应用与审计日志进 ES。
边界提醒:可观测性体系自身也可能成为故障源——Prometheus 单点、ES 磁盘写满、Filebeat 断链都会造成"盲飞"。对监控系统要做自监控(metrics 的 up、EFK 链路的 filebeat registry 滞后与 Kafka 日志 topic 的 consumer lag),并定期演练"告警链路是否真的能打通电话"。