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

31 篇文章 313 订阅
30 篇文章 7 订阅

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

3、重要的代码片段

图片服务系统工程的示例代码放置在 http://download.csdn.net/detail/yinwenjie/9740380 这里,读者可以自行进行下载。

3-1、Nginx中的Proxy Cache配置

根据前文对图片服务系统的系统架构规划,Nginx充当了第三级缓存的作用和对图片请求服务的负载均衡作用,所以在Nginx配置文件中,至少需要对Nginx Proxy Cache功能和UpStream功能进行配置。以下为主要的Rewrite部分和负载均衡部分的配置:

......
upstream imageserver {
    server 192.168.61.1:8080;
    server 192.168.61.2:8080;
    #建议配置健康检查部分
}
......
server {
    ......
    location /imageQuery {
        # 后续还要增加proxy cache部分的配置
        # cluster loader
        proxy_pass http://imageserver;
    }
    ......
}

请记得按照我们在负载均衡专题中讨论的Nginx优化细节进行其它配置信息的调整工作《架构设计:负载均衡层设计方案(2)——Nginx安装》,以下配置信息是和Proxy Cache相关的配置信息:

......
proxy_cache_path  /nginxcache/imagecache  levels=1:2 keys_zone=imagecache:500m inactive=300s max_size=1g;
server {
    ......
    # 带有特效参数的
    location ~* ^/image/(.*)\.(jpg|jpeg|png|gif)&(.*)$ {
        # rewrite
        rewrite ^/image/(.*)\.(jpg|jpeg|png|gif)&(.*)$ /imageQuery/$1.$2?special=$3 last;
    }

    # 不带有特效参数的
    location ~* ^/image/(.*)\.(jpg|jpeg|png|gif)$ {
        # rewrite
        rewrite ^/image/(.*)\.(jpg|jpeg|png|gif)$ /imageQuery/$1.$2 last;
    }

    # 重写后的访问路径
    location /imageQuery { 
        # proxy cache
        proxy_cache imagecache;
        proxy_cache_valid  200 304 300s;
        proxy_cache_key $uri$query_string;

        # cluster loader
        proxy_pass http://imageserver;
    }
    ......
}

为了便于客户端拼凑字符串,图片服务系统向客户端提供了一个约定俗成的url地址结构,然客户端可以在原始图片的url后面使用“&”符号给出要求处理的图片特效,类似如下的结构

http://imagenginxproxy/image/20170114/e1733711-8e4d-4d9e-b230-dd22898313ef.png&zoomimage%7Cratio%3D0.9-%3Emarkimage%7CmarkValue%3Dwww.yinwenjie.net111

