千万级图片服务器架构与设计

原创精品,请慢慢品尝,手机阅读体验也不错!大屏电脑放大到125%阅读效果更佳!

写在前面

曾在中国N0.1的相亲平台待过一段时间,也是收获满满的。那时电视相亲节目好火,如江苏卫视的《非诚勿扰》,东方卫视的《百里挑一》或贵州卫视的《爱情保卫战》等,应该是火遍了大江南北吧,反正街边随便个大妈肯定都略有所闻。当然我们平台的生意也很火,印象中,会员费从399元飙到3999元,高级会员甚至去到9999元,嘿,你还别说,充值的人还挺多的,没记错的话,季营收已超过1亿RMB,真的好猛。后来去了YY,知道我背景的人,也经常问我,有没有近水楼台先得月,老婆是不是这个平台找的,我说,这真没有,老婆是我老妈介绍的。当时28吧,还单身,老妈急得很,我老姐又添油加醋说,过年一定叫他去相亲,到时候找个外省妹回来,一把辣椒要你命。我妈听后,吓个半死,在酒席间知道谁家有女后,直接杀到他家,三下五除二,就搞定了电话,后面的臭事就不分享了。不过我妈也是个人才,她在农村凑合了25对夫妻,了不起的成就,逢年过节都过来走下门,也挺热闹的。不过我也不差,凑合了三对,其中一对男方还是清华大学的,算是对得起祖传家业了。想想也有意思。不过话又说回来,刚面试相亲平台时,也觉得婚姻介绍所有点Low,但老妈说,媒人大过天,亲生爹娘在二边,这么伟大的事业,为什么不去呢?后来就真去了,说起来也是缘分。在这里待得也不错,技术与管理都在这里得到了成长。从我角度来看,大概就核心三大块:1、广告系统 ; 2、用户系统及主站系统; 3、红娘销售管理系统。不过讲完三大块的话,有点耗时,所以,再挑细点,也是社交平台重要子系统:图片服务器,一个无处不在的应用。

温馨提示:本文章属纯技术分享,不涉及公司具体业务,本文按我现在的理解重新设计。


1、需求原型

百花有百眼,条女说得再怎样漂亮,靓出花,酱爆镜,不如放张照片上来,特别是张高清的照片,更是起到关键的作用,当然,如果能自适应去掉一些皱纹,斑点,再磨下皮就完美了。不过,萝卜青菜各有所爱,武状元喜欢的还是石榴姐如花,虽然大家都说秋香姐最漂亮,所以照片真实也很重要,总不能因为美颜过度的照片,错失了真正想要的灰姑娘或王子。最后这里是相亲平台,不是交友平台,一切依归于结婚为目的,照片也不能搞得太花,太露,严肃点。

看完上面这段话,挺美好的,第一个反应就是,是不是有一个刚刚好的照片美颜功能,我也想,但实力未到,你也知道,我们是后端出身,不是专业的数字图像处理专家,说白了,就是找到合适的开源框架,再调用API,然后整合出产品功能,就有点像现在的AI技术,大家也都是调用API,组合出一堆参数,然后美其名曰自研,真正发明AI算法的还是少吧(研究型公司除外)。但我们也是起步阶段,能整合出这么一堆复杂的东西,也算不容易了,至少你没有经历过的话,你想不到这样做。最后,当年还是2010年左右,AI技术也还没有流行,也没有阿里云,腾讯云这些基础设施平台。如果真有这些平台,美颜效果虽然不敢保证100%满意,但至少图片审核会省点事,当年用户上传照片还是人工审核,上传一张审核一张,不过我发现某直播公司已经具备一定的AI审图能力,先用机审,再过一遍人审,效率确实提升了不少。

也正是基于这种技术现状,我们能做的就是,保证图片缩放不变形,而且清晰度上不能打大折扣,不过也满足了95%以上的使用场景。也算对得起真实,真实才是美麻。在做了上面这些点后,就要发挥我们后端技术的力量,怎样做到千万级别的流量访问,怎样节省成本等这些问题了,这又回到了我们熟悉的领域。

说了那么多,看下真实的需求吧,相亲平台,首先要支持大图,但各种列表,聊天窗口,推荐列表等,又要支持各种小图标,所以就存在一张图片,要生成不同尺寸的缩略图。相信这里,大家很容易想到头像,但相亲平台,照片比头像复杂得多了。特别是后面到集团创新公司搞项目,缩略图就变得更多样化了,下面是一些真实的案例。

