1. 高并发大流量解决方案;
问题:PHP 如何解决网站大流量与高并发的问题?
- 高并发架构相关概念
- 我们所说的高并发是什么
- 在互联网时代,所讲的并发、高并发、通常是指并发访问。简单来说,就是在某一个时间点,有多少个访问同时到来。
- 通常如果一个系统的日 PV 量在千万以上,有可能是一个高并发的系统。
- 有些公司完全不走技术路线,全靠机器堆,这不在我们的讨论范围。
- 高并发的问题,我们具体该关心什么?
- QPS:每秒钟请求或者查询的数量(Queries Per Second)。在互联网领域,指每秒响应请求数(指 HTTP 请求)
- 吞吐量:单位时间内处理的请求数量(通常由 QPS 与并发数决定)
- 响应时间:从请求发出到收到响应花费的时间。例如系统处理一个 HTTP 请求需要 100 ms,这个 100 ms 就是系统的响应时间
- PV:综合浏览量(Page View),即页面浏览量或者点击量,一个访客在 24 小时内访问的页面数量。同一个人浏览网站的同一个页面,只记做一次 PV
- UV:独立访客(Unique Visitor),即一定时间范围内相同访客多次访问网站,只计算为 1 个独立访客
- 带宽:计算带宽大小需关注两个指标,峰值流量和页面的平均大小。日网站带宽 = PV / 统计时间(换算到秒)* 平均页面大小(单位 KB)* 8
- ‐ 峰值一般是平均值的倍数,根据实际情况来定
- ‐ QPS 不等于并发连接数。QPS 是每秒 HTTP 请求数量,并发连接数是系统同时处理的请求数量。一个并发连接数可能会有多个 HTTP 请求(访问一个页面会加载很多静态文件和图片)
- ‐ (总 PV 数 * 80%) / (6小时秒数 * 20%) = 峰值每秒请求数(QPS)。意思是:80% 的访问量集中在 20% 的时间内(二八定律)
- 压力测试:测试能承受的最大并发数、测试最大承受的 QPS 值
- 对于 QPS 来说,需要做一些压力测试。为什么要做测试?对于我们的服务器来说,应该知道一台服务器最大承受多少 QPS。而网站一天的 PV 多少是可以计算出来网站整天的峰值 QPS 是多少,这样就可以根据需要来优化,并不是说感觉到网站的请求比较大了,这时候再请求优化。这是要拿相关的依据来进行优化的。首先需要知道网站的日 QPS 是多少,然后单台服务器的 QPS 承受能力是多少。对于压力测试而言,就是用来了解单台服务器所能承受的 QPS 的值。
- 比如日 QPS 是 200,单机 QPS 的承受力是 50,那就至少需要 4 台服务器才能完成正常的访问。
- 对于 QPS 测试来说,不光要考虑服务器的相关性能,还要考虑程序的一些相关优化
- 常用性能测试工具:ab、wrk、http_load、Web Bench、Siege、Apache JMeter
- ‐ 重点介绍 ab:全称 apache benchmark,是 apache 官方推出的工具。
- ‐ 工作原理是:创建多个并发访问线程,模拟多个访问者同时对某一 URL 地址进行访问。它的测试目标是基于 URL 的,因此它既可以用来测试 apache 的负载压力,也可以测试 nginx、lighthttp、tomcat、 IIS 等其它 web 服务器的压力。
-
‐ ab 的使用:例如:模拟并发请求 100 次,总共请求 5000 次。
ab -c 100 -n 5000 [网站 url]
- ‐ 注意事项:测试机器于被测试机器分开
- ‐ 不要对线上服务做压力测试
- ‐ 观察测试工具 ab 所在机器,以及被测试的前端机的 CPU,内存、网络等都不要超过最高限度的 75%(top 命令)
# 安装 apache benchmark
yum -y install httpd-tools
# 测试(并发 100,总共 500 次请求)
ab -c 100 -n 500 http://192.168.1.214:2000/student/index
# 测试结果如下
- QPS 达到极限:随着 QPS 的增长,每个阶段需要根据实际情况来进行优化,优化的方案也与硬件条件、网络带宽有关。
- QPS 达到 50:可以称之为小型网站,一般的服务器就可以应付
- QPS 达到 100:假设关系型数据库的每次请求在 0.01 秒完成,假设单页面只有一个 SQL 查询,那么 100 QPS 意味着 1 秒钟完成 100 次请求,但是此时我们并不能保证数据库查询能完成 100 次。方案:数据库缓存层、数据库的负载均衡
- QPS 达到 800:假设使用百兆带宽,意味着网站出口的实际带宽是 8M 左右,假设每页页面只有 10k,在这个并发条件下,百兆带宽已经吃完。方案:CDN 加速、负载均衡
- QPS 达到 1000:假设使用 Memcached 缓存数据库查询数据,每个页面对 Memcached 的请求远大于直接对 DB 的请求。Memcached 的悲观并发数在 2w 左右(而且 QPS 为 800 左右 Memcached 就开始不稳定),但有可能在之前内网带宽已经吃光,表现出不稳定。方案:静态 HTML 缓存
- QPS 达到 2000:在这个级别下,文件系统访问锁都成为了灾难。方案:做业务分离,分布式存储。
- 高并发解决方案案例
- 流量优化
- 防盗链处理:把一些恶意的请求拒之门外(B站调用A站的js、css、图片资源)
- 前端优化
- 减少 HTTP 请求:js、css文件,图片文件合并
- 添加异步请求:不要一次性把数据都给用户。一些不是很重要的数据,用户第一次访问先不展示,旁边放一些事件做 Ajax 异步请求
- 启用浏览器缓存和文件压缩:启用浏览器缓存 HTML 文件,还可以把静态资源文件做一个过期时间的缓存,把图片压缩的小一些,还可以启用 nginx 的 gzip 服务把文件整体压缩的小一些。
- CDN 加速:用户可以就近访问 CDN 节点的内容,用户就不会访问真实的服务器,解决访问速度。也能解决带宽不够用的问题
- 建立独立图片服务器:图片比吃 I/O,所以建立图片服务器和 WEB 服务器完全分离开,这样 WEB 服务器本身的 I/O 就不会被损耗。还可以针对性的对图片服务器进行优化,比如对硬盘转速进行提高,把 CPU 的计算能力降下来,还可以把图片服务器做集群
- 服务端优化
- 页面静态化:可以把 PHP 最终生成的一些 HTML 的内容缓存起来,直接缓存成 HTML 代码,对 CPU 负载减少很多(从 QPS 上就能看出来)
- 并发处理
- 队列处理
- 数据库优化
- 数据库缓存:Memcached、Redis、MongoDB
- 分库分表(垂直拆分、水平拆分)、分区操作
- 读写分离
- 负载均衡
- WEB 服务器优化
- 负载均衡:Nginx 的反向代理、LVS 负载均衡
8. 动态语言的并发处理;
- 相关概念:什么是进程、线程、斜程
- 什么是进程:
- 摘自百度百科:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。关键词:运行活动
- 进程一个“执行中的程序”。
- 进程的三态模型:多道程序系统中,进程在处理器上交替运行,状态不断的发生变化。一个程序会至少开启一个进程,假设现在有三个程序,CPU 不会去同时处理这三个程序,它会去交替进行处理。因为它很快,所以我们感受不到它在交替处理,感觉上是在同时进行。其实是在非常快速的切换处理状态。
- 三态是指: 运行、就绪、阻塞。
- 运行:当一个进程在处理机上运行时,则称该进程处于运行状态。处于此状态的进程的数目小于等于处理器的数目(处理器只能同时处理一个程序),对于单处理机系统,处于运行状态的进程只有一个。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
- 就绪:当一个进程获得了除处理机以外的一切所需资源,一旦得到处理机即可运行,则称此进程处于就绪状态。就绪进程可以按多核优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由 I/O 操作完成而进入就绪状态时,排入高优先级队列。
- 阻塞: 也称为等待或者睡眠状态,一个进程正在等待某一件事情发生(例如请求 I/O 或者等待 I/O 完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。
- 进程的五态模型:对于一个实际的系统,进程的状态及其转换更为复杂。
- 五态是指: 新建态、活跃就绪/静止就绪、运行、活跃阻塞/静止阻塞、终止态
- 新建态:对应于进程刚被创建时没有被提交的状态,并等待系统完成创建进程的所有必要信息。
- 终止态:进程已结束运行,回收除进程控制块之外的其他资源,并让其它进程从进程控制块中收集有关信息。
- 活跃就绪:是指进程在主存并且可被调度的状态。
- 静止就绪(挂起就绪):是指进程被对换到辅存时的就绪状态,是不能被直接调度的状态,只有当主存中没有活跃就绪态进程,或者是挂起就绪态进程具有更高的优先级,系统将把挂起就绪态进程调回主存并转换为活跃就绪。(类似于球场替补队员,先热身再替补出场)
- 活跃阻塞:是指进程已在主存,一旦等待的事件产生便进入活跃就绪状态。(球员在场上偷懒)
- 静止阻塞:进程对换到辅存时的阻塞状态,一旦等待的事件产生便进入静止就绪状态
- 由于用户的并发请求,为每一个请求都创建一个进程显然是行不通的,从系统资源开销方面或是响应用户请求的效率方面来看。因此操作系统中 线程的概念变被引进了。
- 什么是线程:
- 线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元
- 线程是包含于进程,是进程的一部分,一个进程可以有多个线程。
- 线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。(线程是进程的一个寄生虫之一)
- 一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行。
- 线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派 CPU 的基本单位,指运行中的程序的调度单位。(线程是协程的一部分,是协程的一个独立单元)
- 在单个程序中同时运行多个线程完成不同的工作,称为多线程。
- 每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
- 线程的状态: 就绪、阻塞、运行。
- 就绪状态:线程具备运行的所有条件,逻辑上可以运行,在等待处理机。
- 阻塞状态:线程在等待一个事件(如某个信号量),逻辑上不可执行。(App 在手机后台)
- 运行状态:线程占有处理机,正在运行。
- 什么是协程:
- 协程是一种用户态的轻量级线程,协程的调度完全由用户自己控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其它地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换和开销,可以不加锁的访问全局变量,所以上下文的切换非常快
- 协程有点类似于轻量级线程,但是和线程还不一样。协程调度由用户控制,线程调度由操作系统控制。
- 线程和进程的区别:
- 线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间
- 进程是资源分配和拥有的单位,同一个进程内的线程共享进程的所有资源
- 线程是处理器调度的基本单位,但进程不是
- 二者均可并发执行(所谓并发执行就是同时运行,并发执行交给处理机执行的时候是要等待的,只不过处理机执行和切换的时候非常快,用户感知不到,感觉同时运行)
- 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
- 线程和协程的区别:
- 一个线程可以有多个协程,一个进程也可以单独拥有多个协程(协程就是程序员来进行控制的)
- 线程进程都是同步机制,而协程是异步(异步模型操作协程)
- 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态
- 什么是多进程、多线程
- 多进程:
- 同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,就是多进程(电脑边玩游戏边听歌)
- 多开一个进程,就多分配一份资源,进程间相互独立,通讯不方便
- 多线程:
- 线程就是把一个进程分为很多片,每一片都可以是一个独立的流程
- 与多进程的区别就是智慧使用一个进程的资源,线程间可以直接通信
- 以上多个概念之间的区别:
- 单进程单线程:一个人(线程)在一个桌子上(单进程)吃菜
- 单进程多线程:多个人(多线程)在同一张桌子上一起吃菜
- 多进程单线程:多个人每个人在自己的桌子上吃菜(消耗资源)
- 同步阻塞模型:在进行并发处理的时候,多个程序过来要怎么样进行阻塞
- 多进程
- 最早的服务器端程序是通过多进程、多线程来解决并发 IO 的问题
- 一个请求创建一个进程,然后子进程进入循环同步阻塞地与客户端连接进行交互,收发处理数据
- 多线程
- 用多线程模式实现非常简单,线程中可以直接向某一个客户端连接发送数据
- 步骤
- 创建一个 socket 监听
- 进入 while 循环,阻塞在进程 accept 操作上,等待客户端连接进入
- 主进程在多进程模型下通过 fork 创建子进程
- 多线程模型下可以创建子线程
- 子进程 / 线程创建成功后进入 while 循环,阻塞在 recv 接收调用上,等待客户端向服务器发送数据
- 收到数据后服务器程序进行处理然后使用 send 向客户端发送响应
- 当客户端连接关闭时,子进程 / 线程退出并销毁所有资源。主进程 / 线程会回收掉此子进程 / 线程
- 缺点
- 这种模型严重依赖进程的数量解决并发问题
- 操作系统能创建的进程数量有限,处理能力也是有限的,启动大量的进程会带来额外的进程调度消耗
// 创建文件 demo1.php
cd /data/project/test/demo/
vim demo1.php
// 写入如下内容
<?php
// 创建 socket 监听
$sockserv = stream_socket_server('tcp://0.0.0.0:8000', $errno, $errstr);
// 假设创建 5 个子进程
for($i = 0; $i < 5; $i++){
// 进程 pid 为 0,阻塞,写个死循环监听
if(pcntl_fork() == 0){
while(true){
$conn = stream_socket_accept($sockserv);
// 如果连接失败,重新创建监听
if($conn == false){
continue;
}
// 读取流信息
$request = fread($conn, 9000);
$response = 'hello';
fwrite($conn, $response);
fclose($conn);
}
exit(0);
}
}
// 执行
php demo1.php
// 查看进程
ps -ef
- 异步非阻塞模型(效率更高)
- 异步非阻塞
- 现在各种高并发异步 IO 的服务器程序都是基于 epoll 实现的
- ‐ 注意:Linux 很早就提供了 select 系统的查询,可以在一个进程内维持1024 个连接。后来又加入了 poll 系统调用,做了些改进解决了 1024 限制的问题,维持日益数量的连接。但是 poll 有一个问题就是需要循环去检测连接是否有事件。这样问题就来了,如果服务器有 100 万个连接,在某一个时间内只有一个连接向服务器发送数据,poll 需要循环 100 万次,只有 1 次命中,对系统资源来说是一种浪费。对于 Linux 2.6 以上内核来说提供了一种新的 epoll,它可以维持无限数量的连接,而且无需轮询,真正解决了并发问题。现在各种高并发异步 IO 服务器程序都是基于 epoll 实现的。比如 Nginx、golang、NodeJS。
- IO 复用异步非阻塞程序使用经典的 Reactor 模型,Reactor 顾名思义就是反应堆的意思,它本身不处理任何数据收发。只是可以监视一个 socket 句柄的事件变化。
- ‐ 刚才在做同步阻塞模型的时候,是在循环监听。如果有客户端了,就立马显示出来,是做这样一个循环处理。但是对于 Reactor 来说,是做一个事件监听,而不是循环监听。整体来说它更加节省资源。
- Reactor 的四个核心操作:
- add:添加一个 socket 监听到 Reactor
- set:修改事件监听,可以设置监听的类型,如可读、可写
- del:从 Reactor 中移除,不再监听事件
- callback:事件发生后对应的处理逻辑,一般在 add / set 时指定
- 常见的 Reactor 模型
- Nginx:多线程 Reactor
- Swoole(PHP 著名异步框架):多线程 Reactor + 多进程 Worker
// 创建文件 reactor.php
cd /data/project/test/demo/
vim reactor.php
// 写入如下内容
<?php
// 使用前安装 reactor 扩展
$reactor = new Reactor();
// 生成 socket 监听
$sockserv = stream_socket_server('tcp://0.0.0.0:8000');
// 将 socket 以可读的方式去进行监听
// 触发事件后调用里面的方法,不再是循环监听,效率更高,是异步操作,不影响程序后续执行
$reactor->add($sockserv, EV_READ, function() use($sockserv, $reactor){
// 接收数据
$sockcli = stream_soclet_accept($sockserv);
// 监听接收的 socket
$reactor->add($sockcli, EV_READ, function() use ($sockserv, $reactor){
// 读请求
$request = fread($sockcli, 10000);
// 添加写事件
$reactor->add($sockcli, EV_WRITE, function() use ($sockserv, $request){
fwrite($sockcli, 'test');
$reactor->del($sockcli);
fclose($sockcli);
});
});
});
- PHP 并发编程:需要了解 PHP 的 Swoole 扩展、消息队列、接口的并发请求
- PHP 的 Swoole 扩展
- PHP 的异步、并行、高性能网络通信引擎,使用纯 C 语言编写,提供了 PHP 语言的异步多线程服务器,异步 TCP / UDP 网络客户端,异步 MySQL,异步 Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件读写,异步 DNS 查询
- 除了异步 IO 的支持以外,Swoole 为 PHP 多进程的模式设计了多个并发数据结构和 IPC 通信机制,可以大大简化多进程并发编程的工作。
- Swoole 2.0 支持了类似 Go 语言的协程,可以使用完全同步的代码实现异步程序
- Swoole 的异步 MySQL 实现案例
$db = new Swoole\MySQL;
$server = ['host' => 'localhost', 'user' => 'root', 'password' => 'asdf', 'database' => 'laravel'];
// 事件监听,完全异步操作
$db->connect($server, function($db, $result){
// 查询成功调用回调函数 function(){}
$db->query("show tables", function(Swoole\MySQL $db, $result){
// do something
});
});
- 消息队列
- 场景说明:用户注册后,需要发注册邮件和注册短信
- 串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信(用户体验不好)
- 并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信(还能有优化空间)
- 消息队列方式:将注册信息写入数据库成功后,将成功信息写入队列,此时直接返回成功给用户,写入队列的时间非常短,可以忽略不计,然后异步发送邮件和短信。
- 应用解耦
- 场景说明:用户下单后,订单系统需要通知库存系统
- 假设库存系统无法访问,则订单减库存将失败,从而导致订单失败(串行,不合理)
- 订单系统与库存系统解耦合
- 引用队列:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功(先不管库存的问题)
- 订阅队列(下单的消息),采用拉 / 推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。
- 流量削峰
- 应用场景:秒杀活动,流量瞬时激增,服务器压力大
- 用户发起请求,服务器接收后,先写入消息队列。假如消息队列长度超过最大值,则直接报错或提示用户
- 后续程序读取消息队列再做处理
- 关于流量削峰,可以控制请求量,缓解高流量
- 日志处理
- 应用场景:解决大量日志的传输
- 日志采集程序将程序写入消息队列,然后通过日志处理程序的订阅消费日志
- 消息通讯
- 应用场景:聊天室
- 多个客户端订阅同一个主题,进行消息发布和接收
- 常见消息队列产品
- Kafka、ActiveMQ、ZeroMQ、RabbitMQ、Redis 等
- 接口的并发请求
- curl_multi_init
9. 数据库缓存层的优化;
- 相关概念
- 什么是数据库缓存
- MySQL 等一些常见的关系型数据库的数据都储存在磁盘当中,在高并发场景下,业务应用对 MySQL 产生的增、删、改、查的操作造成巨大的 I/O 开销和查询压力,这无疑对数据库和服务器都是一种巨大的压力,为了解决此类问题,缓存数据的概念应运而生。
- 数据库缓存层面可以极大的解决数据库服务器的压力。
- 提高应用数据的响应速度。因为不需要去请求查询数据库了,少了很多查询和计算的时间,直接返回现有的静态数据。
- 常见的缓存形式:内存缓存、文件缓存。
- 为了避免 I/O 开销,尽量使用内存缓存。
- 为什么要使用缓存
- 缓存数据是为了让客户端很少甚至不访问数据库服务器进行数据的查询。高并发下,能最大程度的降低对数据库服务器的访问压力。
- 打开一个站点,站点上的数据默认都是请求数据库服务器的,会对数据库服务器造成查询的压力。此时数据库服务器会进行 I/O 开销、对文件的读写操作,会对服务器的 I/O 有非常大的开销压力。这时候可以中间去加一层缓存层,这样就可以不去请求 MySQL 服务器了。
- 默认第一次用户请求:用户请求 —> 数据查询 —> 连接数据库服务器并查询数据 —> 将数据缓存起来(HTML、内存、JSON、序列化数据)—> 显示给客户端
- 用户再次请求或者新用户访问 —> 数据查询 —> 直接从缓存中获取数据 —> 显示给客户端
- 缓存需要考虑的内容
- 缓存方式的选择:尽量存储到内存里,降低 I/O 开销,性能方面最高。
- 缓存场景的选择:不经常修改的数据适合缓存。
- 缓存数据的实时性:缓存一个时间长度,不建议更新数据后立即更新缓存。
- 缓存数据的稳定性:缓存服务器的主从和集群。
- MySQL 的查询缓存(了解)
- 启用 MySQL 的查询缓存
- 极大的降低 CPU 的使用率
// query_cache_type
// 查询缓存类型,有 0、1、2 三个取值。
// 0 表示不使用查询缓存。1 表示始终使用查询缓存。 2 表示按需使用查询缓存。
// query_cache_type 为 1 时,亦可关闭查询缓存
SELECT SQL_NO_CACHE * FROM table WHERE condition;
// query_cache_type 为 2 时,可按需使用查询缓存
SELECT SQL_CACHE * FROM table WHERE condition;
// query_cache_size
// 默认情况下 query_cache_size 为 0,表示为查询缓存预留的内存为 0,则无法使用查询缓存
SET GLOBAL query_cache_size = 134217728;
// 查询缓存可以看做是 SQL 文本和查询结果的映射
// 第二次查询的 SQL 和第一次查询的 SQL 完全相同,则会使用缓存
// 如果第二次稍微有点不太一样(多了个空格),也不会使用缓存
SHOW STATUS LIKE 'Qcache_hits'; // 查看命中次数
// 当表的结构或数据发生改变时,查询缓存中的数据不再有效
// 以上就是查询缓存的限制,如果写的评率非常高,查询缓存就没有意义
// 而且查询缓存的力度比较小
// 清理缓存
FLUSH QUERY CACHE; // 清理查询缓存的内存碎片
RESET QUERY CACHE; // 从查询缓存中移除所有查询
FLUSH TABLES; // 关闭所有打开的表,同时该操作将会清空查询缓存中的内容
- 使用 Memcached 查询缓存数据
- 对于大型站点,如果没有中间缓存层,当流量打入数据库层时,即便有之前的几层为我们挡住一部分流量,但是在大并发的情况下,还是会有大量请求涌入数据库层,这样对于数据库服务器的压力冲击很大,响应速度也会下降,因此添加中间缓存层很有必要。
- Memcached 是一套分布式的高速缓存系统,目前被许多网站使用以提升网站的访问速度,尤其对于一些大型的、需要频繁访问数据库的网站访问速度提升效果十分显著。
- 工作原理:Memcached 是一个高性能的分布式的内存对象缓存系统,通过在内存里维护一个统一的巨大的 hash 表,它能够用来储存各种格式的数据,包括图像、视频、文件以及数据库检索的结果等。简单的说就是将数据调用到内存,然后从内存中读取,从而大大提高读写速度
- 工作流程:先检查客户端的请求数据是否在 Memcached 中,如果有就直接把请求数据返回, 不再对数据库进行操作;如果请求的数据不在 Memcached 中,就去查数据表,把从数据库中获取的数据返回给客户端,同时把数据缓存一份到 Memcached 中
- 使用方法: https://www.php.net/manual/zh/class.memcached.php
- 通用缓存机制:用查询的方法名 + 参数作为查询时的 key - value 对中的 key 值
- 使用 Redis 缓存查询数据
- 与 Memcached 的区别
- ‐ 性能相差不大
- ‐ Redis 在 2.0 版本后增加了自己的 VM 特性,突破物理内存的限制。Memcached 可以修改最大可用内存,采用 LRU 算法
- ‐ Redis 依赖客户端来实现分布式读写
- ‐ Memcached 本身没有数据冗余机制
- ‐ Redis 支持(快照、AOF),依赖快照进行持久化,AOF 增强了可靠性的同时,对性能有影响
- ‐ Memcached 不支持持久化,通常做缓存,提升性能
- ‐ Memcached 在并发场景下,用 cas 保证一致性。Redis 事务支持比较弱,只能保证事务中的每个操作连续执行
- ‐ Redis 支持多种类的数据类型
- ‐ Redis 用于数据量较小的高性能操作和运算上
- ‐ Memcached 用于在动态系统中减少数据库负载,提升性能;适合做缓存,提高性能
- 缓存其它数据
- Session:通过 session_set_save_handler
10. MySQL 数据层的优化;
- 相关概念
- 优化方向
- 数据表数据类型优化
- 索引优化
- SQL 语句的优化
- 存储引擎的优化
- 数据表结构设计的优化(分库分表、分区设置)
- 数据库服务器架构的优化(主从复制、负载均衡、双机热备)
- 数据表数据类型优化
- 字段使用什么样的数据类型合适
- 字段使用什么样的数据类型性能更快
- tinyint 存年龄、smallint 存古建筑时间几百年、bigint 存大数字,还要考虑空间的问题和范围的问题(加 unsigned)
- char 存手机号码或者 md5 因为固定长度,varchar 存储长度不固定的字符串
- enum 可以存特定或者固定的分类,效率更快
- IP 地址的存储:存成整型,可以通过 PHP 函数 ip2long() 和 long2ip()
- 索引优化(参考 MySQL 创建高性能索引)
- 建立合适的索引
- 索引在什么场景下效率最高
- 索引的创建原则
- ‐ 索引并不是越多越好,在合适的字段(在有 where 的列上)上创建合适的索引
- ‐ 复合索引的创建原则
- ‐ 复合索引的前缀原则
- ‐ like 查询 “%” 的问题( “%” 在前,like 查询字段的索引会失效)
- ‐ 全表扫描优化(一张表 100 条数据查 99 条,优化器放弃使用索引)
- ‐ or 条件索引使用情况(or 前面有索引,后面没索引,索引失效)
- ‐ 字符串类型索引失效(查询的时候,如果字符串存的是整型,查询的时候不加引号索引失效)
- SQL 语句优化(参考 MySQL 查询优化)
- 优化查询过程中的数据访问
- ‐ 使用 limit
- ‐ 返回列不用“*”,写具体列名
- 优化长难句的查询语句
- ‐ 变复杂为简单
- ‐ 切分查询
- ‐ 分解关联查询
- 优化特定类型的查询语句
- ‐ 优化 count()
- ‐ 优化关联查询:关键字段加索引
- ‐ 优化子查询(使用关联查询替代)
- ‐ 优化 group by 和 distinct
- ‐ 优化 limit 和 union
- 存储引擎的优化
- 尽量使用 InnoDB 存储引擎(崩溃后的安全恢复,支持事务、外键,使用独立空间,行级锁)
- 数据表结构设计的优化(参考 MySQL 高可扩展和高可用)
- 分区操作
- ‐ 通过特定的策略对数据表进行物理拆分(一张表会分出来多个文件,对于这多个文件来说,是针对不同的策略,比如说根据不同的地域分区,或者热点分区)
- ‐ 对用户透明(用户操作还是操作一张表,这一张表下会有多个分区的操作)
- ‐ 使用 partition by 进行操作
- ‐ 如果对现有表分区需要修改表结构
- 分库分表
- ‐ 把现有的数据表分到不同的数据库下,把现有的数据表拆分成多张数据表,在数据量比较大的时候这样操作(比如一张表上亿数据,活跃数据几百条,这时候就可以把活跃数据拆分出来)
- ‐ 水平拆分:把一些记录按照行级进行拆分。比如现在有一亿条记录,其中 100 条活跃数据,其它是僵尸粉数据,这样就可以把 100 条数据拆出来到另外一张表,当请求活跃数据查询,就只要查活跃数据表。如果水平拆分要查询活跃和不活跃就要用 union 进行连接。
- ‐ 垂直拆分:可以把一些常用的列拆分出来,不常用的列拆分到另外一张表。对于垂直拆分,会有一定的问题,如果常用的列和不常用的列都要查询显示,要通过 join 进行连接。
- 数据库服务器架构的优化
- 主从复制:binlog 日志和中继日志。通过主库和从库的 binlog 来回的一个交换、事件来回的传输
- 读写分离
- 双主热备
- 负载均衡:通过 LVS 的三种基本模式实现负载均衡,或者 MyCat 数据库中间件实现负载均衡。
11. Web 服务器的负载均衡、请求分发。
- 相关概念:七层负载均衡的实现(Nginx)、四层负载均衡的实现(LVS)
- 七层负载均衡的实现
- 基于 URL 等应用层信息的负载均衡
- Nginx 的 proxy 是它一个很请强大的功能,实现了七层负载均衡
- 功能强大,性能卓越,运行稳定
- 配置简单灵活:配置 Upstream(参考 Nginx 的反向代理与负载均衡)
- 能够自动剔除工作不正常的后端服务器
- 上传文件使用异步模式
- 支持多种分配策略,可以分配权重,分配方式灵活
- Nginx 负载均衡
- 内置策略(安装 Nginx 已经打到内核里去了)、扩展策略(需要额外 Nginx 模块才能使用)
- 内置策略:IP Hash,加权轮询
- 扩展策略:fair 策略,通用 Hash、一致性 Hash
- 加权轮询策略
- 首先将请求都分给高权重的机器,直到该机器的权值降到了比其它机器低,才开始将请求分给下一个高权重的机器
- 当所有后端机器都 down 掉的时候,Nginx 会立即将所有机器的标志位清成初始状态(立即告诉你所有机器都挂了),以免造成所有的机器都处在 timeout 的状态
- IP Hash 策略
- Nginx 内置的另一个负载均衡的策略,流程和轮询很类似,只是其中的算法和具体的策略有些变化
- IP Hash 算法是一种变相的轮询算法。
- fair 策略
- 根据后端服务器的响应时间判断负载情况,从中选出负载最轻的机器进行分流(类似 cdn)
- 通用 Hash、一致性 Hash策略
- 通用 Hash 比较简单,可以以 Nginx 内置的变量为 key 进行 hash。一致性 Hash 采用了 Nginx 内置的一致性 Hash 环,支持 Memcached
- Nginx 配置
- 如下
http{
# myproject 为集群名称
upstream myproject{
server 192.168.1.10 weight=2;
server 192.168.1.11;
server 192.168.1.12;
}
# 当访问 location / 的时候,会反向代理 upstream
# upstream 里面有一个策略,会去抓里面的机器来返回给 proxy_pass
server{
listen 80;
location / {
proxy_pass http://myproject
}
}
}
- 四层负载均衡的实现
- 通过报文中的目标地址和端口,再加上负载均衡设备设置的服务器选择方式,决定最终选择的内部服务器
- LVS 实现服务器集群负载均衡有三种方式:NAT、DR 和 TUN