记一次解析图片导致的线上OOM事故分析和解决

目录

一、项目说明

二、第一次优化

三、OOM事故的发生与分析

四、解决方案

五、最终优化代码


一、项目说明

该项目用于调用第三方图片识别接口,识别出拍摄图片中的饮品sku数量及排面数量,用于计算销售人员巡店拜访商超店面公司饮品的陈列质量。

用户用手机拍摄图片是存储在阿里云对象存储oss上的,客户端将图片在oss的url作为请求参数传递给服务端,服务端接收到请求后将url转为图片base64用于调用第三方接口获取识别结果。

二、第一次优化

由于读取到的部分图片有翻转情况(手机拍摄导致图片旋转90度),第三方服务无法识别出有翻转情况的图片内容,所以需要服务端自行判断图片是否有翻转情况并矫正,以下是工具类代码:

/**
 * 获取图片旋转角度
 * @param imageUrl
 * @return 图片角度
 */
public static int getAngle(URL imageUrl) throws Exception {
    InputStream intstream = null;
    try{
        intstream = imageUrl.openStream();
        Metadata metadata = ImageMetadataReader.readMetadata(intstream);
        for (Directory directory : metadata.getDirectories()) {
            for (Tag tag : directory.getTags()) {
                if ("Exif IFD0".equals(tag.getDirectoryName()) && "Orientation".equals(tag.getTagName())) {
                    String orientation = tag.getDescription();
                    if (orientation.contains("90")) {
                        return 90;
                    } else if (orientation.contains("180")) {
                        return 180;
                    } else if (orientation.contains("270")) {
                        return 270;
                    }else {
                        return 0;
                    }
                }
            }
        }
    }finally {
        if(intstream != null) {
            intstream.close();
        }
    }
    return 0;
}



/**
 * 纠正图片旋转并返回base64编码
 * @param imageUrl
 * @param angle
 * @return 图片base64编码
 */
public static String correctImgAndToBase64(URL imageUrl, int angle) throws Exception {
    BufferedImage srcImg = null;
    BufferedImage srcImgNew = null;
    Graphics graphics = null;
    BufferedImage targetImg = null;
    Graphics2D g = null;
    ByteArrayOutputStream outputStream = null;
    try {
        // 原始图片缓存
        srcImg = ImageIO.read(imageUrl);
        // 宽高互换
        // 原始宽度
        int imgWidth = srcImg.getHeight();
        // 原始高度
        int imgHeight = srcImg.getWidth
        // 中心点位置
        double centerWidth = ((double) imgWidth) / 2;
        double centerHeight = ((double) imgHeight) / 2;
        // 图片缓存
        targetImg = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_RGB);
        // 旋转对应角度
        g = targetImg.createGraphics();
        g.rotate(Math.toRadians(angle), centerWidth, centerHeight);
        g.drawImage(srcImg, (imgWidth - srcImg.getWidth()) / 2, (imgHeight - srcImg.getHeight()) / 2, null);
        g.rotate(Math.toRadians(-angle), centerWidth, centerHeigh);
        outputStream = new ByteArrayOutputStream();
        ImageIO.write(targetImg, "jpg", outputStream);
        // 对字节数组Base64编码
        BASE64Encoder encoder = new BASE64Encoder();
        return encoder.encode(outputStream.toByteArray());// 返回Base64编码过的字节数组字符串
    }finally {
        if(Objects.nonNull(outputStream)) {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(Objects.nonNull(g)) {
            g.dispose();
        }
        if(Objects.nonNull(targetImg)) {
            targetImg.getGraphics().dispose();
        }
        if(Objects.nonNull(graphics)) {
            graphics.dispose();
        }
        if(Objects.nonNull(srcImgNew)) {
            srcImgNew.getGraphics().dispose();
        }
        if(Objects.nonNull(srcImg)) {
            srcImg.getGraphics().dispose();
        }
    }
}

三、OOM事故的发生与分析

监控发现识别服务有明显堆积,大量用户反馈图片识别结果很慢。查询nacos注册的3台服务下线了2台,查询日志发现发生了oom,导出hprof文件后使用jdk自带的jvisualvm进行分析

1、使用scp命令导出线上服务器的hprof文件到本地

scp 服务器用户名@服务器ip:服务器文件路径 本地路径

