2018年1月

什么是CDN

CDN 的全城是 Content Delivery Network,即内容分发网络,尽可能避开互联网上有可能影响数据传输速度和稳定性的平静和环节,是内容传输的更快、更稳定。

在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层只能虚拟网络,比如说我们现在有一个服务器的集群,现在在北京,有几台服务器在北京,在上海访问北京服务器就会比较慢,我们就可以使用 CDN 解决这样的问题,可以在香港、上海建立一个 CDN 节点,这样当我的用户在某一个节点访问我们的网站时,可以去请求香港的 CDN 节点,这样距离他比较近,CDN 已经把真实服务器的数据缓存到了 CDN 当中,相当于一个镜像。

CDN 系统能够实时地根据网络流量和各节点的连接、敷在情况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。

使用 CDN 的优势

本地 Cache 加速,提高企业站点(尤其含有大量图片和静态资源页面站点)的访问速度。

跨运营商的网络加速,保证不同网络的用户能得到最好的访问质量。

远程访问用户根据 DNS 负载均衡技术只能选择 Cache 服务器。

自动生成服务器的远程 Mirror(镜像)Cache 服务器,远程用户访问时从 Cache 服务器上读取数据,减少远程访问的带宽、分带网络流量、减轻原站点 WEB 服务器负载等能力。

广泛分布的 CDN 节点加上节点之间的智能冗余机制,可以有效地预防黑客入侵。

CDN 的工作原理

传统访问

用户在浏览器输入域名发起请求-->解析域名获取服务器 IP 地址-->根据 IP 地址找到对应的服务器—>服务器响应并返回。

使用 CDN 访问

用户发起请求-->只能 DND 解析(根据 IP 判断地理位置、接入网类型、选择路由最短和负载最轻的服务器)-->获得缓存服务器 IP-->把内容返回给用户(如果缓存中有)—>向源站发起请求—>将结果返回给用户—>将结果存入缓存服务器。

CDN 适用场景

  • 站点或者应用中大量静态资源的加速分发,例如:CSS,JS,图片和 HTML。
  • 大文件下载
  • 直播网站

CDN 的实现

BAT 等都有提供 CDN 服务。

可用 LVS 做4层负载均衡。

可用 Nginx,Varnish,Squid,Apache TrafficServer 做7层负载均衡和 Cache。

使用 squid 反向代理,或者 Nginx 等的反向代理

HTTP 缓存机制

缓存分类

在HTTP缓存模型中,如果请求成功会有三种情况。

  • 200 from cache
  • 304 Not Modified
  • 200 OK

200 from cache: 直接从本地缓存中获取响应,最快速,最省流量,因为根本没有向服务器发送请求I看到。

浏览器本身就有缓存机制,当我们浏览器检测到该资源在本地存在,那么就不需要向服务器发送请求,从下图中我们可以看到size没有大小显示的是 from cache,就是读取的缓存。

查看其响应头我们可以看到下图。

304 Not Modified: 协商缓存,浏览器在本地没有命中的情况下请求头中发送一定的教研数据到服务端,如果服务端数据没有改变,浏览器从本地缓存响应,返回304。

快速,发送的数据很少,只会返回一些基本的响应头信息,数据量很小,不发送实际响应体。

200 OK: 以上两种缓存全部失效,服务器返回完整响应。没有用到缓存,相对较慢,对于200 OK不能称之为缓存,因为根本没有用到缓存机制。

相关 Header

  • Pragma
  • Expires
  • Cache-Control

Pragma: HTTP 1.0 时代的遗留产物,该字段被设置为no-cache时,会告知浏览器禁用本地缓存,即每次都向服务器发送请求。

Expires:HTTP 1.0 时代用来启用本地缓存的字段,expires值对应一个形式如Thu, 31 Dec 2037 23:55:55 GMT的格林威治时间,告诉浏览器缓存实现的时刻,如果还没到该时刻,标明缓存有效,无需发送请求。

