性能调优-磁盘文件读写与网络传输

938 阅读5分钟

麻烦路过的小伙伴点赞、关注,共同学习成长!

本人csdn博客:blog.csdn.net/Sun_ltyy

引子:我们在读取磁盘文件,并经过网络传输时,我们能不假思索的用到如下代码。

private void sendFile(String filePath) {

        try {
            //1.从磁盘读取文件
            File file = new File(filePath);
            DataInputStream dis = new DataInputStream(new FileInputStream(filePath));
            DataOutputStream dos = new DataOutputStream(socket.getOutputStream());

            //2.每次读取的1024字节大小数据,放入用户缓冲区
            byte[] buf = new byte[1024];
            int len = 0;

            //3.循环读取数据
            while ((len = dis.read(buf)) != -1) {
                //4.写入网络输入流
                dos.write(buf, 0, len);

            }
            dos.flush();

            //4.关闭输入输出流
            dis.close();
            dos.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

一、分析以上代码的执行流程

如下图所示:应用开始从磁盘读取一次文件,到发送到网络中,一共需要4次文件拷贝、4次用户态和内核态之间的切换。

  • 四次文件拷贝过程:磁盘到PageCache高速缓存页的拷贝、pagecache到用户缓冲区的拷贝、用户缓冲区到socket缓冲区的拷贝、socket缓冲区到网卡的拷贝
  • 四次用户态和内核态的切换:发起读时用户态到内核态的切换、读取结果返回时内核态到用户态的切换;用户缓冲区数据写回到网络时用户态到内核态的切换,写回结果时内核态到用户态的切换。

二、用户态和内核态间切换和文件多次拷贝的影响

通过上一步的分析,我们得知一次读写和发送,我们要经过4次的文件拷贝,及内核态用户态之间的切换。假如我们有32M的文件需要读取发送,由于我们设置的用户缓冲区大小为1024字节即1K:byte[] buf = new byte[1024];那么32M的文件,需要经过321024/1 = 32768次读写,共计需要327684=131072次切换,每次切换虽然耗时只有几纳秒或者几十微秒,但是当高并发情况下发送文件时耗时就会增加,文件被拷贝4次32M*4=128M需消耗128M的内存空间。

因此我们可以从减少用户态和用户态切换次数、降低文件拷贝次数来提升并发读取发送文件的性能。

三、如何减少切换次数,和拷贝次数

零拷贝技术正式解决此类问题。在读取磁盘文件并发送到网络的过程中,从磁盘文件读取和发送到网络这两步骤是必不可少的,而用户态的两次拷贝是完全可以去掉的。零拷贝技术,并不是没有拷贝,而是去掉了和用户态的缓冲区相关的两次拷贝,如图所示,可以看出零拷贝技术可以成倍的提升并发传输文件的效率。

四、远不止这些,零拷贝也只能用在小文件传输中(先卖个关子)

上文中两张图中在内核态其实都出现了PageCache告诉缓冲区,那么PageCache有什么作用呢。首先我们的内存的处理速度远远高于磁盘的处理速度,但是磁盘的存储容量要比内存大,我们不能无限制的把文件都读入到内存中进行处理,因此就需要敲定哪些文件内容可以放入到内存中,这就是PageCache存在的意义,在内存和磁盘之间加一层,当内存读取磁盘文件时,根据时间局部原理,刚刚被访问的数据,再次被读取的概率就很高,根据这个原理,pageCache会预读一部分内容缓存到高速缓冲区,当下次读取时现在pageCache中查找,如果数据存在就直接返回,大大提高磁盘的读取数据的性能。另外pagaCache使用LRU的淘汰算法,当高速缓冲区满了以后,会先把最久未被访问的缓存淘汰掉。

理论上高速缓存PagaCache可以带来90%的性能提升,但是这是对于小文件来说的。对于非常大的文件的读取会带来负面影响,具体就是由于文件太大,比如几GB,在并发场景下,很快就会把PageCache高速缓存区沾满,由于文件非常大,不同并发线程缓存的数据,可能直接就被剔除掉了,导致缓存失效,但是PageCache的存在,是需要磁盘到pageCache进行拷贝的,也就是说对于大文件的传输零拷贝技术不但不能利用到PageCache带来的缓存提升,反而还增加了一次拷贝。这时异步IO和直接IO就派上用场了。

五、异步IO与直接IO提升大文件传输的效率

先来看下同步IO的处理时序图

异步IO的处理时序图

从同步IO和异步IO的时序图中,我们可以看到异步IO操作过程中是没有从磁盘到PageCache的拷贝过程的。经过了PageCache的IO我们称之为缓存IO,由于内核和PageCache的耦合太严重,异步IO在设计之初就绕过了PageCache,这种绕过了PageCache的IO就叫做直接IO。

在大文件的读取传输过程中我们可以使用异步IO和直接IO,来避免零拷贝这种缓存IO带来的性能损耗。

六、总结

1.小文件传输推荐使用零拷贝技术,零拷贝并不是没有拷贝,而是没有用户态的拷贝。

2.大文件的传输使用异步IO和直接IO,可以避免大文件无法利用传统IO pageCache带来的性能损耗。

ps:根据大小文件决定使用直接IO还是零拷贝的最佳实践参考nginx的directio指令。