1 DMA技术
直接内存访问(Direct Memory Access) 技术。
在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。
DMA将磁盘控制器缓冲区的数据拷贝到内存缓冲区(该过程不占用CPU)
传统的文件传输:
数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。
期间共发生了 4 次用户态与内核态的上下文切换,还发生了 4 次数据拷贝
要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。
2 零拷贝
零拷贝技术实现的方式通常有 2 种:
- mmap + write
- sendfile
mmap + write
用 mmap() 替换 read() 系统调用函数
mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。
减少一次数据拷贝但仍然需要 4 次上下文切换,因为系统调用还是 2 次
sendfile
可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用
直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态
只有2 次上下文切换,和 3 次数据拷贝
网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同)
Linux 系统通过下面这个命令,查看网卡是否支持 scatter-gather 特性:
对于支持网卡支持 SG-DMA 技术的情况下,sendfile() 系统调用的过程发生变化
只进行了 2 次数据拷贝。即这就是零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,
而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。
零拷贝技术可以把文件传输的性能提高至少一倍以上
大文件传输用什么方式实现?
绕开 PageCache 的 I/O 叫直接 I/O,使用 PageCache 的 I/O 则叫缓存 I/O。通常,对于磁盘,异步 I/O 只支持直接 I/O。
在高并发的场景下,针对大文件的传输的方式,应该使用「异步 I/O + 直接 I/O」来替代零拷贝技术。
直接 I/O 应用场景常见的两种:
应用程序已经实现了磁盘数据的缓存,那么可以不需要 PageCache 再次缓存,减少额外的性能损耗。在 MySQL 数据库中,可以通过参数设置开启直接 I/O,默认是不开启;
传输大文件的时候,由于大文件难以命中 PageCache 缓存,而且会占满 PageCache 导致「热点」文件无法充分利用缓存,从而增大了性能开销,因此,这时应该使用直接 I/O。
由于直接 I/O 绕过了 PageCache,就无法享受内核的这两点的优化:
内核的 I/O 调度算法会缓存尽可能多的 I/O 请求在 PageCache 中,最后「合并」成一个更大的 I/O 请求再发给磁盘,这样做是为了减少磁盘的寻址操作;
内核也会「预读」后续的 I/O 请求放在 PageCache 中,一样是为了减少对磁盘的操作;
传输文件的时候,我们要根据文件的大小来使用不同的方式:
· 传输大文件的时候,使用「异步 I/O + 直接 I/O」;
· 传输小文件的时候,则使用「零拷贝技术」;
3 I/O 多路复用
一个进程虽然任一时刻只能处理一个请求,但是处理每个请求的事件时,耗时控制在 1 毫秒以内,这样 1 秒内就可以处理上千个请求,把时间拉长来看,多个请求复用了一个进程(思想类似一个CPU并发多个进程)
select/poll/epoll 这是三个多路复用接口
3.1 select/poll
select实现多路复用的方式是
将已连接的 Socket 都放到一个文件描述符集合,
调用 select 函数将文件描述符集合拷贝到内核里,
内核通过遍历文件描述符集合的方式检查是否有网络事件产生,
当检查到有事件产生后,将此 Socket 标记为可读或可写,
再把整个文件描述符集合拷贝回用户态里,
用户态再遍历找到可读或可写的 Socket,然后再对其处理。
该方式需要 2 次遍历文件描述符集合(一次内核态、一次用户态),发生 2 次拷贝文件描述符集合(从用户态到内核态,内核修改,再从内核态到用户态)
select 使用固定长度的 BitsMap,表示文件描述符集合 ,所支持的文件描述符个数是有限制的
poll
使用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制(还会受到系统文件描述符限制)
poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合
3.2 epoll
1、在内核使用红黑树跟踪进程所有待检测的文件描述字,将需要监控的socket通过 epoll_ctl() 函数传入内核红黑树。因内核维护了红黑树,可保存所有待检测的socket,所以只需传一个待检测的socket
2、使用事件驱动的机制,内核维护一个链表来记录就绪事件,当某个socket有事件发生时,通过回调函数,内核将其加入到就绪事件链表,当用户调用 epoll_wait() 函数时,只返回有事件发生的文件描述符个数。
epoll_wait 实现的内核代码中调用了 __put_user 函数,这个函数就是将数据从内核拷贝到用户空间。
epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET)和水平触发(level-triggered,LT)。
-
使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
-
使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;
两者区别:
水平触发的意思是只要满足事件的条件,比如内核中有数据需要读,就一直不断地把这个事件传递给用户;
而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。
边缘触发模式一般和非阻塞 I/O 搭配使用
边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。
select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。
4 高性能网络模式
4.1 Reactor
Reactor 模式也叫 Dispatcher 模式,I/O 多路复用监听事件,收到事件后,根据事件类型分配(Dispatch)给某个进程 / 线程。
Reactor 模式主要由 Reactor 和处理资源池这两个核心部分组成,它俩负责的事情如下:
Reactor 负责监听和分发事件,事件类型包含连接事件、读写事件;
处理资源池负责处理事件,如 read -> 业务逻辑 -> send;
4.1.1 单 Reactor 单进程 / 线程
C语言实现的是单Reactor 单进程;JAVA语言实现的是单Reactor 单线程
「单 Reactor 单进程」的方案示意图:
「单 Reactor 单进程」方案:
Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
缺点:
1、无法充分利用多核CPU性能
2、Handler对象处理业务时,整个进程无法处理其它连接事件,如果处理时间过长,会造成响应的延迟
单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景。
4.1.2 单 Reactor 多线程 / 多进程
「单 Reactor 多线程」方案的示意图如下:
「单 Reactor 多线程」方案:
Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给子线程里的 Processor 对象进行业务处理;
子线程里的 Processor 对象就进行业务处理,处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将响应结果发送给 client;
一个 Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方
4.1.3 多 Reactor 多进程 / 线程
「多 Reactor 多线程」方案示例图:
方案详细说明如下:
主线程中的 MainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 对象中的 accept 获取连接,将新的连接分配给某个子线程;
子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入 select 继续进行监听,并创建一个 Handler 用于处理连接的响应事件。
如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应。
Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
多 Reactor 多线程的方案虽然看起来复杂的,但是实际实现时比单 Reactor 多线程的方案要简单的多,原因如下:
-
主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理。
-
主线程和子线程的交互很简单,主线程只需要把新连接传给子线程,子线程无须返回数据,直接就可以在子线程将处理结果发送给客户端。
4.2 Proactor
Reactor 是非阻塞同步网络模式,而 Proactor 是异步网络模式。
-
Reactor 是非阻塞同步网络模式,感知的是就绪可读写事件。在每次感知到有事件发生(比如可读就绪事件)后,就需要应用进程主动调用 read 方法来完成数据的读取,也就是要应用进程主动将 socket 接收缓存中的数据读到应用进程内存中,这个过程是同步的,读取完数据后应用进程才能处理数据。
-
Proactor 是异步网络模式, 感知的是已完成的读写事件。在发起异步读写请求时,需要传入数据缓冲区的地址(用来存放结果数据)等信息,这样系统内核才可以自动帮我们把数据的读写工作完成,这里的读写工作全程由操作系统来做,并不需要像 Reactor 那样还需要应用进程主动发起 read/write 来读写数据,操作系统完成读写工作后,就会通知应用进程直接处理数据。
Proactor 模式的示意图:
工作流程:
Proactor Initiator 负责创建 Proactor 和 Handler 对象,并将 Proactor 和 Handler 都通过 Asynchronous Operation Processor 注册到内核;
Asynchronous Operation Processor 负责处理注册请求,并处理 I/O 操作;
Asynchronous Operation Processor 完成 I/O 操作后通知 Proactor;
Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理;
Handler 完成业务处理;
5 一致性哈希
负载均衡问题,不同分配策略应对不同场景:如加权轮询,让性能更好的服务器承担更多的请求,但前提是每个服务器上数据是一致的(面对分布式存储,无法应对)
哈希算法,针对同一关键字计算出的哈希值是相同的,可将某一个key确定到一个服务器上,满足分布式系统的负载均衡。
最简单的做法即取模运算,但问题在于如果节点数量发生变化(系统扩容或缩容),必须迁移改变了映射关系的数据
一致性哈希算法
不同于普通的哈希算法,一致哈希算法是对 2^32 进行取模运算,是一个固定的值
对 2^32 进行取模运算的结果值组织成一个圆环
一致性哈希要进行两步哈希:
- 第一步:对存储节点进行哈希计算,也就是对存储节点做哈希映射,比如根据节点的 IP 地址进行哈希;
- 第二步:当对数据进行存储或访问时,对数据进行哈希映射;
一致性哈希是指将「存储节点」和「数据」都映射到一个首尾相连的哈希环上。
- 首先计算要查询的key的哈希,确定此key在哈希环上映射的位置
- 然后从该位置顺时针找到第一个节点,即存储该key数据的节点
在一致性哈希算法中,如果增加或删除一个节点,只会影响到该节点在哈希环上顺时针相邻的后继节点
但一致性哈希算法并不保证节点能够在哈希环上分布均匀
当节点数量足够大时,哈希环上的节点分布就越均匀
如何通过虚拟节点提高均衡度?
不再将真实节点映射到哈希环上,而是将虚拟节点映射到哈希环上,并将虚拟节点映射到实际节点,所以这里有「两层」映射关系。
如果有访问请求寻址到「A-01」这个虚拟节点,接着再通过「A-01」虚拟节点找到真实节点 A,这样请求就能访问到真实节点 A 了。
虚拟节点除了会提高节点的均衡度,还会提高系统的稳定性。当节点变化时,会有不同的节点共同分担系统的变化,因此稳定性更高。
带虚拟节点的一致性哈希方法不仅适合硬件配置不同的节点的场景,而且适合节点规模会发生变化的场景