浏览器与服务器的时间无法保持一致,如果时间差比较大,就会影响缓存结果。我们可以保证服务器的时间,但是无法保证客户端的时间与服务器时间的一致。(QQ 空间就有对客户端时间进行检测,感兴趣的可以将本地时间调整一下然后去访问QQ空间)

Cache-Control:HTTP 1.1 针对 Expires 时间不一致的解决方案,运用 Cache-Control 告知浏览器缓存过期时间间隔,而不是时刻,即使具体时间不一致,也不影响缓存的管理。

在Cache-Control下我们还可以设置一些头信息:

  • no-store:禁止浏览器缓存响应
  • no-cache:不允许直接使用本地缓存,先发起请求和服务器协商,也就是协商缓存
  • max-age=delta-seconds:告知浏览器该响应本地缓存有效的最长期限,以秒为单位。

优先级: Pragma > Cache-Control > Expires

在上图中,我们可以看到上面的头信息,设置了Cache-Control和Expires。

协商缓存当浏览器没有命中本地缓存,如果本地缓存过期或者响应中生命不允许直接使用本地缓存(Pragma或者Cache-Control设置成了no-cache),那么浏览器肯定会发起服务端请求。服务端会验证数据是否修改,如果没有通知浏览器使用本地缓存。

相关的Header:

  • Last-Modified:通知浏览器资源的最后修改时间,格式如上图。
  • If-Modified-Since:这是与Last-Modified相对应的一个头信息,得到资源的最后修改时间后,会将这个信息通过If-Modified-Since提交到服务器进行检查,如果没有修改则返回304状态码。
  • ETag: HTTP 1.1 推出,文件的指纹标识符,如果文件内容修改,指纹会改变,Last-Modified只能精确到秒,而 ETag 如果发生了改变,它就会改变,它就相当于文件的标识("78437822c-6739"),也更加准确。
  • If-Node-Match:与ETag相对应的一个头信息,本地缓存失效,会携带此值去请求服务端,服务端判断该资源是否改变,如果没有改变直接使用本地缓存,返回304

通过下面的两张图,我们可以看到完整的协商缓存交互:

缓存策略的选择

适合的内容:

  • 不变的图像,如logo,图标等
  • js、css静态文件
  • 可下载的内容,媒体文件

建议使用协商缓存:

  • HTML文件
  • 经常替换的图片
  • 经常修改的js、css
  • js、css问价你的加载可以加入文件的签名来拒绝缓存 (index.css?签名、index.签名.js)

不建议缓存的内容:

  • 用户隐私等敏感数据
  • 经常改变的 api 数据接口

NGINX 配置缓存策略

首先,我们使用PHP模拟NGINX的缓存。

<?php
    //1. 获取If-Modified-Since
    $since = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
    //2. 设置过期时间一个小时
    $lifetime = 3600;
    //3. 如果没有过期直接返回304告诉浏览器使用本地缓存
    if (strtotime($since) + $lifetime > time()) {
        header('HTTP/1.1 304 Not Modified');
        exit;
    }
    //4. 设置最后修改时间
    header('Last-Modified:'. gmdate('D, d M Y H:i:s', time()). ' GMT');
    //5. 网页显示的内容,用于验证是否开启缓存
    echo time();

上述代码就是NGINX的缓存原理,不过NGINX判断的不是3600秒,而是文件的修改时间。

本地缓存配置

add_header指令:添加状态码为2XX和3XX的响应头信息。

add_header name value [always];

可以设置Pragma/Expires/Cache-Control,可以继承

expires指令:通知浏览器过期市场

expires time;

  • 为负值时表示Cache-Control:no-cache;
  • 为正或0时,就表示Cache-Control:max-age=指定的时间;
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
{
    expires 30d;
}
location ~ .*\.(js|css)?$ 
{
    expires 12h;
}

当设为max时,会把Expires设置为"Thu, 31 Dec 2037 23:55:55 GMT",Cache-Control设置到10年。

协商缓存配置

ETag指令:指定签名

etag on | off 默认是on

location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
{
    etag on;
}

