如何写硬盘才能快

一、前言

为什么突然想起写这个话题呢?

这里先抛出两个议题:

  1. 写文件应该怎么写,什么是顺序读写和随机读写?
  2. 更换了SSD硬盘,随机读写也比顺序读写慢吗

在很多初入IT门的人看来,甚是很少关注自己的程序是如何写磁盘的,往往大家认为CPU的处理能力和内存的大小对系统的性能影响更大。其实程序员平常对于文件的读写,大部分是进行小批量小文件的操作,对于读写文件成为系统性能瓶颈的场景见得过少。自从接手了一个系统,性能甚是低下,高峰期直接卡死。运维也发现磁盘IO极高,更换了更贵的SSD磁盘,仍然无济于事。几番排查,竟然是其中一个保存附件的功能导致。几番思考,故决定写一篇博文,作为分享也作为自己的总结。

本文从硬盘的原理(机械硬盘和固态硬盘)和操作系统写硬盘的流程来分析,应该如何写硬盘,最后附上相关程序代码(先Java,C++和Python后续奉上)。

二、机械硬盘结构和原理

如果拆开硬盘,结构大抵如上图。其中几个主要的部件:磁盘、磁头、主轴

磁盘是真正存储数据的介质。一个硬盘一般有多个磁盘,磁盘有上下两个盘面,一般来说上下两个盘面都可以存数据。

一个盘面又分为多个磁道。磁道是以主轴为中心的环形,一个盘面会很多个磁道,磁道上布满了存储数据的磁介质,如上图。

为了更好的利用存储介质,磁道又划分为多个扇区。扇区是磁盘的最小存储单元,大小一般为512b。

好了,上面机械硬盘的结构铺垫完了,开始重点部分:机械硬盘读写数据的流程是怎样的?如下:

  1. 磁道移动到对应的磁道,这是由马达控制的机械动作,一般为10ms左右(取决于磁头位置和目标磁道的距离),这叫做寻道时间
  2. 等待对应的扇区旋转到磁头位置(磁头是不动的),按现在主流磁盘转速7200转/分钟,旋转一周需要8.33ms,这叫等待时间
  3. 对应扇区在磁头旋转而过,数据就被读写完成了,一般一个磁道又63个扇区,一个扇区掠过磁头的时间为 8.33ms/63=0.13ms,我们叫它传输时间

由上面,可以推出数据的 存取时间 = 寻道时间(t1) + 等待时间(t2) + 传输时间(t3)

所有要读写硬盘时间更快,我们要t1,t2,t3尽量小。其中t2和t3是由硬盘的转速决定的,不是程序决定的,要快只能买更快(听说15000转/分钟 是天花板了)。

我们能干预的是寻道时间t1。如果我们存储的数据位置,经过精心设计,让它分布在连续的扇区,这样磁头只需要移动一次到目标磁道,然后可以顺着磁盘的旋转,一次就能读写完成,这种方式就是顺序读写

相反,如果数据的存储位置是杂乱无章的,那么磁头需要在对应的磁道反复移动,反复等待扇区旋转到磁头位置,那么就会耗费更多的时间,这就叫做随机读写

一般测试得到的结果,顺序读写的速度是随机读写的100多倍。由此得到的结论是,要更快的读写磁盘,尽量使用顺序读写,比如闻名的套吞吐量消息系统kafka,采用的就是这个机制。

  • 固态硬盘结构和原理

上面分析了机械硬盘的原理,我们思考下,固态硬盘不像机械硬盘,没有磁头,也不需要等待磁盘旋转,是不是就没有了顺序读写和随机读写的速度差异了呢?

   

 

上图是固态硬盘实际图,原理图可以简化如下:

 

固态硬盘的读写虽然没有了机械硬盘的机械结构,但仍然有寻址操作和数据传输。其中寻址依赖于主控芯片,数据传输依赖于主控芯片和缓存。如果文件在闪存中是分散存储的,则需要主控芯片频繁发送地址指令,过多的指令会消耗时钟周期,如果地址指令过多,比如影响书写效率。

故顺序读写对于固态硬盘,仍比随机读写要快。一般而言,顺序读写比随机读写仍然快10倍左右(不同品牌的固态硬盘差异巨大)。

  • 随机读写实现(Java)

