当前位置: 首页 > 科技观察

高吞吐量和低延迟Java应用程序的垃圾收集优化

时间:2023-03-20 17:07:45 科技观察

高性能应用程序构成了现代网络的支柱。LinkedIn有许多内部高吞吐量服务,每秒可处理数千个用户请求。为了优化用户体验,以低延迟响应这些请求很重要。例如,用户经常使用的一项功能是访问提要——不断更新的专业事件和内容列表。动态信息在LinkedIn上随处可见,包括公司页面、学校页面,最重要的是主页。基础动态信息数据平台索引了我们经济地图中各个实体(成员、公司、集团等)的更新,它必须实现高吞吐量和低延迟的相关更新。图1LinkedInFeed随着这些高吞吐量、低延迟的Java应用程序投入生产,开发人员必须确保在应用程序开发周期的每个阶段都保持一致的性能。确定最佳的垃圾收集(GarbageCollection,GC)设置对于实现这些指标至关重要。本文通过一系列步骤来明确需求和优化GC。目标受众是对使用系统方法优化GC以实现应用程序的高吞吐量和低延迟感兴趣的开发人员。文章中的方法来自LinkedIn构建下一代动态信息数据平台的过程。这些方法包括但不限于以下内容:ConcurrentMarkSweep(CMS)和G1垃圾收集器的CPU和内存开销,避免长寿命对象导致的连续GC循环,优化GC线程任务分配以提高性能,以及OS设置GC暂停时间是可预测的。优化GC的合适时机?GC行为因代码级优化和工作负载而异。因此,在已实现性能优化的近乎完整的代码库上调整GC非常重要。但也有必要对使用存根代码并模拟代表生产环境的工作负载的端到端基本原型进行初步分析。这会捕获架构的延迟和吞吐量的真实界限,以决定是向上扩展还是向外扩展。在下一代动态信息数据平台的原型阶段,几乎实现了所有端到端的功能,模拟了当前产品基础设施所服务的查询负载。从中,我们获得了多种工作负载特征,用于衡量应用程序性能和运行足够长的时间时的GC特征。优化GC的步骤以下是针对高吞吐量和低延迟要求优化GC的一般步骤。还包括动态信息数据平台中原型实现的具体细节。可以看出ParNew/CMS的性能最好,不过我们也用G1垃圾收集器进行了实验。1.了解GC的基础知识了解GC的工作机制非常重要,因为需要调整大量的参数。Oracle的HotspotJVM内存管理白皮书是一个非常好的开始学习HotspotJVMGC算法的资料。要了解G1垃圾收集器,请查看本文。2.仔细考虑GC需求为了减少应用程序性能的GC开销,可以优化GC的一些特性。应该在长时间的测试运行中观察吞吐量和延迟等GC特征,以确保特征数据来自应用程序处理的对象数量发生变化的多个GC周期。Stop-the-world收集器在收集垃圾时暂停应用程序线程。暂停的长度和频率不应对应用程序对SLA的遵从性产生不利影响。并发GC算法与应用程序线程竞争CPU周期。此开销不应影响应用程序吞吐量。未压缩的GC算法会导致堆碎片,导致fullGC的长时间Stop-the-world暂停。垃圾收集需要内存。一些GC算法会产生更高的内存占用。如果应用程序需要较大的堆空间,请确保GC的内存开销不会太大。GC日志和常见JVM参数的清晰视图对于轻松调整GC操作是必要的。GC操作随着代码复杂性的增长或作业特征的变化而变化。我们使用LinuxOS的HotspotJava7u51,32GB堆内存,6GByounggeneration(新生代)和-XX:CMSInitiatingOccupancyFraction值为70(触发老年代GC时的空间占用率)开始实验。设置更大的堆内存以维护长寿命对象的对象缓存。一旦这个缓存被填满,被提升到老年代的对象的百分比就会显着下降。在初始GC配置下,每3秒发生一次80ms的年轻代GC暂停,超过99.9%的应用程序延迟100ms。这样的GC可能适用于SLA对延迟不太严格的许多应用程序。然而,我们的目标是尽可能减少99.9%的应用程序的延迟,为此GC优化是必不可少的。3、理解GC索引优化前的衡量。了解GC日志的更详细信息(使用这些选项:-XX:+PrintGCDetails-XX:+PrintGCTimeStamps-XX:+PrintGCDateStamps-XX:+PrintTenuringDistribution-XX:+PrintGCApplicationStoppedTime)可以让您对GC有一个大概的了解应用程序的特性。LinkedIn的内部监控和报告系统inGraphs和Naarad生成各种有用的指标可视化,例如GC暂停时间的百分比、暂停的最长持续时间以及一段时间内的GC频率。除了Naarad,还有很多开源工具,例如gclogviewer,可以从GC日志创建可视化图表。这个阶段需要判断GC频率和暂停时长是否影响应用满足延迟要求的能力。4.降低GC频率在分代GC算法中,可以通过以下方式降低回收频率:(1)降低对象分配/提升率;(2)增加生成空间的大小。在HotspotJVM中,新生代的GC停顿时间取决于一次垃圾回收后对象的数量,而不是新生代本身的大小。增加年轻代大小对应用程序性能的影响需要仔细评估:如果更多数据存活并复制到幸存者区域,或者如果每次垃圾收集将更多数据提升到老年代,增加年轻代大小可能会导致在更长的年轻一代中,GenerationGC暂停。另一方面,如果每次垃圾回收后存活对象的数量没有明显增加,暂停时间可能不会延长。在这种情况下,降低GC频率可能会降低整体应用程序延迟和/或提高吞吐量。对于大多数具有短生命期对象的应用程序,只需要控制上面提到的参数。对于创建长寿命对象的应用,需要注意被提升的对象可能长时间不会被老年代GC循环回收。如果老年代GC的触发阈值(老年代空间占用百分比)较低,应用程序将陷入恒定的GC周期。设置高的GC触发阈值可以避免这个问题。由于我们的应用程序在堆中维护着大量长寿命对象的缓存,因此将老年代GC触发阈值设置为-XX:CMSInitiatingOccupancyFraction=92-XX:+UseCMSInitiatingOccupancyOnly。我们还尝试增加年轻代的大小以减少年轻代收集的频率,但由于增加了应用程序延迟而没有这样做。5.缩短GC暂停时间减小新生代的大小可以缩短新生代的GC暂停时间,因为更少的数据被复制到幸存者区域或被提升。然而,如前所述,我们想要观察减小新生代大小以及由此导致的GC频率增加对整体应用程序吞吐量和延迟的影响。新生代GC停顿时间还取决于tenuring阈值(boostthreshold)和空间大小(见步骤6)。使用CMS尽量减少堆碎片和相关的老年代完整GC暂停。通过控制对象提升比例,降低-XX:CMSInitiatingOccupancyFraction的值,以较低的阈值触发老年代GC。有关所有选项及其相关权衡的详细调整,请参阅Web服务的Java垃圾收集和Java垃圾收集要点。我们观察到Eden区大部分的新生代都被回收了,survivor区几乎没有对象死亡,所以我们将tenuringthreshold从8降低到2(使用选项:-XX:MaxTenuringThreshold=2),为了缩短新生代垃圾回收复制数据所花费的时间。我们还注意到,随着老年代空间占用的增加,年轻代收集停顿时间也会增加。这意味着来自老年代的压力使得对象提升需要更多的时间。要解决此问题,请将总堆内存大小增加到40GB并将-XX:CMSInitiatingOccupancyFraction的值减小到80以更快地启动老年代收集。虽然-XX:CMSInitiatingOccupancyFraction的值变小了,但是增加堆内存可以避免不断的老年代GC。在此阶段,我们实现了70毫秒的新生代暂停和99.9%的80毫秒延迟。6、为了优化GC工作线程的任务分配,进一步缩短新生代停顿时间,我们决定研究优化GC线程绑定任务的选项。-XX:ParGCCardsPerStrideChunk选项控制GC工作线程的任务粒度,这有助于在不使用补丁的情况下实现最佳性能。该补丁用于优化新一代垃圾回收的卡表扫描时间。有趣的是,随着老年代空间的增加,年轻代的GC时间也随之延长。将此选项值设置为32678,新生代收集停顿时间减少到平均50ms。此时99.9%的应用程序延迟了60毫秒。还有其他选项可以将任务映射到GC线程,如果操作系统允许,-XX:+BindGCTaskThreadsT??oCPUs选项将GC线程绑定到各个CPU内核。-XX:+UseGCTaskAffinity使用affinity参数将任务分配给GC工作线程。但是,我们的应用程序没有从这些选项中看到任何好处。事实上,一些调查表明这些选项在Linux系统上不起作用[1,2]。7.了解GC的CPU和内存开销ConcurrentGC通常会增加CPU的使用率。我们观察到,在CMS的默认设置运行良好的情况下,并发GC和G1垃圾收集器协同工作导致的CPU使用率增加显着降低了应用程序的吞吐量和延迟。与CMS相比,G1可能会占用应用更多的内存开销。对于低吞吐量、非计算密集型应用程序,GC的高CPU使用率可能不是问题。图2ParNew/CMS和G1的CPU使用率:相对来说,CPU使用率变化较大的节点使用G1选项-XX:G1RSetUpdatingPauseTimePercent=20图3ParNew/CMS和G1服务的每秒请求数:吞吐量低该节点使用G1选项-XX:G1RSetUpdatingPauseTimePercent=208。为GC优化系统内存和I/O管理。一般来说,GC暂停发生在(1)低用户时间、高系统时间和高时钟时间以及(2)低用户时间、低系统时间和高时钟时间。这意味着底层进程/操作系统设置存在问题。情况(1)可能表明Linux从JVM窃取页面,情况(2)可能表明Linux在磁盘缓存被清除时启动了一个GC线程,该线程在等待I/O时被困在内核中。这些情况下如何设置参数可以参考这个PPT。为避免运行时性能损失,请在启动应用程序时使用JVM选项-XX:+AlwaysPreTouch访问和清除页面。将vm.swappiness设置为零,除非绝对必要,否则操作系统将不会交换页面。您可能会使用mlock将JVM页面固定在内存中,这样操作系统就不会换出这些页面。但是,如果系统用完了所有内存和交换空间,操作系统会通过终止进程来回收内存。通常情况下,Linux内核会选择常驻内存占用高但运行时间不长的进程(OOM情况下kill进程的流程)。对于我们来说,这个过程很可能就是我们的应用程序。一个服务最好具有优雅降级(适度降级)的特性,而服务的突然失效表明可操作性差——因此,我们不使用mlock而是使用vm.swappiness来避免可能的swap惩罚。LinkedIn动态信息数据平台的GC优化针对该平台的原型系统,我们使用了HotspotJVM的两种算法来优化垃圾回收:新生代垃圾回收使用ParNew,老年代垃圾回收使用CMS。新生代和老年代使用G1。G1旨在解决6GB或更大堆大小的稳定、可预测的暂停时间少于0.5秒的问题。在我们对G1的实验中,尽管调整了各种参数,但我们没有获得与ParNew/CMS相同的GC性能或暂停时间的可预测值。我们使用G1调查了与内存泄漏相关的错误[3],但无法确定根本原因。使用ParNew/CMS,每三秒应用一次40-60ms的新一代暂停,每小时应用一个CMS周期。JVM选项如下://JVMsizingoptions-server-Xms40g-Xmx40g-XX:MaxDirectMemorySize=4096m-XX:PermSize=256m-XX:MaxPermSize=256m//Younggenerationoptions-XX:NewSize=6g-XX:MaxNewSize=6g-XX:+UseParNewGC-XX:MaxTenuringThreshold=2-XX:SurvivorRatio=8-XX:+UnlockDiagnosticVMOptions-XX:ParGCCardsPerStrideChunk=32768//Oldgenerationoptions-XX:+UseConcMarkSweepGC-XX:CMSParallelRemarkEnabled-XX:+ParallelRefProcEnabled-XX:+CMSClassUnloading启用XX:CMSInitiatingOccupancyFraction=80-XX:+UseCMSInitiatingOccupancyOnly//Otheroptions-XX:+AlwaysPreTouch-XX:+PrintGCDetails-XX:+PrintGCTimeStamps-XX:+PrintGCDateStamps-XX:+PrintTenuringDistribution-XX:+PrintGCApplicationStoppedTime-XX:-OmitStackTrace使用这些options数千个读取请求的吞吐量,将99.9%的应用程序延迟降低到60ms。参考:[1]-XX:+BindGCTaskThreadsT??oCPUs在linux系统上好像不行,因为hotspot/src/os/linux/vm/os_linux.cpp的distribute_processes方法在JDK7或者JDK8中都没有实现。[2]-XX:+UseGCTaskAffinity选项似乎并不适用于JDK7和JDK8的所有平台,因为任务的affinity属性总是设置为sentinel_worker=(uint)-1。源代码参见hotspot/src/share/vm/gc_implementation/parallelScavenge/{gcTaskManager.cpp,gcTaskThread.cpp,gcTaskManager.cpp}。[3]G1中存在一些内存泄漏的bug,在Java7u51中可能无法修改。此错误仅在Java8中修复。原文链接:linkedin翻译:ImportNew.com-hejiani翻译链接:http://www.importnew.com/11336.html