Cache-Control

location ~ .*\.(js|css)?$ 
{
    add_header cache-control max-age = 3600;
}

前端代码和资源的压缩

  • 优势

    • 让资源文件更小
    • 加快文件在网络中的传输
    • 让网页更快的展示
    • 降低带宽和流量的开销

压缩方式

  • JS、CSS、图片、HTML代码的压缩
  • Gzip压缩

JavaScript代码压缩

JavaScript压缩的原理一般是去掉多余的空格和回车、替换长变量名、简化一些代码的写法。

压缩工具有很多,有在线工具、有应用程序、有编辑器插件。

常用的压缩工具:

  • UglifyJs: 压缩、语法检查、美化代码、代码缩减、转化
  • YUI compressor:来自Yahoo,只有压缩功能
  • Closure Compiler:来自Google、功能和UglifyJs有些类似,压缩的方式不太一样。

CSS代码压缩

原理和JavaScript压缩原理类似,同样是去除空白符、注释并且优化一些CSS语义规则。

常用的压缩工具:

  • YUI compressor:来自Yahoo,只有压缩功能
  • CSS Compiler:压缩时候可以选择模式,也可以对一些语法进行优化

HTML代码压缩

不建议使用代码压缩,有时会破坏代码结构,可以使用Gzip压缩,当然也可以使用htmlcompressor工具,不过转换后一定要检查代码结构。

图片压缩

除了代码的压缩外,有时对图片的压缩也是很有必要的,一般情况下图片在Web系统的比重都比较大。

压缩工具

  • tinypng
  • JpegMini
  • ImageOptim

Gzip压缩

GZIP最早由Jean-loup Gailly和Mark Adler创建,用于UNⅨ系统的文件压缩。我们在Linux中经常会用到后缀为.gz的文件,它们就是GZIP格式的。现今已经成为Internet 上使用非常普遍的一种数据压缩格式,或者说一种文件格式。

HTTP协议上的GZIP编码是一种用来改进WEB应用程序性能的技术。大流量的WEB站点常常使用GZIP压缩技术来让用户感受更快的速度。这一般是指WWW服务器中安装的一个功能,当有人来访问这个服务器中的网站时,服务器中的这个功能就将网页内容压缩后传输到来访的电脑浏览器中显示出来.一般对纯文本内容可压缩到原大小的40%.这样传输就快了,效果就是你点击网址后会很快的显示出来.当然这也会增加服务器的负载. 一般服务器中都安装有这个功能模块的。

NGINX配置:

gzip on|of #是否开启gzip
gzip_buffres 32 4k| 16 8k; #缓冲(在内存中缓冲几块,每块多大)
gzip_comp_level [1-9] #推荐6 压缩级别(级别越高,压缩的越小,越浪费CPU计算资源)
gzip_disable #正则匹配UA什么样的Uri不进行gzip
gzip_min_length 200 #开始压缩的最小长度
gzip_http_version 1.0|1.1 #开启压缩的http协议版本
gzip_proxied    #设置请求者代理服务器,该如何缓存内容
gzip_types text/plain applecation/xml #对那些文件类型使用压缩
gzip_vary on | off #是否传输gzip压缩标志

其他工具

  • 自动化构建工具Grunt

为什么要减少HTTP请求

性能黄金法则:

只有10%~20%的最终永不响应时间花在接收请求的HTML文档上,剩下的80%~90%时间花在HTML文档所引用的所有组件(图片,script,css,flash等等)进行的HTTP请求上。

如何改善

改善响应时间的最简单途径就是减少组件的数量,并由此减少HTTP请求的数量。

HTTP链接产生的开销

域名解析 -- TCP链接 -- 发送请求 -- 等待 -- 下载资源 -- 解析时间

  • 需要注意 DNS 缓存也需要时间,多个缓存就要查找多次有可能缓存会被清除
  • HTTP1.1 协议规定请求只能串行发送,也就是说一百个请求必须一次逐个发送,前面的一个请求完成才能开始下一个请求。

