1、概述
图片服务系统是各种针对C端系统常见的子系统,它的特点是存储规模大请求频度高,且单张图片的读请求远远高于写请求。后续几篇文章我们将从图片服务系统的需求分析开始,一起来讨论如何进行这类系统的技术选型、概要设计和详细设计,以及在这个过程中需要关注的技术难点。
虽然由于写作计划的变化,图片服务系统中所涉及的分布式文件系统原理、非关系型数据库原理都还没有讲到,但这些知识点也并不是组成整个图片服务的所有关键点,并且后续的文章中我们会尽快补上对这些知识点的介绍。图片服务系统的讲述会分为四篇文章,前两篇文章我们主要分析图片服务的需求、架构选型和技术方案,后两篇文章进行性能优化和详细设计部分的讲解。由于不能讲解全部的详细设计,所以文章会在最后将整个示例工程的源代码放置在网络上,供有兴趣的读者进行下载。
2、需求场景
首先我们给出使用这个图片服务的场景,以便后续内容中根据这个需求场景进行初步架构设计、详细架构设计、技术选型,以及服务中各个功能模块的设计等工作。这是一个中等规模的针对C端的电商服务站点,日平均PV量在百万级别(在国内属于中等规模的站点,例如从Alexa查询到大众点评的日均PV大致为180万、美团日均PV大致为70万、JD日均PV为7500万)。电子商务类型的站点,其特点是一张页面上会出现多张图片,而且基本上不会使用原图而是大量使用缩略图;另外,多个用户经常同时访问同一个页面,所以不但需要使用缓存,而且缓存一般都有多级,否则会产生大量重复的物理I/O请求;最后,这样的电商平台图片服务都属于顶级业务服务,图片规模基本都是以TB计算,小一些规模的也有几个TB,规模更大的可能达到PB级别。
产品团队针对图片服务的基本功能要求是:需要 支持单张或者多张图片上传 ,这样可以为站点运营人员/商户增强编辑体验,提高工作效率。另外需要在一些特定场景支持水印功能和图片特效功能,但这是优先级较低的功能,而 图片的动态缩放 则是必备功能,这是 因为在站点的各种页面上都基本上不会使用原图 而是使用各种等比缩小或中心截取的缩率图片,另外一个原因是一张图片会在各种客户端设备上显示,最常见的是IOS、Android和浏览器,而这些终端设备对图片的像素要求都是不一样的。产品团队针对图片服务的另一个基本要求是 支持图片访问统计和报表 ,这个功能倒不是给终端用户使用的,而是给站点的运营团队准备的。通过访问统计和用户上传图片的报表统计,运营团队可以针对这些基础数据掌握每月的图片热点,而且这些数据可能还会为后期的数据分析所使用。
接着在与需求团队的沟通过程中,技术团队还推导了一些重要的非功能性需求,例如该图片服务属于整个站点的一个顶层子系统,所以需要保证7 * 24小时不间断运行,换句话说至少需要99.999%的稳定性;另外,图片服务的设计需要满足现有图片数据的割接,最好能够实现原有系统特别是存在在磁盘上的图片的无缝割接;最后,整个系统至少需要30%的冗余存储空间,保证技术团队有足够的时间和空间进行后续的技术改造。。。
3、技术问题和选型
那么图片处理系统要关注的主要问题是什么呢?首先是高并发下的图片处理性能问题。图片处理是一项计算密集型操作,例如目前流行的图片缩放算法就有临近值法、双线性插值法、多项卷积法等,图片锐化算法多为拉普拉斯锐化法,图片增强算法可以采用中值滤波法、直方图均衡法或者幂变换法。这些算法都对CPU计算资源和内存资源有较高的要求,特别是高并发请求背景下。而JAVA语言相对于C/C++语言在各种算法执行效率上又有先天的劣势,所以如果不能在性能上进行弥补就需要找到其它在高并发请求背景下加快图片处理效率的办法。其次是图片存储的问题,和存储稳定性安全性的问题。产生这个问题的原因已经在上文中进行了说明,这里就不再进行追溯了。接下来技术团队根据以上这些需求点和技术点,首先圈定了一个图片服务的概要架构设计:
外层组件表示那些可以和其它服务系统共同使用的组件,以及那些可以从第三方花钱购买的不需要技术团队关注的组件,上图中给出的示例包括:LVS、DNS/智能DNS、CDN等组件。接下来我们对上图中的各个服务层/服务模块进行选型分析。
3-1、分布式文件系统选型
分布式文件系统的选型是整个图片服务的重点,因为它提供了所有图片物理文件的存储基础。在分布式文件系统大量应用于生产环境前,解决此类问题一般使用网络块存储方案,例如IBM Storwize V5000 单柜提供24个磁盘位,单柜支持最大72TB存储容量。但这个方案的缺点是单位容量价格较高,另外虽然这各方案也可以较容易扩容,但是扩容费用也是极高的,基于单位建设成本的考虑目前互联网公司都倾向于使用分布式文件系统甚至现成的云存储服务。如果采用分布式文件系统,那可以用的选型就太多了。这里我们先列举几种常用的分布式文件系统方案,然后针对图片文件的特性,再来对这些方案进行排除。这里我们可选择的分布式文件系统包括:HDFS、TFS、MFS、FastdFD和Ceph。
那么图片服务系统涉及的文件有些什么样的特点呢?首先这些文件相对于视频文件、语音文件而言都不会太大,举个例子来说,虽然目前主流手机相机照相后产生的一张JPEG图片分辨率大致在2500 * 3200像素左右,大小在1MB到3MB之间,甚至更高端的手机照相后产生的JPEG图片文件大小可以达到5MB左右 。但是考虑到网络流量和客户端速度体验等问题,上传到服务器的单张图片大小往往会控制在1MB左右(当然也有特殊的产品需求会要求上传完整的原始图片)。
另外这些图片的特点是读请求远远高于写请求,对大多数图片而言一旦上传到了服务器就不会发生更改操作了,甚至有的图片系统都不会提供对某张图片的修改功能。可是图片的读请求却会很多而且带有一定周期性,什么意思呢?例如一张商品图片,当这个商品刚刚上架或者处于优惠期时,这张图片的读请求会处于一个高峰,而平时这张图片的读请求次数会处于一个平均水平,当这张图片对应的商品下架后其访问量就会很低了,但图片系统并不能想当然的删除这张图片,这是因为可能后端的运营团队后续进行商品管理和统计时,还会用到这张图片。
还有一个特点是,虽然图片系统中单张图片的大小不会过大,但是整个图片系统的文件数量规模却会非常庞大了。就像上文说到的:较小的文件规模也会有几个TB,规模更大图片系统甚至可能达到PB级别。最后,图片系统都应该随时留有足够的冗余存储空间,以便应付至少几个月内系统的扩容要求。
通过以上的分析,大致可以得到图片系统中对于文件存储的部分的要求:高稳定性工作的工作特性,文件系统不能出现系统崩溃的情况——可以出现单节点故障,但是不能整系统崩溃。这个要求决定了文件系统不能只有单个节点,而是多个节点协同工作——分布式文件系统是符合这样选型要求的。另外,文件系统的数据写性能可以不必太优秀,但是数据读性能必须要好,这样才能适应上文描述的读密集型业务。最后,文件系统的扩容应该比较方便,以便减少运维难度。在这个图片服务的示例中,我们将使用Ceph作为文件系统的选型。关于Ceph文件系统的介绍,可以参考其官网的介绍http://www.ceph.com/,在后续的文章中本专题还会专门介绍Ceph文件系统的原理和使用。
3-2、缓存系统选型
多级缓存方案在图片服务中是必然的存在,这是由于图片服务的访问特性决定的。一般来说我们会设计三级——四级缓存,它们分别是客户端缓存,网络层缓存,路由层缓存和服务层缓存。如下图所示:
上图展示了一个四级缓存方案,其中基于Nginx的路由层缓存可有可无(后文会讨论原因),但是另外三级缓存在大多数高压力负载的图片服务中都是必须存在的,无非是采用哪种具体技术组件而已。除了在上文中提到的减少大量重复的物理I/O请求的目的外,在图片服务中使用多级缓存方案还有以下这些原因:
- 增加客户端访问速度:这是多级缓存方案中的客户端缓存和网络层缓存的首要目的,从访问效果来说客户端浏览器如果开启了缓存功能,当请求一张图片时,将从服务端上得到图片信息(CDN相对于浏览器来说就相当于服务器了),HTTP的返回代码就是200;当第二次请求同一张图片时,浏览器会自行判断在缓存区中的图片是否过期,就是判断HTTP协议中的max-age/Expires属性,如果没有过期就直接到浏览器缓存中取得原始图片(如果发现过期就请求服务器,不过这时服务器发现资源没有改变,就会给客户端一个HTTP 304的代码,提示浏览器可以继续使用本地缓存)。CDN的作用是解决服务端到客户端的最后一公里问题,当客户端向CDN请求图片时,CDN会到“离客户端最近的”服务节点去的图片信息,如果那个服务节点上没有相应的图片,CDN才会到真实的图片服务上提取图片并备后续使用。
- 拦截/缓解单个图片服务节点上的请求压力:在真实服务之前的路由层缓存,主要起到的就是这个作用。以Nginx上的proxy_cache为例,我们一般基于它使用内存 + 物理磁盘的方式缓存多种没有过期的静态文件资源,包括图片文件。
- 分散真实图片服务节点上的请求压力:路由层上除了缓存功能外,还起到分散请求压力的作用。无论技术团队在路由层使用的是Nginx、Haproxy或者是Spring Cloud——Zuul,要达到的一个目的就是对下层多个图片服务节点做请求负载。由于路由层的定位问题,所以一些图片服务系统中即使路由层没有提供缓存功能,但也一定会提供负载功能用来分散请求压力。
总的来说多级服务缓存以 最终以缩短客户端响应时间,减少图片服务器上真实的物理I/O操作压力为最终目的 ,经过多级缓存逐级削减向下传递的请求规模达到这样的目的。最终传递到最后一级业务层时,可能只剩下10%——30%的请求数量需要做真实的I/O操作(这还要看各级缓存的选型和详细参数设置)。而作为最后一级缓存的选型,除了读写速度上的要求外还有存储规模上的要求,所以Redis是一个比较理想的选择——利用 Redis原生的Cluster技术 ,既可以兼顾访问速度、稳定性还可以获得很灵活的容量扩充方式。关于Redis Cluster的介绍可参考我另一篇文章《架构设计:系统存储(18)——Redis集群方案:高性能》
对缓存模块的设计和选型非常考量架构师对系统的驾驭能力。举个例子,为了保证数据A在靠近客户端的缓存模块失效时,数据A的访问压力不会直接传导到最后端的I/O请求上,就要保证数据A在下一层缓存上在同一时间不会失效。基于这样的考虑架构师可能需要配合调整每一层的缓存过期时间, 否则就可能出现数据A在最上层缓存失效的同时,所有层级的缓存都已经失效,数据A不得不直接在真实服务器的物理磁盘上重新读取,并重建每一级缓存的情况 。但问题是,如果数据A的更新时间设置得过长,且每一级缓存的有效时长依次增大,那么在数据A真正发生变化后,这个变化可能需要很长时间才会体现在客户端上。所以 并不是缓存层级越多约好,也不是缓存上的数据过期时间越长约好 。
3-3、路由层选择
在图片服务系统中,路由层有两个作用:缓存和负载均衡。这两个作用的介绍已经在3-2小节中已经给出了一个概要说明,这里就不再进行赘述了。基于对上文中需求的考虑, 技术团队主要考虑在两种技术组件中进行选择,它们是Nginx和Spring Cloud——Zuul。本小节内容中我们主要对这两种技术对系统功能的契合度进行分析—— 它们在路由层分别使用时所基于的路由层功能定位完全不一样 !Zuul是Spring Cloud服务治理框架(也称为微服务治理框架)的一个重要组件,它可以单独使用也可以和Spring Cloud中的其它组件集成使用。下表给出了Zuul和Nginx对于图片服务需求各个方面的契合度:
组件 | Nginx | Zuul | 目前图片服务中的要求 |
---|---|---|---|
缓存能力 | Nginx带有ProxyCache模块,可以通过配置非常方便的进行数据缓存 | 需要自行实现 | 为了分散自上而下的数据请求压力,图片服务系统的代理层需要对数据进行缓存。后文还详细介绍这部分的考虑细节 |
反向代理能力 | 反向代理是Nginx的主要功能,配置灵活且性能优异 | 有反向代理功能,虽然配置灵活性没有Nginx好,但是通过编程可以很方便的进行扩展 | 没有特别要求 |
路由能力 | 支持基于正则表达式的路由功能配置 | 自带路由配置功能,支持通配符形式的路由配置。还可以通过Zuul中的filters,扩展符合自身业务需求的路由规则 | 没有特别要求 |
负载均衡能力 | 自带非常强大的均衡功能,支持基于正则表达式的负载均衡配置,支持多种负载均衡规则 | 通过listOfServers关键字,可以配置负载均衡功能,但由于Zuul在SpringCloud的定位问题负载均衡功能没有Nginx那么强大 | 需要较灵活的负载均衡配置能力,对负载均衡功能的性能也有一定要求 |
流量控制能力 | 没有原生支持 | 没有现成支持,但通过Zuul的filters规则可以通过编程非常方便的实现 | 没有特别要求 |
安全控制能力 | 可以实现简单的安全控制功能,例如设置客户端黑白名单 | 安全控制是Zuul的主要职责之一,通过Zuul的filters规则可以通过编程非常方便的实现,另外还可以直接集成SpringCloud的安全控制组件SpringCloudSecurity来完成复杂的安全控制 | 没有非常复杂的要求,后期可能需要对图片盗链问题进行控制 |
监控和日志能力 | 有日志功能,包括访问日志、拒绝日志、异常日志在内的多种日志,可以选择开启也可以选择关闭 | 自带Log4j日志,通过结合Flume等数据汇聚组件可能非常方便的进行日志收集 | 目前没有特别要求,不过后续版本中可能会要求 |
编程扩展能力 | 支持技术人员使用C/C++语言开发第三方Module,并在Nginx编译安装时一并安装。但实际情况是,大家都只会使用一些现成的NginxModule | 本来就是SpringCloud微服务治理框架的一个组件,支持使用JVM系列语言进行开发,您可以通过Zuul为代理层加入任何您想要的功能——如果不讨论软件解耦的科学性。 | 没有特别要求 |
(表完)
从上表我们可以看出,Zuul和Nginx的功能虽然有一定的重合度,但是侧重点却是不一样的:Nginx倾向于配置,Zuul倾向于在特定规则下(filters责任链)自行编程实现, 究其根本原因是两者的架构思路和在整体架构上存在的位置不一样 。我们最终选择Nginx的原因实际上还是基于我们对路由层功能的定位:图片服务是单一的业务功能,并不存在再进行下层服务路由/代理的必要,所以没有必要使用Zuul提供的灵活路由规则支持。另外图片服务在至少能够预期的发展规划中并不存在很强的权限控制要求,即使有权限控制要求也是对图片盗链情况的控制,而图片盗链问题通过Nginx就可以很好的解决。最后,我们对路由层的功能定位主要就是缓存和负载均衡,而Nginx提供的负载均衡配置相对于Zuul提供的负载均衡更为灵活——这是因为Spring Cloud框架是一个服务治理框架,Zuul可以直接把请求转向Spring Cloud Eureka服务注册中心,而且Spring Cloud框架内部的负载均衡可以依靠Spring Cloud Ribbon完成。
这里要特别讨论一下路由层的缓存控制问题。如果使用Nginx,那么可以使用它自带的Proxy-Cache功能建立运行的缓存空间;如果使用Zuul则需要自己通过代码实现一个缓存功能:
很明显基于Nginx的方案更方便,因为是现成的;使用Zuul的方案需要自行开发缓存功能,所以会有额外开发工作量(而且不小,看实现到什么程度),却更能契合功能要求。那么实际情况是什么呢?实际情况是 我们还需要考虑对缓存状态的控制力度 :
使用Nginx的Proxy-Cache虽然方便,但是它是独立工作的,只能按照配置好的方式载入载出数据资源。也就是说,当图片文件真正发生变化时我们无法通过Nginx提供的原生API接口,清除Nginx上相应的缓存数据。幸运的是,Nginx提供了一个可选模块proxy_cache_purge,通过HTTP请求的方式清空缓存,但这种主动清理方式在高并发情况下的性能并没有太多可靠性和性能方面的资料可查。这些还是其次,最重要的情况是我们无法在图片A变化时,准确知晓上层若干台设定了Nginx_Cache的Nginx中哪些Nginx节点需要刷新缓存。所以最后得出的路由层结论是: Nginx存在的主要作用是负载均衡,可以为它配置Proxy-Cache模块,但是不能设置过长的有效时长 ,可以设置成10分钟但绝对不能设置成10小时。这样才能保证下层图片数据发生变化时,客户端被延迟通知的时间更短。
Nginx和Zuul 设计之初就针对两种不相同的业务领域,其功能定位和要解决的业务问题也是不一样的。之所以在路由层的技术选型中,比较这两种组件的差异,除了因为两者在功能上有一定的重合度外,更重要的原因是: 功能/业务要求决定技术选型,而不是反过来进行思考 。
3-4、设计细化
基于以上所描述的各种技术关键点的选型,最终我们可以将概要的架构设计进行细化了。如下图所示:
上图中,最外层面向客户端访问加速的智能DNS路由、CDN加速服务通过购买获得(目前市面上还有很多免费的服务可以使用),首先这些组件与图片服务的核心设计基本上没有什么关联,其首要目标是加快客户端的服务响应速度,另外这样做还可以有效减少运维团队的工作量。至于LVS组件,可以和其它独立工作的子系统共享使用。
我们最终采用Nginx作为图片服务的路由层,使用Nginx提供的负载均衡配置将数据请求压力分散到下层的多个服务节点上。使用Nginx原生的Proxy Cache作为处客户端缓存、CDN加速以外的第三级缓存,但不能将过期时间设置的太长(几分钟最合适)。如果图片发生的变更,也不能主动由业务节点基于proxy_cache_purge主动向Nginx通知删除缓存内容,因为根本不知道哪些Nginx节点需要被通知。类似图片文件访问频度这样的统计工作也在这路由层完成,只不过它不是由路由层本身来处理,而是使用类似Flume这样的日志收集组件对Nginx的access.log文件数据进行收集后,送至专门的日志分析系统完成。
为了提高业务层的开发过程这次演示的开发工程将基于Spring Boot进行构建和代码编写,为了让图片文件的处理操作更加灵活,我们将基于责任链模式构建生产线形式的图片处理过程。我们还将在业务层使用Redis Cluster构造最后一级缓存,这级缓存的过期时间是各层缓存中最长的,存储规模也是各层缓存中最大的。一旦图片文件被更新后,业务层服务将直接使用Redis的原生JAVA API删除缓存中对应的数据信息。
最后,持久层的分布式文件系统我们选用Ceph。实际上这一层可用的选型是最多的,只要把握两个规则就行:相同的内存空间中可以存放更多的元数据,存储小文件时浪费的空间更少。您还可以选用TFS、FastDFS,又或者直接使用网络块存储方案——光交换机 + 磁盘柜。
=============================================
(接后文)