背景
广告平台业务做了一个给用户上传视频并生成返回视频 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 因为垃圾回收的机制,资源一定要得到及时的释放
如果有更好的方法,欢迎各位大佬批评指导
评论