想象你正坐在面试官对面,对方递来一张纸:“我们是一家风控公司,每天高峰有上万笔交易需要实时风险评分,每笔必须在 100 毫秒内给出结果。请说说,你会怎么设计这套推理服务?” 别慌。把推理服务想成一家“风控餐厅的后厨”:客户端是顾客,模型是厨师,GPU 和 CPU 是灶台,网络是传菜口。你的任务,就是让这家后厨在爆单夜不翻车、上菜快、口味稳定、还能不断试新菜。这篇文章的八个部分,就是你面试要闯过的八道题——从画后厨布局图,到跟踪一张订单的旅程,再到一锅多炒、烧热冷灶、高峰加人、拉保险丝、新菜试吃,最后把整间后厨打磨成行业标杆。

岗位关联

学透这套架构,你就掌握了 AI 系统的“交付最后一公里”:模型再准,也要靠合理的推理服务设计,以毫秒级、高可用、低成本的方式把效果送到生产线上。这份能力是算法工程师、MLOps 工程师、推理平台工程师面试的核心考点。

目录
  1. 可视化演示
  2. 体系总览:先画一张后厨布局图
  3. 请求旅程:一张订单的后厨之旅
  4. 批量炒制:一锅多炒的数学
  5. 冷启动治理:第一道菜为什么慢
  6. 弹性伸缩:饭点到了不够用怎么办
  7. 稳定性保障:防火于未燃的保险丝
  8. 灰度发布:新菜品怎么端上桌
  9. 全局优化:把后厨打磨成标杆
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

以分层架构叠加展示风控推理服务的接入、路由、推理、缓存与存储层,步进式揭示各层职责与性能优化要点,帮助理解推理服务架构设计。

本节配有交互动效(进入视口后自动加载,约 3KB)。

分层架构 接入 推理 缓存 5层 推理服务

体系总览:先画一张后厨布局图

面试官的第一个追问往往是:“如果让你接手这家后厨,你第一件事做什么?” 有经验的候选人不会急着冲进厨房炒菜,而是先要一张布局图:水槽在哪、灶台有几口、传菜口离出餐口多远、洗碗间会不会堵住动线。没有布局图,所有优化都是无源之水。推理服务架构设计的第一步,同样是画出整条流水线的分层图。

推理服务从来不是“一个模型加一台服务器”那么简单。标准的风控推理链路是分层的:接入层负责接收请求、解析报文、鉴权与限流;调度层负责把请求排队、合批、做背压控制;计算层真正跑模型、输出分数;存储层缓存特征和结果,为链路提速。四层各司其职、层间解耦,才能让每一层独立扩缩——就像后厨里,传菜口忙不过来时只需要加传菜员,不必给每个灶台都配一个人。

分层之后,第一件要量化的事就是可用性目标。风控服务通常要求四个九甚至更高,因为每一笔被漏过的欺诈交易都可能造成真金白银的损失。把可用性翻译成公式,面试官会看到你的工程直觉:

画完布局图、定下可用性目标,后面的七道题才有了舞台。面试官此时会点点头,抛出第二个问题:“那这张订单进了后厨之后,到底经历了什么?”

可用性=1−N失败N总\displaystyle \text{可用性} = 1 - \frac{N_{\text{失败}}}{N_{\text{总}}}
通俗地说:可用性就是“客人吃上饭的比例”。1000 个订单里有 1 个因为超时、报错或被熔断拒绝而没吃上饭,可用性就是 99.9%。
  1. $N_{\text{总}}$进入后厨的订单总数,对应推理服务一段时间内收到的全部请求量,它是分母。
    N总N_{\text{总}}
  2. $N_{\text{失败}}$没能在超时时间内端上桌的菜,对应超时、报错、被熔断快速拒绝的请求,它是分子。
    N失败N_{\text{失败}}
  3. $1$ 减失败占比把失败比例从总盘子里扣掉,剩下的就是顾客成功拿到菜的占比,也就是系统的可用性。
    1−N失败N总1 - \frac{N_{\text{失败}}}{N_{\text{总}}}

请求旅程:一张订单的后厨之旅

你在外卖 App 上点了一份宫保鸡丁,从下单到骑手取餐,订单要经历:商家接单、贴单排队、厨师炒制、打包出餐。推理服务里的一次请求,走的是完全相同的四段旅程——接入校验、排队缓冲、推理计算、结果返回。面试官想听的,就是你能不能把这四段旅程讲清楚,并且知道时间分别花在了哪里。

