基于 Docker 搭建开发环境系列:
基于 Docker 搭建开发环境(一):数据库+监控
基于 Docker 搭建开发环境(二):EFK 日志套件
基于 Docker 搭建开发环境(三):链路追踪
去年,很多同事要换 Mac 本,所以,写了 新 Mac 安装软件脚本,方便大家一键换机。最近想玩一下 Spring Cloud 以及相关周边的部署、监控等开源解决方案。由于组件众多及为了便于迁移和共享,计划基于 Docker 及 Docker Compose 搭建一套相关的开发环境。记录一下,方便有相同需求的朋友借鉴。
最新版的 Docker 在下载镜像时,会先访问一下 Docker 的官方站点。由于国内众所周知的网络情况,访问 Docker 官方站点总失败。所以,即使配置了国内 Docker 镜像站点也会失败。只需要将 Docker 软件回滚到 4.30.0 即可。(Mac 下验证有效,其他操作系统待进一步验证。) MySQL 开发中,最常用的应该就是数据库了。所以,先来搞 MySQL 数据库。
创建如下目录结构,并添加相关相关文件:
$ tree . ├── README.adoc ├── clean.sh ├── data │ └── mysql │ └── .gitkeep ├── docker │ ├── config │ │ └── mysql │ │ └── init.sql │ ├── env │ │ └── mysql.env │ └── images │ └── mysql.dockerfile └── docker-compose.yml
在 DDD 是银弹吗? 中,D瓜哥分享了关于领域驱动设计的三个问题。最近在读一本书 《架构设计2.0:大型分布式系统架构方法论与实践》。(这本书还不错,推荐)这本书中,花了两个章节的篇幅,重点谈论了领域驱动设计。引用书中的观点,结合个人开发经验,再来谈一谈 DDD 是否是银弹?
软件建模的困难 首先,必须面对的一个事实是:软件建模,困难重重;尤其是对于复杂业务的建模,更是难上加难。
对于复杂业务的软件开发,其生命周期大概分为如下五个阶段:
确定业务目标和业务价值。
比如某消费信贷业务。
目标被拆解成一系列核心功能点。
比如消费信贷下的授信、交易、账务等。
围绕这些功能点定义业务流程、业务规则,以及整个过程设计什么样的业务数据或业务对象。
比如账单分期金额必须大于 100 元。
领域建模。
比如对账务系统进行建模。
基于领域模型做技术架构的设计。
比如是否要做读写分离?是否要做分库分表等?
软件建模的本质是找出现实世界中的“不变形”。但是,现实世界中,唯一不变的就是这个世界在不断变化!所以,建模的过程也是一个反复的过程。如下图:
图 1. 复杂业务软件开发的生命周期 几乎不存在稳定的领域模型 我们追求一个稳定的领域模型,但是,现实却给了我们重重一击:稳定的模型几乎不可能做到。原因如下:
意识问题。
在消费、业务及产品等关注的是业务流程。唯独开发人员要将业务流程转化成业务模型。
现实世界的复杂性。
现实业务是复杂的,建模只是抽取了一个现实业务某一时刻的业务形态。但是,业务形态会有变化的,比如取现前期不可分期,后期业务迭代可能就会运行进行分期。
迭代速度。
互联网公司要求“小步快跑,快速迭代”。这与模型的稳定其实是矛盾的。为了业务的迭代速度,只能牺牲模型的稳定性,为了赶工期,只能在模型上不断打补丁。
火候的掌握。
开发人员的设计能力无法一蹴而就。既需要思考,又需要反复练习。在快速的业务迭代和人员流动下,开发人员根本没有时间锤炼自己的设计能力。那么,对于设计火候的掌握,也就很难达到理想中的水平。
领域驱动设计的困难 书中总结了实施领域驱动设计的五个困难,D瓜哥逐一谈谈自己的看法:
领域驱动设计本身只是一套思维方法,而不是要严格执行的标准,所以其本身弹性很大。
这个问题,D瓜哥在上一篇文章中已经讨论过了。弹性太大,就有太多值得商榷的地方,也许初次开发,还可以按照某个人的想法一以贯之。但是,随着参与维护的人员增多,每个人都会不由自主地会带入个人的一些想法,各种想法的碰撞,必然就会引入代码结构的混乱。
思维方式的转换很难。
绝大多数面向业务的开发人员,尤其是 Java 开发人员,对三层架构已经有根深蒂固的认识。思维方式已经被打上深深的烙印,想要改变,坦白讲,极其困难。尤其是,没有一个同一个的标准和广泛认可的实现范例,完全靠摸着石头过河,必然会“一千个读者,就有一千个哈姆雷特。”
领域驱动设计的实施需要强大的技术基础实施来保证。
D瓜哥私以为这个倒不是什么问题。针对技术问题,尤其是一些共性问题,都有成熟的解决方案。只要能合理搭积木,就可以解决相应的问题。
大量存量的老系统,重构成本大于收益,没有重构动力。
编程第一准则:代码能跑就不要动。重构引入的问题谁来解决?重构带来的事故谁来负责?这个时候必须祭出这张图了:
图 2. 代码能跑就不要动 当然,私以为不是程序员反感重构代码,更多是因为下面这个因素。
在互联网的快速开发迭代面前,很少有人可以静下心来在软件方法论层面去精雕细琢,更多的是快速堆砌功能,完成业务需求开发。
业务的快速迭代,导致根本没有时间让开发人员去优化代码。可口的饭菜需要恰当的火候和足够的时间,优雅的软件建模也需要恰当的火候和足够的时间。精心地软件建模需要三个月,业务让你一个月上线,而且还是加班加点才能干完。结合实际来看,绝大多数情况都会想业务妥协吧?!
领域驱动设计的出路 书中的观点是做个折中:在宏观层面,遵循领域驱动设计的方法论;在微观层面,不严格遵循领域驱动设计的方法论。
D瓜哥是这样理解的:可以利用领域驱动设计里面的限界上下文的思想,把领域做个分割,划分成业务更聚合的子域。在子域内部,提炼出统一语言,来规范业务、产品和开发沟通的业务术语。在子域交互的接口层面,进行精心设计,精雕细琢。至于子域及接口的内部实现,就交给开发团队自己决策,只要满足对应的技术指标(比如每秒要支撑多大的访问量)即可。
在部门内部讨论时,D瓜哥还给出了一个更具操作性和落地性的方案:现实面临的问题是代码冗余,技术欠债,不容易维护。先放下关于领域驱动设计的无谓讨论,利用每一次开发的机会,把冗余代码删除,把代码重构和优化,一步一步地精炼代码,即使不谈领域驱动设计,相信在逐步重构和优化下,技术欠债会逐渐弥补,可维护性也会逐步提高。
史前时期最骇人的景象,莫过于一群巨兽在焦油坑里做垂死前的挣扎。不妨闭上眼睛想像一下,你看到了一群恐龙、长毛象、剑齿虎正在奋力挣脱焦油的束缚,但越挣扎,焦油就缠得越紧,就算他再强壮、再厉害,最后,都难逃灭顶的命运。过去十年间,大型系统的软件开发工作就像是掉进了焦油坑里……
— 佛瑞德·布鲁克斯(Frederick P. Brooks) 《人月神话》 应该早在 2019 年,在 左耳朵耗子哥 的推荐下阅读了 《领域驱动设计》,并将读书摘要整理成几篇文章:
《领域驱动设计》读书笔记(一):运用领域模型
《领域驱动设计》读书笔记(二):模型驱动设计的构造块
《领域驱动设计》读书笔记(三):通过重构来加深理解
《领域驱动设计》读书笔记(四):战略设计
部门要搞 DDD 和体系化建设,正好有一个核心项目要做重构,领导让实践一下领域驱动设计,苦于没有范例可以参考,感觉无处下手,所以又读了 《中台架构与实现·基于DDD和微服务》(最早读的是极客时间专栏,后专栏编撰成该书)。
后来,又陆陆续续看了好多领域驱动设计的相关文章。对于领域驱动设计,即了解过,也实践过。所以,结合自身的经历和体会,谈一谈我的感受。不吹不黑,重点谈三个问题。
1. 如何快速上手? 上面介绍了一下D瓜哥的个人经历,是付出了一点的时间和精力的,由此引出了第一个问题:如何快速上手?对于一个工作多年,经验丰富,也算勤奋好学的高级码农,上手还如此困难重重,那么对于一个刚刚参加工作的职场新人,上手是否会更加困难?又该如何克服这个困难?
任何一家公司,尤其是大型技术公司,都是由初中高级工程师组成的,而且成员人数也是由多到少,参与实际开发工作,大概率也会由多到少,初级开发工程师干了大量的实际编码工作。如果无法吸引大多数的初级工程师参与进来,只有个别的高级工程师去落地,那么,所谓的领域驱动设计,只能成为空中楼阁,海市蜃楼。华而不实,无法落地。
但是,由于经验少,这对于初级工程师来说,也许是一个优势。毕竟,一张白纸,可以画出各种美丽的画卷。中高级工程师已经习惯于传统的开发模式,思维已经定格。但是,初级工程师,反倒是嗷嗷待哺,更容易塑性。可惜的是,现在没有好的示例可以学习。
2. 哪里有可以参考的示例? 快速上手的最好办法,就是给一个完整的示例,拿来直接抄作业。对于入门的程序员,学东西上手最快的办法就是抄代码。把示例代码,拿过来改吧改吧就能跑起来,无形中就学会怎么写代码了。对于传统的三层架构,有太多的示例可以来学习了,比如 SpringSide。
从 《Domain-Driven Design》 这本书在 2003 年出版到现在,已经有 21 年了。到现在为止,也没有见到一个开源的、能运行起来的基于领域驱动设计的项目。也可能是鄙人孤陋寡闻,坐井观天,没有发现。如果谁发现了,欢迎向我反馈。
作为对比,我们来看一下 Spring 的发展过程。Spring 的思想最早是在 《J2EE Development without EJB》 这本书里出现的,这本书是在 2004 年 6 月出版的。这本书出版后,开源社区根据这本书里面的思想及代码片段,开发出了 Spring 框架。在两年后,Spring 之父 Rod Johnson 接着出版了 《Professional Java Development with the Spring Framework》,系统介绍了一下 Spring 框架的各种使用案例。到 2008 年我上大学的时候,在国内的培训行业,已经开始重点讲解 Spring 了。
其实,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 的一半。
J21-Gen-ZGC 和 J21-G1 还是一如既往的稳。
这次测试中,J17-ZGC 也很稳,有些出乎意料。但是,结合下面 JVM CPU 使用率 和 系统 CPU 使用率 来看,J17-ZGC 和 J21-ZGC 的 CPU 使用率早早就达到 90%+,再结合上面两个测试,从稳定性来看,J17-ZGC 和 J21-ZGC 只能被排除掉。
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
1050,00:09:30 ~ 00:19:30
1100,00:19:30 ~ 00:29:30
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;
CPU 消耗:J21-G1 < J8-G1 < J17-ZGC < J21-Gen-ZGC < J21-ZGC;
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 来做测试,但是,这样面临好几个问题:
问题 什么是大页(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}
“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 } } 理论上,没有什么能阻止我们这样做。我们甚至可以把这种优化看作是 循环判断外提,只不过这里是针对锁而已。然而,这样做的缺点是有可能使锁变得过于粗糙,从而导致特定线程在执行大循环时占用锁。
实验 要回答这个问题,最简单的方法就是找到当前 Hotspot 优化的正面证据。幸运的是,有了 JMH,这一切都变得非常简单。它不仅有助于建立基准,还有助于工程中最重要的部分—基准分析。让我们从一个简单的基准检查程序开始:
@Fork(..., jvmArgsPrepend = {"-XX:-UseBiasedLocking"}) @State(Scope.Benchmark) public class LockRoach { (1) int x; @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE) public void test() { for (int c = 0; c < 1000; c++) { synchronized (this) { x += 0x42; } } } } 1 完整代码在 这里。 这里有几个重要的技巧:
使用 -XX:-UseBiasedLocking 禁用偏向锁可以避免更长的预热时间,因为偏向锁不会立即启动,而是会在初始化阶段等待 5 秒(参见 BiasedLockingStartupDelay 选项)。
禁用 @Benchmark 的方法内联有助于在反汇编时将其分离。
增加一个神奇的数字 0x42,有助于在反汇编中快速找到增量。
当对垃圾回收性能做调优时,不仅能改善垃圾回收暂停时间,还能改善整个应用程序的响应时间并降低云计算成本。最近,我们帮助调整了一个流行应用程序的垃圾回收行为。仅仅是一个微小的改动,就带来了巨大的改善。让我们在这篇文章中讨论一下这个垃圾回收调整的成功案例。
垃圾收集关键绩效指标 有句名言叫“无法衡量的东西就无法优化”。说到垃圾回收的调整,您只需关注 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 秒。
图 1. 基线 GC KPI(由 GCeasy 生成)