系统架构

JVM GC 性能测试(三):真实流量

JVM GC 性能测试(三):真实流量

D瓜哥
JVM GC 性能测试系列: JVM GC 性能对比方法 JVM GC 性能测试(一):相同流量 JVM GC 性能测试(二):递增流量 JVM GC 性能测试(三):真实流量 书接上文,在 JVM GC 性能测试(二):递增流量 的最后,D瓜哥提到了一个问题,对于在 JVM GC 性能测试(一):相同流量 和 JVM GC 性能测试(二):递增流量 中存在的巨大 QPS 差异疑惑不解。所以,D瓜哥决定将测试机器接入到线上环境,在真实访问中,观察各个 GC 的表现。 一言以蔽之 J21-Gen-ZGC 和 J21-G1 无论在稳定性,吞吐量以及响应时效性上都非常优秀。 再极端峰值情况,J21-G1 是更好的选择,更加稳定,不容易出凸点。 日常使用,J21-Gen-ZGC 响应性更好,接口耗时更低。 鉴于 OpenJDK 21 G1 GC 一如既往的惊艳表现,D瓜哥准备整理一下 G1 GC 的主要优化,敬请关注: Java G1 垃圾收集器主要优化。 1. 服务调用监控数据 监控服务调用的相关数据,这是对于用户来说,感知最强烈的相关数据,也是直接关系到服务质量的数据。 1.1. 服务调用次数 从调用次数上来看,五个分组没有大的变化,可以说根本没有达到系统的极限峰值。当然,这才是正常现象,如果日常运行都爆峰值,那说明系统早该扩容了。 图 1. 服务调用次数(秒级) 图 2. 服务调用次数(分钟级) 1.2. 服务调用耗时 整体上讲,J21-Gen-ZGC 的耗时更短,从数据上来看,TP999 能比 J21-G1 的少 10~20ms;TP99 更加夸张,J21-Gen-ZGC 的耗时只有 J21-G1 的一半。
JVM GC 性能测试(二):递增流量

JVM GC 性能测试(二):递增流量

D瓜哥
JVM GC 性能测试系列: JVM GC 性能对比方法 JVM GC 性能测试(一):相同流量 JVM GC 性能测试(二):递增流量 JVM GC 性能测试(三):真实流量 在上一篇文章 JVM GC 性能测试(一):相同流量 中,D瓜哥使用一个总量请求对所有分组的所有机器进行性能测试。但是,经过测试发现了一个问题,同时产生了另外一个问题,有两个问题没有得到很好的解答: 由于服务响应时长直接关系到服务调用次数,当某一台机器出现问题时,整体调用次数就会急剧下降,调用次数加不上去。一个机器出问题,所有机器的访问量就上不去了。这是测试中发现的一个问题。当然,这属于测试工具的问题,别不是 GC 的问题。但是,也影响到我们的压测,也需要解决。 上次测试,这是针对某一个指定服务调用量进行性能测试,那么,无法确定每个 GC 能支撑的极限调用峰值。另外,在极限峰值和超极限峰值的情况下,各个 GC 的表现如何?这个也有待验证。 针对上述两个问题,设计了本次测试。测试方法如下: 各个分组使用一套相同的流量策略: 各个分组几乎同时开始执行测试任务; 调用量从低到高,以此同时使用相关的调用量进行测试; 除最开始预热阶段的调用量外,后续每个调用量都持续进行十分钟的测试。 针对每个 GC 分组单独设定一套调用发量程序,这个保证各个 GC 分组直接不相互影响。 最后,再分析调用量相同时段的各个 GC 表现,就可以看到各个 GC 的极限峰值。 为了保留更多细节,本文所有截图都是在 34 吋带鱼屏下,使用全屏模式展示并截图的。如果看不清楚,可以右击在新页面打开图片来查看。 具体流量及时间段: 750, 23:14:30 ~ 23:19:30 800, 23:19:30 ~ 23:29:30 850, 23:29:30 ~ 23:39:30 900, 23:39:30 ~ 23:49:30 950, 23:49:30 ~ 23:59:30 1000,23:59:30 ~ 00:09:30
JVM GC 性能测试(一):相同流量

JVM GC 性能测试(一):相同流量

