记一次开发组内课题 【 24G ng log 统计域名、uri访问次数top10,最终程序使用内存64M、花费时间16s】

组内发布一个课题:给一个4核、4G内存的环境,分析统计ng log 中访问域名和uri数量的top10在没有OOM并结果正确的前提下,比拼处理速度。
在大神帮助下、我的程序轻松夺冠。
程序很简单,在写了多版程序后分析程序效率瓶颈、如何更省内存、如何避免发生GC等问题,过程经历难能可贵,写一篇帖子记下来。

ng log 格式:
[02/Sep/2019:00:00:03 +0800] - 47.91.455.21 service.domain-api.com “GET /http/task/uri.json HTTP/1.1” 200 112 65 0.011 0.011 110.10.61.227:3122 200 curl/7.229.0 - service.domain-api.com 443 abb-was-web root=

有用的部分拆解如下:
【[时间]】【\t】【-】【\t】【IP】【\t】【域名】【空格空格空格空格(4个)】【"【请求方式(GET or POST)】【空格】【uri】【空格】【HTTP版本】"】【空格空格空格空格(4个)】【请求状态】…【\n】

分析需求,程序需要实现以下几个步骤:

  • 读取文件(需考虑读到半行合并、瞬时读进程序的大小,保证不会OOM)
  • 分行(如果一次读到一块,需要考虑将行拆开)
  • 解析出目标字符串、统计次数
  • 文件读完、用次数排序、打印

经过测试,单线程用BufferedInputStream读取文件速度最快。
第一版本程序模型:
主线程单线程读取文件,由于用byte[]读取,长度固定,每次读取到结尾都是半行,把结果丢到一个阻塞队列里,再启动一个线程不停的从队列里取到结果进行半行合并处理,再分成一行一行的String丢给一个固定线程数量的线程池,线程池的每个线程用split()方法对单行拆解,将结果字符串做key放到一个ConcurrentHashMap中,用AtomicInteger做value计数,避免并发问题(此处用computeIfAbsent保证原子性)。阻塞队列的长度可以控制程序瞬时使用的内存大小(长度*byte[]大小)。
经过测试,结果数量是对的,可执行速度并不快,打印GC日志,观察到频繁GC会导致程序效率变低。

第二版本程序模型:
用nio的MappedByteBuffer分片(RandomAccessFile、MappedByteBuffer)多线程读取。主线程先计算分片位置,分片时用RandomAccessFile 的 seek、readByte方法找到整行位置,保证每片是完整的。线程池每个线程一次性读取整片(经测试每片128M、8线程比较快),每个线程自己进行分行、拆解域名等操作,也用ConcurrentHashMap 和 AtomicInteger 做统计。程序会快一些,但MappedByteBuffer 使用堆外内存,程序瞬时内存大约是线程数*每片大小,线程数量增加会增加线程切换协调开销,不是很完美。GC依旧很频繁。

思考:

  1. 哪个步骤是瓶颈:测试过1.2G文件读取最快速度大约300ms,但是整个程序处理完需要1.6s到2s,所以,文件读取不是瓶颈,处理解析才是。
  2. 程序为什会频繁GC:加大线程数量(分片读取)或者阻塞队列长度(单线程读取)甚至会发生FullGC,一次FullGC会多花1.5s,得不偿失。

结论:

  1. 在解析过程中byte[] buffer没有复用,每次new 出来一个String非常耗时也会花大量内存,做split操作会生成没用的String对象,也超级耗时。那么,去掉String成了首要优化点。
  2. 硬盘读取速度是有上限的,读取时单线程或者多线程没有多大差异,只要将这个指标打满即可。拆解单行操作则是cpu的工作,再想办法让cpu打满,读取、分析两件事势必有一个会慢一点,找到这个均衡点就会达到程序的最大速度。

第三版本程序模型:单线程读取,buffer数组 做成一个池,每次读取用一个,把结尾半行复制到下一个buffer,把buffer直接丢到线程池拆解分析,单个byte判断,中间不产生String。放到map时需要先判断一下key是否存在,那么每次都需要生成一个key,这个key也是垃圾对象。把buffer包装一下,用一个起始指针和一个结束指针表示有效字符,重写equals方法,直接把buffer对象丢进map比较,节省内存。经过优化后,1.2G
文件仅需800~900ms,12G文件8s左右24G文件16s左右,而且,程序内存仅仅用64M不到,不会发生任何GC!!!

最终代码放到Git上,以便参考:
https://github.com/tanguosheng/demo_final_save_memory

中途各种开发测试版本代码:
https://github.com/tanguosheng/demo_final

ps:最终程序有些点为了追求执行速度写的并不标准,比如重写的equals方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章