接入校验就像前台核对订单:报文解析、接口鉴权、参数校验、限流拦截,这些都在抢时间。排队缓冲是等餐架:请求到达后不可能立刻被处理,先放进队列,由调度器统一安排。推理计算是灶台:模型在这里真正跑一次前向,输出风险分数。结果返回则是传菜口:序列化、压缩、网络传输,把分数送回调用方。整段旅程的端到端延迟,就是这四段时间之和:

优化延迟的正确姿势,是先拿监控数据拆解这四段,找出最大的那一块再动手。就像后厨发现客人抱怨上菜慢,结果调监控发现菜炒好后在出餐口积压了半分钟——这时候该加的不是厨师,而是传菜员。延迟优化的顺序,永远先找大头,再抠细节。

L=Q排队+B合批+R推理+T传输\displaystyle L = Q_{\text{排队}} + B_{\text{合批}} + R_{\text{推理}} + T_{\text{传输}}
面试官最常听到的答案是把延迟全算在“模型推理”头上。但真实情况里,队列阻塞和合批等待往往才是 100 毫秒预算里的隐形杀手——炒菜只用了 30 毫秒,菜却在等餐架上躺了 60 毫秒。
  1. $Q_{\text{排队}}$请求在队列里等待被调度的时间,取决于队列深度和处理速度,是延迟的大头隐患。
    Q排队Q_{\text{排队}}
  2. $B_{\text{合批}}$为了攒够一批而故意等待的时间,批处理策略会直接影响这一项,是延迟与吞吐的跷跷板。
    B合批B_{\text{合批}}
  3. $R_{\text{推理}}$模型真正计算的时间,通常由模型结构、batch 大小和硬件算力决定。
    R推理R_{\text{推理}}
  4. $T_{\text{传输}}$请求与响应的网络传输和序列化开销,在大模型场景下可能因为输出 token 多而显著放大。
    T传输T_{\text{传输}}

批量炒制:一锅多炒的数学

为什么餐厅高峰期的厨师能同时处理几十个订单?因为一份宫保鸡丁是炒,四份宫保鸡丁也是炒——火候共享、油温共享、翻勺的动作共享。推理服务里的“一锅多炒”叫动态批处理(Dynamic Batching):调度器把多个请求攒成一锅,一起喂给 GPU 做矩阵运算。GPU 是典型的并行吃货,给它一锅数据它能同时算,给它一个数据反而浪费算力。

动态批处理的核心问题是“什么时候开炒”。开太早,锅里只有一两个菜,浪费了并行能力;开太晚,前面的客人等到花儿都谢了。成熟的推理框架(如 NVIDIA Triton)采用两个条件:数量条件——凑满 batch_size 个请求就开锅;时间条件——即使没凑满,到了 max_wait 等待时限也必须开锅。前者保吞吐,后者保延迟。凑好的 batch 还要做输入对齐,把不同长度的样本 padding 到同一长度,否则 GPU 无法并行计算。批处理的收益可以用一个简洁的公式描述:

批大小不是越大越好。锅太大,第一份菜要在锅里等最后一份菜入锅,P99 延迟直接击穿红线。风控场景通常把 batch_size 设在 4 到 32 之间,配合 30 到 50 毫秒的 max_wait 作为守时兜底。大语言模型服务更进一步,用 Continuous Batching(连续批处理)做到“边吃边上菜”:一个请求的生成长度不同,不再等整批结束,而是每步都动态调整锅里的菜。面试时能讲出这层演进,说明你真的下过后厨。

T=B×Nbatch\displaystyle T = B \times N_{\text{batch}}
吞吐量等于每批大小乘以每秒能炒的批次数。把批大小从 1 提到 4,理论上吞吐涨 4 倍,但代价是每个请求的合批等待时间变长。
  1. $B$批大小,也就是一锅炒几份,GPU 并行度越高,单份均摊的算力成本越低。
    BB
  2. $N_{\text{batch}}$每秒能开多少锅,受限于 GPU 计算速度和每锅的耗时。
    NbatchN_{\text{batch}}
  3. 两者相乘总吞吐量等于每锅的菜数乘以每秒锅数,批大小翻倍并不能让吞吐线性翻倍,因为锅数会下降。
    T=B×NbatchT = B \times N_{\text{batch}}

冷启动治理:第一道菜为什么慢

