记一次图片压缩引起的服务宕机优化

1、场景

在做保险在线理赔业务中,用户申请理赔时,需要上传理赔单据照片,申请理赔后在理赔详情页并列展示上传的单据缩略图照片,单击某张照片后可放大查看该张照片。介于如今的手机拍照后形成的照片基本在2-4MB左右的大小,在展示理赔详情缩略图时,如果使用原图图片URL进行展示,将会耗费巨大的流量来加载图片。对于用户处于网速不好的环境下,会造成图片加载不出来的问题。同时也为了节省用户的流量,故要针对上传的图片进行压缩处理,展示的缩略图使用压缩后的图片展示,点击图片放大后因为清晰度要求从而展示原图的URL来加载图片。

前端采用VUE,后端Java项目。可以理解为H5端的图片上传。
大体的图片上传页面展示如下:

基本交互逻辑如下:

2、压缩方案调研

2.1、阿里云的OSS对象存储

根据阿里云的OSS对象存储帮助文档可知,OSS图片处理详细介绍,阿里云针对图片有多种可选择的处理方式:

  • 获取图片信息
  • 图片格式转换
  • 图片缩放、裁剪、旋转
  • 图片添加图片、文字、图文混合水印
  • 自定义图片处理样式
  • 通过管道顺序调用多种图片处理功能

选择其中的图片缩放即可实现场景中的功能。

2.2、腾讯云的COS对象存储

腾讯云的COS对象存储也提供了图片的相应处理功能,COS图片处理详细介绍,针对上传至COS的图片,只需在url中加入一些参数即可实现图片压缩。

只不过貌似提供该功能的为腾讯云的另一款产品 数据万象 CI,如果使用的话,需要购买两款产品。其功能如下:

  • 图片处理 数据万象可对存储在 COS 上的图片资源直接进行处理,如缩放、裁剪、转码、压缩、水印等。
  • 持久化处理 通过数据万象,您可在上传时直接实现图片处理,也可以对已存放在 COS 的图片资源进行处理,并将处理结果持久化保存。
  • 原图保护 针对图片这种易被非法盗用的资源,数据万象提供原图保护功能,开启后资源仅能以样式化 url 访问,有效防止原图泄露。
  • 盲水印 数据万象提供独有的盲水印功能,能够将水印图以不可见形式添加到图片频域,在图片资源被攻击泄露后(裁剪、涂抹等)仍可提取出水印信息,有效鉴权追责。
  • 域名管理 您可通过域名管理选择是否开启 CDN 加速功能、设置自定义域名,并可通过设置防盗链(黑白名单)来防止流量盗刷。
  • 回源设置 数据万象的回源设置功能可以帮助您在不中断访问的情况下,无缝迁移原站内容至腾讯云对象存储。
  • 内容审核 敏感内容审核功能提供鉴黄、鉴政、鉴暴恐等多种类型的敏感内容检测,帮助您有效规避违规风险。

2.3、Google开源Thumbnailator

Thumbnailator 是一个优秀的图片处理的Google开源Java类库。处理效果远比Java API的好。从API提供现有的图像文件和图像对象的类中简化了处理过程,两三行代码就能够从现有图片生成处理后的图片,且允许微调图片的生成方式,同时保持了需要写入的最低限度的代码量。还支持对一个目录的所有图片进行批量处理操作。

其提供的功能如下:

  • 图片缩放
  • 区域裁剪
  • 水印
  • 旋转
  • 保持比例

2.4、java的ImageIO处理图片

大体就是使用javax.imageio.ImageIO类是读取图片后,针对图片做下压缩处理后,在写入文件流中。

2.5、调研结论

因为公司问题,要求使用公司的基础服务对象存储功能(其实就是对腾讯云做了层封装),询问相关负责人得知,没有针对图片进行压缩的API可提供。所以只好选择 Google开源Thumbnailator 或者 java的ImageIO来自己做图片压缩了。PS:如果条件允许的话,还是推荐使用云存储三方提供的图片处理API来处理图片。

3、使用Thumbnailator来做图片压缩

使用

引入Thumbnailator的依赖包

<dependency>
    <groupId>net.coobird</groupId>
    <artifactId>thumbnailator</artifactId>
    <version>0.4.8</version>
</dependency>

实现图片压缩的代码很简单,只有一行

//图片尺寸不变,压缩图片文件大小outputQuality实现,参数1为最高质量
Thumbnails.of("原图文件的路径") 
        .scale(1f) 
        .outputQuality(0.5f) 
        .toFile("压缩后文件的路径");

测试

发布到dev环境中,使用压测,上传个3MB的图片,起10个线程,循环5次,10秒内依次启动完所有线程,测试结果如下:

结果很惨,50个请求,Error率达到百分之80。

返回的结果为503,服务直接不可用了

查找原因

这次改用小图片进行压缩上传处理,大小:300KB左右,压测指标还是按照上面的来。
压测结果如下:

错误率0%,初步怀疑为图片过大,在进行压缩时,导致了OOM问题。

在压缩方法里打印出内存的占用情况,加入如下日志:

log.info("内存剩余:{},总内存:{},百分比:{}",runtime.freeMemory(),runtime.totalMemory(),new BigDecimal(runtime.freeMemory()).divide(new BigDecimal(runtime.totalMemory()),2,RoundingMode.HALF_DOWN));
Thumbnails.of(executeFile.getAbsolutePath()).scale(0.5f).outputQuality(0.25f).toFile(executeFile1);	
log.info("压缩后内存剩余:{},总内存:{},百分比:{}",runtime.freeMemory(),runtime.totalMemory(),new BigDecimal(runtime.freeMemory()).divide(new BigDecimal(runtime.totalMemory()),2,RoundingMode.HALF_DOWN));

