架构设计:系统存储(21)——图片服务器:详细设计(1)

(接上文《架构设计:系统存储(20)——图片服务器:需求和技术选型(2)》)java

一、概述

以前的两篇文章介绍了图片系统的技术组件选型和技术方案设计,从这篇文章开始咱们将搭建工程进行详细的编码开发和效果测试。这里要说明一下,因为文章篇幅的限制不可能贴出全部的代码,这样也不符合读者的阅读习惯。因此笔者的办法是,只经过后续的文章内容介绍详细的设计要点和代码片断,经过这些讲解读者基本能够清楚整个详细设计的思路,而整个图片服务工程代码会上传到了CSDN的下载区(http://download.csdn.net/detail/yinwenjie/9740380),若是对工程感兴趣那么读者能够直接下载——免费下载。 web

二、简单的图片处理

2-一、位图的构成基础

因为咱们将要进行的是图片处理操做,而图片处理比起读者常常涉及的业务系统来讲是一个比较生涩的领域,在通常的业务系统中也就只是须要作到上传或者下载/显示图片就OK了。而图片服务是专门进行图片处理的,简单的处理包括透明度、旋转、缩放、翻转、重合等,复杂的处理还有背景虚化、人脸识别等等。因此在正式进入编码前,本文有必要向读者介绍一些基本的图片知识,特别是图片颜色模型的知识。固然,若是读者自己就有很丰富的图片处理知识了,则能够直接跳过本节的介绍。算法

目前在互联网上使用最多的图片都属于位图,例如JPG、GIF、PNG和BMP图片都属于位图。它们的相同点在于都依靠RGB色彩模式描述图片中的每个点,从而构成一张图片。它们的不一样点则体如今文件头结构、压缩算法、扫描方式以及像素深度支持等方面。举个例子,BMP(bitmap)图片使用一种无损压缩算法可以保证还原全部像素点,可是图片大小过大,因此相同像素规模的图片若是使用BMP格式,须要的存储容量就更大,并且不利于进行网络传输。再例如,PNG图片除了能够存储24位深,还能够存储额外8位甚至16位的Alpha通道描述来表示每一个像素点的透明度,可是JPG图片却没有这个特性,它只支持24位深度。数组

那么什么是GRB呢?这些图片格式的每个点都由三个基础色进行描述: 红(Red)、绿(Green)、蓝(Blue),经过三原色之间的比例就能够调制出不一样的颜色,将必定规模的颜色点组合起来就是一张图片了。通常来讲计算机为每个原色准备了8位bit进行描述(就是1个byte),三原色就是24位bit,而且大多数图片的颜色深度就是24位。缓存

这里写图片描述

这样看来24位颜色深度的一个图片点,最多能够记录256 * 256 * 256 = 16777216种颜色。那么相似PNG图片使用的32位/48位颜色深度有表明什么意思呢?多出来的8位/16位为了记录这个点的可见度(透明度),这个记录范围的数值又称为Alpha通道。这样同一个颜色配合不一样的可见度就能够知足更丰富颜色展现要求。另外,这也是JPG文件不能支持透明度的缘由——它的格式规范中不带有对Alpha通道的支持。服务器

这里写图片描述

另外因为一些图片类型也更浅的位深,因此一些图片为了减小极端状况下的使用/传输空间,也会使用16位/8位的RGB描述模式。例如使用16位RGB模式时,红色位描述为5个bit、绿色位描述为6bit、蓝色位描述为5bit。在Java原生的图片处理模块中,使用TYPE_USHORT_565_RGB、TYPE_USHORT_555_RGB标识对16位RGB模式进行描述。网络

2-二、JAVA对图片进行处理

JAVA模块中带有的图片处理功能(Java Image I/O API),可以支持对JPG、PNG、BMP、WBMP、GIF格式的文件进行读写操做,并且能够支持到单个像素点级别的操做——也就是说读者能够经过这套API对具体的某一个RBG描述信息进行操做。经过这套API要完成在图片上添加形状、缩放图片、裁剪图片等操做也很是简单,只须要几行代码。首先咱们来看一下Java Image I/O API中会使用的几个概念,以便后续进行代码编写:多线程

2-2-一、BufferedImage对象

从这个类的直观名称能够理解成被缓存的图片信息,它将一张图片通过格式分析后,放置在内存中的一个可访问区域。开发热源能够从这个可访问区域提取到不少关于这张图片有用的信息,例如能够取得ColorModel表示的颜色份量,里面包括了每个像素的RGB信息和Alpha信息;还能够取得Raster表示的像素矩阵,经过它能够读取一个范围内的像素点。开发人员对这个区域进行读写操做,实际上就是对这张图片进行像素级别操做。如下代码能够加载一张位图到BufferedImage中:架构

...... BufferedImage srcImage = javax.imageio.ImageIO.read(new File("mmexport1444022819048.jpg")); ......

须要注意的是,BufferedImage一旦被被加载其像素规模就不能改变了,例如您最初加载了一个800 * 600的JPG文件到BufferedImage中,在操做过程当中您就不能将这个BufferedImage缩小成600 * 400的。若是要这样作,您只能建立一个新的BufferedImage,并将进行缩放后的计算结果加载到这个新的BufferedImage中。如下的方式能够建立一个空的BufferedImage:并发

...... // 如下代码建立一个600 * 400的BufferedImage BufferedImage outputImage = new BufferedImage(600, 400, BufferedImage.TYPE_INT_RGB); ......

注意最有一个参数BufferedImage.TYPE_INT_RGB,它指定了BufferedImage须要支持的RGB规则。TYPE_INT_RGB表示使用一个int数值表示24位深度的RGB规则,TYPE_USHORT_565_RGB表示,使用一个short数值,表示16位深度的RGB规则,其中红色占5位,绿色占6位,蓝色占5位。再例如TYPE_INT_ARGB和TYPE_4BYTE_ABGR分别表示以int数值或一个4位的byte数组表示ARGB规则,换句话说就是这个BufferedImage支持图片透明度的表示。

最直观的理解就是,BufferedImage至关于在内存区域中的画布,这个画布上能够有一张或者多张图片,也能够没有任何图片。你能够在画布上对每一个像素进行读写操做,可是你不能改变画布的大小

2-2-二、Graphics对象

Graphics类抽象一点说是进行图形操做的上下文控制的类,具体一点说是图形画笔工具。经过这个类(以及它的子类)开发人员能够方便的在画布上绘制不一样的规则形状、图片或者文字。

这里写图片描述

上图中呈现的Graphics子类结构能够支持不一样的绘图场景,例如Graphics2D类基本上能够处理全部主流2维位图的画布绘制,在咱们的图片服务系统中使用最多的画笔类也是它。如下代码能够在画布上绘制一个规则的矩形,并填充颜色:

...... // 建立一个100 * 80 的画布 BufferedImage outputImage = new BufferedImage(100, 80, BufferedImage.TYPE_INT_RGB); // 得到这个画布的画笔,也可使用createGraphics建立画笔 Graphics graphics = outputImage.getGraphics(); // 从画布上10,10的坐标开始,绘制一个60 * 40的矩形 graphics.setColor(Color.RED); graphics.drawRect(10, 10, 60, 40); // 处理 graphics.dispose(); ......

输出到文件,就能够看到如如下所示的效果了。

这里写图片描述

红色矩形在画布上被绘制出来,但为何初始化的画布是黑色呢?这是由于咱们使用的BufferedImage构造方法将使用RGB=‭0000 0000 0000 0000 0000 0000的数值进行每一个像素点的初始化,实际上就是黑色的RGB值。咱们换一种方式进行BufferedImage的初始化,就能够将BufferedImage中的像素点初始化成白色:

......
int width = 100, height = 80;
int size = width * height;
int[] pixels = new int[size];
// 如今设置每个像素点的RGB值为白色(整数的表示就是‭16777215‬,16进制的表示就是FFFFFF)
for(int index = 0 ; index < size ; index++) {
    pixels[index] = 0xFFFFFF;
}
// size就是像素规模大小
DataBuffer dataBuffer = new DataBufferInt(pixels, size);
// 初始化的Raster类,就是像素矩形数组的封装
WritableRaster raster = Raster.createPackedRaster(dataBuffer, width, height, width, new int [] { 0xFF0000, 0xFF00, 0xFF }, null );
DirectColorModel directColorModel = new DirectColorModel(24, 0xFF0000, 0xFF00, 0xFF);
// 生成 BufferedImage, 这样BufferedImage中的每一个像素就是白色了
BufferedImage outputImage = new BufferedImage(directColorModel, raster, true , null );
......

2-三、JVM进行针对性优化

上文多处已经提到,图片处理操做是计算密集型操做,很是消耗CPU资源和内存资源。而JAVA Image I/O API又是基于java进行的图片像素级操做,其处理性能自己就不及C/C++。因此对JVM的内存优化就显得很是重要了。这里咱们假设读者已经知道了JVM的基本构造,直接讲解JVM的几个优化注意点:

  • 关于-Xmx最大堆内存:虽然咱们假设的图片处理场景是一个百万级PV的中等电商平台/对C端系统,因此单个图片服务系统单位时间内须要处理的图片请求数量也是比较大的,首先建议设置Xmx内存大小在8+GB或者设置内存大小为操做系统可用内存的60%以上。注意也不能太大了,这要依据您的CPU性能设定,不然就会出如今时不时进行full gc出现明显卡顿现象——CPU性能不够形成full gc耗时过长。

  • 关于回收器的选择:回收器的设置是最关键的,咱们知道在早期的JDK版本中提供的回收器,都是采用中断用户线程的方式进行,不管是针对新生代的Serial、ParNew仍是针对年老代的SerialOld,都是这样。但若是在高并发环境下,若是出现用户线程的停顿现象,就会在很是短的时间内形成大量请求等待,严重影响服务器的处理效率。因此对JVM的优化要特别注意这个点,建议采用JDK 1.7+ 64位以上的运行版本,并设置JVM到server模式

    首先是指定年老代使用的回收器,这个推荐使用标记-清除算法的CMS(响应时间优先回收器)就行了,它会在尽可能保证用户线程运行的状况下对待回收区域进行屡次标标记回收。还要注意,CMS回收器并不能保证在回收时用户线程绝对不中止,而是使用两次短暂的挂起操做取代以前回收器使用的一次较长的挂起操做。另外注意,CMS的线程数量有一个默认值,这个默认值是(cpu内核数量 + 3)/4。虽然这个数量是能够设置的,可是笔者并不建议本身去设定这个值,而是保证您的操做系统上至少有8个或以上(16个最佳)CPU内和数量(若是达到或超过24核,就要经过-XX:ParallelGCThreads参数控制一下了),-XX:+UseConcMarkSweepGC参数可开启CMS。

    接着是新生代使用的回收器,若是您指定了年老代使用回收器为CMS,那么新生代的回收器就不能使用ParallelScavenge回收器了(吞吐量优先回收器),由于两种回收器不兼容。首先CMS也支持 可使用ParNew回收器,这是一个单线程Serial GC的一个多线程版本,虽然在进行GC时会出现用户线程挂起的状况,但因为它是多线程的版本且咱们存储的数据特色,决定了ParNew不会出现太大的性能瓶颈。

  • 关于年老代和新生代的比例问题:虽然咱们常说JVM优化的目的减小移动到年老代的对象数量和减小full gc的状况,但因为咱们在内存中将要存储和操做的数据比较特别——图片文件数据,形象来讲是BufferedImage对象、较大的byte[]数组对象。这首先些对象的特色是数据量较大,一张原始图片小则500KB,大则会达到1MB(不会再大了,由于咱们对上传文件的大小作了限制)。其次图片处理速度较慢,一张颜色深度在24位大小在1MB的JPG文件,等比例缩放成一张200KB的图片耗费的时间在200ms左右,甚至有时会超过500ms(看CPU性能了等客观因素了)。因此这样的数据存放存在年轻代区域,就会形成很是频繁的复制操做和向年老代的移动操做。这样咱们的优化思路就须要有针对性:减小年轻代的内存空间,并设置一个阈值,在图片数据超过这个阈值时就直接将对象放入年老代,而后在年老代中由CMS GC进行回收。例如当JVM堆内存数量为8GB时,能够经过-Xmn参数设置年轻代(eden+ 2 survivor space)的空间大小为1GB;经过-XX:PretenureSizeThreshold=<byte size> 参数设置直接进入年老代的对象大小值;经过-XX:CMSInitiatingOccupancyFraction=50参数设定当年老代已使用的内存大小达到50%时,开始CMS GC。

  • 关于持久代的问题:在咱们的图片服务中,JVM的持久代并不会存储太多数据,特别是咱们的工程中须要加载IOC容器的class信息并很少,且常量信息也很少的状况下。何况在JDK Version 1.8+ 的版本中JVM模型已经取消了对持久代的支持。因此持久代并不须要作太多针对性的优化,最后JDK Version 1.8+ 也是本文推荐使用的。

2-四、其它说明

除了JAVA 提供的JAVA Image I/O API之外,还有一些基于JAVA开发的第三方图形组件,不过若是您搜索Goolge就会发现不少第三方图形组件已经没有再维护了,若是各位读者有兴趣能够进行研究:例如Java Image Filters、JMagick等。

JAVA提供的JAVA Image I/O API 图片处理工具虽然能够进行像素级别的操做,可是相对于C/C++提供的图片处理性能来讲仍是较弱,那么要进行图形高效运算的语言基础仍是C/C++为宜。目前流行的2D和3D图像处理软件也可能是基于C/C++构建,例如OpenGL、DirectX等。另外图形处理都是CPU密集型工做,对计算机的运算资源和内存资源要求都比较高,目前的发展趋势是使用专门的GPU代替CPU进行运算,这也是为何不管是咱们使用JAVA Image I/O API仍是基于Nginx的Image模块为系统提供简单的图片处理功能,对CPU要求都很是高的缘由。

============================= (接下文)

相关文章
相关标签/搜索