[译]Linkedin对JVM垃圾回收暂停的研究和解决方案

原文:Eliminating Large JVM GC Pauses Caused by Background IO Traffic

译文由杰微刊兼职译者张帆翻译。

在生产环境中,我们屡次看到在JVM(Java虚拟机)中运行的应用程序偶尔会遭遇比较严重的暂停,我们称之为STW(Stop-The-World),这个现象是因为JVM的GC日志记录进程被后台IO阻塞(比如系统页缓存的回写)锁定所引起的。在STW暂停期间,JVM暂停了所有的应用线程,应用程序停止响应用户的请求,因此对于一些等待敏感的用户操作,这种暂停会造成不可接受的延迟。

我们的调查显示,暂停是由于JVM GC(垃圾回收)在GC日志记录过程中的write()系统调用诱导引起的。这类写日志的操作,即便是采用了异步写模式(比如缓冲IO或者非阻塞IO),仍然会被操作系统的机制包括页缓存写会锁定相当长的时间。

我们讨论了多种方案来缓解该问题。对于延迟敏感的Java应用,我们建议将Java日志文件已到一个独立的或高性能的磁盘上(如SSD,tmpfs等)。

生产问题

当JVM管理的堆空间进行了垃圾回收,JVM可能会停止,从而导致了应用的STW暂停。鉴于启动Java实例时配置的选项不同,不同类型的GC和JVM活动会被记录到GC日志文件中。

尽管一些GC导致的STW暂停比如扫描、标志、整理堆对象是我们众所周知的,但是我们发现依然有很多大型的STW暂停是有后台IO阻塞引起。在我们的生产环境中,我们看到了在关键任务的Java应用中很多无法解释的大型STW暂停(>5s)。这样的暂停不能被应用层面的逻辑和JVM GC活动所解释。如下面所示,展现了一个大型的超过4s的STW暂停和一些GC信息。垃圾收集器是G1。G1具有一个8GB的堆空间和并行的新生代垃圾收集,一般来讲整个垃圾回收过程仅需小于1s即可完成,简单的GC选项使开销较小。然而,应用线程竟然暂停了超过4s。GC(如回收堆空间)的工作量不足以解释4.17s这样巨大的暂停时间。

