AI负载上生产:实时数据管道与弹性算力调度实战

发布时间:2026/10/31 21:55:32
AI负载上生产:实时数据管道与弹性算力调度实战

1. 负载上生产的两大抓手:实时数据管道与弹性算力调度

九月份我花了大半个月时间,带着团队把两套AI负载从“能跑就行”的状态,真正推到了生产级。一是在线推理链路里把实时数据管道彻底打通,模型不再吃T-1的离线特征;二是把推理服务的算力调度策略从固定副本改成了“按流量伸缩、空闲缩到零”。这两件事看着不相关,实际上都是在解决同一个问题:AI负载在生产环境里的成本与响应速度。

先说结论:实时数据喂模型这件事,难点从来不在模型侧,而在数据侧。模型推理本身几百毫秒就能完成,但特征数据从业务库到模型服务之间要走的消息管道、清洗逻辑、特征拼接、缓存失效机制,任何一个环节多出几秒延迟,端到端延迟就压不下来。而“算力缩到零”这事,难点也同样不在服务框架,而在“怎么缩、缩了之后怎么拉起来、拉起来之后会不会打爆数据库”这一整套调度逻辑上。

这篇文章我把自己踩过的坑、验证过的配置、以及目前线上稳定运行的那套方案完整梳理一遍。适合正在做模型服务化改造、或者被推理成本压得头疼的团队参考,不管是已有一套传统架构想升级,还是从零搭一套生产级推理链路,都能从中找到可以直接抄作业的部分。

我把整件事拆成四个部分讲:实时数据链路的整体设计与组件选型、核心环节的配置细节、完整实操过程、以及我们上线后处理过的典型故障。每个部分都会附上我自己总结的经验,保证你照着做不会走太多弯路。

2. 实时数据管道:从离线批处理到在线特征服务的改造全记录

2.1 为什么一定要“实时喂”:离线特征的时效性瓶颈

我先算一笔账。当时线上的推荐模型用的是LightGBM,特征主要分两类:用户长期偏好画像(一周更新一次)和短期行为序列(一天更新一次)。在离线批量更新的架构下,模型读到的用户最近一次点击行为,实际上已经是几个小时甚至一天前的数据。这意味着什么?用户在凌晨三点浏览了一款商品,早上八点再看推荐位时,模型根本不知道他昨晚看过什么,推荐结果和用户当下的意图完全是错位的。

延迟的代价,我举一个更直观的例子。电商大促期间,用户把商品加入购物车后退出,两分钟后又回来了。这时候如果模型能看到“刚刚加购了A商品”这个实时特征,推荐位就应该立刻展示A的配套商品、优惠券、同类替代品。但批量更新的特征服务要等到下一个调度周期才能刷新,用户回来时看到还是旧推荐,转化机会就这样白白溜走。实测下来,把特征从T-1改成准实时后,推荐位的点击率提升了大概11%,这个数字背后就是时效性的价值。

所以“数据要实时喂模型”,本质上是把特征新鲜度从小时级压到秒级。但这句话说起来容易,真正做起来,牵扯到的第一个决策就是:用什么方式把数据从业务系统搬到特征服务。

2.2 管道选型:为什么我们选了消息队列+流处理而不是直连数据库

刚开始我们走过弯路,图省事直接让特征服务定时去业务库查增量数据。上线第一天就出问题:业务库是MySQL主从架构,从库本来就扛着报表查询,我们每30秒轮询一次大表,直接导致从库CPU飙到90%,连主库都受到拖累。后来才明白,生产环境的数据库不是给你模型随便轮询的,特征管道必须和业务系统解耦。

最终方案是用消息队列做中间层。业务系统把行为日志以事件方式异步写入Kafka,流处理任务消费Kafka里的原始事件,实时清洗、拼接、计算,再写入特征存储供模型服务查询。这套架构的好处是:业务系统只需要关心“把事件发出去”,不关心下游谁在消费;特征服务也不直接碰业务库,数据压力被彻底隔离。

选型上我们用的是Kafka + Flink的组合。Kafka负责事件缓冲和解耦,Flink负责窗口计算和特征聚合。可能有人会问,特征聚合用Spark Streaming行不行?我的答案是能跑,但实时性天花板不一样。Flink是真正的流处理引擎,处理延迟可以做到毫秒级,窗口计算是纯内存状态;Spark Streaming本质是微批次,最小批次间隔也要几百毫秒到秒级,而且状态管理不如Flink方便。对于特征服务这种对延迟敏感的场景,Flink更合适。