照片类型


2、图片编辑器

从上面需求,可以看到,一张图片可能被切成不同的尺寸,所以得找个图片编辑器,这个比较重要。当然,我们知道Java也自带有图片处理器,但性能上没有ImageMagick(简称IM)好(做集团创新项目时测试过,缩略1000张图片的总时间,IM速度快很多,应该是IM充分利用多CPU并行计算技术),这是以前测试的结论,不知现在新版本的JDK性能如何,而且ImageMagick有一些简单的特效与滤镜(专家可以搞复杂的效果,如Photoshop的滤镜效果),可以一定程度上美化图片,也是我们需要的。


2.1、什么是ImageMagick

看下官网简介还是需要的,它支持非常多的图片格式,图片编辑功能也足够强大,还有一定的特效,文字与画图等功能。除了这些,还支持多语言程序接口,业界良心,如调试环境使用命令行CLI,生产环境使用Java、C、C++、GO、Python类库等,灵活性好高。官址是:https://imagemagick.org/script/index.php,描述摘要如下:

Use ImageMagick® to create, edit, compose, or convert digital images. It can read and write images in a variety of formats (over 200) including PNG, JPEG, GIF, WebP, HEIC, SVG, PDF, DPX, EXR and TIFF. ImageMagick can resize, flip, mirror, rotate, distort, shear and transform images, adjust image colors, apply various special effects, or draw text, lines, polygons, ellipses and Bézier


2.2、图型变换

图型转换,这个我们最关心,也是图片服务器最核心的功能之一。1、如一张大图等比缩放成一张小图; 2、如一张大图片先固定宽度,高度再等比缩放; 3、如图片等比缩放到刚好被某宽度与高度正包围。基本就是上面三种变换,实际上,当时搞了6种图象变换,如图片缩放到一定比例,再截取中间的部分或剪掉多余部分等。现在的AI技术可以识别出人脸,如有需要仅截取人脸部分(信息最重要的部分),也挺高级的,后面有机会,也玩下。


2.2.1、软件安装

现在的新版本安装就是爽,还提供了不用安装版本,直接下载解压,开箱就可以用,太爽了。以前旧版本全部是源代码安装,第一次安装,真的要哭,缺这包少那库,安装完又发现某类库不兼容,改好,又发现字体缺失,花你很长时间,但现在新版本很方便,就二行命令即可用。

#这个下载大概要花20分钟
wget https://imagemagick.org/archive/binaries/magick
#给权限
chmod +x magick
#执行
magick -version

如,下面是我刚安装好的软件调试信息:

$ ~/magick -version
Version: ImageMagick 7.1.0-47 Q16-HDRI x86_64 d91623c12:20220827 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI OpenMP(4.5) 
Delegates (built-in): bzlib djvu fontconfig freetype jbig jng jpeg lcms lqr lzma openexr png raqm tiff webp x xml zlib
Compiler: gcc (7.5)


2.2.2 缩放基本操作

下面是从蜂鸟网找的一张公开图片,尺寸是400x500,将以这个图为基础,演示IM对图片缩放的基本操作,让大家对IM有简单了解。

raw

2.2.2.1、对图片进行等比缩放。如缩小50%,相应命令是-resize '50%',如源图片叫law.jpg,输出图片叫raw_50P.jpg,则整个命令是:

magick raw.jpg -resize '50%' raw_50P.jpg

原图尺寸是400x500,缩放50%后,理论结果是200x250,运行上面命令后,输出见下图,刚好就是200x250,符合预期。那什么时候使用这种场景呢?也还挺多的,如爬了好多第三方的网页数据,其中就包括了图片,但把他们转换到手机WAP页面时,把宽度超过800的图片全部批量进行50%缩略,文件更小,更适合手机浏览,这就派上了用场。

raw_50P

2.2.2.2、图片宽度固定,高度自适应等比缩放。如宽度缩放到100,相应命令是-resize '100',如输出图片叫raw_100x.jpg,则整条命令是:

magick raw.jpg -resize '100' raw_100x.jpg

原图尺寸是400x500,宽度变成100,相当宽度缩小到了4倍,那么高度也缩小到4倍,理论结果是100x125,输出见下图,刚好就是这个尺寸,符合预期。这种类型的缩略图,应用非常广,像我们手机坚屏看新闻时,或看资讯时,都得把图片宽度固定,高度自适应,我们往下拉时就能看更多,符合用户使用习惯。