新店开张的第一天,客人坐下点菜,后厨却迟迟不出餐。原因很简单:灶台是凉的。烧热一口锅需要好几分钟,第一道菜永远比之后的菜慢。推理服务里也有同样的“冷锅”问题,叫冷启动(Cold Start):一个新启动的模型实例,第一次处理请求时要把模型权重从磁盘读进内存、创建 CUDA 上下文、分配显存、编译算子、填充特征缓存。这一套热身动作可能耗时数秒到数十秒,而后续请求只需要几十毫秒。

冷启动治理的第一板斧是预热机制(Warmup):服务启动后主动喂一批探针请求,让模型把该加载的都加载好,再对外宣告“可以接客”。第二板斧是资源预占:提前分配好显存池和线程池,避免请求到来时临时申请。第三板斧是维护常驻热备副本(Warm Pool):像火锅店永远留着一锅烧开的汤底,扩容时直接把热备实例拉进服务,而不是现场烧锅。冷启动对首请求延迟的贡献,可以拆解为:

冷启动治理必须和弹性伸缩联动。如果你的扩容策略是“流量涨了才加机器”,那新加的机器就是一口冷锅,第一批打过去的请求全都会超时。面试官在这里会追问:“那你怎么保证扩容后不用现场烧锅?” 答案就是热备池——平时多养几口烧开的锅,随取随用。好的架构,让第一道菜和第 N 道菜一样快。

T首延迟=T加载+T预热+T推理\displaystyle T_{\text{首延迟}} = T_{\text{加载}} + T_{\text{预热}} + T_{\text{推理}}
敲黑板:首请求延迟不是单纯的推理时间,而是加载权重、预热环境、真正推理三段相加。加载和预热是“一次性成本”,可以被预热机制提前支付掉。
  1. $T_{\text{加载}}$把模型权重从磁盘加载到内存/显存的时间,大模型动辄几百 GB,这一步可能是最贵的。
    T加载T_{\text{加载}}
  2. $T_{\text{预热}}$CUDA 上下文创建、算子编译、显存分配等一次性初始化开销,预热脚本就是提前把这部分付掉。
    T预热T_{\text{预热}}
  3. $T_{\text{推理}}$真正的前向计算时间,也是热启动后每个请求都要付的固定成本。
    T推理T_{\text{推理}}

弹性伸缩:饭点到了不够用怎么办

七夕那天晚上 7 点,订单量是平日的十倍。后厨如果只靠固定人数硬撑,要么排队排到门外,要么厨师累到崩溃。成熟的餐厅会提前一小时加聘临时厨师、多开几口灶;过了 9 点订单回落,再让临时工下班。这就是弹性伸缩。推理服务的流量同样有昼夜节律和活动脉冲:白天比深夜高、大促比平日高、新功能上线可能带来流量尖峰。系统必须能自动加实例、也能自动减实例。

弹性的第一步是选对观测指标。最常见的错误是死盯 CPU 利用率——但推理服务的瓶颈往往在 GPU 利用率、显存占用、请求队列深度和 P99 延迟上。CPU 闲着不代表不忙,可能请求全堵在 GPU 排队里。第二步是预测式扩容:基于历史流量曲线和时间序列预测,提前 10 到 30 分钟把实例拉起,因为冷启动需要时间——就像后厨不能等订单爆了才打电话叫临时工。目标实例数可以这样估算:

缩容比扩容更考验功力。流量回落后不能立刻杀掉实例,要先让调度器停止分配新请求,等锅里存量请求全部处理完,再优雅退出——就像临时厨师不能扔下炒到一半的菜就走。多租户场景下还要做配额隔离:每个业务线设置资源上限,防止一个爆款活动把整间后厨的灶台全占走。面试官听到这里,就会知道你真正经历过“爆单夜”。

N目标=⌈Q预测cap单实例⌉\displaystyle N_{\text{目标}} = \left\lceil \frac{Q_{\text{预测}}}{\text{cap}_{\text{单实例}}} \right\rceil
需要几个厨师,等于预测的订单量除以一个厨师能扛的单量,再向上取整。这个估算能帮你快速回答“需要扩多少”。
  1. $Q_{\text{预测}}$未来一段时间预估的请求量,来自流量预测模型或历史同期曲线。
    Q预测Q_{\text{预测}}
  2. $\text{cap}_{\text{单实例}}$单个推理实例在满足延迟红线的前提下能扛住的请求量,通常用压测标定。
    cap单实例\text{cap}_{\text{单实例}}
  3. $\lceil \cdot \rceil$ 向上取整实例数必须是整数且不能四舍五入,多扩一个保证冗余,少扩一个就是排队的开始。
    ⌈Q预测cap单实例⌉\left\lceil \frac{Q_{\text{预测}}}{\text{cap}_{\text{单实例}}} \right\rceil