当然Flink也有学习成本,团队如果没有流处理经验,初期建议先从Kafka + 轻量消费脚本开始,把链路跑通后再逐步替换成Flink。我就是这么干的,先把复杂问题简单化,再在简单方案的基础上迭代。

2.3 特征存储的读写策略:Redis缓存与冷热分层

数据流到了特征存储层,我们又遇到一个新问题。模型服务一次推理请求需要读取几十个特征,每个特征在存储里对应一条记录。如果每次推理都去查一遍数据库,查询开销比模型计算本身还大,端到端延迟根本压不下来。

解决思路是给特征存储加上Redis缓存层,而且是要区分热点特征和冷特征的分层策略。我们当时把特征分成了三层:

第一层是Redis热缓存,存放活跃用户最近的短期行为特征,这类特征访问频率最高、变动最频繁,缓存过期时间设成5分钟。第二层是特征数据库,存放长期偏好类特征,这种特征相对稳定,直接从数据库读取就行。第三层是大规模离线特征,更新频率以天为单位,直接从数仓同步过来,数量级大但访问频率不高,不需要进缓存。

这里最关键的配置是Redis的过期策略和缓存击穿保护。我做了一个很细节的调整:给每个特征设置了动态过期时间,不是固定5分钟,而是根据特征更新时间动态推算。比如某个特征刚刚更新完,就意味着未来5分钟内它一定是新的,直接命中缓存就行。这样能避免一个典型问题:缓存刚刚过期,特征存储恰好还没更新完,同一时刻大量请求同时穿透到数据库,把下游打挂。

2.4 滑动窗口特征的计算难点与工程化处理

实时特征里有一类比较特殊:滑动窗口特征。比如“用户最近3分钟点击了哪些商品”“最近1小时内浏览的品类分布”。这类特征不是简单查一条记录,而是要维护一个时间窗口内的行为集合,窗口不停滑动,特征值持续变化。

我们最初的实现是用Redis的List结构存行为序列,每次新事件进来就LPUSH进去,然后定期清理过期元素。但流量上来后发现问题很大:List越来越长,清理时要从尾部遍历删除,而且每次计算特征时还要把整个List读出来反序列化,CPU开销和内存开销都扛不住。

后来换成了Redis的ZSet,用时间戳做score,事件ID做member。新事件进来直接ZADD,过期元素用ZREMRANGEBYSCORE一条命令清掉。计算“最近N分钟的行为”用ZRANGEBYSCORE直接取,不用读全量数据。这一改,单条特征计算的耗时从原来的几十毫秒降到了不到5毫秒,内存占用也降了一个量级。

这里有个细节值得强调:窗口大小的选择不能拍脑袋,要根据业务场景试出来。窗口太短,特征抖动剧烈,模型输出的稳定性差;窗口太长,特征失去实时性意义。我们在推荐场景里分别试过1分钟、3分钟、5分钟、10分钟窗口,最后3分钟窗口在“实时性”和“稳定性”之间取得了最好平衡,模型评估的AUC也最高。所以说,工程上很多参数不是越短越好,是要拿数据说话的。

3. 算力缩到零:Serverless推理的弹性扩缩容实践

3.1 缩到零的动机:推理服务闲时占用资源的账目细算

说完实时数据,再来说算力缩到零这件事。当时我们线上有一个面向内部运营的模型服务,白天业务高峰时每秒大概要处理300个请求,到了凌晨到早上六点,流量几乎归零,每秒不到5个请求。但原来的部署方式是固定副本数,始终常驻4个推理Pod,每个Pod占1张GPU卡。凌晨那六个小时,这4张GPU卡就是白烧电费。

我算了一笔账:按当时的GPU租赁行情,一张A10卡每小时成本大概在3块左右,4张卡跑24小时是288块,但其中6个小时的服务流量几乎为零。也就是说每天有72块钱是花在“空转”上的,一个月就是两千多。这还算少的,如果服务更多、业务更闲,浪费比例更大。所以“算力要能缩到零”,不是炫技,是实实在在的成本优化。

