磁盘IO java代码落地实战演练

前言

上篇更新了linux中IO相关的部分基础知识,主要偏向于理论。本篇会将上章节的理论落地并扩展关于磁盘IO相关的知识点。
如需补充理论知识的同学点击这里。linux之内存管理

一、磁盘IO性能的比较

我们经常说buffered IO比Base IO快,但是我们知道为什么他比基本的IO快吗?
下来我们来看一组代码。简单说明下:在固定时间内,向磁盘中循环写入固定字节数的数据,通过改变输入流的方式,来观察最终生成文件的大小,得出IO性能排名。

package com.dxg.disk;

import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class DiskFileIo {

    /**
     * 输出目录
     */
    private static String PATH = "/home/sysio/";

    /**
     * 输入的数据
     */
    private static final byte[] DATA = "123456789\n".getBytes();

    /**
     * 执行写入的时间
     */
    private static final int EXECUTE_TIME = 3;

    /**
     * 控制写入的状态
     */
    private volatile static boolean FLAG = true;


    public static void main(String[] args) throws Exception {
        DiskFileIo diskFileIo = new DiskFileIo();
        switch (args[0]) {
            case "1":
                diskFileIo.baseWrite("out1.log");
                break;
            case "2":
                diskFileIo.bufferedWrite("out2.log");
                break;
            case "3":
                diskFileIo.randomAccessFileTest1("out3.log");
                break;
            case "4":
                diskFileIo.randomAccessFileMmap("out4.log");
                break;
        }
    }


    /**
     * 基础文件流写入
     */
    public void baseWrite(String fileName) throws Exception {
        FileOutputStream outputStream =
                new FileOutputStream(new File(PATH + fileName));
        startMonit();
        while (FLAG) {
            outputStream.write(DATA);
        }
    }

    /**
     * buffered文件流写入
     *
     * @param fileName
     */
    public void bufferedWrite(String fileName) throws Exception {
        BufferedOutputStream outputStream =
                new BufferedOutputStream(new FileOutputStream(new File(PATH + fileName)));
        startMonit();
        while (FLAG) {
            outputStream.write(DATA);
        }
    }

    /**
     * NIO基础文件写入
     *
     * @param fileName
     * @throws Exception
     */
    public void randomAccessFileTest1(String fileName) throws Exception {
        RandomAccessFile accessFile = new RandomAccessFile(PATH + fileName, "rw");
        startMonit();
        while (FLAG) {
            accessFile.write(DATA);
        }
    }

    /**
     * NIO mmap文件写入
     *
     * @param fileName
     * @throws Exception
     */
    public void randomAccessFileMmap(String fileName) throws Exception {
        RandomAccessFile accessFile = new RandomAccessFile(PATH + fileName, "rw");
        FileChannel channel = accessFile.getChannel();
        // 在程序和内核中创建一个共享内存空间
        // 调用了内核的mmap
        MappedByteBuffer mmap =
                channel.map(FileChannel.MapMode.READ_WRITE, 0, Integer.MAX_VALUE);
        startMonit();
        while (FLAG) {
            mmap.put(DATA);
        }
    }


    /**
     * 开启监控线程
     */
    public void startMonit() {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(() -> {
            try {
                TimeUnit.SECONDS.sleep(EXECUTE_TIME);
                FLAG = false;
                executor.shutdownNow();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }

}

为使得观察的结果更加精确,我们将源代码copy到linux中。
分别执行命令:

  1. javac DiskFileIo.java && strace -ff -o /home/sysio/trace/trace1 java DiskFileIo 1
  2. strace -ff -o /home/sysio/trace/trace2 java DiskFileIo 2

将源码编译执行并通过strace追踪当前进程的系统调用。执行完成后观察生成的log文件大小如下图:
在这里插入图片描述

  1. out1.log 是通过基础的IO流生成的文件;
  2. out2.log 是通过buffered IO流生成的文件;

很明显的可以看到在相同执行条件下buffered IO写入速度高于Base IO。此处只能看到buffered IO 比不同IO快,但是我们还是不清楚什么?

我们再执行以上代码时,加入了追踪系统调用的命令,那么我们再在看看系统调用的情况:

  1. Base IO
    在这里插入图片描述
  2. Buffered IO
    在这里插入图片描述

通过系统调用的对比,可以看到对于Base IO每当我们写入数据时,都会触发一次系统调用,而Buffered IO则是在jvm 的堆中有同一个8K的内存空间,每满8k才会触发一次系统调用写入。程序每触发一次系统调用都触发用户态内核态的切换,这是非常消耗时间的。到此我们开始提出的疑问也得到了答案,Buffered IO 通过减少系统调用来增加IO RW的效率
不过随着业务量的发展,对于一些大文件Buffered IO 的系统调用也是不容忽视的。对此人们就想到,既然系统调用如此的消耗时间,那么在读写数据时,能不能不彻底去掉系统调用?
技术服务于业务,现在还真有此技术,并且已经很熟练。接着上面的验证,我们继续在服务器上分别执行:

  1. strace -ff -o /home/sysio/trace/trace3 java DiskFileIo 3
  2. strace -ff -o /home/sysio/trace/trace4 java DiskFileIo 4

观察输入的文件大小结果:
在这里插入图片描述

哇,这个4太牛了,与1和2都是相同的执行条件和相同的环境,生成的文件竟然比Buffered IO还要大七八倍。忍不住的赶紧去查看的3和4的系统调用文件,如下图:

这个4与1和2都是相同的执行条件和相同的环境,生成的文件竟然比Buffered IO还要大七八倍。简直太牛了,忍不住的赶紧去查看了3和4的追踪文件,发现3和1的追踪文件一致,此处就不展示了。下面看看4的追踪文件,如下图:
在这里插入图片描述
从系统调用文件中无法看到像1和2中的write系统调用,确实移除了大量系统调用,增加了IO的读写效率。那么数据文件是如何进入到内核呢?
其实在追踪文件中我们可以看到mmap调用,此调用意为:在进程内和内核中创建一个共享空间,程序和内核共享此块内存区域的数据,且这个内存区域是pagecache到文件的映射。

二、堆内、堆外、共享内存

通过上面的演示,我想大家对于磁盘IO已经渐渐有点感觉了。接下来针对上面的知识做个简单总结以及扩展一些知识点。

在Java 磁盘NIO体系内,对于IO buffer的分配有三种方式:

  1. 堆内
  2. 堆外
  3. 共享内存
ByteBuffer onHeap = ByteBuffer.allocate(1024);
ByteBuffer offHeap = ByteBuffer.allocateDirect(1024);

// 共享内存上面已经演示

下面通过一张图让你更加清晰的了解这三个名词的概念(图解磁盘NIO):
在这里插入图片描述

操作系统为每个进程独立开辟了一个虚拟内存地址,每个进程内都具备基础的元素,这些知识在,在上个章节有详细描述。
图中可以很看到堆内内存是存在于JVM的heap上的,堆外内存存在于进程内的堆上,在使用堆内内存中的数据时,需要先将堆内数据先copy到堆外才可以使用,所以在进行创建缓存区时,可以自定义的情况下,优先使用堆外内存。
共享内存中,java进程内的逻辑地址直接映射到内核中的pagecahe且内核中的pagecache直接映射到磁盘中的文件,所以程序中直接通过put调用时不需要通过系统调用。

以上三种分配方式都是收到内核pagecache dirty 的影响,所以说OS没有绝对的数据可靠性。加入pagecahce减少硬件的IO,优先使用内存,以达到提速。但是也带来了丢数据的风险,即使将数据刷新至磁盘的频率调节至0,单机IO负载将会成为瓶颈,所以现在主流的中间件都加入的副本集,以此来保障数据的可靠性。

其实看看吧,理论知识都是相通,要学会举一反三。磁盘IO相关的知识,就先更新到这里。下一章节开始更新网络IO,其中我会讲到BIO到NIO的多种多路复用器直至推导出Netty的IO模型,欢迎大家的关注。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我的青铜时代

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值