稳定性保障:防火于未燃的保险丝

爆单夜最可怕的不是忙,而是崩。后厨必须有一套保险丝体系:超时、重试、熔断、降级,四件套缺一不可。超时是给每道菜定一个“最长等待时间”,超过 90 秒还没上菜,直接告诉顾客这菜不上了,别傻等;重试是偶尔传菜员手滑洒了一道菜,后厨赶紧重炒一份补上;熔断是发现灶台起火了,立刻拉闸——后续新单直接拒绝,先灭火再说;降级则是招牌菜没了,换一道备选菜顶上,保证顾客不至于饿着肚子出门。

超时时间的设置有个经验法则:用正常流量的 P99 延迟乘一个安全系数。这样既不会因为偶发毛刺误杀正常请求,又能在系统真正恶化时快速止损:

重试必须配指数退避:第一次失败等 100 毫秒再试,第二次 200 毫秒,第三次 400 毫秒,再加一点随机抖动,防止所有客户端同时重试形成“重试风暴”。熔断器有三态:闭合(正常放行)、断开(快速失败)、半开(放少量流量试探是否恢复)。最忌讳的是熔断都断开了,客户端还在疯狂重试——那等于火灾现场所有消防车都不灭火,围在外面一起按喇叭。优雅退出也是稳定性的一部分:收到停机信号后,先摘除负载均衡节点、停止接新单,再等存量请求处理完。把这道保险丝系统讲清楚,面试官会认定你见过真正的线上事故。

Tout=p99×α,1<α<2\displaystyle T_{\text{out}} = p99 \times \alpha, \quad 1 < \alpha < 2
超时不是拍脑袋定的。先测出正常情况下 99% 的请求都能在多长时间内完成,再乘一个 1 到 2 之间的系数留出余量。比 P99 还小的超时会让系统天天误杀。
  1. $p99$正常状态下 99% 请求的完成时间,它是系统“正常体温”的标尺。
    p99p99
  2. $\alpha$安全系数,取值在 1 到 2 之间,太小容易误伤,太大起不到保护作用。
    α\alpha
  3. $T_{\text{out}}$最终设定的超时阈值,超过它就放弃这次请求并返回错误或降级结果。
    Tout=p99×αT_{\text{out}} = p99 \times \alpha

灰度发布:新菜品怎么端上桌

后厨研发了一道新菜,怎么上菜单?直接把老菜单全部换掉是赌徒行为——万一口味翻车,整家店的口碑都没了。成熟的餐厅会让前 10% 的客人试吃新品,观察反馈,好评率达标再逐步放量;翻车了立刻把旧菜单换回来。这就是灰度发布(灰度:从黑到白的渐进过程)。推理服务上线新模型版本,走的完全是同一套逻辑。

金丝雀发布(Canary Release)和 A/B 测试常被混为一谈,但面试官喜欢听区别:金丝雀发布关注的是“系统稳不稳定”——新版本有没有报错、延迟有没有恶化,目的是降低发布风险;A/B 测试关注的是“业务效果好不好”——新模型风控的误杀率有没有降低、召回率有没有提升,目的是做科学对照。两者的技术底座是同一个:按比例或按用户 ID 哈希,把流量分流到不同版本。灰度比例的估算很简单:

灰度发布必须配套“一键回滚”和“线上监控”。监控维度不止错误率和延迟,还要看业务指标——风控场景里就是拒绝率、误杀率、人工审核量。如果新版本把正常用户的交易误杀了,指标会瞬间报警,运维立刻把流量切回旧版本,就像后厨发现新菜被顾客投诉,马上把菜单换回去,损失只有那 10% 的试吃客人。模型版本管理要跟推理服务打通:每个版本有唯一标识,路由规则、监控面板、回滚动作都围绕版本号自动执行。