但“缩到零”也不能盲目缩。有些服务必须保持实时响应,如果缩到零之后冷启动要两分钟,用户点一下等两分钟,那是灾难。所以我们的策略是区分场景:核心在线服务设置最小副本数,保证底线容量;非核心的、能容忍冷启动延迟的服务,才真正缩到零。

3.2 弹性伸缩策略配置:HPA、冷启动与突发流量之间的平衡

Kubernetes原生的HPA只能看CPU、内存这类指标,但推理服务的负载和CPU占用并不是线性关系。我们最终是用自定义指标做扩缩容,监听每个Pod的“在途请求数”,以此为信号触发伸缩。这个指标的意义在于:它能直接反映服务的真实排队压力,比CPU监控精准得多。

配置上我给出几个关键参数的参考值。扩张阈值设在“平均每个Pod在途请求数超过5”,收缩阈值设在“低于1”,扩缩容冷却时间给60秒。为什么冷却时间给60秒而不是默认的5分钟?因为5分钟太长,流量突然起来时反应不过来,容易在冷启动期内把新Pod打爆。但60秒也不能再短了,否则流量小波动就会导致频繁扩缩,Pod反复重建反而更浪费资源。

真正的难点在缩容策略。Pod缩容下去之后,K8s会先等它处理完当前请求再终止,这是优雅退出机制,默认的terminationGracePeriodSeconds是30秒。但如果正好有长连接请求在跑,30秒不够,请求就会被强制掐断。我们在配置里把这个值调到了120秒,同时给服务加了一个PreStop钩子,在Pod收到终止信号后先主动从负载均衡摘除自己,再sleep 5秒,留出时间让在途请求排空。这套组合拳下来,缩容期间的错误率从0.3%降到了0.02%以下。

3.3 GPU资源池化:显存碎片与设备插拔的工程细节

真正把算力缩到零之后,又冒出一个新问题:GPU显存被占着不释放。推理服务退出后,显卡驱动层的显存回收有时候不是即时的,下一批Pod调度上来时发现显存居然不够用。我们排查下来,这涉及NVIDIA Container Toolkit的设备插拔逻辑:如果上一个容器没有干净地释放CUDA上下文,驱动层会有一段残留状态。

这个问题的标准解法是使用GPU资源池化方案,把物理GPU切成更细粒度的算力单元,让多个推理任务可以共享一张卡,同时做到任务结束立即回收显存。我们在生产环境用了一套相对成熟的方案:按显存大小分配虚拟GPU单元,一个推理任务分到几分之一张卡,跑完自动归还。

这里我提醒一句:GPU池化不是所有场景都适用。如果模型输入张量特别大,几个任务挤一张卡会导致显存溢出。我们在试点时发现,轻量级BERT类模型和LightGBM模型适合池化,但SD类图片生成模型、超长序列的LLM推理就不建议池化,容易互相拖垮。要不要池化,得先量化好单次推理的峰值显存。

4. 实操过程:从代码改造到生产验证的完整拆解

4.1 第一阶段:模型服务化与实时特征接入的代码改造

整个改造我是分三个阶段推进的,每个阶段都有明确的验收标准。第一阶段是代码层改造,目标是让模型服务能读到实时特征。

我拿当时用的验证模型举例。这是一个基于LightGBM的点击率预估服务,原始版本是从离线特征表批量加载特征,模型预测时用内存里的静态字典查特征。第一版改造,我把特征查询从“本地静态字典”改成了“远程特征服务调用”,代码看起来简单,但至少动了三个关键点:加上了远程调用的超时控制、加上了缓存降级逻辑、加上了特征缺失时的默认值填充。

实战中这段逻辑很容易写翻车。比如远程调用超时时间设太短,流量一大就频繁超时,导致整批请求都走到默认特征,模型预测结果全部失真。我最后的配置是:连接超时200ms,读取超时300ms,超过就降级到本地缓存特征,再不行就用默认值。宁可特征旧一点,也不能让推理链路因为特征服务被拖垮。

模型服务的接口改造也值得一提。我们用gRPC替代了原先的HTTP接口,不是因为HTTP不行,而是批量推理场景下gRPC的protobuf序列化更高效,长连接也更省资源。改造后同一批请求的吞吐量提升了近40%,这属于白捡的优化。手里是Python技术栈的团队,我推荐用gRPC或至少用HTTP/2,都能明显改善性能。

