关于网站高并发大流量的解决思路

高并发和大流量,一直是让企业头疼的问题,所以今天我也专门去看了些视频,在这里做个总结,这里不考虑那种企业有钱堆硬件的情况。

在面对高并发和大流量之前,我们先来了解几个概念:
我们所说的高并发是什么?
在互联网时代,所讲的并发、高并发,通常是指并发访问。也就是在某个时间点,有多少个访问同时到来。

QPS:QPS每秒钟请求或者查询的数量,在互联网领域,指每秒响应请求数(HTTP请求);

吞吐量:单位时间内处理的请求输了(通常由QPS与并发数决定);

响应时间:从请求发出到收到响应花费的时间。例如系统处理一个HTTP请求需要100ms,这个100ms就是系统的响应时间。

PV:综合浏览量(Page View),即页面浏览量或者点击量,一个访客在24小时内访问的页面数量。(PS:同一个人浏览你的网站同一页面,只记作一次PV)。
UV:独立访客(UniQue Visitor),即一定时间范围内相同访客多次访问网站,只计算为一个独立访客。
带宽:计算带宽的大小需要关注两个指标,峰值流量和页面的平均大小。
日网站带宽=PV/统计时间(换算到秒)平均页面大小(单位KB)8;峰值一般是平均值的倍数,根据实际情况来定。

需要注意的是,QPS并不等于并发连接数。QPS是每秒HTTP请求数量,并发连接数是系统同时处理的请求数量。
(总PV数80%)/(6小时秒数20%)=峰值每秒请求数(QPS);80%的访问量集中在20%的时间。

对于高并发的问题,我们需要做一个压力测试,才能更好地了解服务器的承受能力,我们需要测试服务器能承受的最大并发和最大承受的QPS值。

测试工具很多,我们这里使用ab这个测试工具来测试,ab是apache自带的压力测试工具。ab非常实用,它不仅可以对apache服务器进行网站访问压力测试,也可以对或其它类型的服务器进行压力测试。比如nginx、tomcat、IIS等。

注意,为了测试结果准确,测试机器与被测试机器最好分开,重要!!! 不要对线上服务器做压力测试,不要对线上服务器做压力测试,不要对线上服务器做压力测试。
一般来说,被测试的前端机的COU,内存,网络等不应该超过最高限度的75%;

然后说说各等级的QPS应该如何优化:
QPS达到100:
假设关系型数据库的每次请求在0.01s完成
假设单页面只有一个sql查询,那么100QPS意味着1s钟完成100次请求,但是此时我们并不能保证数据查询能完成100次;
方案:数据库缓存层、数据库的负载均衡

QPS达到800:
假设我们使用百兆带宽,意味着网站出口的实际带宽是8M左右
假设每个页面只有10K,在这个并发条件下,百兆带宽已经吃完
方案:CDN加速,负载均衡

QPS达到1000
假设使用Memcache缓存数据库查询数据,每个页面对Memcache的请求远大于直接对DB的请求
Memcache的悲观并发数在2W左右,但有可能在之前内网带宽已经吃光,表现出不稳定。
方案:静态HTML缓存

QPS达到2000:
这个级别下,文件系统访问锁都成为了灾难
方案:做业务分离,分布式存储。

对应大并发,由前至后,可以从以下方面优化:流量优化(防盗链处理),前端优化(减少HTTP请求、添加异步请求、启用浏览器缓存和文件压缩、CDN加速、建立独立图片服务器),服务端优化(页面静态化、并发处理、队列处理),数据库优化(数据库缓存,分库分表、分区操作,读写分离,负载均衡),Web服务器优化(负载均衡)。

注意:以下代码仅为示例,请勿直接用于生产环境。

流量优化:

防盗链处理
首先,看一下什么是盗链:盗链是指服务提供商自己不提供服务的内容,通过技术手段绕过其它有利益的最终用户界面(如广告),直接在自己的网站上向最终用户提供其它服务提供商的服务内容,骗取最终用户的浏览和点击率。受益者不提供资源或提供很少的资源,而真正的服务提供商却得不到任何的收益。
常见的是小站盗用大站的图片、音乐、视频、软件等资源,虽然通过盗链的方法可以减轻自己服务器的负担(因为真实的空间和流量均是来着别人的服务器),但这是一种不道德的行为。