当Nginx收到这个请求后,将会rewrite这个url到图片服务层的真实url地址,并且将请求得到的数据通过Proxy Cache进行缓存。关于upstream功能的配置和rewrite功能的配置,我们已经在负载均衡专题中进行了讲解,这里就不再赘述了。这里主要再说明一下Proxy Cache的一些基本配置(关于Proxy Cache模块的更多配置项,可参考官方文档:http://nginx.org/en/docs/http/ngx_http_proxy_module.html。):

  • proxy_cache_path:这个参数指定Nginx Proxy Cache的主要配置项目,其中“/nginxcache/imagecache”表示Proxy Cache存储数据的主要目录,注意这个目录必须已经存在,否则启动时会报错;levels参数指定hash结构层次,例如“levels=1:2”表示有两级hash目录结构,其中第一级目录结构有一个字符,第二级目录结构有两个字符;keys_zone参数描述了proxy cache的名称和最大可使用的内存大小,这个名称在Nginx节点中必须是唯一的,而内存大小视实际情况进行设定,例如这里设置的就是500MB;inactive参数是指当数据持续多长时间没有被后,就删除这个缓存数据,这里设置“inactive=300s”表示300秒的过期时间,注意这个过期时间不要超过Redis中数据的过期时间,否则Redis的数据缓存就不起作用了,并且这个时间需要和proxy_cache_valid的时间区分开来。max_size参数是指Proxy Cache可以使用的最大磁盘空间,例如设置成“max_size=1g”,表示最大可以使用1GB的硬盘空间,很明显这个空间在正式环境下还应该加大。

  • proxy_cache_valid:该参数可以为不同的HTTP响应码,设置不同的缓存时间。例如在以上的配置信息中设置了“proxy_cache_valid 200 304 300s”表示当Nginx反向代理收到的HTTP响应码为200或者304时,才进行数据缓存,缓存时间为300秒——无论这300秒的时间内是否有请求,都是缓存300秒,这个就和proxy_cache_path设置项中的inactive参数的意义就不一样了。您还可以使用多个proxy_cache_valid为不同的HTTP响应码设置不同的缓存过期时间,例如:

    .....
    
    # http200 和 302的缓存时间为5分钟
    
    proxy_cache_valid  200  302   5m;
    
    # http 404的缓存时间为4分钟
    
    proxy_cache_valid  404    4m;
    ....
  • proxy_cache_key:Nginx Proxy Cache模块缓存数据的原理,还是建立K-V的映射关系。使用这个参数可以设定Proxy Cache模块计算Key的依据。例如下面的设置,就是使用HTTP请求的URL地址(不带参数)作为Key的计算依据:

    proxy_cache_key $uri

    但是作为图片服务的上层缓存模块来说,我们不能这样进行设置。这是因为同一张图片的不同特效要求,其URL地址都是一样的,只是参数不一样而已。否则同一张图片的不同显示效果,将会被Nginx缓存视为统一返回数据:

    以下两个URL代表的特效,将会出现缓存错误
    /imageQuery/20170114/e1733711-8e4d-4d9e-b230-dd22898313ef.png?special=zoomimage|ratio=0.55
    /imageQuery/20170114/e1733711-8e4d-4d9e-b230-dd22898313ef.png?special=zoomimage|ratio=0.9

    正确的设置方式,是必须指定Proxy Cache模块在进行Key计算时,要参考URL的参数设置。所以以下设置方式才是正确的:

    ......
    
    #其中$query_string,就代表URL中的参数信息
    
    proxy_cache_key $uri$query_string;
    ......

3-2、责任链进行图片处理

前文我们还提到,我们将基于责任链模式以生产线的方式,进行客户端要求的特效处理过程。本小节我们对这部分主要的代码进行说明。在示例的工程中,我们已经实现了三个图片特效:图片等比例缩放操作、图片裁剪操作和字符串性质的水印操作:

这里写图片描述

上图给出的类图中,除了前文介绍的责任链模式外还有一个创建者用来简化整个生产线的构建过程。创建者模式在很多组件中都有使用,例如Protobuf中创建对象所使用的手段就是先创建一个创建者,然后再由这个创建者进行实际对象的创建:

......
// Protobuf中的实力创建:
MessageS.Message.Builder messageBuilder = MessageS.Message.newBuilder();
// 用户名
messageBuilder.setUserName(username);
// 商品id
messageBuilder.setBusinessCode(buid);
// 等一系列其它属性值
......
// 再进行实际对象的创建
MessageS.Message messagePB = messageBuilder.build();
......

以下是ImageHandler抽象类的部分定义,和ZoomImageHandler图片缩放处理器的重要代码片段:

  • dispose抽象方法定义:
/**
 * 这个dispose方法,就是子类需要主要实现的方式。<br>
 * 如果处理过程中,不需要变更画布的尺寸,则可以在处理后将srcImage代表的画布直接返回<br>
 * 如果在处理过程中,需要变更画布尺寸,则可以在实现的方法中创建一个新的画布,并进行返回
 * @param srcImage 从上一个处理器传来的处理过得图片信息(画布信息)
 * @return 处理完成后一定要返回一个画布。
 */
public abstract BufferedImage dispose(BufferedImage srcImage);
  • ZoomImageHandler缩放操作实现:
......
public BufferedImage dispose(BufferedImage srcImage) {
    /*
     * 处理过程为
     * 1、首先确定外部输入的是按比例缩放还是按照一个高宽数值缩放
     * 2、如果是按照一个高宽数值缩放,则要首先计算一个缩放比例
     * 3、构建一个新的画布,并按照指定的比例或者计算出来的比例进行缩放操作
     * */
    int sourceWith = srcImage.getWidth(); 
    int sourceHeight = srcImage.getHeight();

       //得到合适的压缩大小,按比例。
    int localDestWith,localDestHeight;
    // 如果条件成立,说明是按照比例缩小
    if(ratio != -1) {
        localDestWith = Math.round((sourceWith * ratio));
        localDestHeight = Math.round((sourceHeight * ratio));
    } 
    // 否则是按照输入的宽、高重新计算一个比例,再进行缩小
    else {
        float localRatio;
        // 如果发现输入的目标高宽大于图片的原始高宽,则按照ratio==1处理
        if(sourceWith <= this.destWith || sourceHeight <= this.destHeight) {
            localDestHeight = sourceHeight;
            localDestWith = sourceWith;
        }
        // 按照高计算
        else if(sourceWith > sourceHeight) {
            localRatio = new BigDecimal(this.destHeight).divide(new BigDecimal(sourceHeight), 2, RoundingMode.HALF_UP).floatValue();
            localDestHeight = (int)(sourceHeight * localRatio);
            localDestWith = (int)(sourceWith * localRatio);
        } 
        // 否则按照宽计算
        else {
            localRatio = new BigDecimal(this.destWith).divide(new BigDecimal(sourceWith), 2, RoundingMode.HALF_UP).floatValue();
            localDestHeight = (int)(sourceHeight * localRatio);
            localDestWith = (int)(sourceWith * localRatio);
        }
    }

       // 快速缩放算法
       Image destImage = srcImage.getScaledInstance(localDestWith, localDestHeight, Image.SCALE_FAST);
       // RGB位深为24位,适合互联网显示
       BufferedImage outputImage = new BufferedImage(localDestWith, localDestHeight, BufferedImage.TYPE_INT_RGB);
       Graphics graphics = outputImage.getGraphics();
       graphics.drawImage(destImage, 0, 0, null);
       graphics.dispose();

    // 继续进行下一个处理
    BufferedImage nextResults = this.doNextHandler(outputImage);
    if(nextResults == null) {
        return outputImage;
    }
    return nextResults;
}
......

3-3、Redis缓存操作

3-3-1、Redis Cluster Client对Byte[]操作

在这个示例的图片服务系统中,对于Redis的操作有两个关键点。首先Redis官方推荐的JAVA客户端实现Jedis,提供了一个redis.clients.jedis.JedisCluster类对Redis Cluster进行操作。问题是这里面只提供了针对String类型的Value进行操作,读者可以查看JedisCluster类的源代码进行验证。还好Jedis中的这部分代码不难懂,那么要满足我们图片服务系统中对byte类型的Value进行操作的要求,我们可以仿照JedisCluster类的实现自行实现一个,分别是IBinaryJedisCluster接口和BinaryJedisClusterImpl实现。这里的代码我们不需要按照JedisCluster类中实现对Redis中所有数据接口的操作,只需要实现我们系统中需要的Redis操作即可:

  • 以下是IBinaryJedisCluster接口:

这里写图片描述

  • 以下是BinaryJedisClusterImpl类中的部分实现代码:
......
/**
 * Redis Cluster 客户端操作实现,这个类参考自JedisCluster而来<br>
 * 源自JedisCluster对JedisClusterCommand的封装只能对String类型的value进行处理
 * 不能对byte[]形式的value进行处理
 * @author yinwenjie
 */
public class BinaryJedisClusterImpl implements IBinaryJedisCluster, Closeable {
    public static final short HASHSLOTS = 16384;
    private static final int DEFAULT_TIMEOUT = 2000;
    private static final int DEFAULT_MAX_REDIRECTIONS = 20;

    ......

    @Override
    public String set(final String key, final byte[] value, final String nxxx, final String expx, final long time) {
        return new JedisClusterCommand<String>(connectionHandler, maxRedirections) {
            @Override
            public String execute(Jedis connection) {
                return connection.set(key.getBytes(), value, nxxx.getBytes(), expx.getBytes(), time);
            }
        }.run(key);
    }

    @Override
    public byte[] get(final String key) {
        return new JedisClusterCommand<byte[]>(connectionHandler, maxRedirections) {
            @Override
            public byte[] execute(Jedis connection) {
                return connection.get(key.getBytes());
            }
        }.run(key);
    }

    ......

    @Override
    public Boolean exists(final String key) {
        return new JedisClusterCommand<Boolean>(connectionHandler, maxRedirections) {
            @Override
            public Boolean execute(Jedis connection) {
                return connection.exists(key);
            }
        }.run(key);
    }
    ......
}

3-3-2、保证Key的数据在5KB以内

在前文的系统设计章节,我们还介绍到Redis对超过10KB的Value信息有较高的读写延迟,而我们存储到Redis中的图片信息又都比较大(20KB是普片现象,200KB也是可能的),在设计章节中提到的处理办法是将一个图片信息拆分为多个Value存储到Redis Cluster中的不同节点上。以下是进行Value存储时的主要代码片段:

......
@Override
public void saveCache(String imageURL, byte[] imagebytes) throws BusinessException {
    /*
     * 1、经过合法性验证后,处理的第一步就是判断imagebytes需要被分成几个段
     * 2、然后进行byte的拆分和保存
     * 3、只有所有的段都保存成功了,才进行返回(同步的,连续性的)
     * */
    if(StringUtils.isEmpty(imageURL) || imagebytes == null) {
        throw new BusinessException("参数错误,请检查!", BusinessCode._404);
    }

    // 1、======确定分段
    Integer maxLen = imagebytes.length;
    Integer splitNum = maxLen / IImageEffectsCacheService.PERPATCH_IMAGE_SIZE;
    Integer remainLen = maxLen % IImageEffectsCacheService.PERPATCH_IMAGE_SIZE;
    if(remainLen != 0) {
        splitNum++;
    }   

    // 开始构造key
    String key[] = new String[splitNum];
    for(int index = 0 ; index < splitNum ; index++) {
        key[index] = imageURL + "|" + index;
    }
    String lenkey = imageURL + "|size";

    ......

    // 保存长度信息到缓存系统
    binaryJedisCluster.set(lenkey, maxLen.toString().getBytes());
    for(int index = 0 ; index < splitNum ; index++) {
        byte[] values = null;
        Integer valuesLen = null;
        // 确定本次要添加的分片长度
        if(index + 1 == splitNum) {
            valuesLen = remainLen == 0?IImageEffectsCacheService.PERPATCH_IMAGE_SIZE:remainLen;
        } else {
            valuesLen = IImageEffectsCacheService.PERPATCH_IMAGE_SIZE;
        }
        values = new byte[valuesLen];

        // 复制byte信息
        // System.arraycopy是操作系统级别的复制操作,比arraybyte stream快
        System.arraycopy(imagebytes, index * IImageEffectsCacheService.PERPATCH_IMAGE_SIZE, values, 0, valuesLen);
        binaryJedisCluster.set(key[index], values , "NX" , "EX" , redisProperties.getKeyExpiredTime());
    }
}
......

有的读者指出这里可以在Redis Cluster Client部分使用线程池,同时读取多个Value部分,并通过CountDownLatch控制所有Value读取完成后再进行合并。但实际上这种读取方式在Redis Cluster节点较少的时候对于单个Client来说意义不大。主要还是因为Redis服务节点工作在单线程状态下,完全依靠操作系统的多路复用I/O模型、自身实现的事件分离器、全内存态数据存储和内部的数据结构实现来保证吞吐性能。但是随着Redis Cluster中Master节点的增多,以上所描述的Client多线程方式就有性能优势了,这是因为通过Jedis Client CRC16 Hash算法,同一个图片的不同byte段会分配到更多不同的Redis节点上进行存储,这样就能够实现同一张图片不同byte段的同时读取了。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
前端性能优化实践# 知识体系与小册格局 ## 写给读者 提起性能优化,大家现在脑海里第一时间会映射出什么内容呢? 可能是类似[“雅虎军规”](https://developer.yahoo.com/performance/rules.html?guccounter=1)和[《高性能 JavaScript》](https://book.douban.com/subject/5362856/)这样历久弥香的经典之作,也可能是搜索引擎聚合给你的一篇又一篇以性能优化为主题的个人或团队实践而来的“私货”。至少当我确定自己的研发方向、并接到第一个性能优化任务时,我做的第一件事是向搜索引擎求助,第二件事是买书,然后开始了摸着石头过河,前后花费了大量的时间和精力。我深感性能优化实在是前端知识树中特别的一环——当你需要学习前端框架时,文档和源码几乎可以告诉你所有问题的答案,当你需要学习 Git 时,你也可以找到放之四海皆准的实践方案。但性能优化却不一样,它好像只能是一个摸索的过程。 这个摸索的过程是痛苦的、漫长的,也是紧要的。因为在如今的互联网环境下,一个前端团队如果只把性能优化这个任务写在纸上,而不投入实践,它将缺失最基本的竞争力。 笔者写这本小册,是希望通过短短十数个章节的讲解,尽可能降低一些大家学习性能优化的成本。 一方面,这本小册为没有接触过性能优化的新同学建立起一个正确的前端性能优化的“世界观”,知道性能优化是什么、为什么、怎么做,从而使性能优化这件事情有迹可循,有路可走。这样在面试现场被问到性能优化层面的问题时,能够做到滔滔不绝、言之有物,而非像背书一样罗列干巴巴的知识点,最终淹没在茫茫的求职大军中。另一方面,小册可以为在职的工程师们提供一线团队已经实践过的“方法论”,知道什么场景下该做什么事情,最终在脑海中留下一张涵盖核心原理和实践的、可随时查阅并且高度可扩展的性能优化思路索引表。然后在今后的开发生活中可以去践行它,更进一步去挖掘它。把性能优化变作你前端工程师生涯的一门必修课,进而演化为自己研发方面的核心竞争力。 同时,相信大家可以明确这样一个学习观念:任何技术的掌握,都离不开一定比例的理论基础和实际操作的支撑。 具体到前端性能优化这件事情上,我认为它是 20% 的理论,加上至少 80% 的实践,甚至很多理论本身也都是我们在具体的业务场景中实践出来的。所以希望大家阅读本小册时,能够读到一些“书本之外的东西”——最好是一边读一边回忆自己既有的开发经历,尝试去留意哪些知识是已知的,哪些是未知的。 这样读完之后,就可以有的放矢地把这些知识转换为自己的项目实践——前端技术日新月异,性能方案永远都在更迭,所以一定要形成自己的学习思路。 建议每一位读者都带着“学了就要用”的心态去读这本小册。如果阅读结束,能够为你带来哪怕一个小小的开发习惯或者优化观念上的改变,这数小时的阅读时间就算没有白费。 ## 知识体系: 从一道面试题说起 在展开性能优化的话题之前,我想先抛出一个老生常谈的面试问题: > 从输入 URL 到页面加载完成,发生了什么? 这个问题非常重要,因为我们后续的内容都将以这个问题的答案为骨架展开。我希望正在阅读这本小册的各位可以在心里琢磨一下这个问题——无须你调动太多计算机的专业知识,只需要你用最快的速度在脑海中架构起这个抽象的过程——我们接下来所有的工作,就是围绕这个过程来做文章。 我们现在站在性能优化的角度,一起简单地复习一遍这个经典的过程:首先我们需要通过 DNS(域名解析系统)将 URL 解析为对应的 IP 地址,然后与这个 IP 地址确定的那台服务器建立起 TCP 网络连接,随后我们向服务端抛出我们的 HTTP 请求,服务端处理完我们的请求之后,把目标数据放在 HTTP 响应里返回给客户端,拿到响应数据的浏览器就可以开始走一个渲染的流程。渲染完毕,页面便呈现给了用户,并时刻等待响应用户的操作(如下图所示)。 ![](https://user-gold-cdn.xitu.io/2018/10/18/16685737b823244c?w=489&h=329&f=png&s=19023) 我们将这个过程切分为如下的过程片段: 1. DNS 解析 2. TCP 连接 3. HTTP 请求抛出 4. 服务端处理请求,HTTP 响应返回 5. 浏览器拿到响应数据,解析响应内容,把解析的结果展示给用户 大家谨记,我们任何一个用户端的产品,都需要把这 5 个过程滴水不漏地考虑到自己的性能优化方案内、反复权衡,从而打磨出用户满意的速度。 ## 从原理到实践:各个击破 我们接下来要做的事情,就是针对这五个过程进行分解,各个提问,各个击破。 具体来说,DNS 解析花时间,能不能尽量减少解析次数

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

说好不能打脸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值