想象你开了一家餐厅。起初生意冷清,后来短视频一爆,客人蜂拥而至。你发现后厨开始撑不住了:每道菜都从洗菜、切菜、下锅现做,客人催单声此起彼伏。你站在出菜口,脑子里只有一个问题——这一秒到底能端出几道菜? 这个“每秒出菜数”,就是特征服务的 QPS(Queries Per Second)。特征服务是 AI 系统背后的“大厨房”:推荐系统要查用户偏好、广告系统要查上下文、风控系统要查设备画像,每来一个请求,后厨就要快速端出一道“特征菜”。菜出不来,整个 AI 业务就瘫痪。 好消息是,优化 QPS 的思路和经营餐厅完全同构:把热销菜提前做成半成品(缓存)、一锅炒十份同样菜(请求合并)、先收菜单再慢慢做(异步化)、开门前先把锅烧热(预热)、客满就发号排队(限流熔断)、墙上挂数据看板(监控压测)。这七个动作,就是特征服务 QPS 优化的完整路线图。
对 AI 从业者而言,特征服务是推荐、搜索、广告等系统的“食材供应链”。掌握 QPS 优化,是数据工程师与 AI 架构师落地高并发系统的硬核能力,直接决定模型上线的真实响应速度与服务稳定性。
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
多级缓存架构 特征服务QPS优化的多级缓存架构与性能指标监控,展示不同优化阶段的吞吐量和延迟变化
指标与瓶颈
开餐厅的第一步,不是急着炒菜,而是先盘清自己手里有几口锅、每道菜要炒多久。你站在后厨门口,看着厨师满头大汗却端不出菜,心里只有一个疑问:这一秒到底能出几道菜?这个“每秒出菜数”就是特征服务的 QPS——每秒能处理的请求数量,它是衡量服务产能最直接的标尺。
但 QPS 不是孤立数字,它由两个更底层的指标决定:并发请求数和平均响应时间。并发请求数是同一时刻同时在处理的请求有多少个,就像后厨同时开火的灶位;平均响应时间是从客人下单到菜端上桌的耗时。两者相除,就是出菜速率——流水线上同时有 C 道菜在做,每道菜花 T 秒,每秒就能流出 C÷T 道菜。
生产环境还要盯住 P99 延迟:把一万次请求的响应时间从小到大排列,排在第 99% 位置的那个值,就是 P99。它代表“最倒霉的 1% 客人要多等多久”,专门用来揪出尾部慢请求。瓶颈定位通常从这两个指标入手:QPS 上不去,要么是并发通道太窄,要么是单次响应太慢。当你发现并发已经很高但 QPS 仍低,多半是响应时间失控了。
所以,优化 QPS 之前要像店长盘点后厨一样,先算清并发数和平均响应时间:是灶台不够(并发低),还是做菜太慢(耗时高)?有了这两个数字,后面所有优化手段才有的放矢。
- 并发请求数 C同一时刻正在系统中处理的请求总数。好比后厨同时开火的灶台数,灶台越多,同时在做的菜越多;但灶台本身也占地方,不能无限增加。
- 平均响应时间 T_avg从请求进入系统到返回结果的平均耗时。每道菜从下单、排队到下锅、上桌的平均时长,包含等待和加工两部分。
- QPS 的计算用并发数除以平均耗时,得到每秒能完成的请求数。注意并发上升的同时若耗时也上升,QPS 未必变大——就像灶台加多了人挤人,每道菜反而更慢。
缓存加速
想提高出菜速度,最聪明的办法不是把每道菜都现洗现切,而是把天天有人点的菜提前做成半成品:卤好的牛肉、切好的葱姜、调好的酱汁,客人一点单,热一热就能上桌。这些“半成品库存”,就是特征服务里的缓存。
特征服务的缓存通常分两层:本地缓存和分布式缓存。本地缓存像后厨灶台边的小料盒,取用最快,但容量小、每台灶各自独立;分布式缓存像后门的大冷库,容量大、所有灶台共享,但多一次网络往返。生产系统常常两层都上:先翻小料盒,没有再去冷库,还不行才回源现做。
缓存对平均响应时间的影响可以精确量化:命中率越高,平均耗时越接近缓存耗时。但缓存不是白拿的——特征源更新后,缓存可能变馊。所以要有更新策略:设置过期时间(TTL)、主动失效、异步刷新。为了守住命中率,热点特征要重点保活,冷门特征不必长期占用空间。漫无目的地堆缓存,就像把整个菜单都做成半成品,库存爆炸还大量过期。
厨房的半成品库越丰富,客人等菜时间越短,QPS 自然水涨船高。但半成品也会过期变质,缓存必须配上更新策略,否则端出去的就是一盘馊菜。
- 命中率 h请求在缓存中找到结果的概率。客人点卤牛肉,一下就在小料盒里翻到半成品,这就是命中。
- 缓存耗时 T_cache读一次缓存的延迟,毫秒级。相当于翻一下小料盒、热一下半成品的时间。
- 回源耗时 T_source缓存没命中时,去特征源重新计算或查询的耗时,通常是几十毫秒甚至上百毫秒。相当于从洗菜切菜开始现炒。
- 加权平均命中走快路、未命中走慢路,按比例加权得到平均耗时。半成品库越全,走快路的请求越多,平均出菜就越快。
请求合并
如果你点了宫保鸡丁,隔壁桌也点了宫保鸡丁,厨师会怎么做?当然是凑够一批一起下锅,一次颠勺出十份。这正是请求合并的思想:与其为一个请求单独跑一趟上游,不如把多个相同或相似的请求攒起来,一次性处理。
在特征服务里,大量请求会拿同一个特征(比如“用户城市”“商品分类”),或者同一批用户的不同特征。按一个时间窗口(比如 10 毫秒)收集请求,合并成批量请求向下游发送,这就是窗口聚合。更进一步,服务端可以主动把重复查询折叠成一次,让所有等待方共享同一份结果,这叫请求折叠——相当于几个桌一起点宫保鸡丁,后厨只炒一锅,各家分菜。
合并后的吞吐量可以用一个简洁式子表达:每秒能处理的请求数,约等于“每批能装几个请求 ÷ 每次上游调用耗时”。合并前 N 个请求要打 N 次上游,合并后只需 N 除以批大小那么多次。批越大,削掉的重复调用越多。但注意,合并不是无限的:窗口太长会让响应时间变长,批量太大可能撑爆上游,批大小和窗口时间必须按 P99 预算来调。
后厨的大锅饭策略看似简单,却立竿见影——菜还是那些菜,锅还是那口锅,只因为“一锅炒十份”,翻台率立刻上了一个台阶。
- 批量大小 B一次合并处理的请求数。好比一锅能炒几份宫保鸡丁,锅越大,一次出菜越多。
- 上游耗时 T_upstream一次批量上游调用的总耗时。B 个请求合并后只付一次网络往返和一次源端计算,而不是 B 次。
- 合并后吞吐每秒能完成的批量调用次数乘以每批的请求数,就得到 QPS。
异步化改造
客人多的时候,聪明的服务员会先收下菜单、说声“马上好”,让客人安心落座等,而不是让所有人堵在柜台前干瞪眼。这个“先收单、后做菜”的思路,就是异步化。
同步处理时,每个请求线程全程占着一个灶台,菜没好之前火不能停。而异步化让请求线程发出任务后立刻释放,去接待下一批客人;真正做菜由后台任务完成,结果好了再通知客人。在特征服务里,异步通常体现为非阻塞 IO、Future 回调和消息队列。一个请求往往要查多个特征源(用户画像、商品信息、环境特征):串行查询时耗时累加,并发发出、统一聚合,总耗时只取决于最慢的那个源。
异步化还要处理背压:下游处理不过来时,队列不能无限堆积,否则就像等位的客人把门口堵死。常见做法是限制队列长度、触发降级,宁可拒绝一小部分请求,也不能让系统崩溃。并发与串行的耗时差异,用数学表达就是下面的不等式。
异步化把“堵在柜台前等”变成“落座等菜”,灶位不再被空等浪费,同样的人手和灶台,翻台率却能翻好几倍。
- 每个源耗时 T_i第 i 个特征源的响应时间。比如用户画像 20ms,商品信息 30ms,环境特征 10ms。
- 并发聚合多个源同时请求,总耗时等于最慢的那个源——30ms。相当于三个灶台同时开火,最晚熟的那道菜决定开饭时间。
- 串行求和一个接一个查询,总耗时是累加——60ms。前面没查完,后面就得排队等着。
- 不等关系并发永不慢于串行,多数时候快得多。这是异步化改造最根本的数学依据。
资源与预热
餐厅每天开门前有一件必做的事:开火、热锅、烧油。如果等第一位客人点完菜才起火,第一桌客人要等上半小时。特征服务完全同理——刚启动时缓存是空的、连接是冷的,如果流量直接压上来,第一批请求会全部打到特征源上,延迟飙高、错误率上升。
因此,服务上线前要“热锅”:预先把热点特征灌入缓存、预先建立数据库和下游连接、预热线程池。这就是冷启动预热。它让服务在承接真实流量之前,已经把最耗时的准备工作做完。很多大厂在发布新版本时,会先让服务“空转”几分钟再切流量,原理就是如此。
线程池和连接池的规模则决定厨房能承受的火力。线程不是越多越好:开太多线程,CPU 忙着切换上下文反而变慢。工程上常用一个经验公式估算最优线程数:IO 占比越高,越应该多开线程去并行等待;CPU 密集型的计算,线程数接近核数就够了。连接池同理——池太小请求排队,池太大浪费资源,需要结合压测结果微调。
提前烧热的锅、备好的连接,让流量高峰到来时服务从容不迫。就像老练的餐厅开门迎客前一切就绪,客人涌入也依然行云流水。
- CPU 核数 N_cpu机器可用的 CPU 核心数,是处理能力的硬上限。好比后厨能摆下的灶台总数。
- IO 与计算比IO 等待时间与 CPU 计算时间的比值。特征服务大量时间在等网络返回,这个比值通常远大于 1。
- 线程数公式每个核至少要有一份计算线程,再按等待比例增加并发等待线程。
熔断限流
生意火爆也可能成为灾难:客人源源不断涌进来,后厨迟早崩盘——灶台烧穿、盘子摔碎、厨师累倒。聪明的餐厅会在门口放一个取号机,满员就发号排队;如果某道菜的供应商突然断货,就先暂停这道菜,而不是让客人空等。这就是限流与熔断。
限流为服务划定一个最大速率:每秒最多放 N 个请求进后端,超出的请求要么排队,要么直接返回“稍后再试”。常见算法有令牌桶和滑动窗口。熔断则是在下游持续异常时主动断开:当错误率超过阈值,后续请求不再打向下游,而是快速返回兜底结果——默认值、缓存副本、降级文案——给下游喘息恢复的时间。
令牌桶的限流能力可以这样理解:一个桶以固定速率补充令牌,请求进来必须取走一个令牌才能放行。桶里攒下的令牌允许短时间应付流量尖峰,但长期平均速率仍被补充速率锁死。这就像取号机:平时客流平稳,攒一批号可以集中放行,但总的接待能力是固定的,队伍不会无限放进来。
取号机不是赶客,而是保护厨房不超载;熔断不是没菜吃,而是把有限资源留给真正能上桌的菜。稳定,才是高 QPS 的前提。
- 补充速率 r每秒往桶里放的令牌数,决定了服务长期稳定的处理速率。
- 桶容量 b桶里最多攒多少令牌,决定了能承受多大的突发流量。好比取号机后排队的人数上限。
- 突发余量把桶里攒的令牌匀到一段时间里,就形成了突发速率。
- 最大 QPS稳定速率加突发余量,得到瞬时允许的最大请求速率。
监控压测
一家成熟的餐厅,后厨墙上一定挂着数据看板:今日翻台率、每道菜出餐用时、食材消耗速度、差评关键词。店长每天的功课就是看数据、复盘、调菜单。特征服务的 QPS 优化同样是一场需要数据支撑的持久战,没有监控的优化等于蒙眼开车。
核心监控指标包括:QPS、平均响应时间、P99、错误率、缓存命中率、线程池活跃数、队列长度、下游依赖耗时。这些指标要能按服务、接口、特征维度拆分,才能定位到底是哪台灶、哪道菜拖了后腿。压测则是把问题提前暴露的手段:在灰度环境用模拟流量逐步加压,直到指标恶化,摸出单实例的真实上限。
有了单实例上限,容量规划就变成一道算术题:目标流量除以单实例能力,再乘上冗余系数。比如业务目标 10 万 QPS,单实例实测能扛 5000,留 50% 冗余,大约需要 30 个实例。压测还能帮我们揪出“纸面达标、尾部爆炸”的隐患——平均延迟很漂亮,P99 却已经超时。
数据看板让店长心里有数,压测演习让每个岗位知道自己的极限。只有持续监控、定期压测、复盘调优,餐厅才能在客流高峰中稳稳接住每一单。
- 目标 QPS业务期望的峰值流量。好比节假日预估的客流。
- 单实例上限单台机器压测出的稳定 QPS 上限。相当于一家分店实测的接待能力。
- 冗余率为突发流量预留的系数,通常 20%~50%。多留的椅子,是为了旺季不拒客。
- 实例数计算目标除以单实例,乘上冗余系数,再向上取整得到部署规模。
AI 应用场景
电商大促推荐特征服务
阶段:在线特征存储与特征供给
解决的问题:大促流量尖峰是日常的 10 倍以上,特征服务成为推荐系统的瓶颈。传统做法是每来一个请求就实时算一遍特征,大促时直接打爆数据库。
具体案例:某电商平台大促前夕,推荐系统每秒向特征服务发起峰值 20 万 QPS 的请求。团队采用三层优化:Redis 缓存热点商品与用户特征,命中率超过 95%;设置 10ms 窗口批量合并同批特征的查询;异步并发拉取用户与商品特征。最终 P99 延迟从 120ms 降到 30ms,用 30 个实例平稳扛住峰值流量。
广告实时竞价特征服务
阶段:实时特征计算与召回
解决的问题:程序化广告的每次曝光竞价必须在 50ms 内返回,否则出价无效。特征源散落在多个团队维护的系统中,串行拉取根本来不及。
具体案例:某 DSP 平台将用户兴趣、上下文、广告主画像三类特征做成独立特征服务,本地缓存 + 分布式缓存双层兜底;特征缺失时异步回源并回填缓存;对下游画像服务设置熔断阈值,超时后返回默认画像参与竞价。优化后服务 QPS 从 3000 提升到 1.8 万,竞价超时率下降 95%。
金融风控实时特征服务
阶段:实时特征计算与决策
解决的问题:欺诈检测要求毫秒级响应且必须高可用,特征服务一旦抖动,会拖垮整条支付链路。
具体案例:某支付平台风控系统对每笔交易实时计算设备、IP、用户行为等特征。团队引入消息队列做异步特征补齐,避免同步等待拖慢主链路;用令牌桶限制对黑名单服务的调用速率,熔断时降级为静态规则。在双 11 峰值 30 万 QPS 下保持 99.99% 可用性。
Python 演示
# -*- coding: utf-8 -*-"""特征服务 QPS 优化模拟器:对比缓存与批量合并的吞吐提升"""import timeimport randomfrom collections import deque class FeatureService: """模拟一个带缓存和批量合并的特征服务""" def __init__(self, latency=0.002): self.cache = {} self.hit_count = 0 self.total_count = 0 self.latency = latency self.batch_queue = deque() def query_with_cache(self, key): """缓存优先的单个特征查询""" self.total_count += 1 if key in self.cache: self.hit_count += 1 return self.cache[key] time.sleep(self.latency) value = random.randint(1, 100000) self.cache[key] = value return value def query_batch(self, keys): """批量合并查询:未命中特征只回源一次""" result = {} miss_keys = [k for k in keys if k not in self.cache] for k in keys: self.total_count += 1 if k in self.cache: self.hit_count += 1 result[k] = self.cache[k] if miss_keys: time.sleep(self.latency) for k in miss_keys: value = random.randint(1, 100000) self.cache[k] = value result[k] = value return result def run_single(total=60): svc = FeatureService(latency=0.002) start = time.time() for i in range(total): svc.query_with_cache("user_%d" % (i % 20)) elapsed = time.time() - start hit_rate = svc.hit_count / svc.total_count print("单请求模式: QPS=%.0f 命中率=%.1f%%" % (total / elapsed, hit_rate * 100)) def run_batch(total=60, size=10): svc = FeatureService(latency=0.002) keys = ["user_%d" % (i % 20) for i in range(total)] start = time.time() for i in range(0, len(keys), size): svc.query_batch(keys[i:i + size]) elapsed = time.time() - start hit_rate = svc.hit_count / svc.total_count print("批量合并模式: QPS=%.0f 命中率=%.1f%%" % (total / elapsed, hit_rate * 100)) if __name__ == "__main__": run_single() run_batch()
self.cache = {}用字典充当半成品库:key 是特征名,value 是算好的特征值。if key in self.cache:翻小料盒:命中直接返回,不回源。time.sleep(self.latency)模拟每次回源特征计算的耗时。miss_keys = [k for k in keys if k not in self.cache]批量模式先筛出没命中的 key。if miss_keys: time.sleep(self.latency)关键优化:一批缺失特征只回源一次,而不是每个都回源。run_single()单请求模式:每个请求独立走一遍“查缓存→miss→回源”的流程。run_batch()批量模式:攒满一批再查询,miss 特征合并回源,QPS 大幅提升。
常见误区
误区“QPS 上不去?把线程池从 200 开到 2000,反正线程不占多少内存,开得越多处理得越快!”
正解线程超过最优值后,CPU 大量时间花在线程上下文切换上,性能反而下降。就像厨房太小却硬塞二十口锅,厨师转身都困难,出菜反而更慢。线程数应该根据 CPU 核数和 IO 等待比计算,并压测验证拐点。
为何常见:直觉以为“资源越多越快”,忽略了调度开销这个隐藏成本。
误区“把特征全部缓存起来,命中率做到 100%,QPS 不就飞起了吗?”
正解缓存有内存成本和一致性成本。冷门特征一天被查不了几次,缓存占用内存还容易过期失效;热点特征才是缓存的重点。全量缓存就像把整本菜单的食材都备成半成品,库房爆炸、大量过期浪费,得不偿失。
为何常见:只看到命中率提升的单边收益,忽视了内存占用和特征过期带来的双重代价。
误区“云上资源便宜,QPS 不够就水平扩容,多部署几个实例不就行了?”
正解水平扩展确实有效,但如果瓶颈在下游数据库或特征源,加再多实例只会把下游打挂。而且新增实例冷启动时缓存是空的,流量分摊后整体命中率下降,可能越扩越慢。扩容之前,先想清楚瓶颈在计算、IO 还是下游。
为何常见:分布式时代“加机器”成了条件反射,跳过了最关键的瓶颈分析。
误区“监控面板上平均耗时 10ms,服务很健康,不用优化了。”
正解平均延迟会被大量快请求拉低,掩盖尾部慢请求。P99 才是用户体验的真相——最差的 1% 客人决定餐厅口碑。平均 10ms、P99 500ms 的服务,在大促时同样会超时雪崩。优化必须同时盯住平均和 P99 两个指标。
为何常见:平均值算起来直观,人类直觉偏爱“用一个数字总结一切”,却不知它最会骗人。
误区“限流会把请求拒之门外,影响用户体验,宁可让服务扛着也不要限流。”
正解服务崩溃后是 100% 请求失败,限流后只有超出的那部分请求被拒,剩余的还能正常服务。配合排队等待、降级返回默认特征、缓存副本兜底,可以把体验损失降到最低。限流不是赶客,是保住大部分客人的用餐体验。
为何常见:把限流等同于“拒绝”,没看到它保护长期可用性的价值——服务活着,才有机会服务下一位客人。
面试题
Q1. 特征服务中的 QPS 最接近以下哪种说法?
- A.每秒能处理的特征请求数← 正确答案
- B.每次请求的网络传输速度
- C.特征服务的磁盘读写速度
- D.每秒新增的特征数量
答案:A。QPS 即每秒查询数(Queries Per Second),衡量服务每秒能处理多少请求,相当于餐厅每秒端出几道菜。网络传输速度、磁盘速度和特征新增量描述的都不是“处理请求的速率”。
Q2. 关于 P99 延迟,下列说法正确的是?
- A.99% 请求的平均耗时
- B.排序后第 99% 位置的耗时值← 正确答案
- C.最快 99 个请求的平均耗时
- D.99 秒内的总请求耗时
答案:B。P99 是把所有请求的响应时间排序后,取第 99% 分位的值。它回答的是“最倒霉的 1% 客人等了多久”,而不是平均值或最快值。就像餐厅里 99% 的菜都能在 5 分钟内上桌,剩下 1% 可能要等半小时——P99 抓的就是那半小时。
Q3. 异步化改造的核心收益是?
- A.减少单次请求的 CPU 计算量
- B.释放线程等待时间以提升并发能力← 正确答案
- C.降低特征服务的特征数量
- D.显著降低缓存库存的占用
答案:B。异步化让请求线程发出任务后立刻释放去处理其他请求,而不是阻塞等待上游返回。收益是减少线程空等,让同样的线程数服务更多请求。它不改变 CPU 计算量,也不减少特征数量。
Q4. 以下哪些措施可以直接提升特征服务的 QPS?
- A.提升本地缓存命中率← 正确答案
- B.多个请求批量合并回源← 正确答案
- C.删除所有异常日志输出
- D.把超时时间调大一倍
答案:AB。缓存命中率提升直接降低平均响应时间,批量合并减少上游调用次数,两者都提高吞吐。删除日志可能掩盖问题,调大超时反而让请求挂更久,都不直接提升 QPS。
Q5. 缓存平均耗时 2ms,特征源耗时 20ms,命中率 95% 时平均响应时间约为?
- A.2.9ms← 正确答案
- B.5.0ms
- C.19.0ms
- D.20.0ms
答案:A。平均耗时 = 命中率×缓存耗时 + 未命中率×特征源耗时 = 0.95×2 + 0.05×20 = 2.9ms。95% 的请求走快路,只有 5% 走慢路,平均耗时接近快路。这也是为什么提高命中率对降低延迟立竿见影。
Q6. 关于令牌桶限流算法,下列说法正确的是?
- A.桶容量决定突发流量上限← 正确答案
- B.令牌速率决定长期均值
- C.令牌桶只能限流不能应对突发
- D.桶满后新令牌会加倍补充
答案:A。令牌桶里桶容量 b 决定可突发消耗的令牌总数,令牌补充速率 r 决定长期平均速率。令牌桶的设计目标恰恰是“允许突发但限制均值”。选项 C、D 描述与算法原理相悖。
Q7. 请求合并窗口设置过大的副作用是?
- A.响应时间 P99 上升← 正确答案
- B.服务 QPS 持续下降
- C.缓存命中率显著下降
- D.错误率必然下降
答案:A。合并窗口越大,请求在窗口内等待攒批的时间越长,尾部请求的延迟显著增加,P99 上升。批量合并提升的是吞吐而非降延迟,窗口过大会牺牲响应时间。QPS 不一定下降,命中率和错误率与窗口大小无必然关系。
Q8. 某特征服务 QPS 低但 CPU 利用率也低,最可能的瓶颈是?
- A.下游 IO 等待或依赖瓶颈← 正确答案
- B.本地 CPU 算力不足
- C.内存溢出或 OOM 频繁
- D.日志采集线程过多
答案:A。CPU 利用率低说明计算资源空闲,QPS 却上不去,大多是线程在等下游返回——网络 IO、数据库查询、外部服务调用。就像厨师闲着没菜可炒,问题出在供应链而不是灶台。CPU 不足、OOM、日志过多都会表现为 CPU 或内存异常,而非“低 CPU + 低 QPS”。
Q9. 生产环境监控特征服务,应重点关注哪些指标?
- A.QPS 与错误率统计← 正确答案
- B.P99 延迟与线程活跃数← 正确答案
- C.缓存命中率与队列积压← 正确答案
- D.代码注释行数统计
答案:ABC。QPS、错误率、P99、线程数、命中率、队列长度都是衡量服务健康度的核心指标,每一项都对应一个可能的瓶颈点。代码注释行数与运行性能完全无关,监控它是没有意义的。
Q10. 某特征服务目标 QPS 为 10 万,单实例压测上限 5000,要求 50% 冗余,最少需部署多少实例?
- A.20 个实例
- B.30 个实例← 正确答案
- C.40 个实例
- D.50 个实例
答案:B。先用目标 QPS 除以单实例能力:100000 ÷ 5000 = 20;再乘以冗余系数 1.5,得到 30,向上取整正好是 30。冗余是为了应对流量尖峰和单实例故障,20 个是理论下限,没有任何余量。