防盗链
防止别人通过一些技术手段绕过本站的资源展示页面,盗用本站的资源,让绕开本站资源展示页面的资源链接失效。

防盗链的工作原理:通过Referer或者签名,网站可以检测目标网页访问的来源网页,如果是资源文件,则可以跟踪显示它的网页地址。一旦检测到来源不是本站即进行阻止或者访问指定的页面。

通过Referer的方式:
需要用到Nginx的模块ngx_http_referer_module,用于阻挡来源非法的域名请求。

Nginx指令valid_referers,全局变量$invalid_referer

valid_referers none | blocked |server_name|string...;
none:'Referer'来源头部为空的情况
blocked:'Referer'来源头部不为空,但是里面的值被代理或者防火墙删除了,这些值不以http://或者https://开头。
server_names:"Referer"来源头部包含当前的server_names;

实例代码:

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

加上这段代码,那么对于gif|jpg|png|flv|swf|rar|zip文件,如果referer为空,或是代理,或是包含mrone.top,那么可以正常访问,否则会跳到http://mrone.top/403.jpg这张图片上。
当然也可以针对目录,

location ~.*\.(gif|jpg|png|flv|swf|rar|zip)$改完location /dir 即可

但是,这种方法有一个弊端,就是他人可以通过伪造referer的方式来继续盗链,这时我们可以使用签名方式。

这种方式需要用到Nginx的第三方模块HttpAccessKeyModule实现。

access on|off //模块开关
accesskey——hashmethod md5|sha-1 //签名加密方式
accesskey_arg GET参数名称
accesskey_signature 加密规则

代码如下:

location ~.*\.(gif|jpg|png|flv|swf|rar|zip)$
{
    access on;
    accesskey——hashmethod md5;
    accesskey_arg 'key';
    accesskey_signature 'salt$remote_addr';
}     

然后PHP代码如下:

$key=md5('salt'.$_SERVER['REMOTE_ADDR']);
echo '<img src="./test.png?key=.$key.">';

这种方式虽然安全,但是,如你所见,实现方式也更为复杂,所以需要酌情使用。

前端优化:

减少HTTP请求

为什么要减少HTTP请求?
先来看一下性能黄金法则:
只有10%-20%的最终用户响应时间花在接收请求的HTML文档上,剩下的80%-90%时间花在HTML文档所引用的所有组件(图片,script,css,flash等等)进行的HTTP请求上。
所以最好的改善方法就是减少组件的数量,并由此减少HTTP请求的数量。

HTTP连接产生的开销:
域名解析–TCP连接–发送请求–等待–下载资源–解析时间

减少HTTPS请求(合并CSS,图片等):
使用图片地图:图片地图允许你在一个图片上关联多个URL。目标URL的选择取决于用户单击了图片上的哪个位置。
图片地图

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

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

合并脚本和样式表适:使用外部的js和css文件引用的方式,因为这比直接写在页面中性能要更好一点。
独立的一个js比用多个js文件组成的页面载入要快38%

图片使用Base64编码减少页面请求数:采用Base64的编码方式将图片直接嵌入到网页中,而不是从外部载入:

<img src='data:image/gif;base,/9j/ASD4ASD....'>

启用浏览器缓存和文件压缩:

先来看一下缓存分类:
HTTP缓存模型中,如果请求成功会有三种情况:
200 from cache:直接从本地缓存中获取响应,最快速,最省流量,因为根本没有向服务器发送请求;
304 Not Modifiied:协商缓存,浏览器在本地没有命中的情况下请求头中发送一定的效验数据到服务端,如果服务端数据没有改变,浏览器从本地缓存响应,返回304;
解释一下:就是本地缓存过期了,你带着头信息向服务器验证,如果服务器告诉你,缓存还可以用,没有过期,那么就读取本地的,否则读取服务器的。
这种方式快速,发送的数据很少,只返回一些基本的响应头信息,数据量很小,不发送实际响应体。
200 OK:以上两种缓存全部失败,服务器返回完整响应,没有用到缓存,相对最慢。