raw_100x

2.2.2.3、等比缩放到被某宽度与高度正包围。使用场景也非学广,如固定窗口大小的图片浏览,像上面需求原型里的查看个人资料的大图浏览就是这样,也就是说刚好被宽度与高度正包围,也是一种广泛应用。下面将举三个例子说明,分别是缩放到100x100,100x200,200x100等,操作如下:


100x100例子:

magick raw.jpg -resize '100x100' raw_100x100.jpg

原图尺寸是400x500(又叫原图尺寸),被100x100正包围(又叫计划尺寸,红色圈表示),理论结果是80x100(实际尺寸),输出刚好也是这个尺寸,符合预期。

raw_100x100

100x200例子:

magick raw.jpg -resize '100x200' raw_100x200.jpg

原图尺寸是400x500,被100x200正包围,理论结果是100x125,输出刚好也是这个尺寸,符合预期。

raw_100x200

200x100例子:

magick raw.jpg -resize '200x100' raw_200x100.jpg

原图尺寸是400x500,被200x100正包围,理论结果是80x100,输出刚好也是这个尺寸,符合预期。

raw_200x100

小结:上面的三个例子,有特殊安排,可以认真观察下。如原图尺寸、计划尺寸与实际输出尺寸三者之间的关系。可以发现,实际输出尺寸至少有一个维度(宽或高)是满足计划尺寸的(我叫正包围),如计划尺寸200x100,实际输出是80x100,这里的高100是相等的。因为直接使用API,已经帮你计算好计划尺寸与实际尺寸的转换,如果是其它软件或自己计算实际尺寸,这时候,就得学会如何计算它们的变换算法,你可以想一下怎样实现正包围算法。

2.2.2.4、固定大小,背景填充。像上面的例子,最后输出的实际尺寸总是等于或小于计划尺寸,也可以让二个尺寸相等,就让不够的部分填充某个背景色吧,如下。

magick raw.jpg -resize '300x200' -size 300x200 xc:'#660033' +swap -gravity center -composite raw_300x200_fix.jpg

输出如下,整个图片是300x200,但照片部分是160x200,不足部分填充玫瑰红,但真实场景一般不这样搞,代替方案是,用CSS增加背景颜色,再中间对齐即可,还省玫瑰红的图像空间,如下图。

raw_300x200_fix

2.2.2.5、增加水印。看是否有需要,可以在图片某些位置加上文字或图片水印,简单起见,先加文字水印,如打上beautiful这个单词,其中-font是字体,运行前你最好执行magick -list font看一下支持那些字体,-draw就是要写的水印文字,代码如下:

magick raw.jpg -resize '400x300' -size 400x300 -font "DejaVu-Serif" \
-fill white -stroke white -gravity SouthEast -draw 'text 10,10 "beautiful"' raw_400x300_watermark.jpg

生成的图片如下:

raw_400x300_watermark.jpg

除了生成水印,也可动态生成一些模板图片(如下面的奖状,名字与日期是变的,其它的是不变的),但最好跟核心图片服务器代码分开,因为这类需求可能变化非常快,要经常发布,经常发布对稳定的图片服务器来说,不是一个好办法。又或者这类需求使用Java来写,也OK,因为Java写起来简单点,下面第一个是模板,第二个是执行命令后合成的图像:

magick photo-server-002.jpg -font "DejaVu-Serif" -fill red -stroke red -gravity SouthEast \
-draw 'text 80,30 "10"' \
-draw 'text 145,30 "09"' \
-draw 'text 200,30 "2022"' \
-pointsize 32 -draw 'text 220,170 "friend"' share_award.jpg

效果如下,红色的friend与日期是动态生成:

photo-server-002 share_award

2.2.2.4、其它杂项。identify 查看图片的信息,代码如下:

magick identify raw.jpg 
raw.jpg JPEG 400x500 400x500+0+0 8-bit sRGB 53657B 0.010u 0:00.007

sharpen锐化图像,如下面锐化1像素:

magick raw_400x300_watermark.jpg -sharpen 0x1.0 raw_400x300_watermark_sharp.jpg

第一张是未锐化的图片,第二张是锐化1像素后的图片,可以细心对比一下,还是有效果的,如头发部分就相对明显点。

raw_400x300_watermark.jpg raw_400x300_watermark_sharp


2.2.3、API调用

