在线服务架构:特征服务在推荐/搜索系统中的位置与低延迟要求

聊到实时特征存储,很多人第一反应是“存得快、查得快”。但说实话,存储只是地基。真正让特征产生价值的,是它和在线服务之间的那套配合机制。今天我们就来拆解一下,特征服务在推荐和搜索系统里到底站在哪个位置,以及为什么延迟这个指标会被盯得那么死。

特征服务在推荐系统中的位置

先画一张图,帮你快速建立全局感。下面是我自己常用的一个分层示意,把特征服务放在整个推荐链路里看,它的角色就很清楚了。

推荐系统在线链路中的特征服务位置 用户请求 特征服务(Feature Service) 实时拼接 · 特征计算 · 模型输入组装 模型推理(Ranking) 结果排序与过滤 返回推荐结果 实时特征流(Kafka) 离线特征(Hive/Table) 近线特征(Flink) 特征服务是模型推理的 “数据前置处理器”

你看,用户请求进来之后,第一站不是直接跑模型,而是先经过特征服务。特征服务负责把用户画像、物品属性、上下文信号、实时行为这些散落在不同地方的数据拼起来,组装成模型能吃的特征向量。我习惯把这个过程叫做“特征拼接”。

在推荐系统里,特征服务通常夹在召回和排序之间。召回阶段用粗粒度的特征筛出候选集,排序阶段才用精细特征做打分。特征服务主要服务排序阶段,因为排序对特征的完整性和实时性要求最高。

特征服务在搜索系统中的位置

搜索系统和推荐系统不太一样。搜索多了一个 Query 理解环节,特征服务的位置也略有偏移。

环节 特征服务参与方式 典型特征
Query 理解 提取 Query 侧特征 分词、意图、实体识别
召回 使用倒排 + 粗粒度特征 类目、品牌、价格区间
精排 特征服务全量拼接 用户实时行为、物品实时 CTR
重排 业务规则 + 多样性特征 已曝光列表、同质化惩罚

我个人觉得,搜索场景下特征服务最大的挑战是 Query 侧特征的实时计算。比如用户搜“今天能到的手机”,特征服务需要实时解析出“今天能到”这个时效性意图,然后去查库存和物流特征。这个计算路径比推荐场景更复杂。

低延迟要求:为什么这么苛刻?

好,位置搞清楚了。接下来聊聊延迟。你可能听过“推荐系统 p99 延迟要控制在 50ms 以内”这种说法。为什么是 50ms?不是 100ms 也不是 200ms?

原因其实很朴素。一个完整的推荐请求,从网关到召回、到特征服务、到模型推理、到排序、再到返回,整个链路通常只有 100-200ms 的预算。特征服务如果吃掉 50ms,留给模型推理的时间就只剩 30-40ms。你想想看,模型本身可能就要跑 20ms,特征服务再慢一点,整个请求就超时了。

我的经验: 我曾经在一个项目中把特征服务的 p99 延迟从 80ms 优化到 25ms,结果整个推荐链路的超时率从 5% 降到了 0.3%。很多时候,瓶颈不在模型,而在特征获取。

低延迟要求具体体现在几个方面:

  • 特征读取延迟: 从 Redis、HBase 或本地缓存拿特征,单次读取必须控制在 1-5ms 以内。
  • 特征计算延迟: 实时聚合、交叉特征、时间衰减等计算,不能超过 10ms。
  • 网络开销: 特征服务与下游模型服务之间的 RPC 调用,序列化/反序列化要尽量轻量。
  • 批量 vs 单条: 能批量获取就不要逐条查,减少网络往返。

延迟预算的分配策略

在实际项目中,我习惯把整个在线链路的延迟预算做一个表格,贴在团队 Wiki 上。这样每个人都知道自己的模块该控制在多少毫秒以内。

模块 延迟预算(ms) 说明
网关 & 路由 5 请求分发、鉴权、限流
召回 15 多路召回并行,取最快结果
特征服务 20 实时拼接 + 特征计算
模型推理 30 DNN 模型前向计算
排序 & 过滤 10 业务规则、多样性控制
序列化 & 返回 5 Protobuf 序列化、网络传输
总计 85 预留 15ms 缓冲,p99 控制在 100ms 以内

你看,特征服务占了 20ms。这 20ms 里,又得拆成特征读取、特征计算、网络传输三部分。每一部分都要精打细算。

避坑指南:我曾经踩过的延迟坑

说到延迟优化,我踩过不少坑。挑几个典型的说说:

  • 缓存穿透: 有一回线上特征服务延迟突然飙升,查了半天发现是某个热门物品的特征在缓存里过期了,大量请求同时去查数据库,直接把 DB 打挂了。后来我加了布隆过滤器和本地缓存兜底,才稳住。
  • 序列化选型: 早期我们用 JSON 传特征,一个特征向量序列化出来 2KB,后来换成 Protobuf,压缩到 400 字节。网络开销直接降了 80%。
  • 批量获取的粒度: 我见过有人把 100 个物品的特征拆成 100 次 Redis 请求去拿,延迟直接爆炸。正确的做法是用 mget 或者 pipeline,一次网络往返搞定。
  • 特征计算的热点: 实时 CTR 计算如果放在特征服务里做,遇到高并发场景很容易成为热点。我后来把这类计算前置到 Flink 任务里,特征服务只做读取,不做计算。
注意: 延迟优化不是一锤子买卖。线上流量模型会变,特征数量会涨,模型会迭代。我建议每两周做一次延迟压测,把 p99、p999 都盯住。一旦发现某个分位点开始恶化,立刻定位根因。

特征服务的高可用设计

低延迟和高可用是孪生兄弟。特征服务一旦挂了,模型拿不到特征,整个推荐链路就断了。我常用的高可用手段包括:

  • 多级缓存: 本地缓存(Caffeine)→ 分布式缓存(Redis)→ 持久化存储(HBase)。本地缓存命中率能做到 70% 以上,延迟在微秒级。
  • 降级策略: 如果某个特征源超时,直接返回默认值或者上次缓存的值,不要阻塞整个请求。
  • 超时控制: 每个特征源的读取都要设置超时,我一般设 5ms,超时就走降级。
  • 熔断机制: 如果某个特征源连续失败超过阈值,暂时切断对该源的请求,避免雪崩。

嗯,这里要注意一点:降级不是随便给个默认值就完事了。你得想清楚,这个特征降级之后对模型打分的影响有多大。有些特征(比如用户实时点击序列)降级了影响不大,但有些特征(比如价格、库存)降级了可能导致用户看到错误的信息。所以降级策略要分特征等级来设计。

小结

特征服务在推荐和搜索系统里,位置很明确——它是模型推理的前置数据处理器。低延迟是它的生命线,20ms 以内的预算要求我们必须在缓存、序列化、批量获取、计算前置这些方面做到极致。我个人的经验是,先把延迟预算分清楚,再逐段优化,最后用压测验证。别想着一步到位,迭代优化才是正道。