2. Serving API 设计原则:低延迟、高可用、一致性、可扩展性
做特征平台这么多年,我最大的体会是:Serving API 的设计,本质上是在四个相互制约的目标之间找平衡。低延迟、高可用、一致性、可扩展性——这四个词听起来像教科书上的标准答案,但真正落地时,每一个都是坑。
我个人习惯把 Serving API 的设计原则拆成两层来看:业务承诺层和工程实现层。业务承诺层告诉调用方「我能做到什么」,工程实现层告诉团队「我怎么做到」。下面我们逐个聊。
2.1 低延迟:特征获取的硬约束
特征 Serving 的延迟,直接决定了线上推理的端到端耗时。我见过不少团队,模型推理优化到毫秒级,结果特征获取花了 50 毫秒——白忙活了。
低延迟的核心思路其实就三条:
- 本地缓存优先:能不进网络就别进网络。特征值如果变化不频繁,直接塞进进程内缓存。
- 批量获取:单条查和批量查,网络开销差一个数量级。API 设计上一定要支持一次请求拿多个特征。
- 预计算与物化:实时计算再快也快不过查表。把能提前算好的特征物化到 KV 存储里。
下面这张图展示了特征获取的典型延迟分布,你可以看到缓存命中与否的差距有多大:
2.2 高可用:别让特征拖垮整个系统
特征平台挂了,线上模型就瞎了。这不是开玩笑。我经历过一次线上事故,Redis 集群抖动导致特征超时,所有推荐请求都返回默认值,CTR 直接腰斩。
高可用的设计,说白了就是做好三件事:
- 冗余部署:多机房、多副本,单点故障不影响整体。
- 降级策略:特征拿不到怎么办?用默认值、用缓存旧值、或者跳过这个特征。
- 熔断与限流:下游存储扛不住了,API 层要主动熔断,别让雪崩发生。
高可用不是靠口号实现的,得落实到代码里。下面是一个简单的降级策略配置示例:
// 特征获取的降级策略配置
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 架构:
2.5 四个原则的权衡与取舍
你可能会问:这四个原则都想要,但现实不允许怎么办?
答案是:按业务场景排优先级。
举个例子:
- 广告点击率预估:低延迟 > 高可用 > 一致性 > 可扩展性
- 金融风控:一致性 > 高可用 > 低延迟 > 可扩展性
- 内容推荐:可扩展性 > 低延迟 > 高可用 > 一致性
没有银弹。我见过最成功的团队,不是把每个原则都做到极致,而是清楚地知道「哪个可以妥协,哪个不能让步」。