高并发
【高并发解决方案】
1. 了解 PV、UV、QPS
2. 优化方案:防盗链、减少HTTP请求、浏览器缓存、CDN、数据库缓存、MySQL读写分离、分区分库分表、LVS负载均衡
QPS:每秒钟请求或者查询的数量,在互联网领域,指每秒响应请求数(指HTTP请求);
吞吐量:单位时间内处理的请求数量(通常由QPS与并发数决定);
响应时间:从请求发出到收到响应花费的时间;
PV:综合浏览量(Page View),即页面浏览量或者点击量,一个访客在 24 小时内访问的页面数量。同一个人浏览你的网站同一页面,只记作一次PV;
UV:独立访客(UniQue Visitor),即一定时间范围内相同访客多次访问网站,只计算为1个独立访客;
【QPS】
QPS(Query Per Second),QPS 其实是衡量吞吐量(Throughput)的一个常用指标,
就是说服务器在一秒的时间内处理了多少个请求 —— 我们通常是指 HTTP请求,显然数字越大代表服务器的负荷越高、处理能力越强。
并发用户数 是指系统可以同时承载的正常使用系统功能的用户的数量。
性能测试工具ab的用法:例如模拟并发请求 100次,总共请求 5000次
ab -c 100 -n 5000 待测试网站:
-c 表示 并发数,这儿是100; -n表示 总共请求次数,这儿是5000)
QPS达到极限,该怎么办?
数据库缓存层、数据库的负载均衡。CDN加速、负载均衡。静态HTML缓存。做业务分离,分布式存储。
高并发解决方案案例
(1)流量优化:防盗链处理。
(2)前端优化:减少HTTP请求、添加异步请求、启用浏览器缓存和文件压缩、CDN 加速、建立独立图片服务器
(3)服务端优化:页面静态化、并发处理、队列处理
(4)数据库优化:使用缓存(memcache)、分库分表分区、读写分离、负载均衡
(5)Web 服务器优化:负载均衡(Nginx 反向代理实现负载均衡;七层/四层(LVS)软件实现负载均衡。)
【服务器负载过高的解决方案】
执行top -c命令,找到cpu最高的进程的id
top命令查询服务器负载达到2.0-5之间,nginx的cpu使用率达到104%
问题分析过程:
(1)df -h 命令,查看磁盘是否是否超出正常范围
(2)free 命令,查看内存使用率是否超出正常范围
问题引出:如何确定nginx的100%的cpu使用率到底问题在哪?
ps -ef | grep nginx 查询出nginx的进程PID(eg:8209)
ps -aux | grep nginx 查询出该进程是哪个用户启动的(即使ROOT用户可能也导出线程快照失败)
su root 切换到进程启动用户
【防盗链】
防盗链的工作原理:
1、通过 Referer 或者 签名,网站可以检测目标网页访问的来源网页,如果是资源文件,则可以跟踪到显示它的网页地址;
2、一旦检测到来源表示本站即进行阻止或者返回指定的页面;
3、通过计算签名的方式,判断请求是否合法,如果合法则显示,否则返回错误信息。
防盗链的实现方法
1、Referer
Nginx 模块 ngx_http_referer_module 用于阻挡来源非法的域名请求;
Nginx 指令 valid_referers,全局变量 $invalid_referer;
none :“Referer” 来源头部为空的情况;
blocked:“Referer” 来源头部不为空,但是里面的值被代理或者防火墙删除了;
referer防盗链的实现方法:打开配置文件nginx.conf
#对文件防盗链
location ~.*\.(gif|jpg|png|flv|swf|rar|zip)$
{
if ($invalid_referer) {
# return 403;
rewrite ^/http://www.imooc.com/403.jpg;
}
}
#对目录防盗链
location /images/
{
…… 同上
}
传统防盗链遇到的问题:通过伪造Referer来实现盗链,可以使用加密签名解决该问题。
2、加密签名
使用第三方模块 HttpAccessKeyModule 实现 Nginx防盗链;
accesskey on | off 模块开关;
accesskey_hashmethod md5 | sha-1 签名加密方式;
accesskey_arg GET参数名称;
accesskey_signature 加密规则(在PHP与Nginx中签名的加密规则要一致,这样才能通过,从而进行访问,否则无法进行访问);
加密签名防盗链的实现方法:
location ~.*\.(gif|jpg|png|flv|swf|rar|zip)$
{
accesskey on;
accesskey_hashmethod md5;
accesskey_arg "key";
accesskey_signature "mypass$remote_addr"; // $remote_addr 客户端IP
}
PHP中用如下代码实现:
// 客户端IP 进行md5 加密
$sign = mg5('jason', $_SERVER['REMOTE_ADDR']);
echo '<img src="./logo_new.png?sign='.$sign.'">';
HTTP连接产生的开销:域名解析——TCP连接——发送请求——等待——下载资源——解析时间
减少HTTP请求的方式:图片地图、合并脚本和样式表
【缓存】
HTTP缓存模型中,如果请求成功会有三种情况:
-
200 from cache:本地缓存
-
304 Not Modified:协商缓存,请求头中发送一定的校验数据到服务端,如果服务端数据没有改变,浏览器从本地缓存响应,返回 304
【特点:快速,发送的数据很少,只返回一些基本的响应头信息,数据量很小,不发送实际响应体】;
-
200 OK:以上两种缓存全都失败,服务器返回完整响应。没有用到缓存,相对最慢。
本地缓存:
浏览器认为本地缓存可以使用,不会去请求服务端【它是最快的】。相关Header:
-
Pragma:HTTP1.0时代的遗留产物,该字段被设置为 no-cache 时,会告知浏览器禁用本地缓存,即每次都向服务器发送请求。
-
Expires:HTTP1.0时代用来启用本地缓存的字段,expires 值对应一个格林威治时间,告诉浏览器缓存实现的时刻,如果还没到该时刻,标明缓存有效,无需发送请求。
Cache-Control:HTTP1.1针对 Expires 时间不一致的解决方案,Cache-Control 给的是一个秒数,即该文件多少秒后过期。
Cache-Control 相关设置:
no-store:禁止浏览器缓存响应
no-cache:不允许直接使用本地缓存,先发起请求和服务器协商
max-age=delta-seconds:告知浏览器该响应本地缓存有效的最长气象,以秒为单位
如果三个被同时设定,则优先级如下:Pragma > Cache-Control > Expires
协商缓存:
当浏览器没有命中本地缓存,如本地缓存过期或者响应中声明不允许直接使用本地缓存,那么浏览器肯定会发起服务端请求。
服务端会验证数据是否被修改,如果没有则通知浏览器使用本地缓存。相关Header:
-
Last-Modified:通知浏览器资源的最后修改时间。将这个信息通过 If-Modified-Since 提交到服务器做检查,如果没有修改,返回 304 状态码。
-
ETag:HTTP1.1推出,文件的指纹标识符,如果文件内容修改,指纹会改变。例如:ETag:“78437822c-6739”。
If-None-Match:本地缓存失效,会携带此值去请求服务端,服务端判断该资源是否改变,如果没有改变,直接使用本地缓存,返回 304。例如:If-None-Match:“78437822c-6739”
ETag on | off; //默认是 on;
关闭 Etag;也可以去设置 add_header :
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
{
etag off;
add_header cache-control max-age=3600 ;
}
Nginx中配置expires【常用】
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
{
expires 30d; //图片缓存30天
}
location ~ .*\.(js|css)?$
{
expires 12h; //js 和 css 缓存 12小时
}
缓存策略的选择:
1)适合缓存的内容:不变的图像,如 Logo,图标等;js,css 静态文件;可下载的内容,媒体文件
2)建议使用协商缓存:HTML文件;经常替换的图片;经常修改的 js,css 文件;
如果不想使用缓存,则可以加入签名:index.css?签名 或者:index.签名.js
3)不建议缓存的内容
-
用户隐私等敏感数据(如:购物车信息,用户个人信息等隐私信息);
-
经常改变的api数据接口(如果做了缓存,数据返回就不准确了)。
PHP配置缓存策略:
$since = $_SERVER['HTTP_IF_MODIFIED_SINCE']; // 获取头
$lifetime = 3600; // 假设该文件 3600s 后过期
// 判断该文件是否过期
if (strtotime($since) + $lifetime > time()) { // 未过期
// 通知浏览器使用本地缓存
header('HTTP/1.1 304 Not Modified');
exit;
}
header('Last-Modified:'. gmdate('D, d M Y H:i:s', time()). ' GMT');
echo time();
前端代码和资源的压缩:
优势:让资源文件更小,加快文件在网络中的传输,让网页更快的展现,降低带宽和流量开销。
压缩方式:
(1)JS、CSS、图片、HTML代码的压缩(可以将里面的空白符,变量,进行语法的合并);
(2)Gzip压缩(在服务器开启 Nginx 中的 Gzip压缩)。Nginx配置:
gzip on|off; # 是否开启 gzip
gzip_buffers 32 4K| 16 8K # 缓冲(在内存中缓冲几块?每块多大)
gzip_types text/plain application/xml # 对那些类型的文件用压缩 如:txt,xml,html,css
【CDN加速】
1、CDN:即内容分发网络。CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息,将用户的请求重新导向离用户最近的服务器节点上。
2、使用CDN的优势:a.本地Cache加速,提高了企业站点的访问速度; b.跨运营商的网络加速;c.减少原站点WEB服务器负载等。
3、CDN的适用场景:大量静态资源的加速分发,例如:CSS、JS、图片和HTML;大文件下载;直播网站等(节省带宽、流量)。
【动态语言静态化】
静态化的实现方式
(1)使用模板引擎,例如 Smarty
(2)利用ob系统的函数
ob_start(); 打开输出控制缓冲;
ob_get_contents(); 返回输出缓冲区内容;
ob_clean(); 清空输出缓冲区;
ob_end_flush(); 冲刷出(送出)输出缓冲区内容并关闭缓冲;
【动态语言的并发处理】
【关于进程和线程】
进程:正在运行中的程序。线程:进程中的一条执行路径。
一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
关于进程、线程、协程:
进程是一个"执行中的程序";
进程的三态模型: 运行、就绪、堵塞。
进程的五态模型: 新建态、终止态、活跃就绪、静止就绪(挂起就绪)、活跃堵塞、静止堵塞。
由于用户的并发请求,为每一个请求都创建一个进程显然是行不通的,从系统资源开销方面或者是响应用户请求的效率方面来看,因此就需要线程。
线程:有时被称为轻量级进程(LWP:Light Weight Process),是程序执行流的最小单元;
线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。
一个线程可以创建和撤销另一个线程,同一进程中多个线程之间可以并发执行;
线程是程序中一个单一的顺序控制流程,进程内一个相对独立的,可调度的执行单元,是系统独立调度和分派CPU的基本单位,指运行中的程序的调度单位。在单个程序中同时运行多个线程完全不同的工作,称为多线程;
每一个程序都至少拥有一个线程,若是程序只有一个线程,那就是程序本身;
线程的状态:就绪状态、运行状态、阻塞状态。
协程:是一种用户态的轻量级线程,协程的调度完全由用户控制(线程是由操作系统控制的)
线程与进程的区别:
-
线程是进程内的一个执行单元,进程内至少有一个线程,他们共拥享进程的地址空间,而进程有自己独立的地址空间;
-
进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
-
线程是处理器调度的基本单位,而进程不是;
-
二者均可并发执行;
-
每个独立的线程有一个程序运行的入口,顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制;
线程和协程的区别:
-
一个线程可以多个协程,一个进程也可以单独拥有多个协程;
-
线程进程都是同步机制,而协程是异步;
-
协程能保留上一次调用时的状态每次过程重入时,就想当于进入上一次调用的状态;
什么是多进程,什么是多线程?
多进程:同一时间里,同一个计算机系统中如果允许两个或者两个以上的进程处于运行状态,这就是多进程;
多线程:把一个进程分成很多片,每一片都可以是一个独立的流程;与多进程的区别是只会使用一个进程的资源,线程之间可以通信;
[举例说明]
单进程单线程:一个人在一个桌子上吃饭
单进程多线程:多个人在同一个桌子上吃饭
多进程单线程:多个人每个人在自己的桌子上吃饭
同步堵塞:
多进程:最早的服务器端程序都是通过多进程、多线程来解决并发IO的问题,一个请求创建一个进程,然后子进程进入循环同步堵塞地与客户端连接进行交互,收发处理数据。
多线程:用多线程模式实现非常简单,线程中可以直接向某一个客户端连接发送数据。
缺点:这种模式最大的问题是,进程/线程创建和销毁的开销很大,严重依赖进程的数量解决并发问题。
PHP代码实例:
<?php
//PHP多进程和多线程的处理
//创建socket监听
$socketserv = stream_socket_server('tcp://0.0.0.0:8000', $errno, $errstr);
//创建5个子进程
for ($i = 0; $i < 5; $i++) {
//使用pcntl_fork()创建进程,会返回pid,如果pid==0,则表示主进程
if (pcntl_fork() == 0) {
//循环监听
while (true) {
$conn = stream_socket_accept($socketserv);
//如果监听失败,则重新去监听
if(!$conn){
continue;
}
//读取流信息,读取的大小 是9000
$request = fread($conn, 9000);
//写入响应
$response = 'hello';
fwrite($conn, $response);
//关闭流
fclose($conn);
}
//创建完所有的子进程,然后退出
exit(0);
}
}
运行 php stream_socket.php,使用ps -ef 查看进程,会看到多出了如下的5个进程:
![](%E9%AB%98%E5%B9%B6%E5%8F%91.resources/D2560A11-72BF-4529-BDCF-FDB0BAC6E7AA.png)
异步非阻塞:
-
Epoll是Linux内核为处理大批量句柄而作了改进的poll。 要使用epoll只需要这三个系统调用:epoll_create(2), epoll_ctl(2), epoll_wait(2)。它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44),在2.6内核中得到广泛应用。
-
现在各种高并发异步IO的服务器程序都是基于epoll实现的。
-
IO复用异步非阻塞程序使用经典的Reactor模型,Reactor顾名思义就是反应堆的意思,它本身不处理任何数据收发。只是可以监视一个socket句柄的事件变化。
Reactor有4个核心的操作:
-
add 添加socket监听到reactor
-
set 修改事件监听,可以设置监听的类型,如可读、可写
-
del 从reactor中移除,不再监听事件
-
callback 就是事件发生后对应的处理逻辑,一般在add/set时制定。
(C语言用函数指针实现,JS可以用匿名函数,PHP可以用匿名函数、对象方法数组、字符串函数名)
Reactor只是一个事件发生器,实际对socket句柄的操作,如connect/accept、send/recv、close是在callback中完成的。
Reactor模型还可以与多进程、多线程结合起来用,既实现异步非阻塞IO,又利用到多核。
目前流行的异步服务器程序都是这样的方式:如
-
Nginx:多进程Reactor
-
Nginx+Lua:多进程Reactor+协程
-
Golang:单线程Reactor+多线程协程
-
Swoole:多线程Reactor+多进程Worker
PHP并发编程实践:
1.PHP的Swoole扩展:
-
PHP的异步、并行、高性能网络通信引擎,Swoole 使用纯 C 语言编写,提供了 PHP 语言的异步多线程服务器,异步 TCP/UDP 网络客户端,异步 MySQL,异步 Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步DNS查询。
-
除了异步 IO 的支持之外,Swoole 为 PHP 多进程的模式设计了多个并发数据结构和IPC通信机制,可以大大简化多进程并发编程的工作。其中包括了并发原子计数器,并发 HashTable,Channel,Lock,进程间通信IPC等丰富的功能特性。
-
Swoole2.0 支持了类似 Go 语言的协程,可以使用完全同步的代码实现异步程序。PHP 代码无需额外增加任何关键词,底层自动进行协程调度,实现异步。
2.消息队列
消息队列的应用场景:
(1)注册后,短信和邮件发送。
场景说明:用户注册后,需要发注册邮件和注册短信。
消息队列方式:将注册信息写入数据库成功后,将成功信息写入队列,此时直接返回成功给用户,写入队列的时间非常短,可以忽略不计,然后异步发送邮件和短信
(2)应用解耦。
场景说明:用户下单后,订单系统需要通知库存系统。
用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。
(3)流量削锋。
应用场景:秒杀活动,流量瞬间激增,服务器压力大。
用户发送请求,服务器接受后,先写入消息队列。假如消息队列长度超过最大值,则直接报错或提示用户。
这样做可以控制请求量、缓解高流量。
(4)日志处理。
应用场景:解决大量日志的传输。
日志采集程序将程序写入消息队列,然后通过日志处理程序的订阅消费日志。
(5)消息通讯。
应用场景:聊天室。
多个客户端订阅同一主题,进行消息发布和接收。
常见消息队列产品:Kafka、ActiveMQ、ZeroMQ、RabbitMQ、Redis等
3.接口的并发请求: curl_multi_init
【数据库缓存层的优化】
【MySQL数据库层的优化】
【Web服务器的负载均衡】
七层负载均衡的实现(Nginx):
基于URL等应用层信息的负载均衡,一般使用Nginx来实现
Nginx的proxy是它一个很强大的功能,实现了7层负载均衡
功能强大、性能卓越、运行稳定
配置简单灵活
能够自动剔除工作不正常的后端服务器
上传文件使用异步模式
支持多种分配策略,可以分配权重,分配方式灵活
1.Nginx负载均衡:
内置策略:IP Hash、加权轮询
扩展策略:fair策略、通用hash、一致性hash
(1)加权轮询策略
首先将请求都分给高权重的机器,直到该机器的权值降到了比其他机器低,才开始将请求分给下一个高权重的机器。
当所有后端机器都down掉时,Nginx会立即将所有机器的标志位清成初始状态,以避免造成所有的机器都处在timeout的状态
(2)IP Hash 策略
Nginx内置的另一个负载均衡的策略,流程和轮询很类似,只有其中的算法和具体的策略有些变化
IP Hash 算法是一种变相的轮询算法
(3)fair策略
根据后端服务器的响应事件判断负载情况,从中选出负载最轻的机器进行分流
(4)通用Hash、一致性Hash策略
通用hash比较简单,可以以Nginx内置的变量为key进行hash,一致性hash采用Nginx内置的一致性hash环,支持memcache
2.Nginx配置:
http{
upstream cluster{
server srv1;
server srv2;
server srv3;
}
server {
listen 80;
location /{
proxy_pass http://cluster;
}
}
}
四层负载均衡的实现(LVS/硬件设备):
LVS实现服务器集群负载均衡有三种方式:NAT、DR、ip-TUN。
硬件设备:通过报文中的目标地址和端口,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器
【负载均衡】
MySQL分库分表:垂直分表(列数太多)、水平分表(数据太多)
网站加速技术:Squid代理缓存技术、页面静态化缓存、Memcache、Sphinx搜索加速
Squid反向缓存(动静分离):
静态数据:静态页面、图片、CSS文件、JS文件。
动态数据:从数据库中取出来的数据
sphinx可以结合MySQL、PostgreSQL做全文索引。
Sphinx单一索引最大可包含1亿条记录,在1千万条记录情况下的查询速度为 0.x 秒(毫秒级)。
Sphinx创建索引的速度:创建100万条记录的索引只需3~4分钟,创建1000万条记录的索引可以在50分钟内完成,而只包含最新10万条记录的增量索引重建一次只需几十秒。
网站服务监控:Mrtg网站监控、Postfix邮件服务器、Shell邮件报警、Cacti网站监控、Cacti邮件报警
网站服务、流量监控:Apache web服务监控、mysql数据库监控、磁盘空间监控
Lvs常用的三种负载均衡模式:
(1)Lvs nat模式 (网络地址转换技术)
(2)Lvs ip-tun模式 (IP隧道技术)
(3)Lvs dr模式 (直接路由技术)