r=V新V新+V旧\displaystyle r = \frac{V_{\text{新}}}{V_{\text{新}} + V_{\text{旧}}}
灰度比例就是新版本流量占总体流量的百分比。从 10% 开始,观察稳定后逐步提到 30%、50%、100%,每一步都是一次小型实验。
  1. $V_{\text{新}}$分配给新版本的请求量,比如先把总流量的 10% 路由过去。
    V新V_{\text{新}}
  2. $V_{\text{旧}}$留在旧版本的请求量,它承担了大部分流量,是稳定性的压舱石。
    V旧V_{\text{旧}}
  3. $r$灰度比例,接近 0 表示新版本只是试吃,接近 1 表示即将全量。
    r=V新V新+V旧r = \frac{V_{\text{新}}}{V_{\text{新}} + V_{\text{旧}}}

全局优化:把后厨打磨成标杆

最后一道面试题往往没有标准答案:“你的服务已经能跑了,怎么让它跑得更好、更省?” 这考验的是全局优化意识。优秀的大厨不追求单道菜有多快,而是让整条流水线优雅地运转——从切配到出锅,没有浪费的动作。推理服务的全局优化,同样是从局部提速走向系统提效。

第一板斧是缓存复用。风控场景里大量请求的特征是相同的,同一用户短时间内多次交易、同一设备的重复请求,都可以直接命中缓存,省掉一次完整的模型推理。特征缓存、结果缓存就像后厨的“半成品备料”——提前切好的菜,来单直接下锅。第二板斧是链路瘦身:减少序列化拷贝、复用 HTTP 连接池、用 protobuf 替代 JSON、把多次网络往返合并成一次。这些都是不起眼的小优化,但积少成多,P99 延迟就是这样一点一点抠出来的。第三板斧是成本度量,把每一块钱花在哪里算清楚:

全局优化的闭环是复盘迭代:定期做容量压测找出瓶颈、做故障演练验证保险丝有效、定错误预算让团队知道“这个月还能崩几次”。后厨不是开张那天就完美的,是靠每个营业日结束后的复盘一点点变好的。面试官问出这道题,其实是想知道:你有没有把系统当作品来打磨的耐心。从“能跑”到“跑得好”,再到“跑得省”,这就是推理服务架构师的成长路线。

C每请求=C总Q×t\displaystyle C_{\text{每请求}} = \frac{C_{\text{总}}}{Q \times t}
每处理一个请求要花多少成本,等于总成本除以总请求量。GPU 很贵,如果利用率只有 20%,说明一半的钱在烧空气。
  1. $C_{\text{总}}$一段时间内的总成本,包括 GPU/CPU 资源、内存带宽、网络流量和运维人力。
    C总C_{\text{总}}
  2. $Q \times t$总请求量,等于平均 QPS 乘以运行时长。
    Q×tQ \times t
  3. $C_{\text{每请求}}$单个请求的平均成本,是衡量架构效率的核心指标,也是优化投入产出比的依据。
    C每请求=C总Q×tC_{\text{每请求}} = \frac{C_{\text{总}}}{Q \times t}

AI 应用场景

金融反欺诈实时评分引擎

阶段:模型部署与推理服务化

解决的问题:银行每笔交易必须在几十毫秒内完成欺诈评分,传统方式是每笔交易单独调用一次模型并采购大批服务器硬扛峰值,导致成本高、峰时延迟超标。采用动态批处理 + 弹性扩缩 + 队列保护后,把交易请求按批次喂给 GPU,低谷时自动缩容,高峰时提前扩容,在满足延迟红线的前提下把 GPU 利用率从 20% 提到 70% 以上。

具体案例:某支付平台在双十一大促期间,交易量峰值为平日的 12 倍。风控团队搭建了基于 Triton 的推理集群:接入层限流防抖,调度层动态合批(batch_size=16,max_wait=30ms),并维护 10 个热备实例应对扩容冷启动。最终 P99 延迟稳定在 85ms,大促期间零重大安全事故。

大语言模型在线推理服务

阶段:生成式模型的推理优化

解决的问题:LLM 推理的显存占用大、首 token 延迟敏感,简单的静态批处理会让不同长度的生成请求互相拖累。借鉴“一锅多炒”的思路演进出的 Continuous Batching 技术,在每一步生成时动态调整批成员,让快请求先出锅、慢请求继续炒,大幅提升吞吐。

具体案例:某 SaaS 公司上线代码补全助手,高峰期每秒数千请求,每个请求生成的 token 数从 10 到 500 不等。使用 vLLM 的 continuous batching 后,单卡吞吐提升了约 3 倍,同时通过 GPU 显存 PagedAttention 机制把内存碎片浪费降到最低。

电商推荐系统的多模型级联推理

阶段:多模型组合的服务编排

