• RandomAccessFile读性能优化


    背景

    公司的日志采集器是我自己开发的,没用开源产品。日志采集虽然是个小功能,但是要想写好也没那么容易。对于一个日志采集器来说,它应该稳定、可控、占用尽量少的资源。因为日志采集器是和核心业务服务部署在同一台服务器上,如果它工作时CPU和内存占用率飙升、重度磁盘I/O,显然是不合适的,毕竟只是一个辅助功能,不能因小失大。

    在生产环境运行时发现部分服务日志过多,日志采集器的采集速度跟不上,日志上报有较大延迟。在对我自己编写的代码进行优化后,我想看看jdk的代码有没有优化的空间。

    RandomAccessFile性能低下的原因

    我主要用的是RandomAccessFile的readLine()方法,点进源码发现readLine()调用的是read()方法,read()最终调用的又是read0(),read0()是一个native方法,无法看到它的实现,但我在read()方法的注释上看到它每次只读一个字节。

    /**
     * Reads a byte of data from this file. The byte is returned as an
     * integer in the range 0 to 255 ({@code 0x00-0x0ff}). This
     * method blocks if no input is yet available.
     * 

    * Although {@code RandomAccessFile} is not a subclass of * {@code InputStream}, this method behaves in exactly the same * way as the {@link InputStream#read()} method of * {@code InputStream}. * * @return the next byte of data, or {@code -1} if the end of the * file has been reached. * @exception IOException if an I/O error occurs. Not thrown if * end-of-file has been reached. */ public int read() throws IOException { return read0(); }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    我们都知道,I/O会产生系统调用,因为对I/O设备的操作是发生在内核态的,用户态和内核态之间的切换会有一定的系统开销,频繁切换会带来很大的开销。每次只读一个字节显然性能非常低,可以考虑使用用户进程缓冲区,也就是一次读很多个字节放到buffer里,减少I/O次数。

    RandomAccessFile + Buffer

    我们先做一个基准测试,按行读取一个6MB的日志文件,比较RandomAccessFile.readLine()的性能和BufferedReader.readLine()的性能。测试代码如下:

    public static void randomAccessFile() {
        try (RandomAccessFile raf = new RandomAccessFile(PATH, "r")) {
            int count  = 0;
            long beginning = System.currentTimeMillis();
            while (raf.readLine() != null) {
                count++;
            }
            long end = System.currentTimeMillis();
            System.out.println("RandomAccessFile, line count: " + count + ", cost: " + (end - beginning) + "ms");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public static void bufferedReader() {
        try (BufferedReader br = new BufferedReader(new FileReader(PATH))) {
            int count = 0;
            long beginning = System.currentTimeMillis();
            while (br.readLine() != null) {
                count++;
            }
            long end = System.currentTimeMillis();
            System.out.println("BufferedReader, line count: " + count + ", cost: " + (end - beginning) + "ms");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    RandomAccessFileBufferedReader
    第一次9580ms63ms
    第二次9379ms77ms
    第三次9340ms66ms

    不测不知道,一测吓一跳,使用缓冲区性能提升了100多倍!

    既然这样,那我使用缓冲区对RandomAccessFile进行改造。首先,我们定义一个抽象类,抽象类里的几个抽象方法都是我们需要用到的RandomAccessFile的方法。

    import java.io.UnsupportedEncodingException;
    import java.nio.charset.Charset;
    
    public abstract class LogReader {
    
        /**
         * 获取操作系统默认字符编码的方法:System.getProperties().get("sun.jnu.encoding");
         * 获取操作系统文件的字符编码的方法:System.getProperties().get("file.encoding");
         * 获取JVM默认字符编码的方法:Charset.defaultCharset();
         */
        public static final String DEFAULT_CHARSET = Charset.defaultCharset().name();
    
        protected String charsetName;
    
        abstract long getFilePointer();
    
        abstract void seek(long pos);
    
        abstract String readLine();
    
        /**
         * 字节数组扩容
         * @param arr
         * @return
         */
        public byte[] grow(byte[] arr) {
            int len = arr.length;
            int half = len >> 1;
            int growSize = Math.max(half, 1);
            byte[] arrNew = new byte[len + growSize];
            System.arraycopy(arr, 0, arrNew, 0, len);
            return arrNew;
        }
    
        /**
         * 字节数组解码成字符串
         * @param arr
         * @param arrPos
         * @return
         */
        public String decode(byte[] arr, int arrPos) {
            if (arrPos == 0)
                return null;
            try {
                return new String(arr, 0, arrPos, charsetName);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
                throw new RuntimeException(e.getMessage());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    接着新建一个类BufferedLogReader并继承LogReader和实现Closeable,BufferedLogReader其实是使用缓冲区对RandomAccessFile进行了一层包装。代码如下:

    import java.io.*;
    import java.nio.charset.Charset;
    
    public class BufferedLogReader extends LogReader implements Closeable {
    
        public static final int DEFAULT_BUFFER_CAPACITY = 8192;
    
        private byte[] buffer;
    
        private int position;
    
        private int limit = -1;
    
        private RandomAccessFile raf;
    
        public BufferedLogReader(String pathName) {
            init(new File(pathName), DEFAULT_BUFFER_CAPACITY, DEFAULT_CHARSET);
        }
    
        public BufferedLogReader(String pathName, String charsetName) {
            init(new File(pathName), DEFAULT_BUFFER_CAPACITY, charsetName);
        }
    
        public BufferedLogReader(String pathName, int bufferCapacity) {
            init(new File(pathName), bufferCapacity, DEFAULT_CHARSET);
        }
    
        public BufferedLogReader(String pathName, int bufferCapacity, String charsetName) {
            init(new File(pathName), bufferCapacity, charsetName);
        }
    
        public BufferedLogReader(File file) {
            init(file, DEFAULT_BUFFER_CAPACITY, DEFAULT_CHARSET);
        }
    
        public BufferedLogReader(File file, String charsetName) {
            init(file, DEFAULT_BUFFER_CAPACITY, charsetName);
        }
    
        public BufferedLogReader(File file, int bufferCapacity) {
            init(file, bufferCapacity, DEFAULT_CHARSET);
        }
    
        public BufferedLogReader(File file, int bufferCapacity, String charsetName) {
            init(file, bufferCapacity, charsetName);
        }
    
        private void init(File file, int bufferCapacity, String charsetName) {
            if (bufferCapacity < 1)
                throw new IllegalArgumentException("bufferCapacity");
            Charset.forName(charsetName); // 检查字符集是否合法
            try {
                raf = new RandomAccessFile(file, "r");
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                throw new RuntimeException(e.getMessage());
            }
            buffer = new byte[bufferCapacity];
            this.charsetName = charsetName;
        }
    
        @Override
        public long getFilePointer() {
            try {
                return raf.getFilePointer();
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e.getMessage());
            }
        }
    
        @Override
        public void seek(long pos) {
            try {
                raf.seek(pos);
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e.getMessage());
            }
        }
    
        @Override
        public String readLine() {
            try {
                if (position > limit) {
                    if (!readMore())
                        return null;
                }
                byte[] arr = new byte[336];
                int arrPos = 0;
                while (position <= limit) {
                    byte b = buffer[position++];
                    switch (b) {
                        case 10: //Unix or Linux line separator
                            return decode(arr, arrPos);
                        case 13: //Windows or Mac line separator
                            if (position > limit) {
                                if (readMore())
                                    judgeMacOrWindows();
                            } else
                                judgeMacOrWindows();
                            return decode(arr, arrPos);
                        default: // not line separator
                            if (arrPos >= arr.length)
                                arr = grow(arr);
                            arr[arrPos++] = b;
                            if (position > limit) {
                                if (!readMore())
                                    return decode(arr, arrPos);
                            }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e.getMessage());
            }
            return null;
        }
    
        private void judgeMacOrWindows() {
            byte b1 = buffer[position++];
            if (b1 != 10) // Mac line separator
                position--;
        }
    
        private boolean readMore() throws IOException {
            limit = raf.read(buffer) - 1;
            position = 0;
            return limit >= 0;
        }
    
        @Override
        public void close() {
            try {
                raf.close();
            } catch (IOException e) {
                throw new RuntimeException(e.getMessage());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140

    RandomAccessFile里的readLine()方法局限性比较大,它不支持所有的编码方式,这从源码以及源码的注释可以看出来。这里我自己实现的readLine()可以支持所有的编码方式。

    代码是写完了,那性能如何呢?俗话说,是骡子是马,拉出来遛遛。

    RandomAccessFileBufferedReaderBufferedLogReader
    第一次9580ms63ms96ms
    第二次9379ms77ms90ms
    第三次9340ms66ms77ms

    通过测试发现,BufferedLogReader性能跟BufferedReader在一个数量级,但要略差一点,主要是decode()方法中new String()设置字符集比不设置要慢一点(亲自测试过),但相较于RandomAccessFile还是有较大提升。

    RandomAccessFile + Memory Map

    关于内存映射,可以参考这篇文章:一文搞懂内存映射(Memory Map)原理

    简单来说,read系统调用是先将文件从磁盘拷贝到内核空间,然后再从内核空间拷贝到用户空间,这个过程实际上是需要两次数据拷贝;内存映射后在缺页中断处理时直接将文件从磁盘拷贝到用户空间,只需要一次数据拷贝。所以内存映射比read系统调用的效率要高。代码实现如下:

    import sun.misc.Cleaner;
    
    import java.io.*;
    import java.lang.reflect.Method;
    import java.nio.MappedByteBuffer;
    import java.nio.channels.FileChannel;
    import java.nio.charset.Charset;
    import java.security.AccessController;
    import java.security.PrivilegedAction;
    
    public class MemoryMapLogReader extends LogReader implements Closeable {
    
        private int position;
    
        private int limit = -1;
    
        private MappedByteBuffer buffer;
    
        private FileChannel channel;
    
        private RandomAccessFile raf;
    
        public MemoryMapLogReader(String pathName) {
            init(new File(pathName), DEFAULT_CHARSET);
        }
    
        public MemoryMapLogReader(String pathName, String charsetName) {
            init(new File(pathName), charsetName);
        }
    
        public MemoryMapLogReader(File file) {
            init(file, DEFAULT_CHARSET);
        }
    
        public MemoryMapLogReader(File file, String charsetName) {
            init(file, charsetName);
        }
    
        private void init(File file, String charsetName) {
            Charset.forName(charsetName); // 检查字符集是否合法
            this.charsetName = charsetName;
            try {
                raf = new RandomAccessFile(file, "r");
                channel = raf.getChannel();
                buffer = channel.map(FileChannel.MapMode.READ_ONLY, raf.getFilePointer(), channel.size());
                limit = buffer.limit();
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e.getMessage());
            }
        }
    
        @Override
        public long getFilePointer() {
            try {
                return raf.getFilePointer(); // 一直是0, 说明采用内存映射时这个方法不起作用
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e.getMessage());
            }
        }
    
        @Override
        public void seek(long pos) {
            try {
                raf.seek(pos); // 采用内存映射时这个方法不起作用
            } catch (IOException e) {
                e.printStackTrace();
                throw new RuntimeException(e.getMessage());
            }
        }
    
        @Override
        public String readLine() {
            if (position >= limit)
                return null;
            byte[] arr = new byte[336];
            int arrPos = 0;
            while (true) {
                byte b = buffer.get(position++);
                switch (b) {
                    case 10: //Unix or Linux line separator
                        return decode(arr, arrPos);
                    case 13: //Windows or Mac line separator
                        if (position < limit)
                            judgeMacOrWindows();
                        return decode(arr, arrPos);
                    default: // not line separator
                        if (arrPos >= arr.length)
                            arr = grow(arr);
                        arr[arrPos++] = b;
                        if (position >= limit)
                            return decode(arr, arrPos);
                }
            }
        }
    
        private void judgeMacOrWindows() {
            byte b1 = buffer.get(position++);
            if (b1 != 10) // Mac line separator
                position--;
        }
    
        @Override
        public void close() throws IOException {
            if (raf != null)
                raf.close();
            if (channel != null)
                channel.close();
            if (buffer != null)
                buffer.clear(); // 并不会真正清理buffer里的数据, 只是改变内部数组指针位置, 详情请看源码注释
            clean(); // 这个才会真正清理buffer里的数据
        }
    
        @SuppressWarnings({ "unchecked", "rawtypes" })
        private void clean() {
            AccessController.doPrivileged((PrivilegedAction) () -> {
                try {
                    Method getCleanerMethod = buffer.getClass().getMethod("cleaner");
                    getCleanerMethod.setAccessible(true);
                    Cleaner cleaner =(Cleaner) getCleanerMethod.invoke(buffer, new Object[0]);
                    cleaner.clean();
                } catch(Exception e) {
                    e.printStackTrace();
                }
                return null;
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129

    同样,实践是检验真理的唯一办法,测试数据如下:

    RandomAccessFileBufferedReaderBufferedLogReaderMemoryMapLogReader
    第一次9580ms63ms96ms72ms
    第二次9379ms77ms90ms59ms
    第三次9340ms66ms77ms45ms

    从测试结果来看,memory map比buffer要快一点点。尤其是文件比较大时,memory map的优势会更明显。

    不过,对于我的使用场景来说,MemoryMapLogReader不太适用,因为日志文件并不是一成不变的,而是在不断写入日志,意味着我需要不断调用channel.map(FileChannel.MapMode.READ_ONLY, raf.getFilePointer(), channel.size())进行内存映射,每次映射的数据量比较少,频繁映射可能会带来额外的开销,也会增加代码的复杂度。

    另一个问题,在使用内存映射时,RandomAccessFile的getFilePointer()会失效,不管读了多少字节都返回0。虽然可以自己统计已读字节来实现这个功能,但无疑又增加了代码复杂度。

    综上所诉,基于我的使用场景,我选择BufferedLogReader 。

  • 相关阅读:
    滤波算法_标准卡尔曼滤波(SKF, Standard Kalman filter)_①基础铺垫
    推谷子转化动物饲料建议 国稻种芯:解读制定饲草产业规划
    一般需要输出的文档流程
    苹果被迫弃用 Lightning?欧盟宣布 2024 年 Type-C 将 “一统天下”
    随机森林可视化
    【强化学习论文合集 | 2018年合集】一. ICML-2018 强化学习论文
    工业网关它的功能是什么
    ⟅UNIX网络编程⟆⦔select函数的定义及参数
    生物活性分子库
    深度学习——循环神经网络
  • 原文地址:https://blog.csdn.net/qq_35939417/article/details/127084779