上面介绍了IM的基本命令,知道了它的基本功能,能做什么,那些场景使用等,但真正使用的是各种语言的API,见官网:https://imagemagick.org/script/develop.php,这些API也是需要学习成本的,不能大意,但写文章时,访问不了Jmagick,所以这部分快速略过,Coding时,记得翻阅。如Java的就有二个API类库,Jmagick或Im4java,根据实际情况选择使用,不过理论上Jmagick性能更高。当时我们使用的是JMagick,但这个类库也有些缺点,跟最新版本不同步,维护进度慢,而且还存在内存泄露,跑段时间就变慢或挂机,得定期重启,如一个月重启一次,新版本的没有试过。然后Im4java主要保持与命令行的代码风格,它会生成相应的命令再去IM执行,学习相对简单点,版本升级对代码变动不大,能追新版本,但性能没有JMagick好。


3、工程整合

上面已经进解了图片编辑器的功能,可以看到,IM的功能完全满足我们应用场景,接下来我们将讲解,如何将它应用到生产中去,如何做到流程整合,如何顶住千万级别的流量等问题,看完本节,相信你也能架构与开发出比美大厂的图片服务器了,是不是很开心啊。


3.1、生成时机

用户上传了图片,如果要生成缩略图,什么时候生成比较合适呢?生成时机主要有二种方法。第一种:后台批量生成,等图片审核通过的时候,批量生成最小集的缩略图; 第二种:实时请求生成,等用户真正请求缩略图的时候,如果发现没有相应的缩略图,再生成。


3.1.1、后台批量生成

一般来说,本方法适合头像类的业务,如审核通过的时候,在后台批量生成50x50,100x100二种规格的缩略图(这里尺寸只是举例,根据实际情况来定)。当它被访问时,因为已经存在,直接读取即可,响应时间非常快,但这种图片不能生成太多,怕浪费空间,因为很多照片可能没有被访问。

批量生成


3.1.2、实时请求生成

像用户相册,旅游相册或文章相册等,这种图片,量比较大,而且尺寸规格也是多种多样,没有必要提前生成,可以等用户访问时,再动态生成即可。要支持这种机制,服务器URL要做一定的处理,下面我们就定义规则。


3.1.2.1、URL规则定义

/用户id/类型/参数/原文件引用名.jpg

>>>用户id,就是创建图像的用户。

>>>类型,就是缩略图方案,可以定义一个数字来表示,如1,2,3。
如:
1、表示固定尺寸(不足部分不会补); 
2、表示固定宽度,高度自适应;
3、固定尺寸,不足部分补玫瑰红。
三个都是上面演示过的方案,要增加缩略图方案,增加多一个数字即可。

>>>参数,就是上面类型对应的参数字符串。
如:
第一种类型支持的参数有`50x50`,`400x300`等,
第二种类型的参数有`50`,`120`等,
第三种类型支持的参数有`800x600`,`1000x500`等。

>>>原文件引用名,表示原文件的的引用名,一般是服务器存储名字,
可随机分配一个UUID字符串或数字给它,如上面的`4568751`。

如用户892创建的图像,原文件引用名是4568751的图片,

那么请求一个头像的URL可以是这样:/892/1/50x50/4568751.jpg

访问宽度是300,高度自适应的URL是:/892/2/300/4568751.jpg

访问固定尺寸400x300,不足部分补红色的URL是:/892/3/400x300/4568751.jpg

URL规则主要作用是,能根据这个URL找到服务器的某一个图片文件,相当是一种映射。


3.1.2.2、服务器Filter

定义好了URL规则,当用户访问某个符合规则的URL时,服务器能解析出相应的数据,然后再映射成目标图像文件名,如果存在这个图像,则直接返回,如果不存在,再根据规则数据生成相应图像,保存(下次访问就有了),再返回。还是以上面三个切图类型来做说明,大概流程如下:

filter

我们按上面规则跑一次流程吧,浏览URL: /892/1/50x50/4568751.jpg,服务器解析出{"userId":892, "type": 1, parameters: "50x50", fileId: "4568751"},然后映射到的图像文件名叫:892_4568751_1_50x50.jpg,拿这个文件名到相应的存储位置找,如果能找到,则返回这个图片。如果找不到,那再判断是那个类型,这里是类型1,然后根据类型1的切图规则生成缩略图并保存,再返回这图即可。