相关的一些Header:
本地缓存:
Pragma:HTTP1.0时代的遗留产物,该字段被设置为no-cache时,会告知浏览器禁用本地缓存,即每次都向服务器发送请求。
Expries:HTTP1.0时代用来启用本地缓存的字段,expires值对应一个形如Thu,31 Dec 2037 23:55:55 GMT的格林威治时间,告诉浏览器缓存实现的时刻,如果还没到该时刻,标明缓存有效,无需发送请求。(PS:浏览器与服务器时间无法保持一致,如果时间差距大,就会影响缓存结果)。
Cache-Control:HTTP1.1针对Expires时间不一致的解决方案,运用Cache-Control告知浏览器缓存过期的时间间隔而不是时刻,即使具体时间不一致,也不影响缓存的管理。
常见设置的值:no-store:禁止浏览器响应缓存;no-cache:不允许直接使用本地缓存,先发起请求和服务器协商;max-age=delta-seconds:告知浏览器该响应本地缓存有效的最长期限,以秒为单位。
优先级:Pragma>Cache-Control>Expries

协商缓存:
当浏览器没有命中本地缓存,如本地缓存过期或者响应中声明不允许直接使用本地缓存,那么浏览器肯定会发起服务端请求。服务端会验证数据是否修改,如果没有通知浏览器使用本地缓存。
相关Header:
Last-Modified:通知浏览器资源的最后修改时间(格林威治时间)
If-Modified-Since:得到资源的最后修改时间后,会将这个信息通过If-Modified-Sinc提交到服务器做检查,如果没有修改,返回304状态码。
ETag:HTTP1.1推出,文件的指纹标识符,如果文件内容修改,指纹会改变。
对应ETag的Header:If-None-Match:本地缓存失效,会携带此值去请求服务器,服务器判断该资源是否改变,如果没有改变,直接使用本地资源,返回304.

缓存策略的选择:
适合做本地缓存的内容:
不变的头像,如LOGO,图标等;js,css静态文件;可下载的内容,媒体文件
协商缓存:HTML文件,经常替换的图片,经常修改的js,css文件等
js,css文件的加载可以加入文件的签名来拒绝缓存
index.css?签名
index.签名.js

不建议缓存的内容:用户隐私等敏感数据,经常改变的api数据接口等

Nginx本地缓存配置:.

add_header指令:添加状态码为2XX和3XX的响应头信息
add_header name value [always];
可以设置Pragma/Expires/Cache-Control,可以继承。
expires指令:通知浏览器过期时长:
expires tims;为负值时表示Cache-Control:no-cache;为正或者0时,就表示Cache-Control:max-age=指定的时间;

在nginx.conf指定:
expires 12d;(使用最多)

协商缓存:
etag on|off //默认开启

前端代码和资源压缩:

优势:让资源文件更小,加快文件在网络中的传输,让网页更快的展现,降低带宽和流量开销。
方式:JS、css、图片、html代码的压缩,Gzip压缩
JS和CSS的压缩原理差不多,去空白符,注释并且优化一些语义规则等。网上有很多在线的压缩根据。
HTML则是不台建议代码压缩,有时会破坏代码结构,可以使用Gzip压缩,当然也可以使用htmlcompressor工具,不过转换后一点要检查代码结构。
图片压缩工具:tinypng、JpegMini,ImageOptim。

Gzip压缩:配置Nginx服务

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

CDN加速

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

原理
在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络。
CDN系统能过实时地根据网络流量和各节点的连接、负载以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务器节点上。

CDN的优势
本地Cache加速,提高了企业站点(尤其含有大量图片和静态页面站点的访问速速)
跨运营商的网络加速,保证不同网络的用户都得到良好的访问质量
远程访问用户根据DNS负载均衡技术只能自动选择Cache服务器

它可以自动地生成服务器的远程Mirror(镜像)cache服务器,远程用户访问时从cache服务器上读取数据,减少远程访问的带宽、分担网络流量、减轻原站WEB服务器负载等功能。
CDN广泛分布的节点加上节点之间的智能冗余机制,可以有效地预防黑客入侵。

一般来说,传统的用户访问是这样的:
传统访问

使用了CDN后,访问流程变成了这样:
CDN访问

我们来看一张大图了解流程
大概流程

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

实现方式:
如果是云服务器,BAT都有提供相应的CDN服务。
可以使用LVS做4层负载均衡
可用Nginx,Varnish,Squid,Apache,TrafficServer做7层负载均衡和cache。

建立独立图片服务器;