4.2 第二阶段:特征管道部署与生产参数校准

第二阶段是把特征管道真正部署起来。我们从业务方接入了三类事件:曝光事件、点击事件、加购事件。每条事件从业务系统发出,到Kafka topic落盘,再由Flink消费计算成特征,最终写入Redis并更新特征服务,整条链路的目标是端到端延迟不超过3秒。

当时校准参数花了不少时间。Kafka的topic分区数我们设成了12,消费者并发和分区数对应上,保证消费速度能跟上生产速度。Flink的checkpoint间隔设成了10秒一次,这个值不要设太频繁,否则状态快照写入外部存储的开销会把吞吐拖垮。Redis那边我单独给特征服务开了AOF持久化,防止进程重启后热数据丢失,AOF的刷盘策略用的everysec,平衡了可靠性和写入性能。

管道跑起来之后,我专门对比了一组数据:旧链路下特征更新到模型生效的平均耗时是37分钟,新链路压到了2.1秒。这个数字就是改造效果的直观体现。

提醒一句各位:Flink任务的并行度不要盲目设大。并行度越大,占用的slot越多,状态后端的内存压力也越大。我最初把并行度从2调到8,结果处理延迟没有明显降低,反而内存OOM了三次。因为瓶颈根本不在计算能力,而在Kafka消费速度。在动手调并行度之前,先看监控面板里哪个环节的堆积量最大。

4.3 第三阶段:服务网格化改造与算力伸缩上线

第三阶段才是真正的弹性伸缩改造。我们把推理服务接入了服务网格,流量入口统一走网关,每个Pod的实时指标上报给K8s的metrics-server。然后配置自定义HPA规则,同时把“最小副本数设成0”写进了服务配置。

这里特别说一下“最小副本数设成0”的副作用。K8s默认调度器对0副本服务是不会做任何预热的,流量进来的第一个请求必然经历冷启动。冷启动时长取决于模型加载方式:如果你用ONNX Runtime加载模型,一个LightGBM模型大概2到3秒能完成加载;如果是PyTorch加载大模型,那就要按秒级到分钟级估了。我们的处理是在服务启动脚本里加了一个“预加载模型”的阶段,Pod一旦被拉起就先加载模型,再对外宣告就绪。这样流量真正到达时,模型已经热好了。

整体上线后,我们观察了一周的指标:白天高峰期Pod数量自动从0拉到了12个,吞吐量稳定维持在设计的峰值;凌晨流量回落后,Pod数量自动收缩到0,GPU占用归零。整周算下来,GPU资源成本下降了大概47%,而服务的可用性依然保持在99.9%以上。

5. 上线后遇到的典型故障与排障速查

5.1 缓存击穿引发的特征服务雪崩

上线第二天就遇到一个典型的故障:某个热门特征在Redis里的缓存刚好过期,而特征数据库因为Flink任务恰好延迟了一批更新,导致大量请求在同一个时间窗口内穿透缓存打到了数据库上。结果数据库连接池被打满,特征服务整体变慢,进而拖垮了所有推理请求。

排查过程是这样的:先看监控面板,Redis命中率从正常的95%骤降到30%,数据库的慢查询数量暴涨。然后定位到具体是哪个特征出了问题,最后发现是因为Flink的重启导致窗口状态丢失,特征更新延迟,触发了缓存击穿。

修复分三步走。第一步,给Redis加上了空值缓存,即使数据库里查不到该特征,也缓存一个空标记,防止同一时刻的请求反复穿透。第二步,在特征查询代码里加上了单飞的分布式锁,同一特征同一时刻只有一个请求能去查数据库,其他请求等待锁释放后直接读新缓存。第三步,给Flink任务加了更稳的checkpoint恢复策略,避免窗口状态丢失。这套组合之后,类似的击穿问题再没出现过。

5.2 冷启动争抢资源引发的连锁故障

另一次故障出现在流量突增时。那天运营做了一场活动,流量瞬间从每秒50个请求涨到400个,弹性扩容策略立刻触发,一次性拉起了8个Pod。但这8个Pod同时做模型加载,CPU和内存瞬时飙高,反而把正在处理业务请求的旧Pod挤得资源不足,导致一部分请求超时。

