上周三的晚上,我经历了一次难忘的线上故障排查。我们的大模型多模态服务出了一个诡异的Bug,我从晚上八点一直排查到第二天早上六点,花了整整一夜才找到原因并修复。
这篇文章我想记录一下这次排查的完整过程,包括问题的现象、排查的思路、踩过的坑,以及最终的解决方案。如果你也在做大模型相关的开发,希望我的经历能给你一些参考。
问题的出现
事情是这样的。
那天晚上八点多,我刚吃完饭,正准备休息一下,手机突然响了。是运维同事打来的,说线上的多模态服务出问题了,用户上传图片之后,返回的结果不对,很多请求都报错了。
我赶紧打开电脑,登录监控系统一看,果然,多模态服务的错误率从平时的不到百分之一,飙升到了百分之三十多。而且错误还在持续,用户投诉也在增加。
我第一反应是,是不是模型服务挂了。我检查了一下模型服务的状态,发现服务是正常的,GPU利用率也正常,没有OOM,也没有进程崩溃。
那为什么会报错呢?我开始了漫长的排查过程。
第一步:看日志
排查问题的第一步,当然是看日志。
我打开了多模态服务的日志,发现大量的报错信息都是同一个:"图片解码失败"。
这个错误很奇怪。用户上传的图片,怎么会解码失败呢?我看了一下错误的请求,发现这些请求的图片大小都比较大,一般都在5MB以上。而小图片的请求,基本都是正常的。
我初步判断,可能是大图片的处理出了问题。可能是图片太大,超过了某个限制,或者是处理大图片的时候内存不够。
但我检查了一下配置,图片大小的限制是20MB,这些5MB的图片应该不会超限。内存方面,服务的内存使用率也不高,还有很多余量。
那问题到底出在哪里呢?
第二步:复现问题
光看日志不够,我需要复现问题。
我找了一张6MB的测试图片,用同样的接口调用了一下,果然报错了,"图片解码失败"。然后我又试了一张1MB的小图片,一切正常。
看来问题确实和图片大小有关。但为什么大图片会解码失败呢?
我把这张大图片下载到本地,用Python的PIL库打开,一切正常,图片没有损坏。那为什么线上的服务解码失败呢?
我开始怀疑,是不是线上的图片处理库有问题。我们用的是OpenCV来处理图片,会不会是OpenCV的版本有bug,或者是编译的时候缺少了某些编解码器?
我检查了线上服务的OpenCV版本,和测试环境是一样的。而且测试环境用同样的图片,是可以正常处理的。这就奇怪了,为什么同样的代码、同样的库,测试环境正常,线上就不行呢?
第三步:对比环境
既然测试环境正常,线上有问题,那肯定是环境有差异。
我开始对比测试环境和线上环境的差异。首先是操作系统,都是一样的。然后是Python版本,也是一样的。依赖库的版本,我一个一个对比,发现都是一样的。
那到底差在哪里呢?
我突然想到,会不会是配置的差异。我对比了两个环境的配置文件,发现线上环境的一个配置项和测试环境不一样。线上环境为了提高并发,把工作进程数从4个改成了8个。
难道是进程数的问题?进程数多了,每个进程的内存就少了?但我看了内存,还有很多余量啊。
我又仔细看了一下配置,发现了一个细节。线上环境除了改了进程数,还改了一个参数:每个请求的超时时间,从60秒改成了30秒。
难道是超时?大图片处理需要的时间长,超过了30秒,所以被超时中断了?但报错信息是"图片解码失败",不是"超时"啊。
我决定验证一下。我把线上的超时时间临时改回60秒,然后再试大图片,居然成功了!
看来问题确实和超时有关。但为什么超时会报"图片解码失败"的错误呢?
第四步:深入分析
我开始深入分析代码,看看超时是怎么导致"图片解码失败"的。
我们的多模态服务,处理图片的流程是这样的:接收用户上传的图片,然后用OpenCV解码,然后做预处理,然后调用大模型推理,最后返回结果。
超时是在整个请求层面设置的,也就是说,如果整个请求的处理时间超过了30秒,就会被强制中断。
但问题是,图片解码是第一步,应该很快才对,怎么会超时呢?而且,如果是超时中断,应该报超时错误,怎么会报"图片解码失败"呢?
我仔细看了代码,发现了一个问题。我们的图片解码,不是在请求进程里做的,而是放到了一个线程池里异步处理的。请求线程把解码任务提交给线程池,然后等待结果。
如果等待超时了,请求线程会抛出超时异常。但这个时候,线程池里的解码任务可能还在运行。我们的代码在捕获超时异常之后,又调用了一个方法,去检查解码任务的状态。而这个检查方法,如果任务还在运行,就会返回一个默认的错误结果,错误信息就是"图片解码失败"。
所以,真实的情况是:大图片的解码和预处理时间比较长,超过了请求的超时时间,请求被中断了,然后代码错误地把它报告成了"图片解码失败"。
但这又引出了一个新问题:为什么大图片的解码和预处理需要这么长时间?30秒都不够?
第五步:找到根本原因
我开始分析图片处理的代码,看看为什么大图片处理这么慢。
我们的图片预处理流程是:用OpenCV读取图片,然后缩放到模型需要的尺寸,然后做归一化,然后转成张量,传给模型。
这个流程看起来很简单,应该很快才对。但我用大图片测试了一下,发现光是读取和缩放,就花了二十多秒。
这太不正常了。一张6MB的图片,读取和缩放怎么可能花二十多秒?
我加了一些日志,看看每一步花了多少时间。结果发现,最耗时的是OpenCV的imread函数,读取一张6MB的图片,居然花了十五秒。
这就更奇怪了。OpenCV读图片怎么可能这么慢?
我又仔细看了一下代码,发现了一个致命的问题。我们的图片读取,用的是OpenCV的imread函数,但这个函数是从文件路径读取的。而我们的服务,是先把用户上传的图片保存到临时文件,然后再用imread读取。
保存临时文件这个操作,是同步的,而且是在请求线程里做的。如果磁盘IO比较慢,保存文件就会花很长时间。
但磁盘IO慢,也不至于十五秒啊。我又检查了一下临时文件的目录,发现了问题所在。
线上服务的临时文件目录,是一块网络存储,不是本地磁盘。网络存储的IO延迟很高,特别是写入大文件的时候,速度很慢。保存一个6MB的文件到网络存储,居然要花十几秒。
而测试环境用的是本地磁盘,所以保存文件很快,没有这个问题。
所以,根本原因找到了:线上环境的临时文件目录用的是网络存储,大文件写入很慢,导致整个请求超时,然后被错误地报告成了"图片解码失败"。
第六步:解决问题
找到原因之后,解决就简单了。
第一个解决方案,把临时文件目录改到本地磁盘。这样文件写入就快了,不会再超时。
我改了配置,把临时文件目录指向了本地磁盘的一个目录,然后重启服务。再测试大图片,读取和缩放只花了不到一秒,整个请求也只花了几秒钟,完全正常了。
第二个解决方案,优化图片处理流程。其实我们根本不需要把图片保存到临时文件,可以直接在内存里处理。用户上传的图片,本身就是一个文件流,我们可以直接从内存里读取,不需要先保存到磁盘。
我把代码改了一下,用OpenCV的imdecode函数,直接从内存的字节流解码图片,不再保存临时文件。这样既避免了磁盘IO的问题,也更高效。
改完之后,大图片的处理时间从二十多秒降到了两秒以内,错误率也降回了正常水平。
排查过程中踩的坑
这次排查,花了整整一夜,中间踩了不少坑。
第一个坑是被错误信息误导。"图片解码失败"这个错误信息,把我引向了图片解码的方向,花了很多时间去查OpenCV和图片格式的问题。如果一开始就能看到真实的错误,排查会快很多。
这提醒我,错误信息一定要准确。捕获异常的时候,不要把所有异常都包装成同一个错误信息,要保留原始的错误类型和信息,方便排查。
第二个坑是忽略了环境差异。最开始对比环境的时候,我只对比了软件版本,没有对比配置和基础设施。后来才发现,临时文件目录的差异才是关键。
这提醒我,排查线上问题的时候,要全面对比环境,包括配置、基础设施、网络等,不能只看软件版本。
第三个坑是想当然。我一开始觉得,图片读取肯定很快,不可能是瓶颈。但实际上,因为用了网络存储,图片读取成了最大的瓶颈。
这提醒我,排查问题的时候,不要想当然,要用数据说话。加日志,测时间,一步一步定位,不要凭感觉判断。
第四个坑是异步处理的复杂性。我们用了线程池异步处理图片,结果超时之后的状态处理有bug,导致错误信息不准确。异步编程虽然能提高性能,但也增加了复杂度,要特别注意异常处理和状态管理。
总结和经验
这次一夜的排查,让我总结了几条经验。
第一,线上问题要先看监控和日志,不要急着改代码。通过监控和日志,先搞清楚问题的现象和范围,再有针对性地排查。
第二,要学会复现问题。能复现的问题,就解决了一半。要想办法在测试环境复现,或者在线上用测试请求复现。
第三,要对比正常和异常的差异。哪些请求是正常的,哪些是异常的,它们之间有什么区别。通过对比,往往能快速定位问题。
第四,要加日志。在排查的过程中,要敢于加日志,把关键步骤的时间和状态都打出来。有了日志,就能一步步缩小范围,找到问题所在。
第五,不要忽略基础设施。很多线上问题,不是代码的问题,而是基础设施的问题,比如磁盘、网络、配置等。排查的时候,要把基础设施也考虑进去。
第六,修复问题之后,要做复盘。搞清楚问题的根本原因,总结经验教训,避免以后再犯同样的错误。
写在最后
这次排查,从晚上八点到第二天早上六点,整整十个小时。虽然很辛苦,但当问题解决的那一刻,还是很有成就感的。
每一次线上故障,都是一次学习的机会。它能让你更深入地理解系统,发现平时注意不到的问题,也能让你积累排查问题的经验。
当然,最好还是不要有线上故障。但故障不可避免,重要的是,故障发生之后,能不能快速定位和解决,能不能从中学到东西,避免以后再犯。
如果你也在做大模型相关的开发,希望我的这次经历能给你一些参考。多模态大模型的服务,涉及到图片处理、模型推理、网络通信等很多环节,任何一个环节出问题,都可能导致线上故障。要做好监控,写好日志,设计好错误处理,这样出了问题才能快速排查。
最后用一句话来结束这篇文章:"每一个熬过的夜,都会变成你排查问题的经验。"
愿每一个程序员,都能少熬夜,系统稳定运行。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录