如何避免后台IO高负载造成的长时间JVM GC停顿(转)
译者著:其实本文的中心意思非常简单,没有耐心的读者建议直接拉到最后看结论部分,有兴趣的读者可以详细阅读一下。
原文发表于Linkedin Engineering,作者 Zhenyun Zhuang是Linkedin的一名Staff Software Engineer,联合作者Cuong Tran是Linkedin的一名Sr. Staff Engineer。
在我们的生产环境中,我们不断发现一些运行在JVM上的应用程序,偶尔会因为记录JVM的GC日志,而被后台的IO操作(例如OS的页缓存回写)阻塞,出现长时间的STW(Stop-The-World)停顿。在这些STW停顿的过程中,JVM会暂停所有的应用程序线程,此时应用程序会停止对用户请求的响应,这对于要求低延迟的系统来说,因此所导致的高延迟是不可接受的。
我们的调查表明,导致这些停顿的原因,是当JVM GC(垃圾回收)在写GC日时,由于write()系统调用所造成的。对于这些日志的写入操作,即使是采用异步写模式(例如,带缓存的IO或者非阻塞IO),仍然会被OS的页缓存回写等机制阻塞相当长的一段时间。
我们将讨论解决这个问题的各种方式。对于要求低延迟的Java应用程序来说,我们建议将Java日志文件移动到一个单独的、或者高性能的磁盘驱动上(例如SSD,tmpfs)。
生产环境中的问题
当JVM管理的Java堆空间进行垃圾回收后,JVM可能会停顿,并对应用程序造成STW停顿。根据在启动Java实例时指定的JVM选项,GC日志文件会记录不同类型的GC和JVM行为。
虽然某些因为GC导致的STW停顿(扫描/标记/压缩堆对象)已经被大家熟知,但是我们发现后台IO负载也会造成长时间的STW停顿。在我们的生产环境中曾经出现过,一些关键的Java应用程序发生许多无法解释的长时间STW停顿(> 5秒) 。这些停顿既不能从应用程序层的逻辑、也无法从JVM GC行为的角度加以解释。如下所示,我们展示了一个超过4秒的长时间STW停顿,以及一些GC信息。当时我们选择的垃圾回收器是G1。在一个只有8GB堆内存和使用并行Young Garbage Collection的G1环境下,垃圾回收通常不需要1秒即可完成,并且GC日志的影响也微乎其微。但是应用程序线程却停顿了超过4秒。所有GC完成的工作总量(例如,回收的堆大小)也无法解释这个长达4.17秒的停顿。
2015-12-20T16:09:04.088-0800: 95.743: [GC pause (G1 Evacuation Pause) (young) (initial-mark) 8258M->6294M(10G), 0.1343256 secs] 2015-12-20T16:09:08.257-0800: 99.912: Total time for which application threads were stopped: 4.1692476 seconds使用G1收集器时一次4.17秒的GC STW停顿
另一个例子,下面的GC日志显示了另一次11.45秒的STW停顿。这次使用的垃圾回收器是CMS(Concurrent Mark Sweep (译者注:原文中笔误写成了Concurrent Mode Sweep,已联系原作者修改))。其中“user”/“sys”的时间几乎可以忽略,但是“real”表示的GC时间却超过了11秒。通过最后一行,我们可以确定应用程序发生了11.45秒的停顿。
2016-01-14T22:08:28.028+0000: 312052.604: [GC (Allocation Failure) 312064.042: [ParNew Desired survivor size 1998848 bytes, new threshold 15 (max 15) - age 1: 1678056 bytes, 1678056 total : 508096K->3782K(508096K), 0.0142796 secs] 1336653K->835675K(4190400K), 11.4521443 secs] [Times: user=0.18 sys=0.01, real=11.45 secs] 2016-01-14T22:08:39.481+0000: 312064.058: Total time for which application threads were stopped: 11.4566012 seconds使用CMS收集器时一次11.45秒的GC STW停顿
由于应用程序要求非常低的延迟,所以我们花费了相当多的精力来调查这个问题。最后,我们成功重现了这个问题,发现了根本原因,以及相应的解决方案。
在实验环境中重现问题
对于这个导致无法解释的长时间JVM停顿的问题,我们开始尝试在实验环境中重现它。为了使该过程能够得到更好的控制并重复重现,我们设计了一个简单的压测程序,来代替复杂的生产环境应用程序。
我们将在两个场景下运行这个压测程序:含有后台IO行为以及不含有后台IO行为。不含有后台IO的场景我们称之为“基准线(baseline)”,而含有后台IO的场景用来重现问题。
Java压测程序
我们这个Java压测程序只是不断地生成10KB的对象,并放到一个队列中。当对象数量达到100000时,会从队列中删除一半的对象。因此堆中存放的对象最大数量就是100000个,大概会占用1GB的空间。这个过程会持续一段固定的时间(例如5分钟)。
这个程序的源代码和后台IO的生成脚本,都位于https://github.com/zhenyun/JavaGCworkload。我们考虑的主要性能指标是长时间JVM GC停顿的数量。
后台IO
后台IO我们通过一个bash脚本,不断地复制大文件来模拟。后台程序会生成150MB/s的写入负载,可以使一个普通磁盘的IO变得足够繁忙。为了更好理解生成的IO负载的压力大小,我们使用“sar -d -p 2”来收集await(磁盘处理IO请求的平均时间(以毫秒计)),tps(每秒发往物理设备的传输总数)和wr_sec-per-s(写入设备的扇区数)。它们分别的平均数值为:await=421 ms, tps=305, wr_sec-per-s=302K。
系统准备
情景1 (不含后台IO负载)
运行基准线测试不需要有后台IO。所有JVM GC 停顿的时间序列数据如下图所示。没有观察到超过250ms的停顿。
情景1(不含后台IO负载)中所有的JVM GC 停顿
情景2 (含有后台IO负载)
当后台IO开始运行后,在只有5分钟的运行时间内,压测程序就出现了一次超过3.6秒的STW停顿,以及3次超过0.5秒的停顿!
情景2(含有后台IO负载)中所有的JVM GC 停顿
调查
为了了解是哪个系统调用引起了STW停顿,我们使用了strace来分析JVM实例产生的系统调用。
我们首先确认了JVM将GC信息记录到文件,使用的是异步IO的方式。我们又跟踪了JVM从启动后产生的所有系统调用。GC日志文件在异步模式下打开,并且没有观察到fsync()调用。
16:25:35.411993 open("gc.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3 <0.000073>所捕获的用于打开GC日志文件的JVM系统调用open()
但是,跟踪结果显示,JVM发起的几个异步系统调用write()出现了不同寻常的长时间执行情况。通过检查系统调用和JVM停顿的时间戳,我们发现它们恰好吻合。在下图中,我们分别对比展示了两分钟内系统调用和JVM停顿的时间序列。
时间序列对比(JVM STW停顿)
时间序列对比(系统调用write())
我们集中注意来看,位于13:32:35秒时最长达1.59秒的这次停顿,相应的GC日志和strace输出显示如下:
GC日志和strace输出
我们来试着理解一下发生了什么。
- 在35.04时(第2行),一次young GC开始了,并且经过0.12秒完成。
- 这次young GC完成于时间35.17,并且JVM试图通过一次系统调用 write()(第4行),将young GC的统计信息输出到gc日志文件中。
- write()调用被阻塞了1.47秒,最后于时间36.64(第5行)完成,花费了1.47秒的时间。
- 当write()调用于时间36.64返回JVM时,JVM记录下这次用时1.59秒的STW停顿(例如,0.12+0.47)(第3行)。
换句话说,实际的STW停顿时间包含两部分:(1) GC时间(例如,young GC)和 (2)GC记录日志的时间(例如, 调用write()的时间)。
这些数据说明,GC记录日志的过程发生在JVM的STW停顿过程中,并且记录日志所用的时间也属于STW停顿时间的一部分。特别需要说明,整个应用程序的停顿主要由两部分组成:由于JVM GC行为造成的停顿,以及为了记录JVM GC日志,系统调用write()被OS阻塞的时间。下面这张图展示了二者之间的关系。
在记录GC日志过程中JVM和OS之间的交互
如果记录GC日志的过程(例如write()调用)被OS阻塞,阻塞时间也会被计算到STW的停顿时间内。新的问题是,为什么带有缓存的写入会被阻塞?在深入了解各种资料,包括操作系统内核的源代码之后,我们意识到带有缓存的写入可能被内核代码所阻塞。这里面有多重原因,包括:(1)“stable page write”和(2)“journal committing”。
Stable page write: JVM对GC日志文件的写入,首先会使得相应的文件缓存页“变脏”。即使缓存页稍后会通过OS的回写机制被持久化到磁盘文件,但是在内存中使缓存页变脏的过程,由于“stable page write”仍然会受到页竞争的影响。在“stable page write”下,如果某页正处于OS回写过程中,那么对该页的write()调用就不得不等待回写完成。为了避免只有一部分新页被持久化到磁盘上,内核会锁定该页以确保数据一致性。
Journal committing: 对于带有日志(journaling)的文件系统,在写文件时都会生成相应的journal日志。当JVM向GC日志文件追加内容时,会产生新的块,因此文件系统则需要先将journal日志数据提交到磁盘。在提交journal日志的过程中,如果OS还有其他的IO行为,则提交可能需要等待。如果后台的IO行为非常繁重,那么等待时间可能会非常长。注意,EXT4文件系统有一个“delayed allocation”功能,可以将journal数据提交延迟到OS回写后再进行,从而降低等待时间。还要注意的是,将EXT4的数据模式从默认的“ordered”改成“writeback”并不能解决这个问题,因为journal数据需要在write-to-extend调用返回之前被持久化。
后台IO行为
从JVM垃圾回收的角度来看,通常的生产环境都无法避免后台的IO行为。这些IO行为有几个来源:(1)OS活动;(2)管理和监控软件;(3)其他共存的应用程序;(4)同一个JVM实例的IO行为。首先,OS包含许多机制(例如,”/proc“文件系统)会引起向底层磁盘写入数据。其次,像CFEngine这样的系统级软件也会进行磁盘IO操作。第三,如果当前节点上还存在其他共享磁盘的应用程序,那么这些应用程序都会争抢IO。第四,除了GC日志之外,JVM实例也可能以其他方式使用磁盘IO。
解决方案
由于当前HotSpot JVM实现(包括其他实现)中,GC日志会被后台的IO行为所阻塞,所以有一些解决方案可以避免写GC日志文件的问题。
首先,JVM实现完全可以解决掉这个问题。显然,如果将写GC日志的操作与可能会导致STW停顿的JVM GC处理过程分开,这个问题自然就不存在了。例如,JVM可以将记录GC日志的功能放到另一个线程中,独立来处理日志文件的写入,这样就不会增加STW停顿的时间了。但是,这种采用其他线程来处理的方式,可能会导致在JVM崩溃时丢失最后的GC日志信息。最好的方式,可能是提供一个JVM选项,让用户来选择适合的方式。
由于后台IO造成的STW停顿时间,与IO的繁重程度有关,所以我们可以采用多种方式来降低后台IO的压力。例如,不要在同一节点上安装其他IO密集型的应用程序,减少其他类型的日志行为,提高日志回滚频率等等。
对于低延迟应用程序(例如需要提供用户在线互动的程序),长时间的STW停顿(例如>0.25秒)是不可忍受的。因此,必须进行有针对性的优化。如果要避免因为OS导致的长时间STW停顿,首要措施就是要避免因为OS的IO行为导致写GC日志被阻塞。
一个解决办法是将GC日志文件放到tmpfs上(例如,-Xloggc:/tmpfs/gc.log)。因为tmpfs没有磁盘文件备份,所以tmpfs文件不会导致磁盘行为,因此也不会被磁盘IO阻塞。但是,这种方法存在两个问题:(1)当系统崩溃后,GC日志文件将会丢失;(2)它需要消耗物理内存。补救的方法是周期性的将日志文件备份到持久化存储上,以减少丢失量。
另一个办法是将GC日志文件放到SSD(固态硬盘,Solid-State Drives)上,它通常能提供更好的IO性能。根据IO负载情况,可以选择专门为GC日志提供一个SSD作为存储,或者与其他IO程序共用SSD。不过,这样就需要将SSD的成本考虑在内。
与使用SSD这样高成本的方案相比,更经济的方式是将GC日志文件放在单独一个HDD磁盘上。由于这块磁盘上只有记录GC日志的IO行为,所以这块专有的HDD磁盘应该可以满足低停顿的JVM性能要求。实际上,我们之前演示的场景一就可以看做为这一方案,因为在记录GC日志的磁盘上没有任何其他的IO行为。
将GC日志放到SSD和tmpfs的评估
我们采用了专有文件系统的解决方案,将GC日志文件分别放到SSD和tmpfs上。然后我们按照场景二中的后台IO负载,运行了相同的Java压测程序。
对于SSD和tmpfs二者而言,我们观察到了相似的结果,并且下图展示了将GC日志放到SSD磁盘上的结果。我们注意到,JVM停顿的性能几乎可以与场景一相媲美,并且所有停顿都小于0.25秒。二者的结果均表明后台的IO负载没有影响到应用程序的性能。
将GC日志迁到SSD后的所有的JVM STW停顿
结论
有低延迟要求的Java应用程序需要极短的JVM GC停顿。但是,当磁盘IO压力很大时,JVM可能被阻塞一段较长的时间。
我们对该问题进行了调查,并且发现如下原因:
- JVM GC需要通过发起系统调用write(),来记录GC行为。
- write()调用可以被后台磁盘IO所阻塞。
- 记录GC日志属于JVM停顿的一部分,因此write()调用的时间也会被计算在JVM STW的停顿时间内。
我们提出了一系列解决该问题的方案。重要的是,我们的发现可以帮助JVM实现来改进该问题。对于低延迟应用程序来说,最简单有效的措施是将GC日志文件放到单独的HDD或者高性能磁盘(例如SSD)上,来避免IO竞争。
原文地址:http://www.itnose.net/detail/6455877.html
如何避免后台IO高负载造成的长时间JVM GC停顿(转)的更多相关文章
- Linux上查看造成IO高负载的进程
方法1:使用iotop工具这是一个python脚本工具,使用方法如:iotop -o方法2:使用工具dmesg使用dmesg之前,需要先开启内核的IO监控:echo 1 >/proc/sys/v ...
- linux服务器硬盘IO读写负载高来源定位 pt-ioprofile
首先 .用top命令查看 1 2 3 4 5 top - 16:15:05 up 6 days, 6:25, 2 users, load average: 1.45, 1.77, 2.14 ...
- java处理高并发高负载类网站的优化方法
java处理高并发高负载类网站中数据库的设计方法(java教程,java处理大量数据,java高负载数据) 一:高并发高负载类网站关注点之数据库 没错,首先是数据库,这是大多数应用所面临的首个SPOF ...
- 由 12306.cn 谈谈高并发+高负载网站性能技术
12306.cn 网站挂了,被全国人民骂了.我这两天也在思考这个事,我想以这个事来粗略地和大家讨论一下网站性能的问题.因为仓促,而且完全基于本人有限的经验和了解, 所以,如果有什么问题还请大家一起讨论 ...
- [转]java处理高并发高负载类网站的优化方法
本文转自:http://www.cnblogs.com/pengyongjun/p/3406210.html java处理高并发高负载类网站中数据库的设计方法(java教程,java处理大量数据,ja ...
- PHP高并发高负载系统架构
PHP高并发高负载系统架构 1.为什么要进行高并发和高负载的研究 1.1.产品发展的需要 1.2.公司发展的需要 1.3.当前形式决定的 2.高并发和高负载的约束条件 2.1.硬件 2.2.部署 2. ...
- 烂泥:高负载均衡学习haproxy之安装与配置
本文由秀依林枫提供友情赞助,首发于烂泥行天下 有关高负载均衡的软件,目前使用比较多的是haproxy.nginx和lvs.下面我们就开始学习haprxoy这款软件. 一.haproxy介绍 以下开始介 ...
- Mysql 高负载排查思路
Mysql 高负载排查思路 发现问题 top命令 查看服务器负载,发现 mysql竟然百分之两百的cpu,引起Mysql 负载这么高的原因,估计是索引问题和某些变态SQL语句. 排查思路 1. 确定高 ...
- 高并发高负载系统架构-php篇
首先呢,我罗列一下文章的目录,让大家有个整体轮廓的了解! 1.为什么要进行高并发和高负载的研究 2.高并发和高负载的约束条件 3.解决之道——硬件篇 4.解决之道——部署篇 5.解决之道——环境篇 6 ...
随机推荐
- 关于call和apply的那点事儿
在JavaScript中改变闭包中的this关键字中经常用到的就是call和apply了 首先:call和apply的作用的区别是什么? 答:call和apply 的作用是相同的.都是用来改变函数th ...
- 选择最适合你的Linux学习方法
我们知道Linux只是一个内核,现在的Linux操作系统底层都是用这个内核,包括Android手机,所以Linux操作系统其实是将Linux内核与应用软件做一个打包,我们称之为Linux发行版.现在比 ...
- 枚举IoTimer
/*************************************************************************************** * AUTHOR : ...
- 自己定制ListView,上拉刷新和下拉刷新,加载网络图片,并且添加缓存机制。
package com.lixu.listviewrefresh; import java.util.ArrayList; import java.util.HashMap; import java. ...
- bzoj 1934: [Shoi2007]Vote 善意的投票
#include<cstdio> #include<iostream> #define M 100000 #include<cstring> using names ...
- 使用 JavaScript 修改浏览器 URL 地址栏
现在的浏览器里,有一个十分有趣的功能,你可以在不刷新页面的情况下修改浏览器URL;在浏览过程中.你可以将浏览历史储存起来,当你在浏览器点击后退按钮的时候,你可以冲浏览历史上获得回退的信息,这听起来并不 ...
- svn自动更新
果对svn不熟悉,当svn上面有更新时,想看到实时效果,就得去web目录手动更新,比较麻烦 其它svn有一个自动更新的功能 利用 hook 在svn 仓库目录下面有一个hook目录 在post-c ...
- Rhel6-lanmp架构配置文档
l--操作系统:windows linux unix mac OS a--网页发布软件:apache nginx iis m--数据库:mysql pgsql oracle... p--网页 ...
- 推荐一款好用轻便的在线UML画图工具
刚接触UML时间不长,看了N多教学视频,下载好了几个软件各种不习惯 当我遇见了ProcessOn 从此我彻底“爱上”了它! http://www.processon.com/ UML各类例图它几乎全 ...
- 端口占用问题——netstat命令
1.查看所有的端口占用情况 C:\>netstat -ano 协议 本地地址 外部地址 状态 PID(进程号) TCP 127.0.0.1:1434 ...