先说一个我印象很深的场景:凌晨两点,运营群里突然弹出一条消息,“人群包刚跑完,准备开始推送”,后台监控上的QPS从800一下蹿到2万,模型服务实例从20个被拉到120个,平台自动扩容,消息队列积压从0涨到几十万又慢慢回落到0。整个过程没有值班工程师被电话叫醒,第二天复盘时大家看曲线,发现流量高峰和成本峰值几乎完美错开。
这就是做智能营销AI平台最让人上瘾的地方。也和做常规业务系统完全不同:你不能按峰值流量把机器常年备着,那会让成本高到老板怀疑人生;也不能只按平均流量设计,那大促活动一上来系统必挂。弹性可扩展架构说到底只有两件事:高峰期能不能扛得住,低谷期能不能把钱省下来。但真把这八字落到实处,要从流量特征、系统分层、伸缩策略、稳定性兜底一路抠到GPU调度。
这篇文章是我团队从0到1搭智能营销AI平台、以及后续多次大促压测后的完整记录。包含架构怎么拆、弹性怎么调、扩容过程中踩过的坑、靠什么机制兜底,希望能给正在做同类系统的朋友一些参考。
1. 营销流量和普通流量不一样:先搞清楚到底在“弹”什么
1.1 脉冲式流量才是常态
传统互联网后端,哪怕是电商业务,平时流量和峰值之间通常也就三五倍的差距,而且这种增长是渐进式的。智能营销AI平台的流量模型完全不同:运营一次“618会员召回”、“双11前站预热”、“直播间的实时发券”,流量形态都是一条近乎垂直的脉冲。
举个例子,一个千万级用户的品牌,运营选好人群包、点下“开始投放”按钮的那一秒,人群圈选服务要立刻把几百个筛选条件跑完,随后消息发送请求会在几分钟内打到触达服务上。如果这个营销位是通过Push、短信、微信模板消息触达,后端要处理的不是每秒几百个请求,而是每秒数万条消息的投递。即便加上了限速、分批、队列削峰,瞬时拥塞依然存在。
所以建平台第一天就要接受一个事实:弹性可扩展架构的第一目标不是“快”,而是“宽”——能在几分钟内把系统容量拉大几十倍,顶住脉冲并不被打垮。 谁按常驻流量的3倍去留冗余,谁就会在第一次大促时付出惨痛代价。
1.2 AI推理负载让弹性问题更难办
如果只是消息推送,弹性问题其实好解决,多加无状态实例就行。智能营销AI平台难在它里面混着两类完全不同特征的负载:
第一类是传统业务负载,比如人群圈选、名单导出、触达发送、频控查询。这类服务的关键字是“连接数”和“IOPS”,依赖数据库、Redis、消息队列。
第二类是AI推理负载,比如用lookalike模型找相似人群,用CTR/CVR模型给用户打分排序,用NLP模型抽取用户意图标签。这类服务吃的是CPU算力,更吃GPU显存。一个基于Transformer的打分模型,GPU推理单次可能几百微秒到几毫秒,但一旦并发上来,显存不够就直接OOM。
这两种负载对扩容的反应速度完全不一样。普通Web服务加几个Pod,几十秒内就能开始接流量。带模型的推理服务扩容,要考虑镜像体积、模型文件下载、GPU驱动初始化、CUDA上下文预热,冷启动动不动三两分钟起步。大促流量不可能等你慢慢拉实例。
1.3 营销链路处处是“状态”,扩了容但不一定扛得住
最坑的是,营销平台不像纯展示类系统那样可以随便无状态化。一次完整的营销活动链路是这样的:
- 运营创建活动,选择目标人群圈选条件
- 人群圈选服务实时或离线圈出用户ID集
- 系统对人群做频控、黑名单过滤、疲劳度检查
- AI模型对人群打分排序,选出“最可能转化”的头腰部用户
- 消息触达服务按用户偏好渠道发送内容
- 用户点击/转化行为回流到数据系统,形成下一轮营销的优化素材
每一环几乎都带着状态:圈选任务有进度,频控要查用户N天内是否收过同类型消息,触达渠道有上游限流配额,连AI打分都有特征拼接需要实时读取用户画像。
这种情况容易让人陷入一个误区:以为弹性就是“K8s加Pod”。实际上,扩了无状态应用,瓶颈可能跑到数据库连接数上;把数据库连接池加大,瓶颈又到了Redis热点key上;等这些都搞定,模型服务的新实例又因为冷启动接不上流量。这就是为什么营销AI平台的弹性设计,需要从全局链路去拆。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 弹性架构的分层拆解:先找对弹性边界,再谈扩容
2.1 把平台拆成四个弹性边界不同的层级
在设计之初,我们定了四条最核心的架构原则:接入层无状态、计算层可拆分、存储层按单元隔离、消息层做削峰填谷。顺着这个思路,平台拆成四层:
| 层级 | 核心组件 | 弹性伸缩方式 | 弹性边界 |
|---|---|---|---|
| 接入层 | API网关、统一鉴权、路由 | 水平扩容,按QPS/并发数调度 | 几乎无限扩展,受限于网关和DNS |
| 编排层 | 人群圈选、活动编排、频控、事件处理 | 水平扩容,辅以任务分片;频控类依赖Redis,要做分片 | 受Redis/数据库分片方式的约束 |
| AI计算层 | 特征拼接、模型推理、打分排序 | 按GPU/CPU资源调度,支持模型级副本扩缩 | 受显存、GPU卡数、模型加载速度限制 |
| 数据触达层 | 消息队列、触达通道、回调处理 | 消费端按队列积压动态伸缩 | 受下游第三方渠道的QPS上限限制 |
这四层的伸缩速度、成本、约束条件完全不同,混在一起谈弹性一定会出问题。
2.2 人群圈选和频控这类状态服务的拆分
人群圈选是我们最早遇到瓶颈的地方。最初版本把所有圈选条件拼成一个巨大的SQL直接扔给ClickHouse,同时在线跑几百个任务,ClickHouse副本直接被打爆。
后来把圈选分为“离线圈选”和“实时校验”。离线圈选通过预计算标签索引,把几亿用户缩小到一个较小的候选集,再在候选集上做精确过滤;在线环节只做“判断用户在不在名单”这种点查,把计算压力大幅下沉。
存储也跟着做了改造。用户标签、黑白名单、频控计数这类高并发、低延迟的数据,从MySQL迁到了Redis Cluster,并且按用户ID做哈希分片。这件事直接改变了弹性扩容的边界:以前要扩容的是“扛复杂查询的计算集群”,现在要扩容的是“能线性加片的KV集群”。Redis Cluster的分片一旦确定,扩容就要做slot迁移,不能像无状态应用一样秒级伸缩,所以频控这部分我们保留了一定的buffer容量,而不是把它的水位压得太满。
2.3 离线与实时链路解耦:大促时先断“实时反馈”还是先降级?
AI模型打分大多依赖特征。特征里有实时行为的(比如用户最近1小时看过哪个商品),也有离线统计的(比如用户过去30天购买频次)。这两类数据对时效性要求完全不同,但在服务调用时经常被揉在一个特征服务里。
弹性架构设计里,我强烈建议把特征服务的“实时链路”和“离线兜底”拆开,并设置明确的降级开关。正常情况查询实时特征服务,超时或失败时自动降级到离线特征表读取。大促流量峰值到来,特征服务扛不住时,优先限流实时特征,保住主链路,让模型先用延迟5分钟的准实时特征,而不是让整个AI服务因为一个子依赖被拖垮。
这个设计给我们的最大帮助是:AI推理服务可以直接按“离线特征是否可用”来决定扩容上限。特征服务一旦进入降级模式,整个打分链路对实时数据的依赖大幅下降,反而更容易在流量洪峰中稳定运行。
3. 弹性伸缩落地的关键实操:从KEDA配置到GPU池化
3.1 无状态化改造:不只是把Session移到Redis
很多人以为无状态化就是把本地Session换成Redis。在营销AI平台这种场景,无状态化改造要细得多:
- 业务实例里不能存活动配置的本地缓存,必须从配置中心统一拉取,否则新扩容的Pod没有最新的活动参数,直接导致圈选范围错误
- 模型服务不能把特征表缓存在实例内存里做“热启动”,否则新实例一上线特征数据为空,打出来的分全是垃圾
- 定时任务要收敛到独立的调度器,避免多副本同时触发导致重复发送消息
- 消息消费位点依赖Kafka的group,不依赖本地存储
- 文件、临时结果要放对象存储或共享存储,不能写本地磁盘
这些点每一条都是在故障中换来的教训。最典型的是活动配置缓存,早期版本用了一个带本地过期缓存的配置读取组件,活动开始前10分钟改了一次文案,新扩容的实例拉到了新文案,老实例还在用缓存里旧文案,结果同一次活动出现了两种不同话术的Push,用户侧体验极差。无状态化不是架构洁癖,它是弹性扩缩容能保持一致性的前提。
3.2 自动扩缩容:为什么只依赖CPU指标一定出事
Kubernetes默认的HPA支持CPU和内存指标,但营销场景下CPU水位往往有严重的滞后性。比如Kafka消息积压已经几十万条了,消费者Pod的CPU还不到30%,因为消费者在等下游接口响应;等下游超时恢复,CPU会瞬间拉高,此刻再扩容已经晚了几秒。
我们的做法是双指标驱动:
第一,在线服务以QPS、P99延迟、并发数为扩缩依据。网关层把每个服务实例的实时QPS上报到监控,HPA读自定义指标,比如“单实例QPS超过500就扩容”。注意P99延迟一定要纳入扩容依据,因为如果QPS很低但SQL变慢导致RT上涨,通常意味着有慢查询或连接池打满,单靠QPS不会触发扩容。
第二,消息消费者以队列积压为伸缩依据。这里推荐用KEDA。KEDA是Kubernetes生态里专门做事件驱动自动伸缩的组件,它能直接监听Kafka、RabbitMQ、Pulsar的积压消息数,然后驱动HPA调整副本数。
一段常见的KEDA ScaledObject配置大致长这样:
yaml复制apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: campaign-push-consumer-scaler
namespace: marketing
spec:
scaleTargetRef:
name: campaign-push-consumer
pollingInterval: 10
cooldownPeriod: 120
triggers:
- type: kafka
metadata:
bootstrapServers: kafka-cluster:9092
topic: marketing.push.request
lagThreshold: "5000"
consumerGroup: push-consumer-group
这段配置的意思很直白:每10秒检查一次消费者组在marketing.push.request这个topic上的积压,如果积压超过5000条就触发扩容,每次最多扩容到预设上限;积压清掉后120秒内没有新积压,再把副本缩回去。实际运行下来,它的触发速度比我们以前人工看监控扩容快了一个数量级。
3.3 GPU推理层的弹性:模型文件加载和显存碎片
AI推理服务的弹性是智能营销平台里的硬骨头。CPU类Web服务扩容到100个Pod只需要一分钟,GPU推理服务扩容到10个Pod可能要五分钟。瓶颈通常在三处:
模型文件下载。模型文件动辄几百MB甚至几个GB,Pod启动时要从模型仓库拉取。第一次冷启动极其痛苦。我们把镜像分层做成“基础环境层+模型层”,并提前把高频模型文件缓存到节点本地,大幅缩短了启动时间。如果你用的是K8s,可以给推理服务单独设置节点池,并给节点挂载一块SSD作为模型缓存盘。
GPU显存分配。K8s默认把整张GPU卡分配给一个Pod,如果一个模型只需要2GB显存,但GPU卡有32GB,直接分配整卡会浪费大量资源。我们引入了支持GPU显存细粒度分配的机制,让多个推理Pod共享一张物理卡,前提是显存隔离和算力隔离做可靠。千万注意:不要粗暴地让所有模型混在同一张GPU上,否则一个实例的显存泄漏可能影响同卡的其他模型,排查成本非常高。
推理预热。新启动的模型Pod如果没有预热,第一批请求会包含CUDA初始化、图优化、权重加载的完整开销,P99延迟可能达到正常值的几十倍。我们的做法是在Pod的启动探针之后、就绪探针之前,主动发一批覆盖常见请求形态的暖机流量,等模型推理延迟降到正常水位再挂到负载均衡上。
3.4 稳定性和弹性的兜底网:限流和熔断永远先于扩容
弹性扩缩容不是万能的。扩容有上限,机器资源不是无限供应的;扩容有延时,从触发到实例真正接流量往往要几十秒。这几十秒的空窗期必须靠限流、熔断、降级撑住。
有人把限流看成弹性的对立面,觉得“扩得动就不用限流”,这是非常危险的想法。事实上,限流系统越完善,越敢放心扩容。因为你知道万一扩容没顶住,还有第二道防线能保护核心链路,不至于全站雪崩。
我们的限流分两层:
接入层限流按APP级、活动级、用户级配置。活动级限流尤其重要,因为一个运营配置错误导致的活动流量甚至能高于正常总流量的几十倍,这种异常流量不仅不该服务,反而应该优先挡掉。
服务间限流按依赖级别配置。AI服务调用特征服务、频控服务、人群服务时各有独立的线程池和Semaphore限制,任何一个下游抖动只会影响当前调用场景,不会把所有线程耗尽导致整个服务无响应。
弹性扩容解决的是“流量确实正常上涨”的情况,限流解决的是“流量异常或超出资源供给”的情况。两者叠加,系统才能在极端流量下既保持高可用又不至于烧掉太多钱。
4. 一次真实的活动故障复盘:人群包刚跑完,系统就卡死了
4.1 故障现象和盲区
有一场品牌年度会员日活动,人群圈选任务在上午10点跑完,运营点了“开始触达”。我们当时的设计是:触达请求先进Kafka,消费者异步调用AI模型打分后再发Push。理论上队列可以削峰,模型服务会自动扩容。
但实际运行时,故障在10分钟内集中爆发:
- AI模型服务扩容到40个副本后,耗时不降反升
- Kafka消费者持续重试,消息积压指数上涨
- 频控Redis Cluster出现大量超时
- 最终活动被紧急熔断,3小时后才恢复
这是典型的“弹性扩起来了,但系统没有跟着变宽”的案例。
4.2 排查链路:从监控曲线倒推根因
第一步看模型服务的CPU、内存、GPU利用率。诡异的是,40个实例的GPU利用率平均只有30%,但P99耗时却从80ms涨到了5秒。这说明瓶颈不在算力,在IO或锁。
第二步看下游依赖。特征服务的调用量并不算高,但平均耗时有大量长尾,进一步下钻发现是特征服务依赖的用户画像Redis出现了Hot Key。活动人群里有一批高活用户,他们分布在少数几个Redis分片上,触达请求反复读取这些用户画像,把某一两个分片的CPU打满了。
第三步回头看代码逻辑,找到真正的放大器:模型推理请求里拼接了用户最近浏览序列,实时特征服务拿不到完整序列时,会退化成扫描该用户侧写表中的全量行为数据,一次推理可能触发上百次Redis操作。画像Redis一旦出现局部热点,整个模型服务的延迟都被拖垮,消费者超时重试又进一步放大了对Redis的访问量。
4.3 修复方案:应急和根治分开做
应急阶段,我们直接对特征服务做了三件事:
第一,针对Hot Key开启本地缓存,缓存时间为2分钟。活动触达时,同一用户的打分请求在短时间内本就可能出现多次,2分钟内的次级实时数据丢失对排序效果影响可忽略。
第二,特征服务开启熔断,单用户特征查询失败直接返回离线兜底特征,不再做重试风暴。
第三,对Kafka消费者加信号量限制,把打到特征服务的并发压到一个可控水位,宁可消息消费慢一点,也不能把下游打挂。
这三步做完,系统在20分钟内恢复正常。
根治阶段,我们把画像存储从Redis Cluster换成了支持多级缓存的分片架构,用户维度画像在自研缓存层做本地+分布式两级缓存,并把特征拼接从模型推理线程中拆出来,放到独立的预处理阶段,失败不再影响打分。这个改动让特征查询量下降了80%以上,也彻底消除了“一次推理上百次Redis操作”的隐患。
4.4 这次故障给弹性架构留下的一条铁律
复盘时总结出一条铁律:扩容不是把请求分散到更多实例就完事了,必须确认下游每个依赖都具备和上游同等的扩展能力。 如果上游扩容了3倍,下游数据库连接数、Redis分片、第三方渠道配额却还是原来的量级,那么扩容只会更快地打垮下游。
后来我们为每一条主链路都建立了“容量关系图”,明确上游每扩容一个实例,会给下游带来多大的额外压力。做弹性架构,视野一定要越过自己的服务边界,看到整条链路上的每一个环节。
5. 落地过程中的避坑清单和成本控制心得
5.1 用户容易踩的七个坑
| 坑 | 表现 | 规避方案 |
|---|---|---|
| 只按CPU做HPA | 流量先打到服务上才触发扩容,高峰期总慢半拍 | 结合消息积压、QPS、P99延迟多指标 |
| 推理服务无预热 | 新实例P99延迟几千毫秒,拖垮负载均衡 | 启动后主动发送暖机流量,再置为就绪 |
| 模型文件从远端拉取 | 扩容时实例长时间处于ContainerCreating | 节点本地缓存模型文件 |
| 大量Pod同时启动 | 镜像拉取打满带宽,启动时间成倍增加 | 提前在节点池预置好镜像和模型 |
| 消费者无限重试 | 一条坏消息把整个消费者线程卡死 | 重试次数上限+死信队列 |
| 缩容太快 | 积压刚降下来就缩容,下一秒又来峰值 | 合理设置cooldownPeriod,建议120秒以上 |
| 忽略下游配额 | 自己扩容很快,第三方触达通道却限流了 | 触达层按渠道配额做流量整形 |
5.2 冷启动问题的实战解法
不管怎么优化,冷启动在营销AI平台里都很难完全消除。你唯一能做的是让冷启动不发生在流量尖峰的那一刻。我们有一个很土但很有效的做法:每次大促前都有“容量预演”。
运营提前确认活动开始时间后,系统会在活动前30分钟,按照预估流量的50%预先扩容到目标副本数,让所有模型服务都处于已完成预热的状态。活动开始后,HPA只负责在预演容量和实际流量之间微调。这种“预扩容+实时弹性”的组合,比纯粹依赖自动伸缩要稳得多。
5.3 弹性成本控制:钱要花在刀刃上
弹性可扩展架构如果不考虑成本,很容易设计成“无脑扩容,大促后忘记缩容”。我们有几个比较有效的成本控制手段:
按活动维度设置资源预算。每个营销活动在创建时绑定一个资源组,限制最大Pod数量和使用时长。活动结束后由自动化任务强制回收资源,避免活动流量过去后实例还在空转。
区分稳定流量和潮汐流量。模型服务中,日常决策类模型使用Deployment固定副本;大促相关模型使用支持弹性伸缩的Workload。两类模型的资源配额分开,方便财务核算也方便运维。
错峰缩容。Kafka积压消费结束后不要立刻把副本缩到最小,因为渠道触达还有回调流量。我们把缩容策略设置为阶梯式:先缩一半,观察5分钟,稳定后再缩一半。
最后分享一个让我印象深刻的经验:弹性架构里最难的不是“扩”,而是“缩”。很多系统在流量高峰勉强扛住了,却在流量回落后因为缩容不及时、依赖连接池未释放、定时任务重复调度等原因出现次生故障。我们在所有支持自动缩容的服务上,都配置了至少两分钟的冷却期,并强制要求关键服务保留最小副本数。宁可让少量资源空转,也不要在流量抖动时频繁伸缩导致系统震荡。
做了这么久智能营销AI平台,我的体会是:弹性可扩展不是一个能在架构设计文档里一次性画完的东西,它更像一个需要持续演练、持续修正的系统工程。每次大促结束后,我们都会拿到新的监控数据,找到新的容量瓶颈,再对扩容策略做一轮微调。直到今天,我依然不敢说这套架构已经“完全弹性”了——但至少,它已经不会再因为一次活动流量脉冲,就让值班工程师半夜爬起来手动加机器了。
