解决高并发和大流量问题
我们说的高并发是什么?
在互联网时代,所讲的并发、高并发,通常是指并发访问。也就是在某个时间点,有多少个访问同时到来
通常如果一个系统的日PV在干万以上,有可能是一个高并发的系统
高并发的问题,我们具体该关心什么?
QPS
每秒钟请求或者査询的数量,在互联网领域,指毎秒响应请求数(指HTTP请求);
一个页面中可能有多个 http 请求: (总PV数*80%)/(6小时秒数*20%)=峰值每秒请求数(QPS)
- 吞吐量
单位时间内处理的请求数量(通常由QPS与并发数决定)
- 响应时间
从请求发出到收到响应花费的时间。例如系统处理一个HTTP请求需要100ms,这个100ms就是系统的响应时间
PV
综合浏览量(Page view
),即页面浏览量或者点击量,个访客在24小时內访问的页面数量
UV
独立访客(Unique Visitor
),即一定时间范围内相同访客多次访问网站,只计算为1个独立访客
- 带宽
计算带宽大小需关注两个指标,峰值流量和页面的平均大小日网站带宽=PV/统计时间(换算到秒)*平均页面大小(单位KB)*8
常用性能测试工具
ab
、wrk
、 http_load
、 Web bench
、 Siege
、 Apache Jmeter
ab
概念
全称是 apache benchmark,是 apache官方推出的工具刨建多个并发访问线程,模拟多个访问者同时对某一URL地址进行访问。
它的测试目标是基于URL的,因此,它既可以用来测试apache的负载压力,也可以测试 nginx、 lighthttp、 tomcat、IS等其它Web服务器的压力
ab
的使用
1 | 模拟并发请求100次,总共请求5000次 |
注意事项
- 测试机器与被测试机器分开
- 不要对线上服务做压力测试
- 观察测试工具ab所在机器,以及被测试的前端机的cPU,内存,网络等都不超过最高限度的75% (top 命令)
详细请看: Linux 命令 「ab」
QPS划分
QPS达到50
可以称之为小型网站,一般的服务器就可以应付,无需优化
QPS达到100
假设关系型数据库的每次请求在0.01秒完成
假设单页面只有一个SQL查询,那么100QPS意味着1秒钟完成100
次请求,但是此时我们并不能保证数据库查询能完成100次
方案:数据库缓存层、数据库的负载均衡
QPS达到800
假设我们使用百兆带宽,意味着网站出口的实际带宽是8M左右假设每个页面只有10K,在这个并发条件下,百兆带宽已经吃完
方案:CDN加速、负载均衡
QPS达到1000
假设使用 Memcache
缓存数据库查询数据,每个页面对Memcache
的请求远大于直接对DB
的请求Memcache
的悲观并发数在2W
左右,但有可能在之前内网带宽已经吃光,表现出不稳定
方案:静态HTML缓存
QPS达到2000
这个级别下,文件系统访问锁都成为了灾难
方案:做业务分离,分布式存储
数据库的优化
- 数据库的缓存(memcache 缓存,redis 缓存等)
- 分库分表、分区操作
- 读写分离
- 负载均衡
数据表数据类型优化
字段使用什么样的数据类型更合适
tinyint
(0-255)smallint
,bigint
char
,varchar
enum
特定、固定的分类可以使用enum
存储,效率更快- `IP地址的存储 //用 php 的 ip2long(‘192.168.1.38’); //3232235814
- 对字段进行
not null
这样,存储的字段就不会有null
值 ,只有空值
索引优化
- 索引不是越多越好,在合适的字段上创建合适的索引
- 复合索引的前缀原则
like
查询%
的问题(% 在前如:%name 则索引失败)- 全表扫描优化
or
条件索引使用情况- 字符串类型索引失效的问题(如字符串类型的字段必须要加引号查询)
SQL语句的优化
- 优化查询过程中的数据访问
- 优化长难句的查询语句
- 优化特定类型的查询语句
- 使用 Limit
- 返回列不用
*
- 变复杂为简单
- 切分查询(如删大量数据时,可分多次删除)
- 分解关联查询
- 优化
count()
(如对统计数据单独存放在一个字段,而不是进行 count() 统计) - 优化关联查询
- 优化子查询
- 优化
Group by
和distinct
- 优化
limit
和union
存储引擎优化
尽量使用 **Inno DB
**存储引擎
数据表结构设计的优化
分区操作
- 通过特定的策略对数据表进行物理拆分
- 对用户透明
- partition by (分区)
- 对新建表进行分区
1 | CREATE TABLE employees ( |
- 对已有表进行分区
1 | ALTER TABLE user PARTITION BY RANGE (id) |
分库分表
- 水平拆分
- 垂直拆分
数据库服务器架构的优化
- 主从复制
- 读写分离
- 双主热备
- 负载均衡:
- 通过
LVS
的三种基本模式实现负载均衡 My Cat
数据库中间件实现负载均衡
- 通过
流量优化-防盗链
防盗链处理
盗链概念
盗链是指在自己的页面上展示一些并不在自己服务器上的内容获得他人服务器上的资源地址,
绕过别人的资源展示页面,直接在自己的页面上向最终用户提供此内容
常见的是小站盗用大站的图片、音乐、视频、软件等资源
通过盗链的方法可以减轻自己服务器的负担,因为真实的空间和流量均是来自别人的服务器
防盗链概念
防止别人通过一些技术手段绕过本站的资源展示页面,盗用本站的资源,
让绕开本站资源展示页面的资源链接失效
可以大大减轻服务器及带宽的压力
工作原理
通过 Referer
或者 签名
,网站可以检测目标网页访问的来源网页,
如果是资源文件,则可以跟踪到显示它的网页地址。
一旦检测到来源不是本站即进行阻止或者返回指定的页面
Referer
方式防盗链
Nginx
模块 ngx_http_referer_module
用于阻挡来源非法的域名请求
Nginx指令 valid referers
, 全局变量$invalid referer
valid_referers none / blocked / server_names,string
1 | none:" Referer"来源头部为空的情况 |
1 | location ~ .*\.(gifljpglpnglflvlswfrarlzips)${ |
加密签名
伪造 Referer
:可以使用加密签名解决
使用第三方模块HttpaccesskeyModule
实现 Nginx
防盗链
1 | accesskey on off模块开关 |
1 | location ~ .*\.(gifljpglpnglflvlswfrarlzips)${ |
1 | $sign= md5('jason'. $_SERVER['remote_addr']; |
CDN
加速
CDN ?
CDN
的全称是Content Delivery Network
,即内容分发网络。
其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。
传统模式
入域名发起请求
–>解析域名获取内容
–>对应的服务器
–>服务器响应并返
使用CDN访问
用户发起请求
–>智能DNS的解析
(根据IP判断地理位置、接入网类型、选择路由最短和负载最轻的服务器)
–>取得缓存服务器IP
–>把内容返回给用户(如果缓存中有)
–>缓存中无,向源站发起请求
–>将结果返回给用户
–>将结果存入缓存服务器
场景
站点或者应用中大量静态资源的加速分发,例如:Css,JS图片和HTML,大文件下载,直播网站等
实现
BAT
等都有提供CDN
服务- 可用
VS
做4层负载均衡 - 可用
Nginx
,Varnish
,Squid
,Apache Trafficserver
做7
层负载均衡和cache
- 使用
squid反向代理
, 或者Nginx
等的反向代理
独立图片服务器部署
独立的必要性
- 分担Web服务器的I/O负载将耗费资源的图片服务分离出来,提高服务器的性能和稳定性
- 能够专门对图片服务器进行优化-为图片服务设置有针对性的缓存方案,减少带宽成本,提高访问速度
- 提高网站的可扩展性-通过增加图片服务器,提高图片吞吐能力
采用独立域名
并非二级域名
原因
- 同一域名下浏览器的并发连接数有限制,突破浏览器连接数的限制
- 由于
cookie
的原因,对缓存不利,大部分Web cache
都只缓存不带cookie
的请求,
导致每次的图片请求都不能命中cache
独立后的问题
如何进行图片上传和图片同步
NFS
共享方式- 利用
FTP
同步(php 可以操作 ftp)
动态语言静态化
什么是动态语言静态化
将现有PHP
等动态语言的逻辑代码生成为静态HTML
文件,用户访问动态脚本重定向到静态HTML
文件的过程。
对实时性要求不高的页面
为什么要静态化
动态脚本通常会做逻辑计算和数据查询,访问量越大,服务器压力越大
访问量大时可能会造成CPU负载过高,数据库服务器压力过大
静态化的实现方式
使用模板引擎
可以使用Smarty
的缓存机制生成静态HTML
缓存文件
1 | $smarty-> cache dir=$RooT."/ cache";/缓存目录 |
利用ob系列的函数
1 | ob_start():打开输出控制缓冲 |
1 | ob_start(): |
实现页面静态化,并且当内容改变时,主动缓存新内容,且如果有$_ GET
参数时候,带参数的静态化页面
例:
1 |
|
动态语言的并发处理
什么是进程、线程、协程?
进程(Process
)
是计算机中的程序关于某数据集合上的一次运行活动,
是系统进行资源分配和调度的基本单位,是操作系统结构的基础,进程是一个“执行中的程序”
进程的三态模型
:多道程序系统中,进程在处理器上交替运行,状态不断地发生变化
- 运行
运行:当一个进程在处理机上运行时,则称该进程处于运行状态。处于此状态的进程的数目小于等于处理器的数目,对于单处理机系统,处于运行状态的进程只有一个。
在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
- 就绪
就绪:当一个进程获得了除处理机以外的一切所需资源,一旦得到处理机即可运行,则称此进程处于就绪状态。就绪进程可以按多个优先级来划分队列。
例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。
- 阻塞
阻塞:也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求Io而等待I/o完成等)而暂时停止运行,
这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态
进程的五态模型
:对于一个实际的系统,进程的状态及其转换更为复杂。
- 新建态
- 活跃就绪/静止就绪
- 运行
- 活跃阻塞/静止阻塞
- 终止态
线程
线程,有时被称为轻量级进程
(Lightweight Process,LWP),是程序执行流的最小单元。
线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,
只拥有一点儿在运行中必不可少的资源但它可与同属一个进程的其它线程共享进程所拥有的全部资源。
一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。
线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU
的基本单位指运行中的程序的调度单位。
在单个程序中同时运行多个线程完成不同的工作,称为多线程
。
每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
线程的状态:就绪
、阻塞
、运行
协程
协程是一种用户态的轻量级线程
,协程的调度完全由用户控制。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,
恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
线程与进程的区别 ?
- 线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间
- 进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
- 线程是处理器调度的基本单位但进程不是
- 二者均可并发执行
- 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,
但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
线程与协程的区别
- 一个线程可以多个协程,一个进程也可以单独拥有多个协程
- 线程进程都是同步机制,而协程则是异步
- 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态
什么是多进程、多线程 ?
多进程
同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,这就是多进程(如同时听歌,玩游戏)
多开一个进程,多分配一份资源,进程间通讯不方便
多线程
线程就是把一个进程分为很多片,每一片都可以是一个独立的流程
与多进程的区别是只会使用一个进程的资源,线程间可以直接通信
同步阻塞模型
多进程
最早的服务器端程序都是通过多进程、多线程来解决并发IO的问题一个请求创建一个进程,
然后子进程进入循环同步堵塞地与客户端连接进行交互,收发处理数据
多线程
用多线程模式实现非常简单,线程中可以直接向某一个客户端连接发送数据
缺点
这种模型严重依赖进程的数量解决并发问题
启动大量进程会带来额外的进程调度消耗
异步非阻塞模型
- 现在各种高并发异步
IO
的服务器程序都是基于epol
!实现的 IO复用异步非阻塞程序
使用经典的Reactor
模型,
Reactor顾名思义就是反应堆的意思,它本身不处理任何数据收发。只是可以监视一个socket
句柄的事件变化
Reactor
有4个核心的操作
add
添加socket
监听到reactor
set
修改事件监听,可以设置监听的类型,如可读、可写del
从reactor
中移除,不再监听事件callback
,事件发生后对应的处理逻辑,一般在add
/set
时制定
->more
Nginx
: 多线程 Reactor
Swoole
: 多线程 Reactor
+ 多进程 Worker
PHP并发编程实践
PHP的 Swoole
扩展
PHP的异步、并行、高性能网络通信引擎,使用纯C语言编写,提供了PHP语言的异步多线程服务器,异步TCP/UDP网络客户端
,异步 MYSQL
,异步 Redis
, 数据库连接池
, Asynctask
, 消息队列
, 毫秒定时器
, 异步文件读写
, 异步DNS查询
除了异步IO的支持之外, Swoole
为PHP多进程的模式设计了多个并发数据结构和IPC通信机制,可以大大简化多进程并发编程的工作
Swoole2.0
支持了类似Go
语言的协程,可以使用完全同步的代码实现异步程序
Swoole
的异步MYSQL
实现:
1 | $db = new Swoole\MYSQL; |
并发处理
消息队列
- 场景说明
用户注册后,需要发注册邮件和注册短信
- 串行方式
将注册信息写入数据库成功后,发送注册邮件,再发送注册短信
- 并行方式
将注册信息写入数据库成功后,发送注册邮件的同时发送注册短信
- 消息队列方式
将注册信息写入数据库成功后,将成功信息写入队列,此时直接返回成功给用户,
写入队列的时间非常短,可以忽略不计,然后异步发送邮件和短信
应用解耦
- 场景说明
用户下单后,订单系统需要通知库存系统。
假如库存系统无法访问,则订单减库存将失败,从而导致订单失败订单系统与库存系统耦合
- 引用队列
用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功,
订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作
流量削锋
- 应用场景
秒杀活动,流量瞬时激增,服务器压力大。
用户发起请求,服务器接收后,先写入消息队列。假如消息队列长度超过最大值,则直接报错或提示用户
后续程序读取消息队列再做处理
控制请求量
缓解高流量
日志处理
- 应用场景
解决大量日志的传输,日志采集程序将程序写入消息队列,然后通过日志处理程序的订阅消费日志
消息通讯
- 应用场景
聊天室
多个客户端订阅同一主题,进行消息发布和接收
常见消息队列产品
Kafka
、 Activemq
、 Zeros
、 Rabbitmq
、 Redis
等
接口的并发请求
curl_multi_init
php
端可同时调用多个接口
数据库缓存
MySQL
等一些常见的关系型数据库的数据都存储在磁盘当中,在高并发场景下,业务应用对MYSQL
产生的增、删、改、查的操作造成巨大的I/O开销和查询压力,
这无疑对数据库和服务器都是一种巨大的压力,为了解决此类问题,缓存数据
的概念应运而生。
优点
极大地解决数据库服务器的压力
提高应用数据的响应速度
常见的缓存形式
- 内存缓存
- 文件缓存
启用 MYSQL查询缓存
极大地降低CPU使用率
query_cache_type
查询缓存类型,有0
、1
、2
三个取值:
0
表示不使用查询缓存。1
表示始终使用查询缓存。
1 | // 对某一条不进行缓存 |
2
表示按需使用查询缓存
1 | // 在需要缓存时,添加SQL_CACHE |
query_cache_size
默认情况下 query_cache_size
为0,表示为查询缓存预留的内存为0,无法使用查询缓存
- 设置
1 | SET GLOBAL query cache size =134217728; |
注意事项
查询缓存可以看做是SQL文本和查询结果的映射
第二次查询的SQL和第一次查询的SQL完全相同,则会使用缓存SHOW STATUS LIKE 'Qcache_hits
;查看命中次数
表的结构或数据发生改变时,查询缓存中的数据不再有效
清理缓存
FLUSH QUERY CACHE;
// 清理查询缓存内存碎片RESET QUERY CACHE;
// 从查询缓存中移出所有查询FLUSH TABLES;
// 关闭所有打开的表, 同时该操作将惠清空查询缓存中的内容
使用 Memcache
缓存查询数据
对于大型站点,如果没有中间缓存层,当流量打入数据库层时,即便有之前的几层为我们挡住一部分流量,但是在大并发的情况下,还是会有大量请求涌入数据库层,
这样对于数据库服务器的压力冲击很大,响应速度也会下降,因此添加中间缓存层很有必要。
工作原理
Memcache
是一个高性能的分布式的内存对象缓存系统,通过在内存里维护一个统一的巨大的hash
表,
它能够用来存储各种格式的数据,包括图像、视频、文件以及数据库检索的结果等。
简单的说就是将数据调用到内存,然后从内存中读取,从而大大提高读取速度
方法
- 获取:
get(key)
- 设置:
set(key, val, exp)
- 删除:
delete(key)
使用 Redis
绶存查询数据
与 Memcache的区别
- 性能相差不大
Redis
在2.0版本后增加了自己的VM特性,突破物理内存的限制,Memcache
可以修改最大可用内存采用LRU算法
Redis
,依赖客户端来实现分布式读写Memcache
本身没有数据冗余机制Redis
支持(快照、AOF
),依赖快照进行持久化,aof
增强了可靠性的同时,对性能有所影响Memcache
不支持持久化,通常做缓存,提升性能;Memcache
在并发场景下,用cas
保证一致性,redis
事务支持比较弱,只能保证事务中的毎个操作连续执行Redis
支持多种类的数据类型Redis
用于数据量较小的高性能操作和运算上Memcache
用于在动态系统中减少数据库负载,提升性能;适合做缓存,提高性能
Web
服务器的负载均衡-Ngnix
反向代理
七层负载均衡的实现
基于URL等应用层信息的负载均衡Nginx
的proxy
是它一个很强大的功能,实现了7
层负载均衡
Nginx
负载均衡
- 内置策略:
IP Hash
、加权轮询
- 扩展策略:
fair策略
、通用hash
、一致性hash
内置策略
- 加权轮询策略
首先将请求都分给高权重的机器,直到该机器的权值降到了比其他机器低,才开始将请求分给下一个高权重的机器
当所有后端机器都down
掉时, Nginx
会立即将所有机器的标志位清成初始状态,以避免造成所有的机器都处在 timeout
的状态
IP Hash
Nginx
内置的另一个负载均衡的策略,流程和轮询很类似,只是其中的算法和具体的策略有些变化
IP Hash算法是一种变相的轮询算法
扩展策略
fair
策略
根据后端服务器的响应时间判断负载情况,从中选出负载最轻的机器进行分流
- 通用
Hash
、一致性Hash
策略
通用hash
比较简单,可以以Nginx
内置的变量为key
进行hash
,
一致性hash
采用了Nginx
内置的一致性hash
环,支持 memcache
1 | // Nginx配置 |
四层负载均衡的实现
通过报文中的目标地址和端口,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器
LVS
实现服务器集群负载均衡有三种方式: NAT
, DR
和 TUN