3. 特征数据延迟故障:在线/离线特征数据产出延迟的根因分析与告警配置

数据延迟,说白了就是特征没按时到位。

在线特征晚了几秒,推荐系统可能就推了旧内容。离线特征晚了半小时,今天的训练任务就得重跑。我见过最夸张的一次,离线特征延迟了 6 个小时,整个模型 pipeline 全部阻塞,业务方直接打电话到值班室。

这一章,咱们就聊聊延迟到底从哪来,以及怎么在它变成事故之前就抓住它。

3.1 延迟的两种面孔:在线 vs 离线

先分清楚场景。在线特征和离线特征的延迟,根因完全不同。

维度 在线特征延迟 离线特征延迟
典型延迟窗口 毫秒 ~ 秒级 分钟 ~ 小时级
主要瓶颈 缓存击穿、上游 RPC 超时、计算资源争抢 ETL 任务排队、数据源产出晚、Hive/Spark 任务失败
影响范围 单次请求或少量请求 全量训练数据或大批量推理
排查难度 需要链路追踪和实时监控 依赖任务依赖图和日志回溯

我个人习惯,先看延迟发生在哪个阶段。在线延迟查缓存和网络,离线延迟查调度和资源。

3.2 在线特征延迟:根因三板斧

3.2.1 缓存击穿与雪崩

在线特征通常走 Redis 或本地缓存。一旦热点 key 过期,大量请求同时回源 DB,延迟瞬间飙升。

典型场景: 大促期间,某个头部商品的特征 key 刚好过期,几千台机器同时去拉取,DB 扛不住,延迟从 5ms 变成 500ms。

怎么破?我建议做三件事:

  • 缓存预热: 在流量高峰前,主动把热点 key 刷进缓存。
  • 互斥锁回源: 同一时刻只让一个请求去拉取数据,其他请求等待或返回旧值。
  • 本地缓存兜底: 即使 Redis 挂了,本地缓存还能撑几分钟。

3.2.2 上游 RPC 超时与重试风暴

在线特征往往依赖多个上游服务。比如用户画像特征,要调用户中心、行为中心、标签中心。任何一个上游慢了几十毫秒,整个特征链路就慢了。

我记得有一次排查,发现特征延迟从 20ms 涨到 200ms。追了半天,发现是用户中心的一个接口因为代码 bug 偶尔返回 503,客户端重试了 3 次,每次等 50ms,三次就是 150ms。

我的经验: 给每个上游调用设置独立的超时时间,并且严格控制重试次数。一般重试 1 次就够了,重试 3 次以上大概率是雪上加霜。

3.2.3 计算资源争抢

在线特征服务通常是 CPU 密集型的。如果同一台机器上部署了多个特征计算实例,或者被其他服务抢占资源,延迟就会抖动。

排查方法很简单:看 CPU 使用率和 GC 频率。如果 GC 时间超过总时间的 10%,那延迟肯定有问题。

3.3 离线特征延迟:根因四步法

离线特征延迟,说白了就是任务没按时跑完。我把它拆成四个排查步骤。

3.3.1 检查数据源产出时间

离线特征依赖上游数据表。如果上游的 Hive 表或 Kafka 主题晚产出了,下游的特征任务只能干等。

我曾经遇到一个案例:离线特征每天凌晨 2 点产出,但连续三天都延迟到 4 点。查到最后,发现是上游的日志清洗任务因为数据量暴增,从 1 小时跑成了 3 小时。

注意: 不要只看任务结束时间,要看数据分区的最早写入时间。有时候任务跑完了,但数据还没完全落盘。

3.3.2 分析任务依赖 DAG

离线特征任务通常是一个 DAG(有向无环图)。一个节点延迟,会像多米诺骨牌一样传导下去。

我习惯用一张图来展示这个传导过程:

离线特征延迟传导图 上游日志表 特征 ETL 任务 特征表产出 模型训练任务 模型上线部署 延迟 30min 延迟 20min 延迟 50min 延迟 50min 总延迟 = 30 + 20 + 50 + 50 = 150 分钟

你看,每个节点延迟一点,最终累积到模型训练时,已经晚了 150 分钟。所以排查时,要从 DAG 的根节点开始,逐个检查每个任务的运行时间。

3.3.3 检查资源队列与任务排队

离线任务跑在 YARN 或 Kubernetes 上。如果资源队列满了,任务就会排队。排队时间有时候比运行时间还长。

我见过一个案例:某个团队把所有离线特征任务都提交到同一个队列,结果凌晨高峰期,任务排队等了 40 分钟。后来改成按优先级分配队列,延迟直接降了一半。

建议: 给核心特征任务设置高优先级队列,并且监控队列的等待时间。如果等待时间超过 5 分钟,就要考虑扩容了。

3.3.4 定位慢任务与数据倾斜

如果任务没排队,但就是跑得慢,那大概率是数据倾斜。

数据倾斜的表现:同一个 stage 里,大部分 task 几秒就跑完了,但有一个 task 跑了半小时。

怎么查?看 Spark UI 或 Hive 的 task 耗时分布。如果最大耗时是平均耗时的 10 倍以上,基本就是倾斜了。

我曾经处理过一个用户画像特征的任务,某个 key 的数据量是其他 key 的 100 倍,导致那个 reduce task 跑了 2 小时。后来加了随机前缀打散,任务时间降到了 15 分钟。

3.4 告警配置:从被动响应到主动发现

延迟不可怕,可怕的是你最后一个知道。

我建议从三个维度配置告警:

3.4.1 在线特征告警

  • P99 延迟告警: 超过基线 2 倍,持续 1 分钟,触发告警。
  • 缓存命中率告警: 低于 90%,持续 5 分钟,触发告警。
  • 上游 RPC 错误率告警: 超过 1%,持续 1 分钟,触发告警。

3.4.2 离线特征告警

  • 任务产出延迟告警: 超过预期时间 30 分钟,触发告警。
  • 任务失败告警: 任何特征任务失败,立即告警。
  • 数据量波动告警: 产出数据量相比昨天波动超过 50%,触发告警。

3.4.3 告警配置示例

下面是一个基于 Prometheus + Alertmanager 的告警规则示例:

groups:
- name: feature_delay_alerts
  rules:
  - alert: OnlineFeatureP99High
    expr: histogram_quantile(0.99, rate(feature_fetch_duration_seconds_bucket[1m])) > 0.1
    for: 1m
    labels:
      severity: critical
    annotations:
      summary: "在线特征 P99 延迟超过 100ms"

  - alert: OfflineFeatureOutputDelay
    expr: time() - feature_table_last_partition_ts{table="user_profile_features"} > 1800
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "离线特征表 user_profile_features 产出延迟超过 30 分钟"

  - alert: FeatureTaskFailure
    expr: increase(feature_etl_task_failures_total[10m]) > 0
    for: 0m
    labels:
      severity: critical
    annotations:
      summary: "特征 ETL 任务失败"

避坑指南: 告警不是越多越好。我曾经见过一个团队配置了 200 条告警规则,结果每天告警几百次,最后大家直接屏蔽了所有告警。我的原则是:每条告警都必须能指导一个具体的操作,否则就删掉。

3.5 总结:延迟排查的思维模型

最后,我分享一个排查延迟的思维模型,说白了就是三步:

  1. 定界: 是在线还是离线?是缓存、网络还是计算?
  2. 定位: 用链路追踪或 DAG 分析,找到具体哪个节点慢了。
  3. 定因: 是资源不够、数据倾斜、还是上游依赖问题?

这三步走完,90% 的延迟问题都能找到根因。剩下的 10%,嗯,那就得靠日志和代码 review 了。