网络系统
零拷贝
磁盘是计算机系统中读写速度最慢的的硬件之一,而零拷贝技术是用于提高文件传输性能的,通过减少上下文切换次数与数据拷贝的次数从而提高传输性能。
操作系统中IO的流程
大体流程
收到IO请求由用户态切换到内核态,CPU发送指令给磁盘控制器,磁盘控制器收到指令以后开始准备数据,数据被放入到磁盘控制器的内部缓冲区中,然后产生中断。CPU收到中断信息以后,从内部缓冲区中一个字节一个字节的读取到PageCache中,然后再从PageCache写入到用户态缓冲区中,最后再从内核态切换到用户态。
DMA技术
为了提高效率,提高CPU的使用性能,于是有了DMA技术(直接内存访问技术),数据运输工作全部交给DMA进行解决,而此时CPU就可以去做别的事情。于是流程就变成了这样:
改进以后的流程
收到IO请求由用户态切换到内核态,操作系统接收到IO请求,将请求分发给DMA,让CPU去做别的事情。DMA进一步将IO请求发送给磁盘,磁盘收到请求将数据放入到缓冲区,当缓冲区放满以后向DMA发送中断,DMA收到中断以后将缓冲区的数据读取到PageCache中,读取完毕以后再向CPU发送中断请求,最后CPU收到中断请求,将内核态的PageCache中的数据拷贝到用户态缓冲区中,再从内核态切换到用户态。
零拷贝技术
如果服务端想要对外提供文件传输的功能,那么就大体的流程就是将磁盘里面的内容读取出来然后通过网络协议发送到客户端。这期间由于传输性能太低所以就有了零拷贝技术。
没有零拷贝技术之前的流程
没有使用零拷贝需要进行四次数据拷贝以及两次上下文切换
- 收到传输请求,从用户态切换到内核态,从磁盘中读取数据DMA拷贝到PageCache中,然后CPU再从PageCache将数据拷贝到用户态缓冲区中,再从内核态切换到用户态。
- 再从用户态切换到内核态,将CPU用户态缓冲区中的数据拷贝到socket的缓冲区中,然后DMA再从socket的缓冲区中拷贝到网卡的缓冲区中
使用零拷贝技术之后的流程
零拷贝的实现有两种方式一种是使用mmap+write,还有一种是使用sendfile
- mmap+write
- 调用mmap函数以后会将磁盘缓冲区里面的数据拷贝到内核态缓冲区中,此时用户态和内核态一起共享内核态缓冲区中的磁盘数据,CPU无需再从内核态拷贝数据到用户态。用户态再调用write方法,从用户态切换到内核态以后直接将内核态缓冲区中的磁盘数据拷贝到socket中。
- 这样的好处就是减少了一次调用CPU进行数据拷贝。
- sendfile
- DMA直接将磁盘缓冲区里面的数据拷贝到内核态缓冲区中,然后将缓冲区里面的描述符和数据长度传入到socket的缓冲区中,最后网卡中就可以根据socket缓冲区中的内容直接从内核态缓冲区拷贝到网卡中。
- 这样的好处就是减少了两次拷贝过程以及两次内核态与用户态的上下文切换。
什么是PageCache
PageCache实际上就是内核缓冲区,作用有两个,一个是缓存最近被访问的数据另一个是预读功能。
- 读取磁盘的时候优先从PageCache里面找,有就直接返回数据,没有就从磁盘中读取然后缓存到PageCache中。
- 举个例子,假如read方法每次只能够读取100k,虽然read刚开始只会读0100k的数据,但是内核会把后面的101200k的数据都给写入到PageCache中,这样就减少了与磁盘交互的次数。
读取大文件应该怎么办
如果读取大文件的话,由于PageCache的预读功能,会把PageCache吃满导致热点小文件无法使用到PageCache的优点。为了防止这种情况的出现,应该不使用零拷贝的技术而是使用异步IO和直接IO的方式绕开PageCache,流程是:进程发起异步IO请求,然后去处理其他事情,操作系统向内核发送IO请求,磁盘将数据存储到磁盘缓冲区以后,向CPU发送中断,CPU直接将数据从磁盘缓冲区拷贝到用户态缓冲区中。
IO多路复用
网络IO的流程
在进行网络通信之前双方都需要建立一个socket,双方在读取或者传输数据的时候都通过这个socket进行数据的传输。服务端调用socket方法创建socket然后再调用bind方法给这个socket绑定ip和端口。绑定完ip和端口之后调用listen函数进行监听状态,最后通过调用accept函数从内核获取客户端连接。客户端再调用调用socket方法创建socket以后调用conect方法与服务端进行连接,接着就可以进行网络间的通信了。
TCP的最大连接数
Tcp的最大连接数限制于两个,一个是文件操作符一个是系统内存。
- socket实际上就是一个文件,也就对应这一个文件操作符,在linux环境下文件操作符的打开数量是有限的,一般是1024。
- 每个连接在内核中都有对饮的内存,意味着每个连接占用一定的内存。
如果请求量很大,那么就肯定是无法满足的,为了解决这个问题就有了IO多路复用。
IO多路复用
如果为每一个请求都分配一个进程或者线程,假如说来了一万个请求就需要维护一万个进程或者线程,操作系统肯定是扛不住的。所以IO多路复用就是用一个进程来维护多个Socket,虽然说一个进程只能处理一个请求,但是如果把每个请求的时间降低,这样1秒内处理上万个请求了。
select/poll
select和poll的方式是用户态将已经连接的Socket存储在集合中,用户态将集合拷贝到内核态以后内核遍历一遍集合,查看有没有网络事件发生,有就将其进行标记。然后再将其从内核态拷贝到用户态,用户态再遍历一遍集合对标记的socket进行处理。
两者间的区别
select使用bitsmap,是一个固定长度的集合,默认的最大值为1024只能监听0~1023的文件。poll不再使用bitsmap而是使用动态的数组,突破了文件数量的限制。
epoll
epoll使用红黑树来维护待检测的socket和链表来维护就绪的socket,同时是在内核态进行维护的,减少了上下文切换的次数。红黑树用于来维护待检测的socket,当有socket就绪以后通过回调函数将其加入到链表中,用户只需要调用epoll_wait()
方法就可以从链表中获取到就绪的socket而不需要遍历。
边缘触发和水平触发
边缘触发:服务端是从epoll_wait()
苏醒一次,所以必须一次性读完。
水平触发:服务端会一直从epoll_wait()
苏醒,直到客户端read完所有的就绪事件。
网络模式:Reactor模型和Proactor模型
Reactor模型
Reactor模型对多路IO模型的封装,由两部分组成一个是Reactor一个是处理资源池。
- Reactor用于监听响应事件
- 处理资源池负责处理事件
同时分Reactor模型常见的模型又分为单Reactor单线程、单Reactor多线程、多Reactor多线程模型
单Reactor单线程
不需要考虑进程之间通信的问题,实现起来比较简单,但是没有办法充分利用多核CPU,所以处理业务逻辑的事件不能太长,否则其他连接的事务响应会有延迟。
单Reactor多线程
相比较于单Reactor单线程,多线程解决了单线程的缺陷,可以利用多核CPU,但是也带来了数据竞争的关系,但是如果并发量高的话单个Reactor承担了所有的事件监听和响应,而在在主线程运行容易成为性能瓶颈。
多Reactor多线程
相较于单Reactor,多Reactor做到了主Reactor用于监听事件,而从Reactor用于分发事件给资源处理池用于处理事件。
Proactor模型
采用异步 I/O 实现的异步网络模型,感知的是已完成的读写事件,而不需要像 Reactor 感知到事件后,还需要调用 read 来从内核中获取数据。
一致性哈希算法
现在的服务器大都是分布式部署的,一个应用部署在多台服务器上,然后通过网关将请求转发到其中的一台服务器上。为了提高系统的数据容量,会把数据水平的分到不同的节点上,比如一个分布式缓存系统,某一个key就应该到某一个节点上进行获取而不是所有的节点都能够获取。为了解决这个问题,可以使用哈希算法。比如有三台服务器,那么就将key转换成数值取模3获取到响应的服务器然后去获取数据,但是这样有个问题就是如果服务器进行扩容了,那么哈希算法也应该进行改变,数据也应该根据哈希算法的改变重新水平分发到不同的服务器上,十分麻烦。
一致性哈希
一致性哈希也是取模,只不过是对232进行取模得到一个固定值,同时将0=232使其形成一个圆。使用一致性哈希算法获取到相应的服务器分为两步,第一步:将服务器的ip进行一致性哈希处理,得到一个固定值,第二步:将获取数据的key进行一致性哈希处理,也得到一个固定值。这样两个哈希值就会被映射到一个首尾相接的圆上,然后只需要从key的位置顺时针找到第一台服务器就是存储key数据的服务器。
但是一致性哈希不是万能的,他只能减少数据迁移量,并不保证节点能够在哈希环上分布均匀,这样就会带来一个问题,会有大量的请求集中在一个节点上。假设访问请求主要集中的节点 A 上,如果节点 A 被移除了,当节点 A 宕机后,根据一致性哈希算法的规则,其上数据应该全部迁移到相邻的节点 B 上,这样,节点 B 的数据量、访问量都会迅速增加很多倍,一旦新增的压力超过了节点 B 的处理能力上限,就会导致节点 B 崩溃,进而形成雪崩式的连锁反应。
虚拟节点
为了解决节点在哈希环上分配不均的问题,可以引入虚拟节点。比如现在还是有三台服务器ABC,哈希环上只有三个节点,但是现在不再将真实节点映射到哈希环上而是将虚拟节点映射到哈希环上,然后虚拟节点再映射到真实的服务器上,这个时候就能解决哈希环上分配不均的问题,也就是负载均衡的问题。
比如对每个节点分别设置 3 个虚拟节点:对节点 A 加上编号来作为虚拟节点:A-01、A-02、A-03
对节点 B 加上编号来作为虚拟节点:B-01、B-02、B-03
对节点 C 加上编号来作为虚拟节点:C-01、C-02、C-03
引入虚拟节点后,原本哈希环上只有 3 个节点的情况,就会变成有 9 个虚拟节点映射到哈希环上,哈希环上的节点数量多了 3 倍。
参考:小林coding - 图解系统