解决的问题:推荐系统通常要跑召回、粗排、精排、重排多个模型,串行调用会叠加延迟。按“后厨动线”思路设计级联推理:把候选池裁剪逻辑前置,粗排用轻量模型快速过滤,只有少数候选进入重模型精排,并用结果缓存承接热门商品的重复请求。每个模型独立部署、独立扩缩,像后厨的洗菜、切菜、炒菜分离。

具体案例:某内容社区的信息流推荐,通过级联架构把精排模型调用量降为原来的 8%,整体推荐链路 P99 延迟从 220ms 降到 130ms,GPU 成本下降 40%。

Python 演示

python79 行
import queueimport threadingimport timeimport random  class BatchingServer:    """模拟餐厅后厨的动态批处理推理服务:攒够一锅或到点准时开炒。"""     def __init__(self, batch_size=4, max_wait=0.05, infer_cost=0.02):        self.task_q = queue.Queue(maxsize=8)        self.batch_size = batch_size        self.max_wait = max_wait        self.infer_cost = infer_cost        self._lock = threading.Lock()        self._stop = threading.Event()        self.done_count = 0        self.reject_count = 0        self._worker = threading.Thread(target=self._run, daemon=True)        self._worker.start()     def submit(self, feature):        """顾客点菜:把单个请求放进队列,满了就拒绝。"""        try:            self.task_q.put_nowait(feature)            return True        except queue.Full:            with self._lock:                self.reject_count += 1            return False     def _batch_infer(self, batch):        """模拟一次批量推理:一锅多炒,比逐个炒更快。"""        time.sleep(self.infer_cost * (0.5 + 0.2 * len(batch)))        return [sum(b) % 2 for b in batch]     def _run(self):        """后厨主循环:收集请求,凑够一批或到点就开炒。"""        pending = []        deadline = time.time() + self.max_wait        while not self._stop.is_set():            try:                item = self.task_q.get(timeout=0.01)                pending.append(item)            except queue.Empty:                pass            now = time.time()            if pending and (len(pending) >= self.batch_size or now >= deadline):                self._batch_infer(pending)                with self._lock:                    self.done_count += len(pending)                pending = []                deadline = now + self.max_wait     def stop(self):        self._stop.set()  def load_test(server, total=20, interval=0.01):    sent = 0    start = time.time()    for i in range(total):        if server.submit(i):            sent += 1        time.sleep(interval)    time.sleep(0.3)    elapsed = time.time() - start    print("发出请求:", sent)    print("完成推理:", server.done_count)    print("被拒绝:", server.reject_count)    print("耗时: %.2f秒" % elapsed)    print("吞吐: %.1f 请求/秒" % (server.done_count / elapsed))  if __name__ == "__main__":    random.seed(42)    server = BatchingServer(batch_size=4, max_wait=0.03)    load_test(server)    server.stop()
目的:用队列 + 后台线程实现一个动态批处理推理服务,演示“凑够一锅开炒 + 到点必炒”的兜底逻辑,并对其做一次简单的压测。
  1. self.task_q = queue.Queue(maxsize=8)任务队列就是后厨的等餐架,容量有限防止无限堆积。
  2. def submit(self, feature):对外暴露的点菜口,所有请求统一从这里进入系统。
  3. except queue.Full:等餐架满了就礼貌拒绝,避免排队越长、延迟越差,保护服务不被打垮。
  4. pending.append(item)先把请求攒在手里,等待凑成一锅。
  5. len(pending) >= self.batch_size or now >= deadline合批的核心条件:要么凑满一批,要么到了等待时限必须开炒。
  6. time.sleep(self.infer_cost * (0.5 + 0.2 * len(batch)))模拟批量推理:一锅炒 4 份比单炒 1 份的总耗时更省,体现批处理收益。
  7. if __name__ == "__main__":可执行入口,跑起来就能看到吞吐、被拒数和耗时。

常见误区

误区我以为批处理就是攒的请求越多越划算,把 batch_size 从 4 调到 64,想着吞吐能翻好几倍,结果线上 P99 延迟从 80ms 飙到 400ms,客户投诉一片。

正解批大小是吞吐和延迟之间的跷跷板。锅越大,第一份菜等最后一份菜入锅的时间越长,前段请求的合批等待时间线性上涨。尤其风控场景有硬性延迟红线,batch_size 必须结合 max_wait 一起调优:先定延迟预算,再测不同批大小下的吞吐与延迟曲线,找到交叉点。就像后厨一锅最多炒 10 份,硬塞 30 份的结果是前面的菜全凉了。