再次压测3MB的图片,同样的,失败率达到百分之80多,同时服务进入宕机重启状态。

查看日志情况:

物理内存16G能因为图片压缩导致剩余可用内存占用率不到百分之30,基本可以确认是因为大图片处理耗尽了内存导致OOM问题。

4、java的ImageIO处理图片

在使用Thumbnailator时出现了OOM问题,但是其使用方法只有一行代码,无法针对其内部使用的对象进行资源释放,所以使用原生的Java类库中ImageIO来处理图片。
关键有三个类:ImageIO、BufferedImage、Graphics

  • ImageIO类包含两个静态方法:read()和write(),通过这两个方法即可完成对位图文件的读写,调用write()方法输出图形文件时需要指定输出的图形格式。
public static BufferedImage read(File input) throws IOException
public static boolean write(RenderedImage im,String formatName,File output)throws IOException
  • Image类代表位图,但它是一个抽象类,无法直接创建Image对象,为此java为它提供了一个BufferedImage子类,这个子类是一个可以访问图像数据缓冲区的Image实现类。该类提供了一个简单的构造器:BufferedImage(int width,int height,int imageType):创建指定大小、指定图像类型的BufferedImage对象。除此之外,还提供一个getGraphics()方法返回该对象的Graphics对象,从而允许通过该Graphics对象向BufferedImage中添加图形。
  • Graphics是一个抽象的画笔对象,它可以在组件上绘制丰富多彩的几何图形和位图。它提供有一个重要方法,将一个img对象的原始图形宽度缩小为width,高度缩小为height,添加到BufferedImage对象的(x,y)处:public abstract boolean drawImage(Image img, int x, int y,int width,int height, ImageObserver observer)

测试关键代码如下:

        BufferedImage bufferedImage=new BufferedImage(217,190,BufferedImage.TYPE_INT_RGB);
        Graphics graphics=bufferedImage.getGraphics();
        
        //读取原始位图
        Image srcImage= ImageIO.read(executeFile);
       
        //将原始位图缩小后绘制到bufferedImage对象中 
        graphics.drawImage(srcImage,0,0,217,190,null);
        //将bufferedImage对象输出到磁盘上
        ImageIO.write(bufferedImage,"jpg",executeFileNew);

测试

压测指标还是按照上传个3MB的图片,起10个线程,循环5次,10秒内依次启动完所有线程来。
结果同样不理想,同样会造成OOM。

通过网上查询资料得到一些结论:

  • 以上这个方法,在处理相对较小的图片,比如1M左右的图片,是可以的,但是如果 图片较大,在Image src = javax.imageio.ImageIO.read(_file); 就抛内存溢出;
  • 所有压缩格式的图片,都被转换成像素点阵,存放到内存当中,非常消耗资源的,会按照图片的像素转换,如果为3MB的图片,处理的数据大小在几十MB左右。

5、最终解决方案

尽量不去用javax.imageio.ImageIO.read(_file)来读取图片,java还提供了使用Toolkit这个工具包可以用来处理图片。
所以调整代码如下:

    Toolkit toolkit = Toolkit.getDefaultToolkit();
	srcImage = toolkit.getImage(executeFile.getAbsolutePath());
	int wideth = -1;
        int height = -1;
	boolean flag = true;
	//Toolkit加载是异步的,它有一个观察器,要等待它回加载完成才能再draw出去。 
        while (flag) {
	        wideth = srcImage.getWidth(null); // 得到源图宽
		height = srcImage.getHeight(null); // 得到源图长
		if (wideth > 0 && height > 0) {
		    flag = false;
		} else {
		    try {
		        Thread.sleep(100);
		    } catch (Exception e) {
		        e.printStackTrace();
		    }
		 }
	}
	bufferedImage=new BufferedImage(300,400,BufferedImage.TYPE_INT_RGB);
        graphics=bufferedImage.getGraphics();
			     
	graphics.drawImage(srcImage,0,0,300,400,null);
	ImageIO.write(bufferedImage,suffix.substring(1,suffix.length()),executeFile1);
	if(srcImage != null) {
		srcImage.flush();
	}
	if(bufferedImage != null) {
		bufferedImage.flush();
	}
	if(graphics!= null) {
		graphics.dispose();
	}

测试

压测指标上传个3MB的图片,起10个线程,循环5次,10秒内依次启动完所有线程来。

测试结果全部压缩成功,日志打印的内存占用情况:
基本维持在百分之60没有发生太大的变化

压测指标增到到起10个线程,每个线程循环10次,5秒内依次启动完所有线程。
结果报告如下:

结果全部压缩成功。每秒吞吐量在3.2,因为是dev环境,反衬在生产环境中完全够用。

内存情况:同样维持在百分之60左右

6、总结:

  • 1、java对于图片的处理技术在处理小图片时,完全够用,但是在处理大于1MB以上的图片时,就不再推荐本身服务器去处理图片。
  • 2、有条件的还是将图片的处理交给第三方来,调用封装好的API等来处理图片的各种要求。
发布了21 篇原创文章 · 获赞 2 · 访问量 7507

猜你喜欢

转载自blog.csdn.net/qq_35551089/article/details/98069904