背景

公司的日志采集器是我自己开发的,没用开源产品。日志采集虽然是个小功能,但是要想写好也没那么容易。对于一个日志采集器来说,它应该稳定、可控、占用尽量少的资源。因为日志采集器是和核心业务服务部署在同一台服务器上,如果它工作时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.* <p>* 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();
}

我们都知道,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();}
}
RandomAccessFile BufferedReader
第一次 9580ms 63ms
第二次 9379ms 77ms
第三次 9340ms 66ms

不测不知道,一测吓一跳,使用缓冲区性能提升了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());}}
}

接着新建一个类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;}@Overridepublic long getFilePointer() {try {return raf.getFilePointer();} catch (IOException e) {e.printStackTrace();throw new RuntimeException(e.getMessage());}}@Overridepublic void seek(long pos) {try {raf.seek(pos);} catch (IOException e) {e.printStackTrace();throw new RuntimeException(e.getMessage());}}@Overridepublic 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 separatorreturn decode(arr, arrPos);case 13: //Windows or Mac line separatorif (position > limit) {if (readMore())judgeMacOrWindows();} elsejudgeMacOrWindows();return decode(arr, arrPos);default: // not line separatorif (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 separatorposition--;}private boolean readMore() throws IOException {limit = raf.read(buffer) - 1;position = 0;return limit >= 0;}@Overridepublic void close() {try {raf.close();} catch (IOException e) {throw new RuntimeException(e.getMessage());}}
}

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

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

RandomAccessFile BufferedReader BufferedLogReader
第一次 9580ms 63ms 96ms
第二次 9379ms 77ms 90ms
第三次 9340ms 66ms 77ms