为什么要建立独立的图片服务器,并且使用独立的域名:
分担Web服务器的I/O负载,将耗费资源的图片服务分离出来,提高服务器的性能和稳定性。
能够专门对图片服务器进行优化,为图片服务设置有针对性的缓存方案,减少带宽成本,提高访问速度。
提高网站的可扩展性,通过增加图片服务器,提高图片吞吐能力。

为什么使用独立域名:
同一域名下浏览器的并发连接数有限制(2-6),突破浏览器连接数的限制,由于cookie的原因,对缓存不理,大部分web cache都只缓存不带cookie的请求,导致每次的图片请求都不能命中cache。

带来的问题:
如何进行图片上传和图片同步:
NFS共享方式
利用FTP同步

服务端优化:

动态语言静态化:

什么是动态语言静态化?
将现有PHP等动态语言的逻辑代码生成为静态HTML文件,用户访问动态脚本重定向到静态HTML文件的过程。(应只静态化对实时性要求不高的页面)
为什么?
动态脚本通常会做逻辑计算和数据查询,访问量越大,服务器压力越大。
访问量大时可能会造成CPU负载过高,数据库服务器压力过大。
静态化可以减低逻辑处理压力,减低数据库服务器查询压力。

实现方式:
使用模板引擎:
可以使用Smarty的缓存机制生成静态HTML缓存文件。

$smarty->cache_dir=$ROOT.'/cache';//缓存目录
$smarty->caching=true;//是否开启缓存
$smarty->cache_lifetime='3600';//缓存时间
$smarty->display(string template[,string cache_id[,string compile_id]]);
$smarty->clear_all_cache()://清除所有缓存
$smarty->clear_cache('file.html');//清除指定的缓存
$smarty->clear_cache('article.html',$art_id);//清除同一个模板下的指定缓存号的缓存

利用ob系列的函数:

ob_start();//打开输出控制缓冲
ob_get_contents();//返回输出缓冲区内容
ob_clean();//清空输出缓冲区
ob_end_flush();//冲刷出(送出)输出缓冲区内容并关闭缓冲

基本用的还是ob,这部分yii2有相关函数的封装,直接使用即可。

并发处理:

如果说用户走到了我们动态化,走到了我们PHP端
要解决并发,首先来了解几个概念:
程:进程(process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结果的基础。
进程是一个“执行中的程序”;
进程的三态模型:多道程序系统中,进程在处理器上交替运行,状态不断地发生变化。
运行:当一个进程在处理机上运行时,则称该进程处于运行状态。处于此状态的进程的数目小于等于处理器的数目,对于单处理机系统,处于运行状态的进程只有一个。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
就绪:当一个进程获得了除处理机以外的一切所需资源,一旦得到处理机即可运行,则称此进程处于就绪状态。就绪进程可以按多个优先级来划分队列。例如,让一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程有I/O操作完成二进入就绪状态时,排入高优先级队列。
阻塞:也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求I/O或等待I/O完成等)而暂停运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。

进程的五态模型:对一个实际的系统,进程的状态及其转换更为复杂。分为新建态、活跃就绪/静止就绪、运行、活跃阻塞/静止阻塞、终止态
新建态:对于进程刚刚被创建时没有被提交的状态,并等待系统完成创建进程的所有必要信息。
终止态:进程已经结束运行,回收除进程控制块以外的其他资源,并让其他进程从进程控制块中收集有关信息。
活跃就绪:是指进程在主存中并且可被调度的状态。
静止就绪(挂起就绪):是指进程被对换到辅存时的就绪状态,是不能被直接调度的状态,只有当主存中没有活跃就绪态进程,或者是挂起就绪态进程具有更高的优先级,系统将把挂起就绪态进程调回主存并转换为活跃就绪。
活跃阻塞:是指进程已在主存,一旦等待的时间产出便进入活跃就绪状态。
静止阻塞:进程兑换到辅存时的阻塞状态,一旦等待的时间产生便进入静止就绪状态。

线程:有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。
线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。
一个线程可与创建和撤销另一个线程,同一进程中的多个线程之间可与并发执行。
线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。
在但个程序中同时运行多个线程完成不同的工作,称之为多线程。
每一个程序都至少有一个线程,若程序只有有个线程,那就是程序本身。
线程的状态:就绪、堵塞、运行,类似进程的三态模型
就绪状态:线程具备运行的所有条件,逻辑上可以运行,在等待处理机。
运行状态:线程占有处理机正在运行。
阻塞状态:线程在等待一个事件(如某个信号量),逻辑上不可执行。