``` 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,17s的GC STW暂停

另一个例子,下面的GC日志快照展示了一个11.45s的STW暂停。垃圾收集器是CMS(Concurrent Mode Sweep)。"用户"/"系统"时间可以忽略不计,然而"real"GC时间大于11s。最后一行明确了11.45s的应用暂停时间。

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.45sGC STW暂停

由于应用是十分延迟敏感的,我们花费了相当大的努力来研究这个问题。最终,我们重现了问题,找到了关键诱因,然后提出了几项解决方案来解决它。

在实验室环境中重现问题

首先,我们先从在实验室环境中重现这个无法解释的大型JVM暂停开始。出于可控性和重复性的考虑,我们设计了一个简单的工作负载来移除生产应用程序的复杂性。

我们在两个场景中运行这个工作负载:具备和不具备后台IO活动。不具备后台IO的场景用作“基线”,另一个引入后台IO的场景用于重现问题。

Java工作负载

我们所使用的Java工作负载用向队列中持续分配10KB的对象。当对象数量达到100,000时,一半的对象会被移除队列。所以堆中对象的最大值时100,000个对象,约占用原始大小10GB。进程会持续固定的一段时间(如5min)。

Java源码和生成后台IO的脚本,都开源在这里

https://github.com/zhenyun/JavaGCworkload。我们考虑的主要性能指标是大JVM GC暂停的次数。

后台IO

后台IO是由脚本引入,该脚本负责持续的复制大文件。后台工作负载大约产生150MB/s的写负载,足以使单个硬盘跑满。为了看到产生的IO负载有多严重,我们使用"sar -d -p 2"来收集一下统计数据:await(设备发出的IO请求送达的平均时间(单位,毫秒)),tps(每秒传输到物理设备上的数量总和)和wrsec-per-s(写入到设备的扇区数)。平均数值是:await=421ms,tps=305,wrsec-per-s=302K。

系统配置

场景1(无后台IO负载)

作为基线,本次运行无后台IO负载。所有JVM GC暂停的时间序列数据所示如下图所示。没有发现超过250ms的单一暂停。


场景一中的所有JVM GC暂停(无后台IO负载)

场景2(有后台IO负载)

当后台IO运行时,同样的Java工作负载在5分钟的运行期间,能够看到3.6s的STW暂停,和三个超过0.5s的暂停。


场景二中的所有JVM GC暂停(有后台IO负载)

调查

为了弄明白是什么系统调用引起的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> 捕获的JVM打开GC日志文件产生的open()系统调用

然而,快照显示由JVM调用的异步write()系统调用有不同寻常的较长的执行时间。检查系统调用和JVM暂停的时间戳,我们发现他们具备强烈的关联关系。在下面的两张图表,我们展示了2分钟内的延迟情况。

时间序列相关(JVM STW暂停)。

时间序列相关(write()系统调用延迟)

接着,我们放大焦点,关注发生在13:32:35时最大1.59s暂停。下面展示了相关的GC日志和strace输出:


GC日志和strace输出

让我们来尝试理解究竟发生了什么:

1、35.04时(第2行),新生代GC启动,花费0.12s完成

2、新生代GC在35.17完成,JVM进行系统调用write(),尝试将新生代GC统计输出到GC日志文件(第4行)

3、write()调用被锁定1.47s,最终在36.64(第5行)完成,花费1.47s。

4、当write()调用在36.64返回JVM,JVM纪录了1.59s的STW暂停(0.12+1.47)(第3行)

换句话说,实际的STW暂停由两部分组成:(1)GC时间(本例中新生代GC所花费时间);(2)GC日记录日志所花费的时间(本例中的write()时间)。

这些数据表明,GC日志记录过程算在了JVM STW暂停中,日志记录的时间被当作STW暂停的一部分。特别的,整个应用的暂停主要包含两部分:因为JVM GC活动引起的暂停和因为在JVM GC日志记录时由操作系统阻塞write()系统调用所产生的暂停。下面的图表展示了两者的关系。


日志记录期间JVM和操作系统的相互作用

如果GC日志记录被操作系统阻塞,阻塞的时间也被计算为STW暂停的一部分。然而新问题是,为何缓冲写会被阻塞?翻阅大量的资源包括内核源码,我们意识到缓冲写可能卡在内核代码。包括多个原因,如:(1)稳定页写操作;(2)日志提交。

稳定页写操作:JVM向GC日志文件中的写操作首先修改了相关的文件缓存页内容。即使缓存页稍后会通过操作系统的写回机制持久化到磁盘文件中,对内存中页的修改仍然会造成由“稳定页写操作”引起的页面争用。在“稳定页写操作”时,如果页面正在被操作系统写回,对这个页的write()操作必须等待写回完成。页面锁定来确保数据的一致性,避免部分新的一页保留到磁盘。

日志提交:对于日志文件系统,适当的日志在文件写入期间生成。写入新内容到GC日志文件导致新的块被分配,文件系统需要先将日志数据提交到文件。在日志提交期间,如果操作系统具有其他IO活动,提交操作可能需要稍作等待。如果后台IO活动比较繁重,则等待时间会明显加长。值得注意的是,EXT4文件系统具备一项“延迟分配”的特性,来推迟某些特定日志数据的操作系统写回时间,可以有效缓解这个问题。此外要注意的是,将EXT4的数据模式从默认的“有序”更改为“写回”并不会真正处理这个问题,因为日志需要在写扩展调用返回前就被持久化。

后台IO活动

从特定的JVM垃圾收集的角度来说,在典型的生产环境中,后台IO活动是不可避免的。有几类产生IO活动的源头:(1)系统活动;(2)管理软件;(3)其他协同应用;(4)同一JVM实例产生的IO。首先,操作系统包含许多机制(比如“/proc”文件系统)将数据写入底层磁盘。第二,系统级的软件比如CFEngine也会产生磁盘IO。第三,如果节点需要和其他应用协同共享磁盘,则其他应用会竞争IO。第四,特定的JVM实例也可能产生除GC日志之外的磁盘IO。

解决方案

尽管在当前的HotSpot JVM实现(也存在于其他虚拟机)中,GC日志记录过程会被后台IO活动阻塞,我们仍然有很多解决方案来帮助在写入到GC日志文件时缓解这个问题。

首先,增强JVM可以完全的解决这个问题。尤其,当GC日志记录活动和引起STW暂停的JVM GC进程分裂,这个问题就会消失不见。举例来说,JVM可以将GC日志记录功能,放入一个不同的线程来单独处理日志文件写操作,因此,不会造成STW暂停。但是独立线程来记录日志会存在JVM崩溃时丢失GC日志信息的风险。暴露给用户一个JVM标志位允许用户指定偏好应当时不错的选择。

由于后台IO引起的STW暂停时间的长短依赖于究竟IO负载有多繁重,那么我们可以采用各种方式来减少后台IO的强烈程度。举例来说,在同一个节点上取消已分配的其他IO密集型应用程序,减少其它类型的日志记录,改进日志循环等。

对于延迟敏感的应用来讲,比如服务用户进行交互操作,稍微大的STW暂停(比如大于0.25s)都是不可容忍的。因此,需要实施特别的方案。确保没有由系统诱导产生的较大STW暂停的最底线就是使GC日志记录动作避免被操作系统IO活动阻塞。

一种解决方案是将GC日志放入tmpfs(比如,-Xloggc:/tmpfs/gc.log)。因为tmpfs没有磁盘备份,写入到tmpfs不会产生磁盘活动,因此也不会被磁盘IO阻塞。但是这种方式存在两个问题:(1)GC日志文件会在系统宕机时丢失;(2)tmpfs消耗物理内存。一种缓解方案就是隔段时间将日志文件持久化备份,可以减少丢失的数量。

另一种方案就是将GC日志文件放置在SSD(Solid-State Drives,固态硬盘)上,固态硬盘具备更好的IO性能。根据IO负载,SSD可用作GC日志记录专属的驱动器,或者与其他IO负载应用共享。然而,SSD的价格需要考虑在成本中。

相较使用SSD,一个更具成本效益的优胜方案是将GC日志文件放入一个专用的硬盘上面。由于该硬盘仅提供GC日志记录,所以产生的磁盘IO看起来能达到降低暂停,提高JVM性能的目标。事实上,上面我们展示的场景1中,就用了这种配置方式,因此采用这种方式可以保证在GC日志记录驱动器上没有其它IO活动存在。

评估将GC日志放置在SSD和tmpfs上

我们采用了专有文件系统的方式,将GC日志放置在SSD和tmpfs驱动器上。我们运行了和场景2中同样的Java负载和后台IO负载。

对于SSD和tmpfs,我们观察到了相似的结果,下面的图表展示了将GC日志文件放置在SSD磁盘上的结果。我们发现JVM暂停性能比场景1中略差,但是所有的暂停均小于0.25s。结果表明,后台IO负载并没有影响应用的性能。

将GC日志记录到SSD上所有的JVM STW暂停

结论

延迟敏感的Java应用程序需要较小的JVM GC暂停。然而,当磁盘IO繁重时,JVM明显会被阻塞一段时间。

我们研究了这个问题,并且得出以下结论:

1、JVM GC需要write()系统调用来记录GC活动;

2、由于后台磁盘IO,像write()这样的调用会被阻塞;

3、GC日志记录过程是JVM暂停的一部分,因此write()调用的时间会计算在JVM STW暂停时间中。

我们提出了一系列的解决方案来缓解这个问题。尤其的,调查结果可以用于提高 JVM 实现,以避免此问题。对于延迟敏感的应用程序,一个即刻生效的解决方案即是将GC日志文件放置在单独的硬盘或者高性能的磁盘驱动器,如SSD上面,来避免IO竞争。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 译者著:其实本文的中心意思非常简单,没有耐心的读者建议直接拉到最后看结论部分,有兴趣的读者可以详细阅读一下。 原文...
    重度恐高症阅读 7,750评论 6 51
  • 垃圾回收算法具体实现 翻译原文 => plumbr Java GC handbook 前文参见: Java垃圾回收...
    foxracle阅读 2,783评论 0 15
  • 原文阅读 前言 这段时间懈怠了,罪过! 最近看到有同事也开始用上了微信公众号写博客了,挺好的~给他们点赞,这博客我...
    码农戏码阅读 5,879评论 2 31
  • Java 应用性能优化是一个老生常谈的话题,典型的性能问题如页面响应慢、接口超时,服务器负载高、并发数低,数据库频...
    Rick617阅读 7,224评论 1 9
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139