D瓜哥
JVM GC 性能测试系列: JVM GC 性能对比方法 JVM GC 性能测试(一):相同流量 JVM GC 性能测试(二):递增流量 JVM GC 性能测试(三):真实流量 在上一篇文章 JVM GC 性能对比方法 介绍了性能对比的方法,这篇文章就根据该方法对上述提到的5种 JVM GC 进行性能测试。 在正式测试之前,D瓜哥进行了多次小流量试探性测试,来探索一个合适的量。找到一个比较平稳的量后,乘以机器数量,获得一个每秒总计请求量,最后使用该总量数据去做压测。 根据多次测试的数据来看,最后选择的是每台每秒 500 QPS,5 个分组,每个分组 5 台机器,所以,每秒的请求总量是: 500 * 5 * 5 = 12500 QPS;每个分组每分钟的总量是:500 * 5 * 60 = 150000 QPS。使用每台机器以此使用 100 QPS,200 QPS,300 QPS,400 QPS 各运行一分钟来对系统进行预热。最后以每台每秒 500 QPS 的访问量来对测试机器进行持续十分钟的性能测试,最后分析这十分钟的相关数据。 一言以蔽之 服务稳定性:J21-Gen-ZGC、J21-G1、J8-G1 稳定性最好;J17-ZGC 有轻微波动;J21-ZGC 有剧烈波动; 服务耗时 TP999:J21-Gen-ZGC < J17-ZGC < J21-G1 < J8-G1 < J21-ZGC;
JVM GC 性能对比方法

JVM GC 性能对比方法

D瓜哥
JVM GC 性能测试系列: JVM GC 性能对比方法 JVM GC 性能测试(一):相同流量 JVM GC 性能测试(二):递增流量 JVM GC 性能测试(三):真实流量 现在部门内部绝大部分应用都还在使用 OpenJDK 8,计划推进部门升级 JDK 到 OpenJDK21。本着实事求是,用数据说话的原则,准备对如下 GC 做性能测试: OpenJDK 8 G1 GC(以下称 J8-G1。具体版本号:1.8.0_321-b07。) OpenJDK 17 ZGC(以下称 J17-ZGC。具体版本号:17.0.9+9。) OpenJDK 21 G1(以下称 J21-G1。具体版本号:21.0.2+13-LTS。) OpenJDK 21 ZGC(以下称 J21-ZGC。具体版本号:21.0.2+13-LTS。) OpenJDK 21 Gen ZGC(以下称 J21-Gen-ZGC。具体版本号:21.0.2+13-LTS。) 所有 OpenJDK 版本都是选用相同大版本号里的最高的版本。所有的机器都是 4C8G 的配置,JVM 堆栈内存设置为 4608M 。 为了减少不必要的干扰,JVM 相关参数也尽可能做到了一致或者接近。(等测试完,D瓜哥会把相关参数也分享出来。) 测试对象 由于D瓜哥所处的部门是一个直接面向用户的线上业务部门,所以,大部分系统是直接面对用户,接受用户访问的在线业务系统。所以,为了服务线上业务系统的需求,测试对象的选择就限定在了类似的场景中。测试对象是线上接受用户访问的一个服务。结构如下: 图 1. 压测接口依赖关系图 该接口有外部依赖服务,也有数据库查询,是一个微服务架构下典型的在线服务接口。 测试方法 原本计划是想直接通过上线,将线上不同分组的机器使用不同的 GC 来做测试,但是,这样面临好几个问题: 由于正是环境上线需要审批,每次上线是针对一个构建包做上线,由于基于三个不同版本的 OpenJDK,所以,至少需要上线三次。 线上环境,不同分组的机器数量是不一样的,所以,不方便对比。比如,对比不同分组的响应请求数量。 性能测试势必会影响到线上的业务处理。如果引发客诉,鱼有没有迟到不确定,但是绝对惹一身腥。
JVM 剖析花园:2 - 透明大页

JVM 剖析花园:2 - 透明大页

