高吞吐、低延迟 Java 应用的 GC 优化实践
本篇原文作者是 LinkedIn 的 Swapnil Ghike,这篇文章讲述了 LinkedIn 的 Feed 产品的 GC 优化过程,虽然文章写作于 April 8, 2014,但其中的很多内容和知识点非常有学习和参考意义。
背景
高性能应用构成了现代网络的支柱。LinkedIn 内部有许多高吞吐量服务来满足每秒成千上万的用户请求。为了获得最佳的用户体验,以低延迟响应这些请求是非常重要的。
例如,我们的用户经常使用的产品是 Feed —— 它是一个不断更新的专业活动和内容的列表。Feed 在 LinkedIn 的系统中随处可见,包括公司页面、学校页面以及最重要的主页资讯信息。基础 Feed 数据平台为我们的经济图谱(会员、公司、群组等)中各种实体的更新建立索引,它必须高吞吐低延迟地实现相关的更新。如下图,LinkedIn Feeds 信息展示:
为了将这些高吞吐量、低延迟类型的 Java 应用程序用于生产,开发人员必须确保在应用程序开发周期的每个阶段都保持一致的性能。确定最佳垃圾收集(Garbage Collection, GC)配置对于实现这些指标至关重要。
这篇博文将通过一系列步骤来明确需求并优化 GC,它的目标读者是对使用系统方法进行 GC 优化来实现应用的高吞吐低延迟目标感兴趣的开发人员。在 LinkedIn 构建下一代 Feed 数据平台的过程中,我们总结了该方法。这些方法包括但不限于以下几点:并发标记清除(Concurrent Mark Sweep,CMS(参考[2]) 和 G1(参考 [3]) 垃圾回收器的 CPU 和内存开销、避免长期存活对象导致的持续 GC、优化 GC 线程任务分配提升性能,以及可预测 GC 停顿时间所需的 OS 配置。
优化 GC 的正确时机?
GC 的行为可能会因代码优化以及工作负载的变化而变化。因此,在一个已实施性能优化的接近完成的代码库上进行 GC 优化非常重要。而且在端到端的基本原型上进行初步分析也很有必要,该原型系统使用存根代码并模拟了可代表生产环境的工作负载。这样可以获取该架构延迟和吞吐量的真实边界,进而决定是否进行纵向或横向扩展。
在下一代 Feed 数据平台的原型开发阶段,我们几乎实现了所有端到端的功能,并且模拟了当前生产基础设施提供的查询工作负载。这使我们在工作负载特性上有足够的多样性,可以在足够长的时间内测量应用程序性能和 GC 特征。
优化 GC 的步骤
下面是一些针对高吞吐量、低延迟需求优化 GC 的总体步骤。此外,还包括在 Feed 数据平台原型实施的具体细节。尽管我们还对 G1 垃圾收集器进行了试验,但我们发现 ParNew/CMS 具有最佳的 GC 性能。
1. 理解 GC 基础知识
由于 GC 优化需要调整大量的参数,因此理解 GC 工作机制非常重要。Oracle 的 Hotspot JVM 内存管理白皮书(参考 [4] )是开始学习 Hotspot JVM GC 算法非常好的资料。而了解 G1 垃圾回收器的理论知识,可以参阅(参考 [3])。
2. 仔细考量 GC 需求
为了降低对应用程序性能的开销,可以优化 GC 的一些特征。像吞吐量和延迟一样,这些 GC 特征应该在长时间运行的测试中观察到,以确保应用程序能够在经历多个 GC 周期中处理流量的变化。
- Stop-the-world 回收器回收垃圾时会暂停应用线程。停顿的时长和频率不应该对应用遵守 SLA 产生不利的影响。
- 并发 GC 算法与应用线程竞争 CPU 周期。这个开销不应该影响应用吞吐量。
- 非压缩 GC 算法会引起堆碎片化,进而导致的 Full GC 长时间 Stop-the-world,因此,堆碎片应保持在最小值。
- 垃圾回收工作需要占用内存。某些 GC 算法具有比其他算法更高的内存占用。如果应用程序需要较大的堆空间,要确保 GC 的内存开销不能太大。
- 要清楚地了解 GC 日志和常用的 JVM 参数,以便轻松地调整 GC 行为。因为 GC 运行随着代码复杂性增加或工作负载特性的改变而发生变化
我们使用 Linux 操作系统、Hotspot Java7u51、32GB 堆内存、6GB 新生代(Young Gen)和 -XX:CMSInitiatingOccupancyFraction 值为 70(Old GC 触发时其空间占用率)开始实验。设置较大的堆内存是用来维持长期存活对象的对象缓存。一旦这个缓存生效,晋升到 Old Gen 的对象速度会显著下降。
使用最初的 JVM 配置,每 3 秒发生一次 80ms 的 Young GC 停顿,超过 99.9% 的应用请求延迟 100ms(999线)。这样的 GC 效果可能适合于 SLA 对延迟要求不太严格应用。然而,我们的目标是尽可能减少应用请求的 999 线。GC 优化对于实现这一目标至关重要。
3. 理解 GC 指标
衡量应用当前情况始终是优化的先决条件。了解 GC 日志的详细细节(参考 [5])(使用以下选项):
可以对该应用的 GC 特征有总体的把握。
在 LinkedIn 的内部监控 inGraphs 和报表系统 Naarad,生成了各种有用的指标可视化图形,比如 GC 停顿时间百分比、一次停顿最大持续时间以及长时间内 GC 频率。除了 Naarad,有很多开源工具比如 gclogviewer 可以从 GC 日志创建可视化图形。在此阶段,可以确定 GC 频率和暂停持续时间是否满足应用程序满足延迟的要求。
4. 降低 GC 频率
在分代 GC 算法中,降低 GC 频率可以通过:(1) 降低对象分配/晋升率;(2) 增加各代空间的大小。
在 Hotspot JVM 中,Young GC 停顿时间取决于一次垃圾回收后存活下来的对象的数量,而不是 Young Gen 自身的大小。增加 Young Gen 大小对于应用性能的影响需要仔细评估:
- 如果更多的数据存活而且被复制到 Survivor 区域,或者每次 GC 更多的数据晋升到 Old Gen,增加 Young Gen 大小可能导致更长的 Young GC 停顿。较长的 GC 停顿可能会导致应用程序延迟增加和(或)吞吐量降低。
- 另一方面,如果每次垃圾回收后存活对象数量不会大幅增加,停顿时间可能不会延长。在这种情况下,降低 GC 频率可能会使整个应用总体延迟降低和(或)吞吐量增加。
对于大部分为短期存活对象的应用,仅仅需要控制上述的参数;对于长期存活对象的应用,就需要注意,被晋升的对象可能很长时间都不能被 Old GC 周期回收。如果 Old GC 触发阈值(Old Gen 占用率百分比)比较低,应用将陷入持续的 GC 循环中。可以通过设置高的 GC 触发阈值可避免这一问题。
由于我们的应用在堆中维持了长期存活对象的较大缓存,将 Old GC 触发阈值设置为
来增加触发 Old GC 的阈值。我们也试图增加 Young Gen 大小来减少 Young GC 频率,但是并没有采用,因为这增加了应用的 999 线。
5. 缩短 GC 停顿时间
减少 Young Gen 大小可以缩短 Young GC 停顿时间,因为这可能导致被复制到 Survivor 区域或者被晋升的数据更少。但是,正如前面提到的,我们要观察减少 Young Gen 大小和由此导致的 GC 频率增加对于整体应用吞吐量和延迟的影响。Young GC 停顿时间也依赖于 tenuring threshold (晋升阈值)和 Old Gen 大小(如步骤 6 所示)。
在使用 CMS GC 时,应将因堆碎片或者由堆碎片导致的 Full GC 的停顿时间降低到最小。通过控制对象晋升比例和减小 -XX:CMSInitiatingOccupancyFraction 的值使 Old GC 在低阈值时触发。所有选项的细节调整和他们相关的权衡,请参考 Web Services 的 Java 垃圾回收(参考 [5] )和 Java 垃圾回收精粹(参考 [6])。
我们观察到 Eden 区域的大部分 Young Gen 被回收,几乎没有 3-8 年龄对象在 Survivor 空间中死亡,所以我们将 tenuring threshold 从 8 降低到 2 (使用选项:-XX:MaxTenuringThreshold=2 ),以降低 Young GC 消耗在数据复制上的时间。
我们还注意到 Young GC 暂停时间随着 Old Gen 占用率上升而延长。这意味着来自 Old Gen 的压力使得对象晋升花费更多的时间。为解决这个问题,将总的堆内存大小增加到 40GB,减小 -XX:CMSInitiatingOccupancyFraction 的值到 80,更快地开始 Old GC。尽管 -XX:CMSInitiatingOccupancyFraction 的值减小了,增大堆内存可以避免频繁的 Old GC。在此阶段,我们的结果是 Young GC 暂停 70ms,应用的 999 线在 80ms。
6. 优化 GC 工作线程的任务分配
为了进一步降低 Young GC 停顿时间,我们决定研究 GC 线程绑定任务的参数来进行优化。
-XX:ParGCCardsPerStrideChunk 参数控制 GC 工作线程的任务粒度,可以帮助不使用补丁而获得最佳性能,这个补丁用来优化 Young GC 中的 Card table(卡表)扫描时间(参考[7])。有趣的是,Young GC 时间随着 Old Gen 的增加而延长。将这个选项值设为 32678,Young GC 停顿时间降低到平均 50ms。此时应用的 999 线在 60ms。
还有一些的参数可以将任务映射到 GC 线程,如果操作系统允许的话,-XX:+BindGCTaskThreadsToCPUs 参数可以绑定 GC 线程到个别的 CPU 核(见解释 [1])。使用亲缘性 -XX:+UseGCTaskAffinity 参数可以将任务分配给 GC 工作线程(见解释 [2])。然而,我们的应用并没有从这些选项带来任何好处。实际上,一些调查显示这些选项在 Linux 系统不起作用。
7. 了解 GC 的 CPU 和内存开销
并发 GC 通常会增加 CPU 使用率。虽然我们观察到 CMS 的默认设置运行良好,但是 G1 收集器的并发 GC 工作会导致 CPU 使用率的增加,显著降低了应用程序的吞吐量和延迟。与 CMS 相比,G1 还增加了内存开销。对于不受 CPU 限制的低吞吐量应用程序,GC 导致的高 CPU 使用率可能不是一个紧迫的问题。
下图是 ParNew/CMS 和 G1 的 CPU 使用百分比:相对来说 CPU 使用率变化明显的节点使用 G1 参数 -XX:G1RSetUpdatingPauseTimePercent=20:
下图是 ParNew/CMS 和 G1 每秒服务的请求数:吞吐量较低的节点使用 G1 参数 -XX:G1RSetUpdatingPauseTimePercent=20
8. 为 GC 优化系统内存和 I/O 管理
通常来说,GC 停顿有两种特殊情况:(1) 低 user time,高 sys time 和高 real time (2) 低 user time,低 sys time 和高 real time。这意味着基础的进程/OS设置存在问题。情况 (1) 可能意味着 JVM 页面被 Linux 窃取;情况 (2) 可能意味着 GC 线程被 Linux 用于磁盘刷新,并卡在内核中等待 I/O。在这些情况下,如何设置参数可以参考该 PPT(参考[8])。
另外,为了避免在运行时造成性能损失,我们可以使用 JVM 选项 -XX:+AlwaysPreTouch 在应用程序启动时先访问所有分配给它的内存,让操作系统把内存真正的分配给 JVM。我们还可以将 vm.swappability 设置为0,这样操作系统就不会交换页面到 swap(除非绝对必要)。
可能你会使用 mlock 将 JVM 页固定到内存中,这样操作系统就不会将它们交换出去。但是,如果系统用尽了所有的内存和交换空间,操作系统将终止一个进程来回收内存。通常情况下,Linux 内核会选择具有高驻留内存占用但运行时间不长的进程(OOM 情况下杀死进程的工作流(参考[9])进行终止。在我们的例子中,这个进程很有可能就是我们的应用程序。优雅的降级是服务优秀的属性之一,不过服务突然终止的可能性对于可操作性来说并不好 —— 因此,我们不使用 mlock,只是通过 vm.swapability 来尽可能避免交换内存页到 swap 的惩罚。
LinkedIn 动态信息数据平台的 GC 优化
对于该 Feed 平台原型系统,我们使用 Hotspot JVM 的两个 GC 算法优化垃圾回收:
- Young GC 使用 ParNew,Old GC 使用 CMS。
- Young Gen 和 Old Gen 使用 G1。G1 试图解决堆大小为 6GB 或更大时,暂停时间稳定且可预测在 0.5 秒以下的问题。在我们用 G1 实验过程中,尽管调整了各种参数,但没有得到像 ParNew/CMS 一样的 GC 性能或停顿时间的可预测值。我们查询了使用 G1 发生内存泄漏相关的一个 bug(见解释[3]),但还不能确定根本原因。
使用 ParNew/CMS,应用每三秒进行一次 40-60ms 的 Young GC 和每小时一个 CMS GC。JVM 参数如下:
使用这些参数,对于成千上万读请求的吞吐量,我们应用程序的 999 线降低到 60ms。
解释
[1] -XX:+BindGCTaskThreadsToCPUs 参数似乎在Linux 系统上不起作用,因为 hotspot/src/os/linux/vm/oslinux.cpp 的 distributeprocesses 方法在 JDK7 或 JDK8 中没有实现。
[2] -XX:+UseGCTaskAffinity 参数在 JDK7 和 JDK8 的所有平台似乎都不起作用,因为任务的亲缘性属性永远被设置为 sentinelworker = (uint) -1。源码见 hotspot/src/share/vm/gcimplementation/parallelScavenge/{gcTaskManager.cpp,gcTaskThread.cpp, gcTaskManager.cpp}。
[3] G1 存在一些内存泄露的 bug,可能 Java7u51 没有修改。这个 bug 仅在 Java 8 修正了。
高吞吐、低延迟 Java 应用的 GC 优化实践的更多相关文章
- 高吞吐低延迟Java应用的垃圾回收优化
高吞吐低延迟Java应用的垃圾回收优化 高性能应用构成了现代网络的支柱.LinkedIn有许多内部高吞吐量服务来满足每秒数千次的用户请求.要优化用户体验,低延迟地响应这些请求非常重要. 比如说,用户经 ...
- JVM:从实际案例聊聊Java应用的GC优化
原文转载自美团从实际案例聊聊Java应用的GC优化,感谢原作者的贡献 当Java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步提高性能,称为GC优化.但GC算法复杂 ...
- 从实际案例聊聊Java应用的GC优化
转自美团点评技术博客:https://tech.meituan.com/jvm_optimize.html 当Java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步 ...
- 从实际案例聊聊Java应用的GC优化--转
https://tech.meituan.com/jvm_optimize.html 当Java程序性能达不到既定目标,且其他优化手段都已经穷尽时,通常需要调整垃圾回收器来进一步提高性能,称为GC优化 ...
- 如何降低90%Java垃圾回收时间?以阿里HBase的GC优化实践为例
过去的一年里,我们准备在Ali-HBase上突破这个被普遍认知的痛点,为此进行了深度分析及全面创新的工作,获得了一些比较好的效果.以蚂蚁风控场景为例,HBase的线上young GC时间从120ms减 ...
- 转: 低延迟系统的Java实践
from: http://blog.csdn.net/jacktan/article/details/41177779 在很久很久以前,如果有人让我用Java语言开发一个低延迟系统,我肯定会用迷茫的 ...
- Unity3D游戏GC优化总结---protobuf-net无GC版本优化实践
protobuf-net优化效果图 protobuf-net是Unity3D游戏开发中被广泛使用的Google Protocol Buffer库的c#版本,之所以c#版本被广泛使用,是因为c++版本的 ...
- java 11 ZGC(可伸缩,低延迟的gc)
ZGC, A Scalable Low-Latency Garbage Collector(Experimental) 可伸缩,低延迟的gc ZGC, 这应该是JDK11最为瞩目的特性, 没有之一. ...
- kafka高吞吐,低延迟的分布式消息队列
核心概念 broker是kafka的节点,多台broker集群就是kafka topic消息分为多个topic partition分区,topic划分了多个partition分区,存在负载均衡策略 每 ...
随机推荐
- Spring Boot集成sharding-jdbc实现分库分表
一.水平分割 1.水平分库 1).概念:以字段为依据,按照一定策略,将一个库中的数据拆分到多个库中.2).结果每个库的结构都一样:数据都不一样:所有库的并集是全量数据: 2.水平分表 1).概念以字段 ...
- 【springMVC】<mvc:annotation-driven />标签的使用、作用?
不牵扯源码的显式的作用 在使用interceptor时,显式的作用. 这是不配置<mvc:annotation-driven/>标签时的public boolean preHandle(H ...
- Webpack5构建速度提升令人惊叹,早升级早受益
为什么要升级? webpack4用的好好的,运行稳定,为什么要升级到webpack5, 每次升级,都要经历一场地震,处理许多loader和plugin API的破坏性改变. 请给我们一个充分的升级理由 ...
- 容器环境下如何将NuGet包XML文档添加到Swagger
容器环境下将NuGet包XML文档添加到Swagger 在.NET Core项目开发过程中,为了实现代码复用,我们将可以重复使用的部分拆分成一个个小的NuGet包.这些NuGet包可以在其他系统中复用 ...
- [bug] HMaster启动后几秒消失
参考 https://blog.csdn.net/weixin_44896798/article/details/97800045 https://blog.csdn.net/liudi1993/ar ...
- 使用find命令查找大文件
使用find命令查找大文件 find命令是Linux系统管理员工具库中最强大的工具之一.它允许您根据不同的标准(包括文件大小)搜索文件和目录. 例如,如果在当前工作目录中要搜索大小超过100MB的文件 ...
- 如何在CentOS 7上安装Htop
在本教程中,我们将向您介绍如何在CentOS 7服务器上安装和配置Htop.对于那些不知道的人,Htop 是为Linux编写的一个交互式实时系统监视进程查看器.它被设计为替代Unix程序的顶部.它显示 ...
- 7.12-7.19 id、w、who、last、lastb、lastlog
7.12-7.19 id.w.who.last.lastb.lastlog 目录 7.12 id:显示用户与用户组的信息 7.13 w:显示已登录用户信息 7.14 who:显示已登录用户信息 显示最 ...
- linux基础之基础命令一
本节内容: 1. ls:列出当前目录下的文件和目录 -l: 长输出,显示文件的详细信息(-普通文本,d目录) -a: 显示所有文件,包括隐藏文件 -h: 人类易读(-lh) -d: 显示目录信息(-l ...
- mysql基础之视图、事务、索引、外键
一.视图 视图是一个虚拟表,其内容由查询定义.同真实的表一样,视图包含一系列带有名称的列和行数据.但是,视图并不在数据库中以存储的数据值集形式存在.行和列数据来自由定义视图的查询所引用的表,并且在引用 ...