高性能是每个程序员的追求(😄),我们都希望代码或者系统都能达到一个高性能效果。但是高性能又是最复杂的一环。磁盘、操作系统、CPU、内存、缓存、网络、语言、架构都可能会影响到高性能
高性能架构设计主要集中在两方面:
- 尽量提高单服务器性能,将单服务器性能发挥到极致
- 如果单服务器无法支撑性能,设计服务器集群
如上方面,只能决定系统性能的上限,性能的下限和具体编码和细节有关
🖥️1 单服务器高性能
单服务器高性能关键之一就是服务器采用的网络编程模型
网络编程模型有如下两个关键点
- 服务器如何管理连接
- 服务器如何处理请求
这两个关键点最终都和操作系统的I/O模型和进程模型相关 - I/O模型:阻塞,非阻塞,同步,异步
- 进程模型:单进程,多进程,多线程
1.1 PPC
Process per Connection,每次有新的连接就新建一个进程去处理
- 父进程接受连接accept
- 父进程fork子进程
- 子进程处理连接的读写请求
- 子进程管理连接
父进程的close只是让连接文件描述符计数减一,正在关闭是子进程的close
利弊
-
实现简单,适合服务器连接数没那么多的情况
-
fork代价高,操作系统层面来说,创建一个进程的代价比较高
-
父子进程通信复杂
-
进程数量增大后对操作系统压力较大,不使用池技术可能导致系统崩溃
1.2 prefork
prefork是在fork的基础上提前创建好进程等待连接使用,关键在于多个子进程都accept同一个socket,所有连接进入时,只保证一个进程成功
利弊
-
比fork快了,不需要频繁创建进程和销毁进程
-
因为一次只有一个连接成功,所以会阻塞其他进程,一旦可以连接时所有进程都会被唤醒,出现"惊群"现象,导致不必要的进程调度和上下文切换
-
和PPC一样,还是存在父子进程通信复杂、支持并发数量有限的问题
1.3 TPC
Thread per Connection,每次有新的连接就创建一个线程去处理这个请求,与进程相比,线程更加轻量
利弊
-
解决了PPC的fork代价高和父子进程通信复杂的问题
-
痛点在于每次连接都会创建线程,每次结束都会销毁线程
-
线程的互斥以及共享引入了复杂度,需要考虑线程安全问题
-
某个线程的异常也许会导致整个进程的退出
1.4 preThread
和prefork同理,就是在请求来之前就创建好线程,类似于线程池,会省去线程创建时间,会让用户感觉更快
1.5 Reactor
PPC主要问题就是每个连接都要对应一个进程,并且每个进程不使用还会被销毁,所以如果使用池技术复用进程会大大改善这个问题。
但是池技术会引出新的问题,进程如何高效处理多个连接的业务。当一个连接一个进程时,进程可以采用read->业务处理->write处理,但是当没有数据可读时,也就是read被阻塞了,如果在一个连接对应多个进程的情况下就会导致其他可以读或者操作的进程不能操作,无法做到高性能
如果要解决上述问题,可以将read操作改为非阻塞,轮询多个连接。
但是这种操作并不优雅,并且轮询消耗CPU,如果一个进程处理几千上万个连接,轮询肯定效率低。
I/O多路复用技术可以很好的解决这个问题,这个术语在通信行业比较常见(时分复用,码分复用,频分复用)
计算机网络领域的I/O多路复用,多路是指多条连接,复用指的是复用同一个阻塞对象。如Linux使用的select模型,那么这个阻塞对象就是fd_set,如果为epoll模型就是epoll_create的文件描述符
I/O多路复用归纳的两个实现关键点:
- 当多条连接公用一个阻塞对象,就只需要等待一个阻塞对象,不需要轮询
- 当阻塞对象有数据可以处理时,操作系统通知线程干活
I/O多路复用技术结合线程池可以完美解决PPC和TPC模型,这个组合的名字叫Reactor,响应式或者事件反应。
Reactor模式的核心组成包括Reactor和处理资源池。Reactor复制监听和分配事件,处理资源池复制处理事件
由于Reactor的数量可以变化,资源池的数量可以变化,所以Reactor模式是灵活的,可以使的不同数量的Reactor和资源池进行组合。
单Reactor和单进程/线程资源池
- Reactor通过select监控连接时间,收到后使用dispatch分发
- 如果是连接建立事件,则由Acceptor通过accept接受连接,并创建一个Handler来继续处理事件
- 如果不是连接建立事件,Reactor会调用对应的Handler(上一步创建的Handler)来响应
- Handler会完成read->业务处理->send的业务流程
优缺点
-
简单、没有进程间通信和进程竞争
-
只有一个进程无法发挥多核心性能
-
Handler处理某个连接上的业务时,整个进程无法处理其他事件
单Reactor和多线程资源池
避免上面方案的确定,引入多线程是有必要的
- Reactor通过select监控连接时间,收到后使用dispatch分发
- 如果是连接建立事件,则由Acceptor通过accept接受连接,并创建一个Handler来继续处理事件
- 如果不是连接建立事件,Reactor会调用对应的Handler(上一步创建的Handler)来响应
- Handler只负责响应事件,不进行业务处理,Handler通过read到数据后将发给Processor处理
- Processor会在在子线程中完成真正的业务处理,然后响应给Handler,Handler再将结果send给客户端client
优缺点
-
充分利用多核CPU资源
-
多线程数据共享和访问比较复杂,涉及到共享数据的互斥和保护机制
-
Reactor承担所有事件的监听和响应,瞬间的高并发会成为性能瓶颈
多Reactor和多进程/线程资源池
多Reactor的目的就是解决上面方案的缺陷,提高瞬间的高性能能力瓶颈,采用多Reactor多进程的大佬是Nginx,Reactor多线程的是Memcache和Netty
- 父进程中的mainReactor对象通过select监控建立连接事件,收到事件后通过Acceptor接收,分配给某个子进程
- 子进程的subReactor将分配的连接加入队列监听,并创建一个Handler用于处理连接的各种事件
- 新事件发生,subReactor会调用对应的Handler进行响应
- Handler完成read->业务处理->send的业务流程
优缺点
- 父子进程职责明确,父进程只负责接受新连接,子进程负责完成后续业务
- 父子进程的交互简单,父进程只需要给子进程连接,子进程无需返回数据
- 子进程之间独立,无法同步共享之类的处理
1.6 Proactor
Proactor就是将Reactor的read和send操作的I/O操作改为异步,是一个异步网络模型
- Proactor Initiator负责创建Proactor和Handler,将其通过AOP(Asynchronous Operation Processor)注册到内核
- AOP负责处理注册请求并完成I/O操作
- AOP完成注册操作后通知Proactor
- Proactor根据不同的时间类型回调不同的Handler处理
- Handler完成业务处理,也可以注册新的Handler到内核进程
优缺点
- 理论上Proactor比Reactor效率更高,毕竟异步I/O能利用DMA特性,让I/O操作和CPU计算重叠,实现真正的异步I/O
🗃️2 集群高性能
单服务器无论如何优化,采用多好的架构以及硬件设备,都会有一个上限在那,所以如果系统是可集群可拓展的,那么可以更好的满足高性能
本质上就是通过增加服务器提高系统整体能力,原因也说过了,逻辑相同,那么一个输入只会相同的一个输出
集群的主要的复杂度在于如何负载均衡(任务分配器)
2.1 负载均衡分类
负载均衡系统包括三种:
DNS负载均衡
、硬件负载均衡
和软件负载均衡
。
DNS负载均衡
一般用来实现地理级别的负载均衡,可能在各个地区都部署了机房,那么可能成都的用户访问了www.faofull.cn那么可能就会被负载到北京的机房中
- 成本低,简单,负载均衡只交给DNS处理,无需自己维护和开发,给用户分配最近的节点访问,加快访问速度
- 但是颗粒度大,负载均衡算法少,更新不及时,缓存时间长。扩展性差
硬件负载均衡
通过单独的硬件设备实现负载均衡,这类设备和路由器交换机类似,有F5、A10这种性能强劲,功能强大的典型的设备
- 优点
- 功能强大,支持全面的负载均衡算法,支撑全局和各层级的负载均衡
- 性能强大,可以支持100万以上的并发
- 性能高,商用的硬件负载,经过了严格的测试,并且大规模使用,稳定
- 支持安全防护,除了负载均衡,还具备防火墙和DOOS攻击防护
- 缺点
- 贵,一台好一点的F5就是一台宝马
- 扩展性能差,硬件设备,可以根据业务配置,但是无法扩展和定制
软件负载均衡
通过负载均衡软件来实现负载,比如Nginx和LVS,前者是7层负载均衡,后者是Linux内核的4层,Nginx支持HTPP、E-mail,LVS和协议无关,任何应用都可以做
如下就是Nginx的负载均衡架构图
- 优点
- 简单:部署和维护简单
- 便宜:买个linux服务器,装上软件就行
- 灵活:LVS和Nginx等可以根据业务选择,还可以Nginx插件实现定制化功能
- 缺点
- 性能一般:一个Nginx大约能支持5万并发
- 功能没办法和硬件负载做对比,差别太大了
- 不具备防火墙和防DDOS工具的安全功能
2.2 负载均衡架构
三种负载均衡机制,不代表我们只能选其中一个,然后对比使用,我们一般通过其优缺点进行组合,比如DNS为最顶级的负载均衡,负载到某个机房中的F5中,由F5再负载到各个Nginx的集群中,再由Nginx负载到各个计算服务器
2.3 负载均衡算法
负载均衡的算法数量较多,抛开细节差异,大体可以分为以下几类
- 任务平分类:负载均衡系统,通过权重或者绝对数量/比例的平均进行转发
- 负载均衡类:根据服务器的负载来转发,服务器负载可以理解为当前服务器上的系统压力(可以是CPU负载、I/O负载、连接数、网络吞吐量)
- 性能最优类:根据各个服务器的响应时间来进行任务分配,也可以叫做自适应负载均衡
- Hash类:通过某个关键信息进行hash运算,计算分配到哪个服务器
-
轮询:按照顺序轮流分发,简单是优点也是缺点
- 优点在于简单,无需关注各个节点状态
- 缺点在于无感知,不会因为某个节点负载很高而不分发给他,也不会根据每个节点的真正性能比例来进行权重分发
- 除非节点断开连接,不然还是会分发给他
-
加权轮询:按照节点权重分配,权重可以是静态配置也可以是动态配置
- 缺点在于无法根据服务器状态差异进行真正负载均衡
-
负载最低优先:分发给负载最低的节点,负载的定义可以是不同的任务类型和业务场景,以及不同的指标
- LVS可以以连接数来判断服务器状态,连接数越多,那么压力越大
- Nginx可以以Http请求数来判断服务器状态
- 还可以自己开发负载均衡系统,通过CPU、I/O负载等作为指标
- 解决了无法感知节点状态的问题,但是也带来了更高的复杂度
-
性能最优类:优先分发给处理速度最快的服务器,和负载最低优先不同,性能最优是站在客户端的角度分配。
- 通过收集节点的状态信息来进行实时的负载均衡,但是收集和统计的这个动作也会消耗性能
- 可以通过采样的方式来统计,避免上述问题的性能消耗
- 但是采用统计也是要选择合适的周期的,是10秒呢还是1分钟呢,还是5分钟呢,这需要根据系统上线后不断调优来选择
-
Hash类:将关键信息进行Hash运算以及取余,按照计算后的值分发到各个节点
- 源地址Hash:根据请求的IP地址进行分发,适合有状态以及事务、会话的业务,同一个IP可以认为是同一个用户,分配到同一个服务器可以更好的利用会话和事务
- ID Hash:例如上述,可以将同一个用户id的请求分发到相同节点等
📖3 总结
- PPC模型:新连接对应一个新的进程
- TPC模型:新的连接对应一个新的线程
- Reactor模型是基于I/O多路复用
- Proactor模式是基于非阻塞异步网络模型
- DNS LB、硬件 LB、软件 LB是常见的负载均衡系统
- DNS是最简单的LB方式,一般用来实现地理级别的LB
- 硬件LB 用于集群级别的LB
- 软件LB用于实现机器级别的LB
- 负载均衡算法:任务平分类、负载均衡类、性能最优类、Hash类