减少HTTP请求的方式

图片地图

图片地图允许你在一个图片上关联多个URL。目标URL的选择取决于用户单击了图片上的哪个位置。

我们可以通过使用五个分开的图片,然后每个图片对应一个超链接产生了5个HTTP请求,我们的目标是要减少HTTP请求。

将五个图片合并成为一张图片,然后以位置信息定位超链接。

把HTTP请求减少为一个 ,可以暴增设计的完整性和功能的齐全性。

<map><area></map></map>

实例

未使用图像地图的例子:

http://stevesouders.com/hpws/imagemap-no.php

使用图像地图的例子:

http://stevesouders.com/hpws/imagemap.php

CSS Sprites

CSS Sprites 中文翻译为 CSS 精灵,通过使用合并图片,通过指定 CSS 的 backgroud-image 和backgroud-position来显示元素。

backgroud-position属性

backgroud-position:x,y; x和y可以写负值也可以写正值,我们可以想象图片的左上方为(0,0),以(0,0)坐标向右是为负数的 X 轴,以(0,0)坐标向下是为负数的 y 轴。

使用图片精灵的案例:

http://stevesouders.com/hpws/sprites.php

图片地图和 CSS Sprites 的响应时间基本相同,但比使用各自独立图片的方式要快50%以上。

合并脚本和样式表

使用外部的 JS 和 CSS 文件引用的方式,因为这要比直接写在页面中性能要更好一点。

独立的一个 JS 比用多个 JS 文件组成的页面载入要快38%。

把多个脚本合并为一个脚本,把多个样式表合并成为一个样式表。

http://stevesouders.com/hpws/combo-none.php

http://stevesouders.com/hpws/combo.php

图片使用base64编码减少页面请求数

采用Base64的编码方式将图片直接嵌入到网页中,而不是从外部载入。

<img src="data:image/gif;base64,/9j/4AAqsKZJ.....">

http://stevesouders.com/hpws/inline-images.php

http://stevesouders.com/hpws/inline-css-images.php

盗链是指通过技术手段获得他人服务器上的资源地址,绕过别人的资源展示页面,直接在自己的页面上向最终用户提供此内容。常见的是小站盗用大站的图片、音乐、视频、软件等资源。

通过盗链的方法可以减轻自己服务器的负担,因为真实的空间和流量均是来自别人的服务器。

防盗链的工作原理

在 http 协议中,我们的每一次请求在 header 中都会包含 Referer 字段,用来表示这个请求的来源,我们就可以通过这个字段来做一个白名单,只有 Referer 来源在我的白名单列表中才允许请求访问资源,如果不允许,我们可以拒绝请求,甚至是返回一张我们自定义的图片来告诉用户,这个图片在哪台服务器上。

Nginx防盗链的实现

Nginx Referer

Nginx 模块 ngx_http_referer_module 用于阻挡来源非法的域名请求。

Nginx 指令 valid_referers,全局变量$invalid_referer。

valid_referers none | blocked | server_names| string ...;

none: "Referer" 来源头部为空的情况

blocked: "Referer"来源头部不为空,但是里面的值被代理或者防火墙删除了,这些值都以 http://或者 https://开头。

server_names: "Referer" 来源头不包含当前的 server_names

location ~.*\.(gif|jpg|png|flv|swf|rar|zip)$ {
    valid_referers none blocked maksim.website *.maksim.website;
    if ($invalid_referer)
    {
        #return 403;
        rewrite ^/http://www.maksim.website/403.jpg;
    }
}

针对目录

location /images/ {
    valid_referers none blocked maksim.website *.maksim.website;
    if ($invalid_referer)
    {
        #return 403;
        rewrite ^/http://www.maksim.website/403.jpg;
    }
}

但是这种方法存在一个问题,那就是 Referer 是可以伪造的,只要对方在每一次请求的时候设置 Referer 那么就可以完全绕考这个限制,直接访问我们的资源。

Nginx HTTPAccessKeyModule 加密签名