协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

线程和进程的区别:
1) 地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间
2) 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
3) 线程是处理器调度的基本单位,但进程不是
4) 二者均可并发执行

5) 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制

线程与协程的区别:
1) 一个线程可以多个协程,一个进程也可以单独拥有多个协程。

2) 线程进程都是同步机制,而协程则是异步

3) 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态

多进程:同一个时间里,同一个计算机系统中如果允许两个或者两个以上的进程处于运行状态,这就是多进程。
多开一个进程,多分配一份资源,进程间通讯不方便。
多线程:线程就是吧一个进程分为很多片,每一片都可以是独立的流程。
与多进程的区别是只会使用一个进程的资源,线程间可以直接通信。

PHP并发编程实例:
使用PHP的Swoole扩展、消息队列、接口的并发请求。
Swoole2.0支持了类似Go语言的协程,可以使用完全同步的代码实现异步程序。
消息队列,常见产品有Kafka、ActiveMQ、ZeroMQ、RabbitMQ、Redis等。
curl_multi_add_handle。

由于这部分内容较为复杂,这里只提供方向,准备之后再深入了解学习。

数据库优化:

数据库缓存

查询缓存:query_cache_type,通过该值来确定,有0、1、2三个取值,0不使用查询缓存,1始终使用查询缓存,2按需使用查询缓存
该值为1时,可以通过SELECT SQL_NO_CACHE FROM my_table WHERE condition来使本条语句不使用查询缓存。
为2时,可按需使用查询缓存,SELECT SQL_CACHE
FROM my_table WHERE condition。
query_cache_size,默认情况下为0,表示为查询缓存预留的内存为0,则无法使用查询缓存,可用SET GLOBAL query_cache_size=XXXX来设置。

查询缓存可以看做是SQL文本和查询结果的映射。
注意:第二次查询的SQL和第一次查询的SQL完全相同,则会使用缓存。
SHOW STATUS LIKE ‘%Qcache_hits%’;可查看命中次数
表的结果或者数据发生改变时,查询缓存中的数据不再有效。

清理缓存:

FLUSH QUERY CACHA;//清理查询缓存内存碎片
RESET QUERY CACHE;//从查询缓存中移出所有查询
FLUSH TABLES;// 关闭所有打开的表,同时该操作将会清空查询缓存中的内容。

综上所述,这并不是很好的缓存方式。 我们更应该使用的是Memcache或者是Redis。
Memcache相对使用简单,Redis则更复杂些,不是简单说得完,建议后期补课。这里讲讲2者的区别:
性能相差不大;
Redis在2.0版本后增加了自己的VM特性,突破物理内存的限制,Memcache可以修改最大可用内存,采用LRU算法。
Redis,依赖客户端来实现分布式读写
Memcache本身没有数据冗余机制
Redis支持(快照、aof),依赖快照可以进行持久化,aof增强了可靠性的同时,对性能有所影响。
Memcache不支持持久化,通常说缓存,提升性能,
Memcache在并发场景下,用cas保证一致性,redis事务支持比较弱,只能保证事务中的每个操作连续执行。
Redis支持多种类的数据类型。
Redis用于数据量较小的高性能操作和运算上
Mencache用于在动态系统中减少数据库负载,提升性能;适合做缓存,提高性能。

MySql数据库层的优化:

优化方向:
数据表数据类型优化:字段使用什么样的数据类型更合适,字段使用什么样的数据类型性能更快
索引优化:索引在什么情况下效率最高,注意复合索引的前缀原则,like查询%的问题,全面扫描优化,OR条件索引使用情况,字符串类型索引失效的问题
SQL语句的优化:优化查询过程中的数据访问,优化长难句的查询语句,优化特定类型的查询语句
存储引擎的优化:尽量使用InnoDb引擎,
数据表结构设计的优化:分区操作(通过特定的政策对数据表物理拆分;用户无感知),分库分表(水平拆分,垂直拆分),
数据库服务器架构的优化:主从复制,读写分离,双主热备,负载均衡,

优化方案:
分库分表、分区操作,读写分离,负载均衡等;

WEB服务器优化:负载均衡,相关优化过程在我另一篇文章上有写,使用的是阿里提供的slb实现负载均衡,点击这里查看