背景
广告平台业务做了一个给用户上传视频并生成返回视频 url 以及视频第一帧图片的 url
有一次用户反馈上传视频返回 error,但是过一会就好了,排查后发现线上容器重启。
接下来我会讲讲这次线上问题的修复
OOM 的原因
首先是代码问题,获取视频第一帧图片的资源没有及时得到释放,对文件的处理没有使用流式处理
因为同时多人上传多个视频,导致内存 OOM
如何排查定位
首先需要定位容器重启的问题
通过 Kubernet 命令 kubectl describe pod <podName>来查看具体的重启原因(问题发生的截图并没有保存下来)
根据用户上传视频时出现报错可以定位 OOM 接口为上传视频接口
代码调整以及遇到的问题
看到代码的时候看到了一个很危险的方法
代码在内存中生成了临时文件来保存视频第一帧的图片,然后对图片进行大小/分辨率压缩,最后上传 cdn
生成的临时文件没有得到及时的清理,导致内存 OOM
修改代码对临时文件进行清理,进行压力测试发现还是 OOM
boolean delete = imgTemp.delete(); //第一次我以为是这个删除函数的问题
if (!delete) {
imgTemp.deleteOnExit(); //添加了另一种删除方式
}
复制代码
最后发现我并没有删除临时文件的权限....
选择将图片存放到/tmp 目录下,定时对这个目录进行清理
String suffix = Objects.requireNonNull(multipartFile.getOriginalFilename()).substring(
multipartFile.getOriginalFilename().lastIndexOf("."));
String fileName = getUuidName(suffix);
String dir = FileApollo.jarFolder;
String savePath = dir + File.separator + "download" + File.separator;
checkDir(savePath);
复制代码
进行压力测试发现好了很多,但是多线程的情况下还是会出现 OOM
使用 try-catch-withresourse 以及 finally 对资源进行及时的释放
视频和图片文件采用流式处理以减少内存占用
// 使用流的方式写入文件以减少内存占用
try (InputStream in = multipartFile.getInputStream()) {
Files.copy(in, Paths.get(absolutePath));
}
grabber = FFmpegFrameGrabber.createDefault(new File(absolutePath));
grabber.start();
String imgPath = savePath + md5 + ".jpg";
imgTemp = new File(imgPath);
converter = new Java2DFrameConverter();
BufferedImage image = converter.getBufferedImage(grabber.grabImage());
// 调整分辨率
BufferedImage resized = Thumbnails.of(image).size(720, 405).asBufferedImage();
// 压缩图片到指定大小。
// 请注意,我们可能需要多次尝试以获得500KB大小的输出,因为质量降低通常不能精确控制输出大小。
float quality = 1.0f;
ImageWriter writer = ImageIO.getImageWritersByFormatName("jpg").next();
try (FileOutputStream outStream = new FileOutputStream(imgTemp);
ImageOutputStream output = ImageIO.createImageOutputStream(outStream)) {
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(quality);
do {
output.reset(); // 重置流以便于重新开始下一次尝试
writer.setOutput(output);
writer.write(null, new IIOImage(resized, null, null), param);
// 每次尝试降低质量
quality -= 0.1f;
} while (output.length() > 500 * 1024 && quality > 0.1f);
} finally {
image.flush();
writer.dispose();
resized.flush();
}
复制代码
这里还有一个问题每次降低质量,都只降低了 0.1f
可能会导致循环多次,质量因子需要根据自身情况来进行调整减少循环的次数
try (FileOutputStream outStream = new FileOutputStream(imgTemp);
ImageOutputStream output = ImageIO.createImageOutputStream(outStream)) {
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(quality);
do {
output.reset(); // 重置流以便于重新开始下一次尝试
writer.setOutput(output);
writer.write(null, new IIOImage(resized, null, null), param);
// 每次尝试降低质量
quality -= 0.3f;
} while (output.length() > 500 * 1024 && quality > 0.1f);
} finally {
image.flush();
writer.dispose();
resized.flush();
}
复制代码
压力测试
首先单个用户上传大视频文件 100 次
然后 10 个线程,每个线程上传大视频文件 100 次
进阶调整
通过设置线程池来限制同时可以执行请求的线程数量,为接口提供了抗压性,但是因为要排队,用户耗时可能会增加
// 创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
// 创建一个Callable任务
Callable<UploadData> task = new Callable<UploadData>() {
@Override
public UploadData call() throws Exception {
MaterialUtil.checkVideoFile(multipartFile);
return MaterialUtil.uploadVideo(multipartFile, md5);
}
};
// 提交任务
Future<UploadData> future = executorService.submit(task);
UploadData result = null;
try {
//get方法会阻塞,直到任务完成
result = future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
//...你可以在后面使用result
// 关闭线程池
executorService.shutdown();
复制代码
总结
写接口的时候需要考虑到接口的量级
对接口进行多次压测是有必要的
java 因为垃圾回收的机制,资源一定要得到及时的释放
如果有更好的方法,欢迎各位大佬批评指导
评论