网络通信优化之高并发下的IO瓶颈

1. 什么是IO

  1. IO是机器回去和交换信息的主要渠道,而流式完成IO操作的主要方式

  2. 流是一种信息的转换,流是有序的。

  3. 机器间或者程序在进行信号交换时,总是将对象或者数据转换为某种形式的流,再通过流的传输,到达指定机器或程序后,再将流转换为对象数据。

  4. 流可以被看做是一种数据的载体,通过它可以实现数据交换和传输。

2. 传统IO的性能问题

  1. 磁盘IO:从磁盘中读取数据源输入到内存中,之后将读取的信息持久化输出在物理磁盘中。

  2. 网络IO:从网络中读取信息输入到内存,最终将信息输出到网络中。

1. 多次内存复制

  1. DMA:外部设备不通过CPU而直接与内存交换数据的技术接口。这样数据传输的传送速度就取决于存储器和外设的工作速度。

  2. 计算机硬件上使用DMA来访问磁盘等IO,也就是请求发出后,CPU就不再管了,直到DMA处理器完成任务,再通过中断告诉CPU完成了。
    所以单独的一个IO事件,对CPU的占用是很少的,阻塞了就更不会占用CPU了。因为程序都不继续运行了,CPU时间交给其他线程和进程了。
    虽然IO不会占用大量的CPU时间,但是非常频繁的IO还是会非常浪费CPU的时间的。面对大量的IO任务,有时候需要算法来合并IO,或者通过cache来缓解IO压力的。

  3. 过程:数据先从外部设备复制到内核空间,再从内核空间复制到用户空间,发生了两次内存复制操作。这种操作会导致不必要的数据拷贝和上下文切换,从而降低IO性能。

  • JVM 会发出 read() 系统调用,并通过read系统调用向内核发起读请求。

  • 内核向硬件发送读指令,并等待就绪

  • 内核把将要读取的数据复制到指定的内核缓存中

  • 操作系统内核将数据复制到用户空间缓冲区,然后read系统调用返回

2. 阻塞

  1. 在传统的IO中,INputStream的read是一个while循环操作,它会一直等待数据读取,直到数据就绪才返回。
    这就意味着如果没有数据就绪,这个读取操作将会被一直挂起,用户线程将会处于阻塞状态(但是会让出CPU)。

  2. 在少量连接请求的情况下,这种方式没有问题,响应速度也很高,但是发生大量连接请求时,就需要创建大量监听线程。
    这时如果线程没有数据就绪就会被挂起,然后进入阻塞状态,一旦发生线程阻塞,这些线程将会不断地抢夺CPU资源,从而导致大量的CPU上下文切换,增加系统的性能开销。

3. 优化IO操作

1. 使用缓冲区优化读写操作

Buffer 可以将文件一次性读入内存后再做后续处理,而传统的方式是边读文件边处理数据。

2. 使用DirectBuffer减少内存复制

  1. 普通的Buffer分配的是JVM堆,而DirectBuffer是直接分配物理内存(非堆内存)。

  2. 数据要输出到外部设备,必须先从用户空间复制到内核空间,再复制到输出设备。
    在java中,在用户空间中又存在一个拷贝,那就是从java堆内存中拷贝到临时的直接内存中,通过临时的直接内存拷贝到内存空间中去。
    此时的直接内存和堆内存都是属于用户空间。

  3. 使用非堆内存可以降低GC的压力

  4. DirectBuffer则是直接将步骤简化为数据直接保存到非堆内存,从而减少了一次数据拷贝。

  5. DirectBuffer是通过unsafea.allocateMemory(size)方法分配内存,也就是基于本地类Unsafe类调用native方法进行内存分配的。

  6. 优化用户空间和内核空间的拷贝:MappedByteBuffer 是通过本地类调用mmap进行文件内存映射的。
    map()系统调用方法会直接将文件从硬盘拷贝到用户空间,只进行一次数据拷贝,从而减少了传统的read() 方法从硬盘拷贝到内核空间这一步。

3. 避免阻塞,优化IO操作

  1. 传统IO的数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的IO接口从磁盘读取或写入。

  2. 最开始,在应用程序调用操作系统IO接口时,是由CPU完成分配,这种方式最大的问题是 发生大量IO请求时,非常消耗CPU。
    之后,操作系统引入了DMA(直接存储器存储),内核空间与磁盘之间的存取完成由DMA负责。
    但这种方式依然需要向CPU申请权限,且需要借助DMA总线来完成数据的复制操作。如果DMA总线过多,就会造成总线冲突。

  3. channel有自己的处理器,可以完成内核空间和磁盘之间的IO操作。在NIO中,我们读取和写入数据都需要通过channel。由于channel是双向的,所以读写可以同时进行。

  4. selector是基于事件驱动实现的,我们可以在selector中注册accept、read监听事件,selector会不断轮询注册在其上的channel。
    如果某个channel上面发生监听事件,这个channel就处于就绪状态,然后进行IO操作。

  5. 一个线程使用一个selector,通过轮询的方式,可以监听多个channel上的事件。我们可以注册channel时设置该通道为非阻塞。
    当channel上没有IO操作时,该线程就不会一直等待了,而是不断轮询所有channel,从而避免发生阻塞。

  6. 目前操作系统的IO多路复用机制都使勇了epool,相比传统的select机制,epoll没有最大连接句柄1024的限制。所以selector在理论上可以轮询成前上万的客户端。

你可能感兴趣的:(网络通信优化之高并发下的IO瓶颈)