为何常见:人容易只看到批处理带来的吞吐收益,忽略批等待本身也是一种延迟,因为“一锅多炒”的直觉暗示着锅越大越高效,却忘了客人等不起。

误区我在服务启动脚本里加了一段预热代码,上线后首请求延迟确实正常了,就以为冷启动问题彻底解决了。后来扩容时新实例一加入,打过去的请求又大面积超时。

正解预热解决的是“进程级冷启动”,但每次新实例拉起都是一口新冷锅。弹性伸缩新增的实例、发布新版本替换的实例、故障后重建的实例,全部需要重新预热。正确的做法是维护热备实例池:常驻一批已完成预热的副本,扩容时直接拉进负载均衡,而不是现烧现用。同时把预热纳入发布流程的检查项,实例没有预热完成就不允许接流量。

为何常见:因为“启动时预热一次”的做法能直观解决眼前的慢请求问题,让人误以为这是一次性投入,忽略了架构里实例是会不断创建和销毁的。

误区我认为反正有重试机制,超时了重发一次就行;熔断器?多此一举。结果下游一抖动,所有请求都在疯狂重试,把故障放大了十倍,最后还是熔断才救回来。

正解三者的分工完全不同:超时是“我不等了”,防止请求无限悬挂;重试是“再试一次”,适用于偶发瞬时失败,但必须配指数退避和抖动;熔断是“我不试了”,当下游连续失败达到阈值时快速拒绝,给系统喘息时间。三者是配合关系而非替代关系——正确姿势是超时兜底、重试经过退避、熔断在前两者之上做宏观保护。就像后厨:菜洒了重炒(重试),炒了三次还洒就换菜(超时),灶台起火了直接拉闸(熔断)。

为何常见:从字面上看三者都跟“失败”有关,初学者容易把它们当成同一种机制的三种叫法,忽略了它们作用于不同粒度和不同阶段。

误区我给扩缩容策略配的指标是 CPU 超过 70% 就扩容,结果 CPU 一直很闲,请求却大量超时。后来才发现,瓶颈根本不在 CPU,而在于 GPU 排队和显存不足。

正解推理服务是典型的异构计算系统,瓶颈往往在 GPU 利用率、显存占用、请求队列深度和 P99 延迟上。CPU 低可能只是因为在等 GPU 返回结果。正确指标组合是:GPU 利用率反映算力水位,队列深度反映积压程度,P99 延迟反映用户体验,三者联合触发扩容。把这些指标做成 Dashboard,比单看 CPU 靠谱得多。

为何常见:传统 Web 服务的容量规划通常以 CPU 为首要指标,这套经验被直接搬到 GPU 推理场景,忽略了计算主体的变化——后厨的瓶颈在灶台,不在收银台。

面试题

Q1. 推理服务中“请求队列”的核心作用是什么?

单选题 · easy

  • A.让所有请求立即进入模型计算
  • B.临时缓冲并削平流量尖峰← 正确答案
  • C.提高模型预测的准确率
  • D.替代人工完成数据标注

答案:B。队列像后厨的等餐架,核心价值是削峰填谷——请求来得再猛,先排队缓冲,让下游灶台按自己的节奏消化,避免流量尖峰直接冲垮模型实例。A 恰好相反,队列恰恰让请求不能立即进入模型;C 和 D 与准确率、标注无关,是明显的干扰项。

Q2. 动态批处理选择“合批时机”的主要依据是什么?

单选题 · easy

  • A.已凑够批或到等待时限← 正确答案
  • B.模型训练精度足够高
  • C.队列长度超过固定批大小
  • D.机器内存完全空闲后

答案:A。合批时机看两个条件,凑够 batch_size 就开炒,或者即使没凑够但到了 max_wait 等待时限也必须开炒,前者保吞吐、后者保延迟。C 只提了数量条件、漏了时间兜底;B 和 D 与训练精度、内存空闲没有直接关系,属于典型干扰项。

Q3. 模型冷启动导致首请求慢的根本原因是什么?

单选题 · mid

  • A.权重加载与显存分配← 正确答案
  • B.网络带宽不足导致
  • C.预处理代码写得太乱
  • D.训练数据质量太差

答案:A。冷启动的“冷”在于新实例需要加载模型权重、创建 CUDA 上下文、分配显存、编译算子,这些一次性开销可能耗时数秒。这些步骤都在模型推理之前,与网络带宽(那是传输阶段的问题)、代码风格、训练数据质量无关。记住公式:首延迟 = 加载 + 预热 + 推理。

