1、特征获取的现状与挑战:为什么延迟是核心指标

做在线推荐系统这些年,我踩过最大的坑,就是特征获取太慢。

你想想看,用户点开一个页面,后台要在几十毫秒内完成召回、排序、重排。排序模型再强,特征拿不到也是白搭。我见过不少团队,模型 AUC 涨了 2 个点,上线后业务指标反而跌了——原因很简单,特征延迟太高,模型看到的都是过时的用户状态。

说白了,特征获取的延迟,直接决定了你能用多复杂的模型、服务多少用户。这不是一个可以事后优化的指标,而是架构设计之初就必须死磕的核心约束。

1.1 为什么延迟是核心指标

在线服务的延迟,通常用 TP99 或 TP999 来衡量。特征获取作为整个推理链路的前置环节,它的延迟会直接叠加到模型推理时间上。

举个例子:

  • 用户请求到达 → 10ms
  • 特征获取 → 30ms
  • 模型推理 → 20ms
  • 后处理 → 5ms
  • 总耗时:65ms

特征获取占了将近一半。如果特征延迟翻倍到 60ms,整个服务就奔着 100ms 去了。很多业务场景的 SLA 就是 100ms,超了就是事故。

核心观点:特征获取延迟是推理链路中的"乘法器"。它慢一点,整个链路就跟着崩。我习惯把特征获取的 P99 延迟控制在模型推理时间的 1/3 以内,这样即使有抖动,整体也能兜住。

还有一个容易被忽视的点:特征延迟影响模型效果。我记得有一次排查线上 CTR 下降,查来查去发现是用户实时行为特征的更新延迟从 50ms 涨到了 500ms。模型拿到的特征滞后了将近半秒,用户已经滑走了,模型还在推旧内容。你说这能准吗?

1.2 在线特征服务的典型架构

先画一张图,看看一个典型的在线特征服务长什么样。

在线特征服务典型架构图 在线特征服务典型架构 客户端 / App API 网关 特征聚合层 (Feature Fetcher) 合并、去重、超时控制、降级 Redis / 本地缓存 用户画像、实时计数 KV 存储 (HBase) 离线特征、历史行为 MySQL / 关系库 元数据、配置信息 模型推理服务 返回结果 💡 特征聚合层是关键瓶颈 它需要并发请求多个存储, 并在毫秒级内完成合并返回。

这个架构看起来简单,但实际落地时坑很多。特征聚合层要同时请求 Redis、HBase、MySQL 等多个数据源,任何一个慢了都会拖累整体。我见过一个项目,特征聚合层用了串行请求,结果每次都要等最慢的那个存储返回,P99 直接飙到 200ms+。

我的习惯:特征聚合层一定要做并行请求 + 超时熔断。宁可丢掉部分特征,也不能让整个请求等死。我曾经把超时时间从 50ms 降到 20ms,P99 延迟直接降了 60%。

1.3 低延迟的三大瓶颈:网络、计算、存储

做特征获取优化这么多年,我总结下来,瓶颈就三个:网络、计算、存储。咱们一个一个说。

1.3.1 网络瓶颈

网络延迟是特征获取中最不可控的因素。你想想看,一次特征请求可能要跨机房、跨集群,甚至跨地域。每次网络往返都是几毫秒到几十毫秒的消耗。

我遇到过最夸张的情况:特征服务部署在北京机房,但用户画像数据在上海机房。每次请求都要走专线,光网络往返就 15ms。再加上存储查询时间,一个特征就要 30ms+。后来我们把用户画像做了异地多活,才把网络延迟降下来。

网络场景 典型延迟 优化手段
同机房内网 0.1 - 0.5ms 连接池、长连接
同城跨机房 1 - 3ms 就近部署、本地缓存
跨地域专线 10 - 50ms 异地多活、数据同步
公网 50 - 200ms 避免在线获取,离线预计算

避坑指南:我曾经在跨机房场景下用了短连接,每次请求都新建 TCP 连接。三次握手 + 慢启动,光建连就占了 5ms。后来改成连接池 + 长连接,网络开销直接砍半。记住:连接复用是网络优化的第一课。

1.3.2 计算瓶颈

计算瓶颈往往被忽视。很多人觉得特征获取就是查个表,能有多复杂?

其实不然。特征聚合层要做的事情很多:

  • 特征拼接:把多个来源的特征拼成一条样本
  • 特征变换:比如对实时计数做归一化、对类别特征做 embedding 查询
  • 特征过滤:根据请求上下文过滤掉不需要的特征
  • 序列化/反序列化:从存储拿到的数据要反序列化,返回给模型要再序列化

这些计算虽然单个不重,但叠加起来就很可观。我记得有一次排查,发现特征聚合层在反序列化 Protobuf 时用了反射,每次都要解析 schema。换成预编译的代码生成后,反序列化时间从 2ms 降到了 0.3ms。

核心观点:计算瓶颈的优化方向是"能预计算的就预计算,能缓存结果的就缓存结果"。我习惯把特征变换逻辑做成离线预计算 + 在线兜底的双层架构,大部分特征在离线阶段就算好了,在线只做简单的查表。

1.3.3 存储瓶颈

存储瓶颈是最常见的。特征数据量大、访问模式多样,单一存储很难满足所有需求。

我总结了几种典型场景:

  • Redis:适合实时计数、用户画像等小数据量高频访问的场景。但 Redis 是单线程模型,大 key 或慢查询会阻塞整个实例。
  • HBase:适合海量离线特征,但读延迟不稳定,尤其是 region split 时。
  • 本地缓存:延迟最低(纳秒级),但容量有限,需要做好淘汰策略。

存储瓶颈的优化,说白了就是分层缓存。我常用的策略是:

  1. L1 本地缓存:热数据放本地,延迟 < 1ms
  2. L2 分布式缓存:Redis 集群,延迟 1-5ms
  3. L3 持久化存储:HBase/MySQL,延迟 5-20ms

每一层都做超时降级,L1 没命中就去 L2,L2 超时就降级到 L3。这样即使底层存储抖动,上层也能兜住。

我的经验:本地缓存不要用 Guava Cache 那种堆内缓存,GC 时会 STW,导致特征获取延迟抖动。我习惯用 Caffeine 的 off-heap 模式,或者直接上 Redis 的本地代理(如 RedisCell)。

1.4 三大瓶颈的关系

这三个瓶颈不是孤立的,它们会互相影响。举个例子:

  • 网络延迟高 → 你会想多查几次来补偿 → 计算量增加
  • 存储慢 → 你会想加缓存 → 缓存一致性变复杂 → 计算逻辑变重
  • 计算重 → 你会想用更快的序列化 → 存储格式要改 → 存储又要适配

所以优化的时候,不能只盯着一个点。我习惯先做全链路压测,找到真正的瓶颈在哪里。有时候你以为瓶颈在存储,结果发现是网络连接池不够用;有时候你以为瓶颈在计算,结果发现是序列化格式太冗余。

一句话总结:特征获取的低延迟优化,本质是在网络、计算、存储三者之间找平衡。没有银弹,只有持续的监控、压测和迭代。