通过测试发现,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());}}@Overridepublic long getFilePointer() {try {return raf.getFilePointer(); // 一直是0, 说明采用内存映射时这个方法不起作用} catch (IOException e) {e.printStackTrace();throw new RuntimeException(e.getMessage());}}@Overridepublic void seek(long pos) {try {raf.seek(pos); // 采用内存映射时这个方法不起作用} catch (IOException e) {e.printStackTrace();throw new RuntimeException(e.getMessage());}}@Overridepublic 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 separatorreturn decode(arr, arrPos);case 13: //Windows or Mac line separatorif (position < limit)judgeMacOrWindows();return decode(arr, arrPos);default: // not line separatorif (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 separatorposition--;}@Overridepublic 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;});}
}

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

RandomAccessFile BufferedReader BufferedLogReader MemoryMapLogReader
第一次 9580ms 63ms 96ms 72ms
第二次 9379ms 77ms 90ms 59ms
第三次 9340ms 66ms 77ms 45ms

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

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

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

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

RandomAccessFile读性能优化相关推荐

  1. HBase最佳实践-读性能优化策略

    任何系统都会有各种各样的问题,有些是系统本身设计问题,有些却是使用姿势问题.HBase也一样,在真实生产线上大家或多或少都会遇到很多问题,有些是HBase还需要完善的,有些是我们确实对它了解太少.总结 ...

  2. Elasticsearch-32.生产环境常用配置与上线清单 he 集群写性能优化 he 集群读性能优化

    Elasticsearch 生产环境常用配置和上线清单 Development vs.Production Mode 从ES 5开始,支持Development 和Production 两种运行模式 ...

  3. HBase最佳实践-HBase中的读性能优化策略

    任何系统都会有各种各样的问题,有些是系统本身设计问题,有些却是使用姿势问题.HBase也一样,在真实生产线上大家或多或少都会遇到很多问题,有些是HBase还需要完善的,有些是我们确实对它了解太少.总结 ...

  4. HBase读性能优化策略

    使用HBase可能会遇到各种问题,有些是系统本身的设计的问题,有些是使用的问题,常见的问题:FULL GC异常导致宕机,RIT问题,写吞吐量太低以及读延迟较大. 这篇文章就以读延迟优化为核心内容展开, ...

  5. HBase性能优化方法总结(4):读表操作

    来自:http://www.cnblogs.com/panfeng412/archive/2012/03/08/hbase-performance-tuning-section3.html 本文主要是 ...

  6. linux利用内存加快读盘速度,Linux性能优化从入门到实战:10 内存篇:如何利用Buffer和Cache优化程序的运行效率?...

    缓存命中率 缓存命中率,是指直接通过缓存获取数据的请求次数,占所有数据请求次数的百分比,可以衡量缓存使用的好坏.命中率越高,表示使用缓存带来的收益越高,应用程序的性能也就越好. 实际上,缓存是现在所有 ...

  7. 读数据库论文-- 多核处理器下事务型数据库性能优化技术综述》

    论文:多核处理器下事务型数据库性能优化技术综述 https://wenku.baidu.com/view/102b5939f61fb7360a4c65bd.html

  8. oracle 朱志辉_《DB2设计、管理与性能优化艺术》(王飞鹏,李玉明,朱志辉,王富国)【摘要 书评 试读】- 京东图书...

    本书完美诠释了DB2性能优化艺术,作者团队全部是IBM中国软件开发中心的资深专家,具有丰富的从Oracle向DB2迁移实施经验,他们的书一定能带领广大的读者实现华丽的从容转身. --IBM中国开发中心 ...

  9. Java IO 性能优化大PK,什么场景用啥,都给你总结好啦!

    作者:莫那·鲁道 ,来自:http://thinkinjava.cn 前言 Java 在 JDK 1.4 引入了 ByteBuffer 等 NIO 相关的类,使得 Java 程序员可以抛弃基于 Str ...

  10. 推荐:Java性能优化系列集锦

    Java性能问题一直困扰着广大程序员,由于平台复杂性,要定位问题,找出其根源确实很难.随着10多年Java平台的改进以及新出现的多核多处理器,Java软件的性能和扩展性已经今非昔比了.现代JVM持续演 ...

最新文章

  1. 0x52. 动态规划 - 背包(习题详解 × 19)
  2. Linux系统配置交换分区
  3. 在虚拟机环境下,电脑间拷贝配置好的伪分布式Hadoop环境,出现namenode不能启动的问题!...
  4. Windows Embedded CE 6.0开发初体验(一)Windows CE概述
  5. Java集合之Vector源码分析
  6. php微信绑定银行卡_PHP实现微信提现功能
  7. xamarin.android 图片高斯模糊效果
  8. git clone 遇到的坑
  9. Cause: the class org.apache.tools.ant.taskdefs.optional.ANTLR was not found.
  10. 常见方案 目录 1. 发现目前 WEB 上主流的视频直播方案有 HLS 和 RTMP, 1 2. 实现直播的方法有很多,但是常用的,就这几个。 3个直播协议:rtmp、rtsp、hls。 和三个端:
  11. IDEA 删除SVN文件
  12. OpenCL学习入门
  13. mac下的c语言程序开发,mac VS Code配置C语言开发环境(小白简单Clang版)
  14. C语言练习①一英寸是多少厘米?
  15. 中国特殊灯具行业市场供需与战略研究报告
  16. 腾讯地图 多个异步script互相依赖加载问题
  17. 考虑交通网络流量的电动汽车充电站规划matlab 采用matlab软件参照相关资料完成电动汽车程序
  18. 人民广场,上海博物馆
  19. 在Ubuntu 16.04中安装Google拼音
  20. 计算机主板用料,揭开用料谜团 教你怎样看主板的质量

热门文章

  1. vue项目启动报错 in ./src/views/pms/components/file-catalogue/file-catalogue.vue?vuetype=sty解决办法
  2. 古月居ROS学习笔记:Publisher
  3. 不同获取方式下TensorFlow(Keras)-CPU性能差异
  4. Java数学竞赛的名次情况_2018年北京数学奥数竞赛成绩分析,名单背后的秘密
  5. 正则表达式获取两个标识中间的内容
  6. tornado: web.py 之 Application
  7. 查询物流单号将信息导出表格的方法
  8. 邮件头字段介绍(一)
  9. xpath爬取站长素材中的免费简历
  10. 软件产品测试选择第三方软件检测机构都有哪些好处