Q4. 下面哪一项最能描述“熔断器”的工作方式?

单选题 · mid

  • A.让失败请求排入等待队列
  • B.永久停止该服务的全部流量
  • C.失败率高时快速直接拒绝← 正确答案
  • D.把失败请求转交人工处理

答案:C。熔断器像后厨的保险丝:当下游连续失败率达到阈值,就进入断开状态,新请求不再转发而是快速失败,给系统喘息机会。它不是排队等待(A),不是永久停服(B,熔断会尝试半开恢复),也不是转人工(D)。熔断的价值在于止损,避免故障被重试放大。

Q5. 与金丝雀发布相比,A/B 测试更关注什么?

单选题 · hard

  • A.新版本系统的稳定性
  • B.业务效果的公平对照← 正确答案
  • C.旧版本代码的修正
  • D.服务器成本的最低化

答案:B。两者的核心区别在目标:金丝雀发布关注“稳不稳”,通过小流量验证新版本没有报错、延迟不恶化;A/B 测试关注“好不好”,通过分流对照验证新模型在业务指标上(如误杀率、转化率)是否显著优于旧版本。稳定性和业务效果是两道不同的题,面试时能分开讲是加分项。

Q6. 推理服务扩缩容决策中最应关注的指标是?

单选题 · mid

  • A.服务器 CPU 的总空闲率
  • B.磁盘每秒读写字节数
  • C.GPU 利用率与排队深度← 正确答案
  • D.前端页面加载平均耗时

答案:C。推理服务是异构计算系统,瓶颈通常在 GPU 算力、显存和请求队列。GPU 利用率反映算力水位,队列深度反映积压程度,两者联合才能准确判断“该不该加厨师”。CPU 空闲率在 GPU 推理场景经常失真(CPU 可能在等 GPU),磁盘和前端耗时更是与推理容量无关。

Q7. 推理服务版本回滚的最佳实践是什么?

单选题 · easy

  • A.随时可回滚且需要预案← 正确答案
  • B.回滚之前先删除新版本
  • C.只在工作日凌晨做准备
  • D.回滚操作依靠人工重启

答案:A。灰度发布必须配套“一键回滚”:每次发布前准备好旧版本镜像和回滚脚本,一旦监控异常能立刻切回,把损失控制在试吃的那一小部分流量内。回滚要快、要自动化、要随时可用,而不是删新版本、挑时间或靠人工重启慢慢折腾。

Q8. 推理服务做“优雅退出”通常包含哪些动作?

多选题 · mid

  • A.停止接收新的推理请求← 正确答案
  • B.等待存量请求处理完成← 正确答案
  • C.立即清空所有排队任务
  • D.向注册中心摘除自身节点← 正确答案

答案:ABD。优雅退出像临时厨师下班前把手里的菜炒完:先向注册中心摘除节点(D),这样新流量不会被路由过来;再停止接收新请求(A);最后等存量请求处理完再真正退出(B)。C 是粗暴退出——直接清空队列等于把客人的菜倒掉,会造成大量超时和重试。

Q9. 以下哪些手段属于冷启动治理?

多选题 · hard

  • A.部署时预加载模型权重← 正确答案
  • B.预留固定显存资源池← 正确答案
  • C.提高模型训练的学习率
  • D.维护常驻热备推理副本← 正确答案

答案:ABD。冷启动治理三板斧:预加载权重(把昂贵的加载动作提前做)、资源预占(显存池、线程池提前备好)、常驻热备副本(扩容时直接拉热锅进服务)。C 是训练阶段的超参调优,与推理服务的冷启动完全无关,属于乱入的干扰项。

Q10. 某风控服务的端到端延迟 L 由哪些部分构成?

多选题 · extreme

  • A.请求排队等待的时长← 正确答案
  • B.攒批等待时机的时长← 正确答案
  • C.模型推理计算的时长← 正确答案
  • D.贷款审批的审核时长

答案:ABC。端到端延迟公式 L = Q(排队) + B(合批) + R(推理) + T(传输),前三项对应 ABC 三个选项,再加上网络传输。D 是业务审批流程,属于人工或规则引擎的线下环节,不属于推理服务的延迟构成。这题考察对延迟公式的完整理解——面试中如果能答出“合批等待也是延迟”,已经赢了大多数候选人。