问题 什么是大页(Large Page)?什么是透明大页(Transparent Huge Page)?它对我有什么帮助? 理论 虚拟内存现在已被视为理所当然。现在只有少数人还记得,更不用说做一些“真实模式”编程了,在这种情况下,你会接触到实际的物理内存。相反,每个进程都有自己的虚拟内存空间,该空间被映射到实际内存上。例如,两个进程在相同的虚拟地址 0x42424242 上拥有不同的数据,而这些数据将由不同的物理内存支持。现在,当程序访问该地址时,应将虚拟地址转换为物理地址。 图 1. 虚拟内存地址与物理内存地址之间的关系 这通常由操作系统维护 “页表”,硬件通过“页表遍历”来实现地址转换。如果在页面粒度上维护翻译,整个过程就会变得简单。但这样做的成本并不低,而且每次内存访问都需要这样做!因此,还需要对最新的翻译进行小型缓存,即 转译后备缓冲区(Translation Lookaside Buffer (TLB))。TLB 通常很小,只有不到 100 个条目,因为它的速度至少要与 L1 缓存相当,甚至更快。对于许多工作负载来说,TLB 未命中和相关的页表遍历需要大量时间。 既然我们无法将 TLB 做得更大,那么我们可以做其他事情:制作更大的页面!大多数硬件有 4K 基本页和 2M/4M/1G “大页”。用更大的页来覆盖相同的区域,还能使页表本身更小,从而降低页表遍历的成本。 在 Linux 世界中,至少有两种不同的方法可以在应用程序中实现这一点: hugetlbfs。切出系统内存的一部分,将其作为虚拟文件系统公开,让应用程序通过 mmap(2) 从其中获取。这是一个特殊的接口,需要操作系统配置和应用程序更改才能使用。这也是一种“要么全有,要么全无”的交易:分配给 hugetlbfs(持久部分)的空间不能被普通进程使用。 透明大页(Transparent Huge Pages (THP))。让应用程序像往常一样分配内存,但尽量以透明方式为应用程序提供大容量页面支持的存储空间。理想情况下,不需要更改应用程序,但我们会看到应用程序如何从了解 THP 的可用性中获益。但在实际应用中,会产生内存开销(因为会为小文件分配整个大页面)或时间开销(因为 THP 有时需要对内存进行碎片整理以分配页面)。好在有一个中间方案:通过 madvise(2) 可以让应用程序告诉 Linux 在哪里使用 THP。 不明白为什么术语中会交替使用 "large "和 "huge"。总之,OpenJDK 支持这两种模式: $ java -XX:+PrintFlagsFinal 2>&1 | grep Huge bool UseHugeTLBFS = false {product} {default} bool UseTransparentHugePages = false {product} {default} $ java -XX:+PrintFlagsFinal 2>&1 | grep LargePage bool UseLargePages = false {pd product} {default} -XX:+UseHugeTLBFS 将 Java 堆映射到 hugetlbfs 中,后者应单独准备。
JVM 剖析花园:1 - 锁粗化及循环

JVM 剖析花园:1 - 锁粗化及循环