Java由于历史渊源,读写文件有很多种方式,如下多组有不同的使用场景,可以拿走使用。

  1. 使用FileWriter和FileReader
    /**
     * 使用FileWriter写文本文件
     * @param fileName 文件名
     * @param content 内容
     * @param append 是否追加形式写文件
     */
    public static void writeText(String fileName, String content, boolean append) {
        try {
            FileWriter writer = new FileWriter(fileName, append);
            writer.write(content);
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     * 使用FileReader读文本,一次读一个字符,然后组装String(效率低不建议用)
     * @param fileName 文件名
     * @return String
     */
    public static String readText(String fileName){
        StringBuilder content = new StringBuilder();

        try {
            File file = new File(fileName);
            FileReader reader = new FileReader(file);
            char[] ch = new char[1];

            while (reader.read(ch) != -1) {
                content.append(ch);
            }

            reader.close();
        }catch (IOException e){
            e.printStackTrace();
        }

        return content.toString();
    }

  1. 带缓存方式BufferedWriter和BufferedReader
    /**
     * 使用缓冲FileWriter方式写文件
     * @param fileName 文件名
     * @param content 内容
     * @param append 是否追加形式写文件
     */
    public static void writeTextByBuffer(String fileName, String content, boolean append) {
        try {
            BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, append));
            writer.write(content);
            writer.close();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 使用缓冲FileReader读取文本文件,常用于读面向行的格式化文件
     * @param fileName 文件名
     * @return String
     */
    public static String readTextByBuffer(String fileName) {
        StringBuilder content = new StringBuilder();

        try {
            BufferedReader reader = new BufferedReader(new FileReader(fileName));

            // 一次读入一行,直到读入null为文件结束
            String line;
            while ( (line = reader.readLine()) != null) {
                content.append(line);
                content.append("\n");
            }

        }catch (Exception e) {
            e.printStackTrace();
        }

        return content.toString();
    }

  1. 流方式FileOutputStream和FileInputStream
    /**
     * 以文件流的方式写文件,常用于读二进制文件,如图片、声音、影像等文件。
     * @param fileName 文件名
     * @param content 内容
     */
    public static void writeFile(String fileName, byte[] content){
        try {
            OutputStream out = new FileOutputStream(fileName);
            out.write(content);
            out.close();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 以文件流的方式读取文件,常用于读二进制文件,如图片、声音、影像等文件。
     * @param fileName 文件名
     */
    public static void readFile(String fileName) {
        try {
            InputStream in = new FileInputStream(fileName);
            // 一次读多个字节
            int buffSize = 100;
            byte[] buff = new byte[buffSize];
            while (in.read(buff) != -1) {

                System.out.println(new String(buff));

                //防止读不满buff,结尾处遗留了上次的内容
                Arrays.fill(buff, (byte)0);
            }

            in.close();

        }catch (Exception e) {
            e.printStackTrace();
        }
    }

  • 顺序读写(Java)

顺序读写和随机读写一个很大的不同点是,顺序读写需要维护一个文件索引信息文件。顺序写首先分配一个固定大小的文件(比如下面例子的500M),然后每次写入都紧跟着上次写入的结尾处,并把每次写入的开始位置和结束位置记录到文件索引,然后保存下来。读的时候根据索引信息读出上次写入的信息。

索引信息file_mapping格式如下,第一位是每次写入的开始位置,第二位是结束位置+1(也就是下次写入的开始位置)。

0,54

54,108

108,162

162,216

216,270

如下是实现代码:

    /**
     * 顺序写文件例子
     * @throws Exception
     */
    public static void writeFileOfSequence() throws Exception {

        // 文件名
        final String fileName = "file.txt";
        // 文件大小
        final long fileSize = 1024 * 1024 * 500;
        // 文件内容索引
        final String fileMapping = "file_mapping.txt";

        RandomAccessFile randomAccessFile = new RandomAccessFile(fileName, "rw");
        FileChannel fileChannel = randomAccessFile.getChannel();

        // 开启一片内存映射,映射大小为文件大小
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileSize);


        // 当前写入的文件位置
        int currentPosition = 0;
        // 写入后,下一个文件位置
        int nextPosition;
        final String content = "[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]";
        Map<Integer, Integer> positionMap = new LinkedHashMap<>();

        System.out.println(String.format("writeFileOfSequence start: %s", new Date()));

        do {
            // 按位置写入内容
            mappedByteBuffer.position(currentPosition);
            mappedByteBuffer.put(content.getBytes());
            nextPosition = mappedByteBuffer.position();
            //String log = String.format("index: %d, content: %s", currentPosition, content);
            //System.out.println(log);

            // 记录位置映射(记录当前位置和下一个位置,nextPosition - currentPosition即为内容长度)
            positionMap.put(currentPosition, nextPosition);
            currentPosition = nextPosition;

        }while (currentPosition + content.getBytes().length <= fileSize);

        mappedByteBuffer.force();
        fileChannel.close();
        randomAccessFile.close();

        System.out.println(String.format("writeFileOfSequence end: %s", new Date()));

        // 把映射信息记录到一个文件(kafka也是这样的)
        StringBuilder mappingInfo = new StringBuilder();
        for(Map.Entry<Integer, Integer> item: positionMap.entrySet()){
            String line = String.format("%d,%d\n", item.getKey(), item.getValue());
            mappingInfo.append(line);
        }
        writeText(fileMapping, mappingInfo.toString(), false);
    }


    /**
     * 顺序读文件例子
     * @throws Exception
     */
    public static void readFileOfSequence() throws Exception{

        // 文件名
        final String fileName = "file.txt";
        // 文件大小
        final long fileSize = 1024 * 1024 * 500;
        // 文件内容索引
        final String fileMapping = "file_mapping.txt";

        // 读入位置映射信息
        Map<Integer, Integer> positionMap = new LinkedHashMap<>();
        BufferedReader reader = new BufferedReader(new FileReader(fileMapping));
        String line;
        while ( (line = reader.readLine()) != null) {
            String[] lineSplit = line.split(",");
            if (lineSplit.length == 2){
                positionMap.put(Integer.parseInt(lineSplit[0]), Integer.parseInt(lineSplit[1]));
            }
        }

        System.out.println(String.format("readFileOfSequence start: %s", new Date()));
        RandomAccessFile randomAccessFile = new RandomAccessFile(fileName, "rw");
        FileChannel fileChannel = randomAccessFile.getChannel();

        // 开启一片内存映射,映射大小为文件大小
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileSize);

        // 按文件内容索引读取内容
        for (Map.Entry<Integer, Integer> item: positionMap.entrySet()){
            int length = item.getValue() - item.getKey();
            byte[] buff = new byte[length];

            mappedByteBuffer.get(buff, 0, length);
            //String log = String.format("index: %d, content: %s", item.getKey(), new String(buff));
            //System.out.println(log);
        }
        System.out.println(String.format("readFileOfSequence end: %s", new Date()));

    }
  • 随机读写和顺序读写对比(Java)

先分别按随机和顺序两种方式个写一个500M的文件。

随机读写主程序:

public class NormalWrite {
    public static void main(String [] args) throws Exception{

        final String fileName = "file-normal.txt";
        FileWriter writer = new FileWriter(fileName, true);

        final String content = "[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]";

        System.out.println(String.format("NormalWrite start: %s", new Date()));

        for(int i=0; i < 10000000; i++) {
            writer.write(content);
        }
        writer.close();

        System.out.println(String.format("NormalWrite end: %s", new Date()));

    }
}



public class NormalRead {
    public static void main(String [] args) throws Exception{

        final String fileName = "file-normal.txt";

        System.out.println(String.format("NormalRead start: %s", new Date()));
        InputStream in = new FileInputStream(fileName);
        // 一次读多个字节
        int buffSize = 54;
        byte[] buff = new byte[buffSize];
        while (in.read(buff) != -1) {
            //System.out.println(new String(buff));
            //防止读不满buff,结尾处遗留了上次的内容
            Arrays.fill(buff, (byte)0);
        }

        in.close();

        System.out.println(String.format("NormalRead end: %s", new Date()));

    }
}

顺序读写主程序:

public class SequenceWrite {
    public static void main(String [] args) throws Exception{

        writeFileOfSequence();

    }
}




public class SequenceRead {
    public static void main(String [] args) throws Exception{

        readFileOfSequence();

    }
}

随机写入后读出的时间是:5s

    

顺序写入后读出的时间是:x毫秒(毫秒没有打印)

    

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值