这个问题的根源是“扩容风暴”和“冷启动资源争夺”。我们的解法不复杂:给扩容加上了一个速率限制,HPA每轮最多新增2个Pod,等它们稳定后再继续扩下一批。同时给Pod加了资源预留,在调度阶段就确保每个新增Pod有足够的CPU配额做初始化,不跟旧Pod抢资源。改造后,流量突增场景下虽然扩容速度略慢了一点,但整体稳定性明显改善,没有再出现过因扩容而导致的连锁故障。

5.3 常见问题速查表

我把上线前后遇到过的典型问题整理成了一张速查表,方便各位对号入座。

| 问题现象 | 可能原因 | 排查思路 | 解决方案 | | 推理延迟偶尔飙升 | 特征查询穿透数据库 | 看Redis命中率是否骤降 | 加空值缓存、分布式锁 | | Pod反复创建销毁 | HPA阈值过于敏感 | 看扩缩容事件频率 | 调大冷却时间、加扩缩速率限制 | | 缩容后请求被掐断 | 优雅退出时间不足 | 看终止阶段的错误日志 | 调大terminationGracePeriodSeconds,加PreStop钩子 | | 扩容后新Pod启动失败 | 冷启动资源不足 | 看Pod调度时的资源请求 | 设置资源预留、限制单轮扩容数量 | | 特征长期不一致 | 事件丢失或重复消费 | 对比Kafka生产和消费条数 | 开启Flink精确一次语义、幂等写入 | | GPU显存不释放 | CUDA上下文未被回收 | 看显卡显存占用率 | 使用池化方案、任务结束显式释放 | | 模型读取特征超时 | 特征服务端到端延迟超标 | 看各环节耗时分布 | 优化Redis查询、加本地缓存降级 |

5.4 几个容易忽略但特别关键的小细节

最后分享几个很小的细节,每一个都在线上真实坑过我。

第一个是Kafka消费组的提交方式。我们最初用的是自动提交,结果Flink任务重启时经常出现重复消费,特征被算了两遍,窗口统计翻倍。改成手动提交,等一批数据处理完再提交offset,虽然稍微复杂一点,但数据一致性有了保证。自动提交适合容忍重复的场景,特征计算这种事必须精确。

第二个是模型推理时特征拼接顺序要和训练时完全一致。这个坑看似低级,但改特征管道后特别容易踩。我们在训练代码里是A、B、C顺序拼接,推理代码不知道什么时候被人改成了C、A、B,模型不报错,但预测结果完全乱套。后来我在推理服务里加了一道校验,用一小批已知样本比对预测输出,每次发布后先跑校验再放流量。

第三个是日志采样策略。推理服务在高峰期每秒几百个请求,全量打日志的话,日志系统先被撑爆。我们一开始没做采样,结果日志服务磁盘直接写满,反噬到主服务。后来改成按比例采样加错误日志全量保留,既保住了排查问题的能力,又控制了日志开销。

6. 生产环境验证与后续可扩展的方向

通过这一轮的改造,我们验证了一个事实:AI负载上生产,模型本身的精度只是一部分,真正决定能否稳定落地的,是它周围的数据管道和资源调度体系。

这套体系跑稳之后,后续还可以往两个方向继续扩展。一是用特征平台统一管理所有特征,把特征从“某个服务的内部实现”升级成“组织级的资产”,实现特征的一次定义、多处复用。二是引入更细粒度的算力调度策略,比如根据请求优先级动态分配GPU份额,把缩到零的钱省得更极致。

如果你也正在做类似的改造,我的建议是从一条链路开始试点,不要一上来就铺开全局。先选一个业务价值最明确的服务,跑通实时数据管道和弹性伸缩,把性能基线打出来,再逐步复制到其他服务。这样每一步都有数据支撑,团队信心也会稳步建立起来。

我个人在实际操作中最深的一个体会是:实时数据链路和弹性算力调度,看上去是两个独立的技术栈,但它们有一个共同点——都是在为“不可预测”做准备。流量不可预测,所以要能缩能放;数据延迟不可预测,所以要用缓存和窗口计算把波动吸收掉。做生产系统,真正有价值的不是把正常的路径做好,而是把异常的路径想清楚。这也是我在这个项目里学到的最重要一课。