写点什么

记一次线上视频接口 OOM 问题

作者:xfgg
  • 2023-12-01
    福建
  • 本文字数:2510 字

    阅读完需:约 8 分钟

记一次线上视频接口OOM问题

背景

广告平台业务做了一个给用户上传视频并生成返回视频 url 以及视频第一帧图片的 url

有一次用户反馈上传视频返回 error,但是过一会就好了,排查后发现线上容器重启。

接下来我会讲讲这次线上问题的修复

OOM 的原因

首先是代码问题,获取视频第一帧图片的资源没有及时得到释放,对文件的处理没有使用流式处理

因为同时多人上传多个视频,导致内存 OOM

如何排查定位

首先需要定位容器重启的问题

通过 Kubernet 命令 kubectl describe pod <podName>来查看具体的重启原因(问题发生的截图并没有保存下来)

根据用户上传视频时出现报错可以定位 OOM 接口为上传视频接口

代码调整以及遇到的问题

看到代码的时候看到了一个很危险的方法

Flie.createTempFile()
复制代码

代码在内存中生成了临时文件来保存视频第一帧的图片,然后对图片进行大小/分辨率压缩,最后上传 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 因为垃圾回收的机制,资源一定要得到及时的释放

如果有更好的方法,欢迎各位大佬批评指导

发布于: 刚刚阅读数: 8
用户头像

xfgg

关注

THINK TWICE! CODE ONCE! 2022-11-03 加入

目前:全栈工程师(前端+后端+大数据) 目标:架构师

评论

发布
暂无评论
记一次线上视频接口OOM问题_Java_xfgg_InfoQ写作社区