2、进入jdk安装目录,进入bin文件夹,启动jvisualvm

/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/bin

3、启用jvisualvm后载入hprof文件 

4、点击跳转查看导致 OutOfMemoryError 异常错误的线程

找到了项目中的代码,结合堆栈信息上下文,找到最终发生oom的代码位置,根据上面的图跟踪查看以下几个位置代码:
at java.awt.image.DataBufferInt.<init>(DataBufferInt.java:75)
Local Variable: java.awt.image.DataBufferInt#1
at java.awt.image.Raster.createPackedRaster(Raster.java:467)
Local Variable: int[]#203
at java.awt.image.DirectColorModel.createCompatibleWritableRaster(DirectColorModel.java:1032)
at java.awt.image.BufferedImage.<init>(BufferedImage.java:324)


5、根据堆栈信息得出结论:
关键代码是 BufferedImage targetImg = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_RGB);
可以看到传入图片的宽和高最终会创建一个DataBuffer d = new DataBufferInt(w*h);
然后创建一个int[],而数组长度大小是图片的宽*高,如果图片很大,这个int[]就会很大,排查事故发生时的图片大小,发现部分图片竟然有20多M,这是导致oom的根本原因
 

四、解决方案

知道问题导致的原因是图片过大后,想到的解决方案是:判断图片大小如果超过某个值时可以将图片等比例缩小,然后再进行后续的纠正图片旋转和调用第三方接口识别图片内容
 

五、最终优化代码

/**
 * 纠正图片旋转并base64编码
 * @param imageUrl
 * @param angle
 * @return 图片base64编码
 */
public static String correctImgAndToBase64(URL imageUrl, int angle) throws Exception {
    BufferedImage srcImg = null;
    BufferedImage srcImgNew = null;
    Graphics graphics = null;
    BufferedImage targetImg = null;
    Graphics2D g = null;
    ByteArrayOutputStream outputStream = null;
    try {
        // 原始图片缓存
        srcImg = ImageIO.read(imageUrl);
        // 这里添加判断,如果图片宽度大于4096时,将图片等比例缩小。为什么选择4096呢?因为第三方识别接口建议图片最长边不大于4096
        if(srcImg.getWidth() > 4096) {
            int targetWidth = srcImg.getWidth() * 20 /100;
            int targetHeight = srcImg.getHeight() * 20 /100;
            srcImgNew = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
            graphics = srcImgNew.getGraphics();
            //将原始位图缩小后绘制到bufferedImage对象中
            graphics.drawImage(srcImg,0,0,targetWidth,targetHeight,null);
            srcImg = srcImgNew;
        }
        // 宽高互换
        // 原始宽度
        int imgWidth = srcImg.getHeight();
        // 原始高度
        int imgHeight = srcImg.getWidth
        // 中心点位置
        double centerWidth = ((double) imgWidth) / 2;
        double centerHeight = ((double) imgHeight) / 2;
        // 图片缓存
        targetImg = new BufferedImage(imgWidth, imgHeight, BufferedImage.TYPE_INT_RGB);
        // 旋转对应角度
        g = targetImg.createGraphics();
        g.rotate(Math.toRadians(angle), centerWidth, centerHeight);
        g.drawImage(srcImg, (imgWidth - srcImg.getWidth()) / 2, (imgHeight - srcImg.getHeight()) / 2, null);
        g.rotate(Math.toRadians(-angle), centerWidth, centerHeigh);
        outputStream = new ByteArrayOutputStream();
        ImageIO.write(targetImg, "jpg", outputStream);
        // 对字节数组Base64编码
        BASE64Encoder encoder = new BASE64Encoder();
        return encoder.encode(outputStream.toByteArray());// 返回Base64编码过的字节数组字符串
    }finally {
        if(Objects.nonNull(outputStream)) {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(Objects.nonNull(g)) {
            g.dispose();
        }
        if(Objects.nonNull(targetImg)) {
            targetImg.getGraphics().dispose();
        }
        if(Objects.nonNull(graphics)) {
            graphics.dispose();
        }
        if(Objects.nonNull(srcImgNew)) {
            srcImgNew.getGraphics().dispose();
        }
        if(Objects.nonNull(srcImg)) {
            srcImg.getGraphics().dispose();
        }
    }
}

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值