AI诊断系统全链路压测:从P99 71秒到17秒的优化实战
发布时间:2026/9/14 7:12:50 作者:尧图编辑部 阅读量:1,286

1. 30s阈值从哪来AI诊断场景对响应时间的真实要求在做全链路压测之前我们内部先吵了一轮AI诊断的接口响应时间到底卡在多少秒才算合格这个问题的答案不能拍脑袋。以我们做的病灶识别诊断系统为例医生在阅片工作流中调用AI辅助诊断时整个诊断流程是串行的——医生看到影像后发起AI分析请求系统返回可疑病灶的标注和量化指标医生再结合这些信息写报告。如果AI接口长时间不返回医生的阅片节奏就被打断了而这个打断带来的代价不只是等待还有注意力的重新聚焦成本。有过阅片经验的人应该能理解盯着屏幕等一个没动静的转圈比看10张片子还让人焦虑。我们当时定下的目标是P99响应时间在30s以内。为什么不是更快因为AI诊断链路里面有一个绕不开的环节深度学习模型推理本身就要消耗时间。以我们线上跑的病灶分割模型为例单张影像输入到模型推理完成在A10 GPU上裸推理时间大约是6到9秒这还是优化过的。后面还有影像预处理、后处理、结构化报告生成、数据库落盘等一系列操作整条链路凑齐20多秒很正常。如果把P99卡到10秒以内意味着大部分场景下必须上更贵的推理卡或者牺牲模型精度从投入产出比来看并不划算。所以30s是一个在业务体验、模型效果和硬件成本三者之间反复权衡后的平衡点。定这个阈值还有一个考量医疗场景下的AI辅助诊断有明确的容错需求系统要留出重试和兜底的时间余量。如果接口在30s内没有完成客户端会自动降级为纯人工阅片流程同时把这个请求标记为超时病例进入异步补偿队列。换句话说30s也是整个容错设计的时间锚点所有上下游超时配置都围绕这个值来设置。真正到了压测阶段问题才暴露出来单链路、单并发、小流量样本下接口P95确实在25s左右看起来离30s有余量。但一旦上了全链路压测模拟几十个医生同时上传影像、同时调用AI分析接口响应时间迅速恶化P99直接飙到70多秒大量请求超时失败。更麻烦的是超时引发了客户端自动重试重试流量又压向后端形成了雪崩效应。当时看着监控面板上的红色告警我和负责推理服务的同事对视一眼都知道这已经不是改改配置能解决的事了。这一轮压测让我意识到一个很重要的点全链路压测暴露的从来不是某一个节点的性能瓶颈而是整条链路的协同能力。单接口测试通过没有任何意义生产环境不可能只有一个用户在调用。2. 压测链路梳理与环境准备把每一段耗时都变成指标全链路压测最关键的前期工作是把系统里所有涉及的节点画出来明确每一跳的耗时构成。我们在压测前先做了链路梳理核心路径大概是这样客户端医生工作站发起影像分析请求API网关鉴权、限流、路由分发诊断服务接收请求做影像数据校验从对象存储拉取影像文件预处理模块对影像做标准化处理推理服务调用深度学习模型后处理模块生成可疑病灶标注结构化报告服务生成诊断结论结果写入数据库和缓存返回最终结果给客户端按这个路径我们把整条链路拆成了七个可观测的环节每个环节设定独立的监控埋点。压测环境的搭建有几个容易踩的坑。第一个坑是压测环境的数据和线上差异太大。最开始我们图省事直接用测试库里的模拟影像数据做压测结果压出来的数据非常漂亮P95只有18秒。后来一查才发现测试数据里的影像尺寸普遍是512×512而线上真实影像很多是1024×1024甚至更高分辨率图像尺寸直接决定了预处理耗时和模型推理计算量这个差异直接让压测结果失真。后来我们专门从线上脱敏了一批真实影像数据按照不同尺寸、不同部位、不同病变类型做了分层抽样才让压测结果有了代表性。第二个坑是压测工具的选择。我们对比了JMeter、wrk和一款开源的分布式压测平台最终选了Go语言实现的分布式压测方案。原因有三一是AI诊断场景的压测请求需要携带真实的影像数据不是简单的GET请求需要能自定义复杂请求体二是压测要从多台压力机同时发起流量单机压测在并发数上来后压力机本身会成为瓶颈三是需要实时监控每个阶段的耗时数据分布式压测平台的指标聚合能力更符合需求。压测脚本的设计上我强烈建议不要只压一个整链路接口。我们的做法是把压测拆成三层全链路压测完整走一遍从网关到返回结果的整条链路验证端到端体验链路单点压测对推理服务单独加压摸清模型服务的吞吐上限混合场景压测按真实业务比例混合调用不同接口模拟实际流量模型而且压测的流量模型要贴近真实的使用习惯不能是恒定并发。医生的阅片请求有显著的时段特征上午和下午各有一次高峰高峰期并发可能是平峰期的5倍以上。我们按照真实的流量曲线设置了压测模型高低峰交替这样压出来的数据才有参考价值。监控体系的搭建同样关键。除了常规的CPU、内存、GPU利用率和IO等待我们重点埋了四个指标排队等待时长请求进入处理队列到开始处理的时间间隔、模型推理耗时分布、上下游调用耗时占比、超时与重试次数。后来排查问题时发现真正揭示根因的不是那些显眼的指标而是容易被忽略的排队等待时长。环境准备还有一个细节数据库和缓存的初始化状态。压测前我们把数据库中的历史诊断记录归档到冷存储确保热库大小和线上接近避免因为数据量差异导致SQL执行计划不同、索引失效等问题。这个细节我们一开始忽略了导致一次压测中数据库响应时间异常偏高排查了半天才发现是测试库涨了太多垃圾数据。3. 模型超时的根因排查从表现到本质的完整链路这一部分我想尽量完整地还原排查过程因为很多人遇到类似问题时容易一上来就怀疑模型本身但真实情况往往比想象中复杂。我们这轮压测中模型超时问题的排查链路大概分四步走。3.1 现象确认问题出在哪个环节压测开始后监控面板上的数据非常直观P99响应时间从25s左右一路上涨到70s超时率在峰值并发时期达到23%。但这个数据只告诉了我们哪里出问题了——整个接口就是慢。具体慢在哪一段当时还没有答案。我们先把各个阶段的耗时拉出来做了个占比分析发现一个反常的现象模型推理阶段的耗时并没有显著增加单次推理的平均时长只从7.2s涨到了8.1s涨幅很小。反而是诊断服务的整个调用过程中排队等待时长从原来的不到500ms暴涨到平均22秒。这个数据点非常关键它说明问题可能不在推理本身而是在任务调度和排队策略上。3.2 排队现象背后的资源瓶颈分析那为什么排队会这么严重我们开始查推理服务的资源使用情况。GPU利用率监控显示压测期间四张A10显卡的利用率始终在95%以上显存占用也接近满载看起来是GPU资源被完全打满了。但这里有个反直觉的发现虽然GPU满载但模型的实际算力利用率并不高。我们用NVIDIA的深度学习性能分析工具做了采集发现GPU的SM流式多处理器利用率只有60%左右而显存带宽利用率达到90%以上。翻译成人话就是GPU的计算核心有一半多的时间在空转等待数据真正的瓶颈在显存带宽和数据的搬运速度。我们的预处理服务每次把影像从CPU内存拷贝到GPU显存时都是整张图片一次性拷贝大尺寸影像在这个环节消耗的时间特别长。这个发现解释了为什么单请求压测时表现正常高并发下就崩了单请求时数据搬运的时间被推理时间掩盖了而高并发下多个请求同时做数据拷贝显存带宽被争抢每个请求都要排队等待数据到位推理卡自然就被拖垮了。3.3 推理服务的并发模型缺陷继续下钻排查我们又发现了第二层问题推理服务的并发模型在高并发场景下存在严重缺陷。我们的推理服务是基于Python的异步框架写的理论上能同时处理大量并发请求。但实际运行中模型推理是同步阻塞操作多个并发请求到达后并没有真正并行执行推理而是被同一个推理进程串行处理。我们按照默认配置启动了一个推理实例进程内部维护了一个任务队列所有请求先进队列再由GPU逐批处理。由于PyTorch默认的CUDA上下文是线程独占的哪怕开了多线程GPU上的kernel还是会串行执行。你可以把它理解成一家只有一个窗口的银行客户再多窗口只能一个个办理业务。我们当时的架构就是这种状态GPU这个唯一的窗口被排队请求堵死了。同时还有一个被忽略的问题推理实例的进程数配置成了2两个进程各自申请了一份显存空间。在显存本来就吃紧的情况下每个进程能用的显存被进一步压缩导致PyTorch在推理时频繁触发显存碎片整理进一步拖慢了速度。这种问题单请求时几乎感知不到但并发一起来就变成了灾难。3.4 确定优化方向的优先级把问题链路梳理清楚后我们得出一个结论模型本身没有问题问题出在怎么调度模型和怎么喂数据给模型这两件事上。于是优化方向就变得非常清晰了改造推理服务将原来的单窗口串行模式改成动态批量调度的并发模式优化影像预处理和GPU数据拷贝流程减少显存带宽的争抢解决显存碎片化问题让显存分配更合理这三个方向对应解决的是排队时间、数据搬运时间和显存利用效率的问题。确定优先级的逻辑很简单哪一项对P99的贡献最大就先做哪一项。从压测数据看排队时间是最大的头号问题所以动态批量调度是优先级最高的改造项。4. 关键优化落地动态批处理、显存治理与链路拓扑调整有了前面的排查结论优化方案就顺理成章了。但方案归方案落地过程中还是踩了不少坑。下面我把每一项优化的具体做法和原理拆开讲方便你直接参考。4.1 动态Batch调度用时间窗口换吞吐量动态批处理Dynamic Batching是推理服务性能优化里经典且有效的方案。它的核心思想是不急着立刻处理每个请求而是把短暂时间窗口内到达的多个请求凑成一批一次喂给GPU处理充分利用GPU的并行计算能力。打个比方以前是来一个顾客就单独开一桌厨房一次只做一道菜动态批处理后变成先把几个客人的菜单收齐凑够一桌再一起下锅虽然每个客人的等待时间稍微变长了但整体翻台率大幅提升。具体实现上我们在推理服务中引入了一个带超时机制的请求收集器。请求到达后先进入收集器收集器按照2秒的时间窗口或者累计16个请求的批量阈值来触发一次推理。时间窗口设得太长会增加单请求的排队延迟设得太短又凑不够批次。2秒和16这两个值是我们根据实际压测数据反复调出来的适用于我们的场景你可以按自己的业务吞吐量来调整。动态批处理带来的提升非常明显。改造后GPU的SM利用率从60%提升到了85%左右单张显卡每秒能处理的影像数量提升了接近2倍。因为GPU本来就是为大规模并行计算设计的一批处理16张影像的耗时只比处理1张影像多了不到40%但吞吐量翻了16倍。这中间的性价比空间就是动态批处理的价值所在。还需要配套做一件事给请求设置最大可等待时间。如果某批请求迟迟凑不满批量阈值收集器到了2秒就必须带着现有请求先去推理不能无限等下去。这个超时时间的设置非常考验功力——设大了低峰期的请求响应时间会变长设小了高峰期的批次总是凑不满吞吐量上不去。我们最终把这两个值做成了动态配置项可以根据实时流量自动调整高峰期自动收紧等待时间低峰期自动放宽。4.2 显存治理碎片整理与显存预分配显存碎片化问题在长跑服务上很常见。PyTorch的默认显存分配策略是按需分配、用完缓存、不够再扩这种策略在长时间运行后会因为不同尺寸张量的反复创建和释放导致显存碎片化。碎片化的直接后果是显存明明有大量空闲但分配不出一块连续的大显存于是CUDA上下文开始频繁做内存整理拖慢执行速度。我们做了两个调整开启PyTorch的显存预分配机制通过设置环境变量让CUDA在服务启动时就按预估峰值申请好显存避免运行过程中反复分配和释放。统一影像张量的尺寸在预处理阶段把所有输入影像统一缩放到固定的高宽比然后pad到统一的尺寸再送进模型。这样做的好处是显存中所有张量的尺寸都是一致的不会产生各种大小的碎片。第一个调整很简单改一行环境变量配置就行。第二个调整需要改预处理逻辑但收益很高。我们验证过统一尺寸后同样的并发场景下显存碎片率降低了约70%GPU的推理性能稳定性显著提升。注意统一尺寸不是简单resize如果影像宽高比差距太大直接拉伸会导致病灶形态变形影响诊断准确率。我们的做法是等比缩放后对剩余区域做零值填充确保送入模型的影像在几何特征上没有失真。4.3 从串行到流水线拆分预处理与后处理优化完模型推理侧之后我们又回头审视整条链路的拓扑结构。发现原来的架构里预处理、推理、后处理是串行执行的——一张影像必须走完拉取→预处理→推理→后处理→报告生成全流程后才开始处理下一个请求。这种串行模式在单个请求上体验没问题但并发上来后前面的预处理慢会阻塞后面的推理。我们把预处理和后处理从推理主流程中拆了出去。预处理服务单独部署在CPU集群上通过消息队列把处理好的张量数据发给推理服务后处理服务同样独立部署异步消费推理结果并生成报告。这个改造的本质是把一条长链路变成了三条流水线并行运行。你可以把它想象成工厂流水线以前的工人一个人从头做到尾现在分成三个工位每个工位只做一件事整条产线的产能自然上去了。有个细节要注意拆分之后消息队列成了新的单点。我们对队列做了主从部署和消息积压告警防止队列堆积导致链路卡死。同时原来同步等待推理结果的地方也要改成异步回调机制让调用方不必死等结果返回。4.4 引入缓存策略与超时熔断双保险优化推理性能能解决大部分问题但系统设计不能只考虑正常情况下跑得快还得考虑极端情况下不崩溃。所以我们同时上了两层保护机制。缓存策略比较简单。AI诊断有一个特点是同一个病例在一天内可能被医生反复查看和调用特别是疑难病例会经过多名医生会诊。我们在诊断服务层加了一个基于影像指纹的缓存同一个影像如果已经生成过诊断结果再次被调用时直接返回缓存数据不再重新走推理链路。压测数据显示加入缓存后诊断服务的整体请求量中大约有12%到15%走了缓存命中这部分请求的响应时间从20多秒直接降到了1秒以内。超时熔断做的是兜底。我们在诊断服务和推理服务之间、推理服务和模型之间都设置了分级超时控制。如果推理服务在下游等待超过10秒诊断服务会主动切断等待把请求转入异步任务队列同时在结果里返回一个初步结论待确认的状态给客户端。这样至少保证医生端的请求不会无限期挂着。这套双保险机制在这次压测中发挥了关键作用即使在高并发峰值期也没有再出现因为超时而引发的雪崩效应。虽然部分请求走了异步降级但P99响应时间被稳定控制在目标值内。5. 重新压测的数据对比与稳定性验证所有优化落地后我们用完全相同的压测脚本和流量模型重新跑了一轮全链路压测。先看整体数据的提升幅度指标优化前优化后提升幅度P95响应时间28s8s71.4%P99响应时间71s17s76.1%最大响应时间120s28s76.7%超时率30s23%0.8%96.5%吞吐量请求/分钟38124226%GPU SM利用率61%87%42.6%P99从71秒降到了17秒这个降幅说实话超出了我们预期。但压测数据好看只是第一步真正的考验是稳定性。我额外做了两轮验证。第一轮是长时间持续压测让系统在模拟高负载下连续运行4个小时观察各项指标有没有缓慢劣化的趋势。这轮验证特别重要因为有些性能问题在短时间压测中看不出来比如显存泄漏、内存碎片累积、消息队列堆积等都需要足够长的时间才能暴露。4小时持续压测跑下来P99响应时间有小幅波动但没有明显上升趋势确认稳定性没有问题。第二轮是突发流量冲击测试。我们模拟了一个极端场景压测启动后前2分钟静默无流量然后瞬间把并发数拉到峰值的两倍观察系统如何应对突发流量。这轮测试暴露了一个新问题在流量突增的瞬间消息队列出现了一次短暂的消息积压导致部分请求排队时间超过了预定的10秒熔断阈值触发了降级策略。虽然最终没有影响整体成功率但暴露出了消息队列的连接池配置不够大的问题。调整连接池参数后重新测试突发流量场景下的排队时间稳定在3秒以内。另外还做了一个很有参考价值的数据统计把压测结果按影像尺寸做了分层对比。大尺寸影像1024及以上分辨率的P99响应时间是23秒中小尺寸影像的P99是12秒。这个差异符合预期但也提示我们可以针对大尺寸影像做单独的队列优先级策略让紧急的小影像请求优先跑完大影像走异步处理。这个优化目前已经在规划了。稳定性的另一个关键指标是错误率。优化前压测期间出现了不少异常主要是三类数据库连接池耗尽、消息队列超时、模型推理显存溢出。优化后这三类异常的数量都降到了接近零的水平。我还建议在压测环境里加入故障注入测试这是验证系统容错能力很有效的手段。方法是在压测进行中主动杀掉一个推理服务实例或者给数据库连接池加延迟观察系统能不能自动降级并恢复。我们的系统在杀掉一个推理实例后剩余实例自动接管流量整体P99从17s短暂升高到21s后又恢复到正常水平整个恢复过程大约花了40秒。这个数据让我们对系统的生产鲁棒性更有信心了。6. 全链路压测常态化从一锤子买卖变成持续保障机制这一部分说说压测之外的事。很多人以为全链路压测做一次就完事了但实际上真正的价值在于把压测常态化让它成为系统稳定性保障体系里常驻的一环。在这次压测中我学到最深刻的一课是性能问题不是一次优化就永久解决的系统的性能容量会随着业务增长和代码演进而不断变化。两个月前测出来的容量上限今天可能已经成了常态负载。所以压测要从项目制变成机制。我们现在的做法是建立了一个压测基线库。每次大版本发布或核心链路重构后自动触发回归压测用同一套压测脚本和流量模型去跑对比各项指标和基线的偏差。偏差超过某个阈值就会自动告警进入性能回归评审流程。通过这个机制我们能尽早发现这次改动让响应时间变长了300ms之类的隐性性能回退避免问题等到生产环境爆发后才被用户发现。做压测基线库还有一个明显的好处积累下来的多次压测数据可以画出性能趋势曲线帮助我们预判系统什么时候需要扩容。比如从趋势曲线上看到推理服务的吞吐量每个月增长8%那么据此可以提前规划GPU资源的扩容时间点而不是等到线上告警才发现扛不住了。关于压测环境本身我强烈建议投入成本建设一套和线上拓扑完全一致、数据完全脱敏的压测环境。这个环境平时作为性能测试和故障演练的基地有版本发布窗口时作为回归压测的固定场地。虽然搭建成本不低但这套环境带来的价值在于每一次压测结果都能直接映射到线上的真实表现不会因为环境差异导致压测数据失真。最后说一个很小但很实在的细节压测完成后一定要记得清理压测产生的大量垃圾数据包括临时影像文件、诊断记录、日志和缓存数据。如果清理不彻底压测环境的数据量会不断膨胀最终导致下一次压测的性能基线和真实环境产生偏差。我们吃过这个亏从那以后给压测环境加了一个自动化清理脚本每次压测结束后自动归档和清理。回头看这次全链路压测的整个过程从最初定30s这个目标到压测中发现P99飙到71s再到底层根因的逐步排查和六项优化的落地最后P99稳定在17s整个过程走下来最深的体会是系统性能优化没有银弹靠的是把每一个环节都打磨到位——模型推理的调度方式、显存的使用策略、链路的拓扑结构、容错机制的设计每一环都值得认真对待而且每一环的优化效果最终都要靠全链路压测这个最终裁判来检验。