请求图片的时候带签名过去,当返回图片的时候判断签名是否正确,也就是暗号。

加密签名的时候需要使用第三方模块HttpAccessKeyModule,因为在服务端php里需要显示图片的时候跟一个签名,交给Nginx的时候,Nginx需要做一个判断,判断前面是否正确,Nginx在判断的时候就需要这个模块了。

accesskey on | off
accesskey_hashmethod md5 | sha-1 
accesskey_arg GET参数名称
accesskey_signatrue 加密规则
location ~.*\.(gif|jpg|png|flv|swf|rar|zip)$
{
    accesskey on
    accesskey_hashmethod md5
    accesskey_arg "key"
    accesskey_signatrue "maksim$remote_addr"
}
<?php
  //md5(maksim.ip)
  $sign = md5('maksim'.$_SERVER['REMOTE_ADDR']);
    echo '<img src="./image/maksim.png?sign='. $sign .'">';

高并发架构的相关概念

高并发的概念

  • 并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任意一个时刻点上只 有一个程序在处理机上运行。

    上面这一段是摘自百度百科的一段话,但是上面的定义很明显不是我们通常所说的并发,在互联网时代,所讲的并发、高并发,通常是指并发访问。也就是在某一个时间点,有多少个访问同时到来。

  • 高并发: 通常如果一个系统的日 PV 在千万以上,有可能是一个高并发的系统,有的公司完全不走技术路线,全靠机器堆,这不再我们的讨论范围。

高并发中需要关注相关概念

  • QPS: 每秒钟请求或者查询的数量,在互联网领域,指的是每秒响应请求数(指的是HTTP请求)。
  • 吞吐量:单位时间内处理的请求数量(通常由QPS与并发数决定)
  • 响应时间: 从请求发出到收到响应花费的时间。例如系统处理一个HTTP请求需要 100ms, 这个 100ms 就是系统的响应时间。
  • PV: 综合浏览量(Page View),即页面浏览量或者点击量,一个访客在24小时内访问的页面数量。同一个人浏览你的网站统一页面,只记录一次 PV。
  • UV: 独立访客(UniQue Visitor),即一定时间范围内相同访客多次访问网站,只计算为一个独立访客。
  • 带宽: 计算带宽大小需关注两个指标,峰值流量和页面的平均大小
  • 日网站带宽: PV/统计时间(换算到秒)*平均页面大小(单位KB)* 8,峰值一般为平均值的倍数,根据实际情况来定。
  • 压力测试: 测试服务器能够最大承受的最大并发数和QPS值,对于计算机来说,我们应该知道这台服务器最大能够承受多少QPS,而我们可以通过一天的PV来计算出峰值的QPS,这样我们就可以根据需求进行优化。

在这里我们需要注意,QPS 不等于并发数数量,QPS是每秒HTTP请求数量,并发连接数是系统同时处理的请求数量。

(总PV数 80%) / (6小时秒数 20%) = 峰值每秒请求数量(QPS), 80%的访问量集中在20%的时间,那为什么是6个小时呢?

6个小时主要是做了一个简单的估计,比如说我们访问网站中午两个小时,下午两个小时,晚上两个小时。

压力测试工具

在我们日常的工作当中下面这几款是我们比较常用的压测工具:

  • ab:Apache benchmark,是 Apache 官方推出的工具创建多个并发访问线程,模拟多个访问者同时访问某一URL 地址进行访问。
  • wrk:是一款开源的性能测试工具,简单易用,没有Load Runner那么复杂,他和 apache benchmark(ab)同属于性能测试工具,但是比 ab 功能更加强大,并且可以支持lua脚本来创建复杂的测试场景。
  • Jmeter:Jmeter 是测试人员最长用的压测工具,他的功能要比 ab 和 wrk 强大很多。

接下来,我们用 ab 来简单说明一下压力测试的使用方法。

ab 的简单使用

[root@stache34 ~]# ab -n 1000 -c 900 http://192.168.176.30/index.html
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 192.168.176.30 (be patient) #完成的进度
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests

Server Software:        Apache/2.4.6   #服务器软件版本
Server Hostname:        192.168.176.30 #服务器主机名
Server Port:            80                          #服务器端口

Document Path:          /index.html      #测试的页面
Document Length:        9 bytes            #页面的字节数

Concurrency Level:      900                     #请求的并发数,代表着访问的客户端数量
Time taken for tests:   0.489 seconds #整个测试花费的时间
Complete requests:      1000                     #成功的请求数量
Failed requests:        0                         #失败的请求数量
Write errors:           0
Total transferred:      267000 bytes     #整个测试过程的总数据大小(包括header头信息等)
HTML transferred:       9000 bytes         #整个测试过程HTML页面实际的字节数
Requests per second:    2045.81 [#/sec] (mean) #每秒处理的请求数,这是非常重要的参数,体现了服务器的吞吐量
                                               #后面括号中的 mean 表示这是一个平均值
Time per request:       439.923 [ms] (mean)      #平均请求响应时间,括号中的 mean 表示这是一个平均值

#每个请求的时间 0.489[毫秒],意思为在所有的并发请求每个请求实际运行时间的平均值
#由于对于并发请求 cpu 实际上并不是同时处理的,而是按照每个请求获得的时间片逐个轮转处理的
#所以基本上第一个 Time per request 时间约等于第二个 Time per request 时间乘以并发请求数
Time per request:       0.489 [ms] (mean, across all concurrent requests)

Transfer rate:          533.43 [Kbytes/sec] received #传输速率,平均每秒的流量
                                                     #可以帮助排除是否存在网络流量过大导致响应时间延长的问题
Connection Times (ms) #连接时间
              min  mean[+/-sd] median   max #median中间
Connect:        0   17  11.8     21      34
Processing:     1  145 153.9     35     446
Waiting:        1  145 153.9     35     446
Total:         16  163 161.2     63     474

Percentage of the requests served within a certain time (ms) #在一定的时间内提供服务的请求的百分比
  50%     63
  66%    244
  75%    255
  80%    260
  90%    468
  95%    471
  98%    474
  99%    474
 100%    474 (longest request)

注意事项

  • 测试机器与被测机器分开,因为 ab 在测试的时候同样会占用机器资源,如果在一台器上进行测试会影响测试结果。
  • 不要对生产服务做压力测试,避免影响生产环境。
  • 观察测试工具 ab 所在机器,以及被测试的前端机的 CPU,内存,网络等都不超过最高限度的75%。

QPS 的增长中会遇到的挑战

随着 QPS 的增长,每个阶段需要根据实际情况来进行优化,优化的方案也与硬件、网络带宽息息相关。

QPS解决收到解决方案
50可以称之为小型网站,一般的服务器都可以应付。
100假设关系型数据库的每次请求在0.01秒完成,并且页面中只有一个 SQL 查询,那么100 QPS 意味着1秒钟完成100个请求,但是此时我们并不能保证数据库查询能完成100次。数据库缓存层、数据库的负载均衡
800假设我们使用百兆带宽,意味着网站出口的实际带宽是8M 左右,假设每个页面只有10K,在这个并发条件下,百兆带宽已经吃完了。CDN 加速、负载均衡
1000假设使用 Memcache 缓存数据库查询数据,每个页面对 Memcache 的请求远大于直接 DB 的请求,Memcache 的悲观并发数在2W 左右,但有可能在之前内王带宽已经吃光,表现出不稳定。静态 HTML缓存
2000这个级别下,文件系统访问锁都成了灾难。做业务隔离,分布式存储

为了应对QPS 增长我们可以使用下面的手段来应对:

场景方案
流量优化防盗链
前端优化减少 HTTP 请求、添加异步请求、启用浏览器缓存和文件压缩、CDN 加速、建立独立的图片服务器
服务端优化页面静态化、并发处理、队列处理
数据库优化数据库缓存、读写分离、分库分表、分区操作、读写分离、负载均衡
集群Nginx、LVS