D瓜哥
“JVM 剖析花园”是由 JVM 研发专家及性能极客 Aleksey Shipilëv 撰写的一个系列文章,专门介绍一些有关 JVM 的基本知识。笔者也是前几年无意间发现的一片宝藏文章。早就有翻译过来,介绍给大家的想法,可惜一直未能付诸实践。最近在查资料时,无意间又翻到了这个系列,遂下定决心,完成这个萌发已久的小想法。 为了便于理解,对该系列的名字做了微调,原文是“JVM Anatomy Quarks”,将原文的“Quarks”(夸克)翻译为了“花园”。 “JVM 解剖花园”是一个正在进行中的小型系列文章,每篇文章都会介绍一些有关 JVM 的基本知识。这个名字强调了一个事实,即单篇文章不能孤立地看待,这里描述的大部分内容都会很容易地相互影响。 阅读这篇文章大约需要 5-10 分钟。因此,它只针对单一主题、单一测试、单一基准和单一观察进行深入探讨。这里的证据和讨论可能是轶事,并没有对错误、一致性、写作风格、语法和语义错误、重复或一致性进行实际审查。请自行承担使用和/或信任的风险。 以上是该系列介绍。这里介绍一次,后续文章不再赘述。 问题 众所周知,Hotspot 可以进行 锁粗化优化,有效合并多个相邻的锁定块,从而减少锁定开销。它能有效地对如下代码做优化: synchronized (obj) { // statements 1 } synchronized (obj) { // statements 2 } 优化后: synchronized (obj) { // statements 1 // statements 2 } 现在,今天提出的一个有趣问题是:Hotspot 是否会对循环进行这种优化?例如: for (...) { synchronized (obj) { // something } } 是否会被优化成如下这样: synchronized (this) { for (...) { // something } } 理论上,没有什么能阻止我们这样做。我们甚至可以把这种优化看作是 循环判断外提,只不过这里是针对锁而已。然而,这样做的缺点是有可能使锁变得过于粗糙,从而导致特定线程在执行大循环时占用锁。
GC 调优成功案例:减少新生代的大小

GC 调优成功案例:减少新生代的大小

D瓜哥
当对垃圾回收性能做调优时,不仅能改善垃圾回收暂停时间,还能改善整个应用程序的响应时间并降低云计算成本。最近,我们帮助调整了一个流行应用程序的垃圾回收行为。仅仅是一个微小的改动,就带来了巨大的改善。让我们在这篇文章中讨论一下这个垃圾回收调整的成功案例。 垃圾收集关键绩效指标 有句名言叫“无法衡量的东西就无法优化”。说到垃圾回收的调整,您只需关注 3 个主要关键绩效指标 (KPI): GC 暂停时间 GC 吞吐量 CPU 消耗量 垃圾回收运行时,会暂停应用程序。“GC 停顿时间”表示应用程序在垃圾回收事件中停顿的时间。该指标以秒或毫秒为单位。 “GC 吞吐量”表示应用程序处理客户事务的总时间与处理垃圾回收活动的总时间之比。该指标以百分比为单位。例如,如果有人说他的应用程序的 GC 吞吐量是 98%,这表明该应用程序有 98% 的时间用于处理客户活动,其余 2% 的时间用于处理垃圾回收活动。 即使是处理一个简单的请求,现代应用程序也会创建成千上万个对象。因此,垃圾收集器必须在后台不断运行,以释放为每个请求创建的成千上万个对象。因此,垃圾回收往往会消耗大量的 CPU。因此,在调整垃圾回收性能时,还应研究 CPU 消耗。要了解有关这些 KPI 的更多信息,请参阅: 内存调整: 关键性能指标。 如何获取这些 KPI? 在调优垃圾回收性能时,垃圾回收日志是您最好的朋友。您可以通过 这篇文章 给出的 JVM 参数在应用程序中启用垃圾回收日志。建议始终开启垃圾回收日志,因为它能提供丰富的信息,有助于预测中断、排除生产问题并帮助进行容量规划。此外,启用垃圾收集不会给应用程序增加任何明显的开销。 启用垃圾收集日志后,您可以使用免费的垃圾收集日志分析工具,如 GCeasy、 IBM GC & Memory visualizer 和 Google Garbage cat 等,查看上述关键绩效指标。 在下面这篇文章,教你 如何进行 GC 日志分析。 垃圾回收行为基线 介绍到此为止。让我们回到本文最初的主题。我们在这个流行的应用程序上启用了垃圾回收日志。我们让应用程序运行了 24 小时。然后,我们将生成的 GC 日志文件上传到 GCeasy 工具。该工具提供了具有洞察力的图表和 GC KPI。该应用程序的 GC 吞吐量为 96.176%,平均暂停时间为 12.429 秒。
如何实现 GC 的高吞吐量?

如何实现 GC 的高吞吐量?

D瓜哥
三四十年前,开发人员负责释放在应用程序中创建的对象。业务应用程序相当复杂,有不同的工作流、用例和场景。即使开发人员在某个场景中少释放一个对象,对象也会在内存中累积,造成内存泄漏。Java 于 1995 年推出时,承诺自动进行垃圾回收。它将删除对象的责任从开发人员转移到了 Java 虚拟机(JVM),从而彻底改变了内存管理。整个行业都积极拥抱了这一创新理念,因为开发人员不再需要操心手动内存管理。从那时起,自动垃圾回收已成为所有现代编程语言的默认功能。 在本篇文章中,我们将探讨垃圾回收过程中的一个关键性能指标:"GC 吞吐量"。我们将了解它的含义、在 Java 应用程序中的重要性以及它对整体性能的影响。此外,我们还将深入探讨提高 GC 吞吐量的可行策略,为现代软件开发释放其优势。 什么是垃圾回收吞吐量? 每当运行自动垃圾回收事件时,应用程序都会停顿,以识别内存中未引用的对象并将其释放。在停顿期间,不会处理任何客户请求。垃圾回收吞吐量请求应用程序处理客户请求的时间占多大比例,垃圾回收活动的时间占多大比例。例如,如果有人说他的应用程序的 GC 吞吐量是 98%,这意味着他的应用程序有 98% 的时间用于处理客户请求,其余 2% 的时间用于处理垃圾回收活动。 高 GC 吞吐量是可取的,因为它表明应用程序有效地利用了系统资源,从而减少了停顿,提高了整体性能。相反,GC 吞吐量低会导致垃圾回收停顿时间增加,影响应用程序的响应速度,造成性能瓶颈。监控和优化 GC 吞吐量对于确保应用程序的顺利执行和响应速度至关重要。在下一节中,我们将探讨查找应用程序 GC 吞吐量的方法,并了解如何解释结果以优化 Java 应用程序性能。继续… 如何找到应用程序的 GC 吞吐量? 垃圾回收日志是研究 GC 性能的最佳来源。如果你的应用程序运行在 JVM 上,你可以通过 如何进行 GC 日志分析 文章中提到的 JVM 参数启用 GC 日志。启用 GC 日志后,让应用程序处理流量至少一天,以观察高流量和低流量时段各自的运行情况。之后,可以将生成的 GC 日志文件上传到 GC 日志分析工具,以获得有价值的分析结果。一些常用的 GC 日志分析工具包括 GCeasy、 IBM GC & Memory visualizer 和 Google Garbage cat 等。这些工具将报告 GC 吞吐量以及其他重要的 GC 指标。下面是 GCeasy 工具的摘录,展示了包括 GC 吞吐量在内的各种 GC 关键性能指标 (KPI) 报告。
Java ZGC 调优

Java ZGC 调优

D瓜哥
ZGC 是一种专门的垃圾回收器,主要用于管理大型堆和尽量减少 Java 应用程序中的停顿。它能应对在内存密集型工作负载和一致的响应时间至关重要的情况下的垃圾回收的挑战。ZGC 利用并发处理能力和先进的算法,为优化现代 Java 应用程序的性能提供了有效的解决方案。在本篇文章中,将专门探讨调整 ZGC 以提高性能的技术。不过,如果想了解更多基础知识,可以观看在 JAX 伦敦会议上发表的 垃圾回收调优 讲座。 如何启用 ZGC? 确保使用的 Java 版本支持 ZGC。OpenJDK 从 JDK11 开始支持 ZGC。在启动应用程序时添加以下 JVM 参数,这样就可以在 Java 应用程序中启用 ZGC 垃圾收集器: # D瓜哥 · https://www.digauge.com -XX:+UseZGC -XX:+ZGenerational (1) 1 D瓜哥注:分代 ZGC 从 OpenJDK 21+ 开始支持。 何时使用 ZGC? 如果应用符合其中任何一项要求,就可以考虑使用 ZGC: 堆大小较大:ZGC 特别适合堆容量较大的应用程序,堆容量通常在数十 GB 或更大。如果应用需要大量内存,ZGC 的低延迟特性将使其成为一个令人信服的选择。 低延迟要求:当应用需要一致的响应时间和低延迟性能时,ZGC 将大显身手。在需要最大限度缩短垃圾回收暂停时间的情况下,ZGC 表现出色,特别适合交互式应用和实时性应用。 具有不同工作负载的应用:ZGC 专为处理不同的工作负载而设计,因此适用于内存使用模式不可预测的应用。无论应用程序经历的是周期性的,突发性的,还是富有变化性的负载,ZGC 都能有效地适应这些调整。 ZGC 调优参数 ZGC 是 Java 中的一种垃圾收集器,它采用了一种不同的调优方法:将暴露的 JVM 参数数量降至最低。与需要细粒度调整的传统垃圾收集器不同,ZGC 专注于优化大型堆的管理,同时以最小的配置开销提供高效的垃圾收集。这种精简的方法允许开发人员主要关注一个关键的 JVM 调整参数:堆大小。
Java 虚拟机操作码探秘:常量指令

Java 虚拟机操作码探秘:常量指令

D瓜哥
在 Java 虚拟机指令(操作码)集 中给出了一个操作码的列表。针对所有的指令,仅仅给出了一个大概介绍,对理解来说可以说毫无助力。为了弥补这个短板,这里也学习 “Hessian 协议解释与实战”系列 那样,来一个详细解释和实战,配合实例来做个深入分析和讲解。这是这个系列的第一篇文章,就以列表中第一部分“常量”指令开始。 从 Java 虚拟机指令(操作码)集 列表上来看,一共 21 个指令;按照处理数据的类型,合并同类项后,剩下有 nop、 aconst_null、 iconst_<i>、 lconst_<l>、 fconst_<f>、 dconst_<d>、 bipush、 sipush、 ldc 和 ldc2_w 等几个指令。下面,按照顺序,对其进行一一讲解。 操作码助记符的首字母一般是有特殊含义的,表示操作码所作用的数据类型: i 代表对 int 类型的数据操作; l 代表 long; s 代表 short; b 代表 byte;c 代表 char;f 代表 float, d 代表 double; a 代表 reference。 尖括号之间的字母指定了指令隐含操作数的数据类型,<n> 代表非负的整数; <i> 代表是 int 类型数据; <l> 代表 long 类型; <f> 代表 float 类型; <d> 代表 double 类型。 另外还需要指出一点:这种指令表示法在整个 Java 虚拟机规范之中都是通用的。