知道规则流程后,实现相应框架的Filter即可。如Servlet的框架,继承Filter父类即可。如Springboot也有相应的Filter类,实现子类即可,技术细节不再展开。不过有一个提醒点,类型下的参数要有一定约束,如我们平台仅支持"100x200", "400x300",那用户乱传的"150x100"就不允许,否则可能生成很多没有用的图像数据。


3.2、文件存储结构

上面已经说明了图片处理的核心部分,但文件存储部分也同样重要,好的文件架构,对后期维护与升级都会有非常大的帮助。当时的主要考虑点有:1、缩略图可能因不同需求,随时被清除,所以原图与缩略图得分开存储。2、考虑到照片的数量会越来越多,所以类似数据库为分表分库技术也被借用到这里,大概结构如下。

files

下面以userId为817的用户,上传一张照片,假设生成的文件引用名叫:54545782,则求得相应文件位置计算过程如下:

1、817 % 2 = 0
2、817 % 512 = 305
3、文件属于共享A硬盘(第一条结果为0)
4、存储第一层等于原图(law),因为这里就是保存原图
5、存储第二层等于0,因为是原图;
6、存储第三层等于305(第二条结果为305)
7、存储第四层等于817_54545782.jpg(原图文件名等于:用户id_文件引用名.jpg)

则最终保存的位置是:/data/a/law/0/305/817_54545782.jpg

还是以这个用户为例,查看他一个类型为3的500x400缩略图,则计算过程如下:

1、817 % 2 = 0
2、817 % 512 = 305
3、文件属于共享A硬盘(第一条结果为0)
4、存储第一层等于缩略图(thumbnail),因为这里就是缩略图
5、存储第二层等于3,因为是类型3缩略图;
6、存储第三层等于305(第二条结果为305)
7、存储第四层等于817_54545782_3_500x400.jpg(缩略图文件名等于:用户id_文件引用名_类型_参数.jpg)

则最终保存的位置是:/data/a/thumbnail/3/305/817_54545782_3_500x400.jpg

按上面文件结构存储,还有如下特点:

1理论上原图数据不能删除但缩略图数据可以删除删除后可以重新生成 
2早期切图算法效果不理想如没有锐化但后期优化了则可以删除缩略图数据又会重新生成照片 
3A盘与B盘是相互按天备份数据如果某一个盘坏了可以灵活设置挂靠盘提升数据恢复能力与速度


3.3、千万流量的秘密(CDN)

图片算是一种大静态文件,如果用户离服务器很远,那么下载速度肯定不快,所以要上线CDN,设置好适当的缓存策略,如图片在浏览器端可以缓存7天,然后CDN回源可以缓存1小时,除了响应码是200外,其它不缓存等。就这样简单,就顶住90%以上的请求量,就算千万级别图片PV量,也轻松应对,当然CDN是收费的,记得及时看贴单。

cdn


总结

读大学时,也搞过一些图片处理类的程序(2004年参加大学生软件设计大赛,使用JAVA对图片进行缩略,然后显示到J2ME与WAP页面),但都是纯Java的应用,根本不知道世界还存在像ImageMagick这么牛X的类库。来相亲网后,发现他们使用一份跟腾讯有渊源的图片处理方案,使用的是ImageMagick,但这份代码是专人负责,而且有一定保密性,代码是没有跟主站系统一样开放的,大佬说这是相亲项目的核心资产。后来去集团创新项目后,简单了解了一下他们架构思想,也跟华哥学习了二招,然后自己研究学习去了。当实现的时候,发现也不难,但特别有成就感,因为当时文档超级少,得看他们example调试来调试去,各种尝试,应该也有点运气了,能调通。成果产出后,老板经常拿来跟QQ空间对比,有二方面:一是图片上传时间; 二是图片效果。我们二方面都是获胜的,也给老板吹了一会牛B(但不代表我们技术比他们牛)。特别记得,老板某次把旅游的照片上传到我们平台,发现显示效果挺好的,特别清晰,因为我对旅游照进行了1-2像素的锐化,还有高光部分进行压暗一点,他特别喜欢,还真以为我们比QQ空间相册高级了(QQ空间不做任何处理,算是尊重事实,也是一种美),有点意思。不过,现在很多云平台也提供图片服务,但缩略图收费比较贵,而且没有定制功能(像锐化、色彩或饱和度等),所以有时候自研个性化的图片服务器,也是有必要的,希望本文对你有帮忙,2022年中秋节快乐。