2. Serving API 设计原则:低延迟、高可用、一致性、可扩展性

做特征平台这么多年,我最大的体会是:Serving API 的设计,本质上是在四个相互制约的目标之间找平衡。低延迟、高可用、一致性、可扩展性——这四个词听起来像教科书上的标准答案,但真正落地时,每一个都是坑。

我个人习惯把 Serving API 的设计原则拆成两层来看:业务承诺层和工程实现层。业务承诺层告诉调用方「我能做到什么」,工程实现层告诉团队「我怎么做到」。下面我们逐个聊。

2.1 低延迟:特征获取的硬约束

特征 Serving 的延迟,直接决定了线上推理的端到端耗时。我见过不少团队,模型推理优化到毫秒级,结果特征获取花了 50 毫秒——白忙活了。

低延迟的核心思路其实就三条:

  • 本地缓存优先:能不进网络就别进网络。特征值如果变化不频繁,直接塞进进程内缓存。
  • 批量获取:单条查和批量查,网络开销差一个数量级。API 设计上一定要支持一次请求拿多个特征。
  • 预计算与物化:实时计算再快也快不过查表。把能提前算好的特征物化到 KV 存储里。
我的经验:曾经有个推荐场景,特征延迟从 15ms 降到 2ms,只做了一件事——把 Redis 的 pipeline 改成 mget。嗯,有时候优化就这么简单。

下面这张图展示了特征获取的典型延迟分布,你可以看到缓存命中与否的差距有多大:

特征获取延迟分布(典型场景) 本地缓存命中 延迟:< 1ms 远程缓存命中 延迟:1-5ms 存储查询 延迟:10-50ms 设计要点: 1. 优先保证本地缓存命中率 > 90% 2. 远程缓存(Redis/Memcached)作为第二级 3. 存储查询只作为兜底,且必须设置超时 4. 批量接口设计:一次请求获取 N 个特征,而非 N 次请求

2.2 高可用:别让特征拖垮整个系统

特征平台挂了,线上模型就瞎了。这不是开玩笑。我经历过一次线上事故,Redis 集群抖动导致特征超时,所有推荐请求都返回默认值,CTR 直接腰斩。

高可用的设计,说白了就是做好三件事:

  • 冗余部署:多机房、多副本,单点故障不影响整体。
  • 降级策略:特征拿不到怎么办?用默认值、用缓存旧值、或者跳过这个特征。
  • 熔断与限流:下游存储扛不住了,API 层要主动熔断,别让雪崩发生。
我曾经踩过的坑:早期设计时只做了超时,没做熔断。结果存储慢的时候,所有请求都在等超时,线程池被打满,整个 API 都 hang 住了。后来加了 Hystrix 风格的熔断器,才彻底解决。

高可用不是靠口号实现的,得落实到代码里。下面是一个简单的降级策略配置示例:

// 特征获取的降级策略配置
FeatureConfig config = FeatureConfig.builder()
    .featureName("user_click_ctr")
    .timeoutMs(50)                    // 超时 50ms
    .fallbackValue(0.05)              // 降级默认值
    .fallbackStrategy(FallbackStrategy.USE_CACHED)  // 优先用缓存旧值
    .circuitBreakerThreshold(0.5)     // 错误率超 50% 熔断
    .circuitBreakerSleepMs(10000)     // 熔断后 10s 半开
    .build();

2.3 一致性:特征值不能「一会儿一个样」

一致性这个话题,在特征平台里其实比数据库要宽松一些。你想想看,模型训练时用的特征和线上 Serving 用的特征,如果对不上,模型效果肯定打折扣。

但这里有个现实问题:强一致性往往意味着高延迟。所以实际工程中,我们通常做的是「最终一致性」+「版本对齐」。

一致性级别 延迟影响 适用场景
强一致性 高(需等待写入确认) 金融风控、实时反欺诈
最终一致性 低(异步写入) 推荐系统、广告排序
读写分离一致性 中(主从延迟可控) 大部分在线推理场景

我个人习惯的做法是:给特征打上版本号。每次特征更新时,带上 batch_id 或 timestamp。Serving 时如果发现特征版本落后于模型要求的版本,就走降级。这样既保证了可用性,又不会让模型吃到太旧的数据。

核心思路:一致性不是非黑即白的。你要根据业务场景,选择「够用」的一致性级别,而不是追求「最强」的。

2.4 可扩展性:别让架构成为瓶颈

特征数量会增长,请求量会增长,特征源会增长。如果 API 设计一开始没考虑扩展性,后面改起来会非常痛苦。

可扩展性设计,我总结了三个关键点:

  • 无状态设计:API 层不持有状态,所有状态下沉到存储层。这样加机器就能线性扩容。
  • 特征注册与发现:新增特征不需要改代码,通过配置中心或注册中心动态加载。
  • 分片与路由:特征数据量大了之后,要能按特征 ID 或实体 ID 做分片,避免单存储节点成为瓶颈。

下面这张图展示了可扩展的 Serving API 架构:

可扩展的 Serving API 架构 客户端 / 模型服务 负载均衡 (LVS/Nginx) API 节点 1 API 节点 2 API 节点 N Redis 分片 1 Redis 分片 2 Redis 分片 M API 层无状态,可水平扩展;存储层按特征 ID 哈希分片,避免热点

2.5 四个原则的权衡与取舍

你可能会问:这四个原则都想要,但现实不允许怎么办?

答案是:按业务场景排优先级。

举个例子:

  • 广告点击率预估:低延迟 > 高可用 > 一致性 > 可扩展性
  • 金融风控:一致性 > 高可用 > 低延迟 > 可扩展性
  • 内容推荐:可扩展性 > 低延迟 > 高可用 > 一致性

没有银弹。我见过最成功的团队,不是把每个原则都做到极致,而是清楚地知道「哪个可以妥协,哪个不能让步」。

一个小建议:设计 API 时,先把 SLA 写清楚。比如「P99 延迟 < 20ms,可用性 99.99%,特征不一致窗口 < 5s」。有了明确的数字,团队才知道往哪个方向使劲。