基于 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瓜哥想拿传统的三层架构的发展来做对比,可惜没有找到更确切的时间线。 期待一个完整的、基于领域驱动设计的、能正常运行起来的开源项目尽早出现!
家里一个亲戚今年参加中招考高中,由于成绩不是很理想,所以,就面临一个问题:选择哪所高中去求学?由此引发的一系列思考和讨论,D瓜哥觉得非常有共性,分享出来,希望给需要的家长一个参考。
1. 去重点高中怕跟不上,选择去上私立高中 一些家长可能会有这样的想法:孩子中考成绩不理想,如果去重点高中,担心孩子跟不上课程,所以,选择去上私立高中。
关于这个问题,是一个伪命题。高中最重要的考核就是三年以后的高考。高考对所有参加的考生都是一视同仁的,不会因为私立高中和公立高中而有什么差别。(同一个省的高考生是一样的,跨省则不一定。这里不予讨论。)所以,只要上高中,就必须努力跟上,尽力向前冲!否则,就会掉队,考不上大学,考不上大学,就失去了上高中的主要意义。所以,根本无需考虑能不能跟上的问题。这个问题只有一个选择:只要求学,就只能加油往前冲!
2. 选择高中的关注点 由于孩子成绩不理想,那么可能无法进入理想的中学。接下来的一个问题就是:如何选择高中?
对于这个问题,私以为对于高中的的考察标准只有一条:就是高考录取率,各个层次本科的录取率。但是在选择高中时,有一些值得关注的点,下面一一说明。
2.1. 生源质量 聪明的学生在任何高中都有好的成绩。脑瓜子不灵的学生,即使送到人大附中这样全国最好的高中也难有好的成绩。
另外,如果身边有一些成绩好的同学,那么当自己学习有问题时,可以更方便地找同学帮忙解答。如果身边事一群瓜娃子,出现学习问题,也无法及时解决,日积月累,成绩自然难以提高!
2.2. 学校的历史成绩 成绩好的学校大概率会一如既往地好下去。而差的学校想变好,却需要付出非常巨大的努力和相当长的时间来改善。它还需要时间,逐步向社会来证实它的实力,以求获取更好的生源。对于学校来说,这个时间是可以耗得起的。但是,作为学生,上学的时间段时是卡死的,耗不起,也等不起。
2.3. 老师资历 好的老师有更好的教学方法,对学生学习更有帮助。
但是,好的师资也需要好的学生来衬托。老师和学生是相互成就的。只有伯乐,没有千里马,伯乐只能悲叹“英雄无用武之地”!
2.4. 学习氛围 大多数人都从众。所以,学习氛围好的地方,大部分人会被带动起来学习。但是,如果学习氛围一般,大多数人就会随波逐流,逐步落伍。
2.5. 私立高中的困境 目前,国内大多数人更愿意相信公立高中。认为公立高中有政府托底,更有保障。而对私立高中,大多数人缺乏足够的信任。
当然,还有一个原因是私立高中普遍学费高昂,这对于很多家长来说,也是一个不小的负担。
2.6. 小结 所以,综合上述情况,导致的结果是,大多数情况下私立高中的生源质量普遍一般,甚至不好。由于生源质量问题,导致无论是学校的历史成绩,还是学校氛围,可能都会差强人意。所以,如果家长希望通过上私立高中来提高孩子成绩。综合来看,个人觉得很难实现,甚至基本不可能(可能比买彩票中大奖的概率要高一点)。最后的结果,可能钱也花了,时间也耽误了,孩子也没有太大的起色,最后高考成绩也不理想。
还有一个种情况,从小就一直上私立学校的情况,这种大多数是双语教学,面向的也是国外的大学,大部分是不把高考作为第一选项的。这种情况,不在此讨论范围。
3. 转学 如果一个学校不行,在条件允许的情况下,尽快转学,转到更好的学校。这个时候,由于入学晚的原因,可能不容易合群,家长要多鼓励和开导,提供足够的情绪价值,帮助孩子早日完成过渡。
另外,学生也不要太在意,一般情况下,等到上高二会进行二次分班,大家又都从新开始了。
4. 培优班 如果一个学生没有考进更好的中学,去了一所一般的高中,而且还进培优班了。这里有三点需要注意:
如果不是托关系进培优班,那么只能说明这个高中生源质量确实一般。
成绩一般的学生在培优班里,有可能每次考试都是班里垫底。这就要多去关注一下学生在整个高中的整体成绩排名。好的情况是,在班级垫底,但在年级总体排名靠前,这种情况家长要多鼓励学生,培养学生有一颗强大的心脏。
另外一种情况是,由于可能长时间在班级成绩排名垫底,这对学生来说,无形中会带来很大的压力,导致有些学生可能会自暴自弃。所以,家长要做好安慰和支持,多发现学生的优点和长处,多鼓励孩子。
5. 相比私立高中的更优解 如果有能力支付私立高中的钱,相对来说,另外一条路可操作性更大一些:尽力去层次更好的公立高中,把省下来的学费给孩子请一对一的私教辅导。
6. 私教辅导 学生有各种各样的性格,老师也有跟种各样的教法,所以,孔子提出要“因材施教”。如果学生和私教老师不是很契合,那么请私教老师不一定就能提高成绩。如果已经请私教的情况下,家长一定要多关注学生的反馈,了解学生的学习情况,判断是否合拍,如果不合适,要尽早更换私教老师。
7. 高中成绩可以突飞猛进 无需太过担心孩子的高中入学成绩排名。以D瓜哥的上学经历来看,在高一,学生的成绩排名会有剧烈的波动,有人入学成绩很差,后来迎头赶上的;也有人入学成绩很好,但后来却跌落谷底。所以,只要学生初中不掉队,上了高中肯下功夫学习,成绩就会逐步提高。(掉队的学生可能就不会想着去上高中了)
8. 不要选择职高或“3+2”大专 还有一个选项:选择职高或者是“3+2”的大专。私以为,这个选项可以直接忽略,除非迫不得已。
大多数成绩还可以的学生都去上高中了,所以,能选择去上职高或者大专的,大部分成绩都一般,甚至很差。结果,大部分人可能就会不学无术,聚众打架斗殴。最后,孩子不仅可能没学好,甚至沾染了一堆坏毛病。
另外,现在学历贬值严重,满大街都是本科生,大专生更没出路(可以看看现在公务员招聘都要求什么学历)。上了大专,如果想有出路,还要考专升本,相当于一次高考,那为何不直接上高中呢?
当然,在职高或者大专里面,也不乏一些学有所成的人才,但这样的概率太低了。这个问题,不再争论,争论就是你对。
总结 综合上述的讨论,对于孩子上学,最优解就是上尽可能好的高中。如果上不了最好的高中,那就选次一点的公立高中。如果家庭条件允许,可以给孩子请一些私教辅导,来帮助孩子提高成绩。
最后,送给正在求学的学子们一句话:你充满了潜能,但你的努力还远远不够!祝福每一个学子学有所成!
D瓜哥在 Spring 扩展点概览及实践 中概要性地介绍了一下 Spring 的核心扩展点。里面也提到了 BeanFactoryPostProcessor 和 BeanDefinitionRegistryPostProcessor,但仅仅提了一句,没有深入研究。在 Spring 扩展点实践:整合 MyBATIS 中,由于 MapperScannerConfigurer 实现了 BeanDefinitionRegistryPostProcessor 接口,也只是简单介绍了一些作用,又一次没有深入研究。
最近,在开发一个插件时,遇到了一个问题:利用 BeanFactoryPostProcessor 对一些特定 BeanDefinition 设置属性,但生成的 Bean 却没有相关的属性值。由此,对 BeanFactoryPostProcessor 做了一些研究。记录一下,以备不时之需。
Spring 启动流程简介 在 Spring 启动流程概述 中,D瓜哥对 Spring 的启动流程做了比较详细的介绍。同时画了一张启动流程图,如下:
图 1. AbstractApplicationContext.refresh — 重塑容器 从该图中可以明显看到,如果需要对 Spring 的 BeanDefinition 做些修改,那么,就需要通过实现 BeanFactoryPostProcessor 接口,来对 Spring 做些扩展。坦白讲,为了上述流程图只展示了一个非常概要性的流程。如果深入一下 invokeBeanFactoryPostProcessors 方法的细节,会发现这又是一番天地。
BeanFactoryPostProcessor 调用详解 D瓜哥把 invokeBeanFactoryPostProcessors 方法的流程图也画了出来,细节如下:
图 2. BeanDefinitionRegistryPostProcessor & BeanFactoryPostProcessor 调用过程 从这张流程图上可以看出 BeanFactoryPostProcessor 的调用过程,比在 Spring 启动流程概述 中介绍的要复杂很多:
首先,执行 BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry 方法,顺序如下:
关于 BeanDefinitionRegistryPostProcessor 的处理流程,D瓜哥在 Spring 扩展点概览及实践:BeanDefinitionRegistryPostProcessor 中有更详细的描述,不了解的朋友请参考那篇文章的介绍。
在 OpenJDK 21 升级指南 中,给大家分享了一下升级到 OpenJDK 21 中遇到的一些问题。文末留了一个小问题:生产环境的 Java 21 启动参数怎么配置?这篇文章将给出 D瓜哥的答案。
先说明一下生产环境的机器配置:4C8G,四个内核,8G 内存。
启动参数 鉴于 JVM GC 性能测试(二):递增流量 和 JVM GC 性能测试(三):真实流量 中,G1 GC 的惊艳表现,这里分别提供 Gen ZGC 和 G1 GC 两个配置。
两个配置差距级小,为了方便复制粘贴,还是分两个来展示。 Gen ZGC 配置 追求极致低延迟,就上 GenZGC,它通过牺牲大约 10% 的吞吐量,换来无与伦比的低延时。
注意:使用时,请修改日志目录! ## 变量配置 ####################################################################### # java -XshowSettings:all --展示所有配置项(测试发现也不全) -Dfile.encoding=UTF-8 # https://zhuanlan.zhihu.com/p/455313866 # https://zhuanlan.zhihu.com/p/455746995 # https://blog.csdn.net/u014149685/article/details/83002405 # 随机数来源 -Djava.security.egd=file:/dev/./urandom -Djava.security=file:/dev/./urandom # https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html # https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/doc-files/net-properties.html # DNS 过期时间 -Dnetworkaddress.cache.ttl=10 # -Dsun.net.client.defaultConnectTimeout=60000 -Dsun.net.client.defaultReadTimeout=60000 #-Dsun.net.inetaddr.ttl=300 # https://mdnice.com/writing/47e729bbf8e44431a396a481ed173dae -Djava.awt.headless=true # https://blog.csdn.net/maverick0/article/details/8282472 -Djmagick.systemclassloader=no # From Cassandra # On Java >= 9 Netty requires the io.netty.tryReflectionSetAccessible system property # to be set to true to enable creation of direct buffers using Unsafe. Without it, # this falls back to ByteBuffer.allocateDirect which has inferior performance and # risks exceeding MaxDirectMemory # https://blog.csdn.net/jdcdev_/article/details/132843927 -Dio.netty.tryReflectionSetAccessible=true # 内部中间件 # 注意:一些中间件会内嵌 Netty,这里建议同步修改其相关参数配置。 -Dump.profiler.shade.io.netty.tryReflectionSetAccessible=true -Dtitan.profiler.shade.io.netty.tryReflectionSetAccessible=true # Revert changes in defaults introduced in https://netty.io/news/2022/03/10/4-1-75-Final.html -Dio.netty.allocator.useCacheForAllThreads=true -Dio.netty.allocator.maxOrder=11 # 内部中间件 # 理由上面已讲 -Dump.profiler.shade.io.netty.allocator.useCacheForAllThreads=true -Dump.profiler.shade.io.netty.allocator.maxOrder=11 # Byte Buddy 支持21 -Dnet.bytebuddy.experimental=true -Dpfinder.shade.net.bytebuddy.experimental=true ## 参数配置 ##################################################################### # https://jacoline.dev/inspect -- JVM 参数诊断 # https://chriswhocodes.com/corretto_jdk21_options.html # https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html # https://blog.csdn.net/wxb880114/article/details/119888587 # https://www.cnblogs.com/three-fighter/p/14644152.html #- https://www.skjava.com/article/2134434173 # 解锁诊断参数 -XX:+UnlockDiagnosticVMOptions # 解锁试验参数 -XX:+UnlockExperimentalVMOptions # 启用 ZGC -XX:+UseZGC # 启用分代ZGC -XX:+ZGenerational # https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html # 加快 GC 的时间和能力 -XX:ZAllocationSpikeTolerance=5 -XX:ConcGCThreads=2 -XX:ParallelGCThreads=4 # G1 GC #-XX:+UseG1GC #-XX:MaxGCPauseMillis=50 # 初始堆大小,等价于 -XX:InitialHeapSize -Xms4608m # 弱最大堆,尽量保持,但是可以突破 #-XX:SoftMaxHeapSize=3g # 最大堆大小,等价于 -XX:MaxHeapSize -Xmx4608m # 归还未使用的内存 #-XX:+ZUncommit # 设置每个线程的堆栈大小,等价于 -XX:ThreadStackSize=512k -Xss512k # https://cloud.tencent.com/developer/article/1408384 # 本地内存大小 -XX:MaxDirectMemorySize=512m # https://cloud.tencent.com/developer/article/2277327 # https://cloud.tencent.com/developer/article/2277328 # https://cloud.tencent.com/developer/article/2277329 # 元空间 # 设置为 256m 时,发生过一次频繁 GC 导致应用无法相应的问题 -XX:MetaspaceSize=512m # 最大元空间 -XX:MaxMetaspaceSize=512m # https://cloud.tencent.com/developer/article/1408773 # https://blog.csdn.net/lidf1992/article/details/75050219 # 编译代码缓存空间 -XX:ReservedCodeCacheSize=256m # https://cloud.tencent.com/developer/article/1408827 # https://malloc.se/blog/zgc-jdk15 # https://tinyzzh.github.io/java/jvm/2022/04/24/JVM_CompressedOops.html # https://www.cnblogs.com/star95/p/17512212.html -- 由于从 JDK15 开始, # -XX:+UseCompressedClassPointers 与 -XX:-UseCompressedOops 之间的强 # 关联被打破,文章里关于上述这种搭配是不正确的。 TODO 可以从新测试验证一线。 # TODO 如果开启 -XX:+UseCompressedClassPointers,不确定 32M 是否够用? # https://www.zhihu.com/question/268392125 -XX:+UseCompressedClassPointers -XX:CompressedClassSpaceSize=48M # 关闭热度衰减 -XX:-UseCounterDecay # 内存占座 -XX:+AlwaysPreTouch # 禁止代码中显示调用GC -XX:+DisableExplicitGC # 关闭安全点间隔 -XX:GuaranteedSafepointInterval=0 # 避免循环无法进入安全点的问题 -XX:+UseCountedLoopSafepoints # https://blog.csdn.net/m0_46596655/article/details/123606813 -XX:LoopStripMiningIter=1000 # 打印命令行参数 -XX:+PrintCommandLineFlags # 显式地并发处理 GC 调用 -XX:+ExplicitGCInvokesConcurrent # https://panlw.github.io/15320998566522.html -XX:AutoBoxCacheMax=20000 # https://blog.csdn.net/zshake/article/details/88796414 # 省略异常栈信息从而快速抛出 -XX:-OmitStackTraceInFastThrow # https://www.jianshu.com/p/c9259953ca38 # 致命错误日志文件 -XX:ErrorFile=/path/to/log/jvm/hs_err_%p.log # https://blog.csdn.net/lusa1314/article/details/84134458 # https://juejin.cn/post/7127557371932442632 # 当JVM发生OOM时,自动生成DUMP文件。 -XX:+HeapDumpOnOutOfMemoryError # 设置上述DUMP文件路径 -XX:HeapDumpPath=/path/to/log/jvm/ # https://juejin.cn/post/6959405798556434440 # 设置 JFR 相关参数 # TODO 感觉这里不全乎,似乎需要 -XX:+FlightRecorder 来启用 # TODO 似乎可以设置文件,例如: -XX:StartFlightRecording=duration=200s,filename=flight.jfr # 不确定文件名是否可以这样配置,测试一下_%p-%t # Amazon Corretto JDK OK;Eclipse Temurin 不识别,并且监控报错 #-XX:StartFlightRecording=delay=5s,disk=true,dumponexit=true,duration=24h,maxage=5d,maxsize=2g,filename=/path/to/log/jvm/jfr_%p-%t.jfr.log #-XX:FlightRecorderOptions=maxchunksize=128m #-XX:StringDeduplicationAgeThreshold=threshold? TODO 测试之后才可以定 # https://zhuanlan.zhihu.com/p/111886882 # https://github.com/apache/cassandra/tree/trunk/conf # https://github.com/elastic/elasticsearch/blob/main/distribution/src/config/jvm.options # java -Xlog:help # 日志配置 -Xlog:gc*=debug,stringdedup*=debug,heap*=trace,age*=debug,promotion*=trace,jit*=info,safepoint*=debug:file=/path/to/log/jvm/gc_%p-%t.log:time,pid,tid,level,tags:filecount=10,filesize=500M # 分开设置可用,使用分开的配置 #-Xlog:gc*=debug,stringdedup*=debug,heap*=trace,age*=debug,promotion*=trace:file=/path/to/log/jvm/gc-%t.log:utctime,level,tags:filecount=10,filesize=200M #-Xlog:jit*=info:file=/path/to/log/jvm/jit_compile-%t.log:utctime,level,tags:filecount=10,filesize=50M #-Xlog:safepoint*=debug:file=/path/to/log/jvm/safepoint-%t.log:utctime,level,tags:filecount=10,filesize=50M # https://stackoverflow.com/a/44059335 # https://openjdk.org/jeps/261 # https://www.diguage.com/post/upgrade-to-openjdk21/ -- 内有详细介绍 # 开启模块权限:下面是D瓜哥需要的模块,请根据自己实际需求来调整。 --add-exports java.base/sun.security.action=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED --add-opens java.base/java.time=ALL-UNNAMED --add-opens java.base/java.util.concurrent.locks=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/jdk.internal.loader=ALL-UNNAMED # Netty 内部需要 https://stackoverflow.com/a/57892679 # https://github.com/netty/netty/issues/7769 # https://blog.csdn.net/thewindkee/article/details/123618476 --add-opens java.base/jdk.internal.misc=ALL-UNNAMED --add-opens java.base/sun.net.util=ALL-UNNAMED # 设置 -Dio.netty.tryReflectionSetAccessible=true 后,不设置该值也会报错 --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED --add-opens java.base/sun.util.calendar=ALL-UNNAMED --add-opens java.management/com.sun.jmx.mbeanserver=ALL-UNNAMED --add-opens java.management/java.lang.management=ALL-UNNAMED --add-opens java.management/sun.management=ALL-UNNAMED --add-opens jdk.management/com.sun.management.internal=ALL-UNNAMED
在 OpenJDK 21 升级指南 中提到, OpenRewrite 可以帮忙解决一些升级 OpenJDK 中发现的问题。随着不断的探索,D瓜哥发现,OpenRewrite 的功能远远不止这些。下面就挑选一些重要的功能来给大家做一些讲解。
为了方便查看改动点,建议将代码交给版本管理工具,比如 Git,来管理。 快速入门 OpenRewrite 是一套对源码做重构的大型生态系统,可以帮助开发人员减少技术债。所以,它提供了一套的相关工具。对于大多数开发人员来说,最方便的也许就是基于 Maven 插件的相关工具。这里以对 Java 的 import 语句排序来为示例展示一下 OpenRewrite 的使用方法。
在项目的 pom.xml 中增加如下配置:
<!-- @author: D瓜哥 · https://www.diguage.com --> <plugin> <groupId>org.openrewrite.maven</groupId> <artifactId>rewrite-maven-plugin</artifactId> <version>5.30.0</version> <configuration> <activeRecipes> <!-- import 排序 --> <!-- https://docs.openrewrite.org/recipes/java/orderimports --> <recipe>org.openrewrite.java.OrderImports</recipe> </activeRecipes> </configuration> </plugin> 然后执行如下命令:
mvn rewrite:run 执行会输出一大堆东西,这里就不再展示,执行完成后,使用 Git 查看一下改动点。如下图:
图 1. 使用 OpenRewrite 排序 import 的改动点 将这些修改点提交,就完成了一次优化, OpenRewrite 的基本使用,你学废了吗?
这里再多说一句: 由于 OpenRewrite 精巧的设计,可以通过使用不同的处方,进行各种各样的优化。所以,最重要的一点就是了解 OpenRewrite 各种不同的处方及使用办法。下面就介绍一下常用的处方及使用办法。
常用处方 升级到 Java 21 在 OpenJDK 21 升级指南 中提到,可以使用“科技与狠活”来解决很多升级中遇到的问题。这里就来实操一把。
OpenJDK 21 已经发布半年有余,在这个版本中, Generational ZGC 也一起发布了。在 ZGC | What’s new in JDK 16 中, Per Lidén 宣称,将 ZGC 的最大停顿时间从 10ms 降低到了 1ms。再加上 JVM GC 性能测试(二):递增流量 和 JVM GC 性能测试(三):真实流量 文中,GenZGC 的惊艳表现,这些种种先进技术,着实充满诱惑,忍不住想吃口螃蟹 🦀。这篇文章,D瓜哥就来分享一下,自己在升级 OpenJDK 21 中的一些经验。
本文仅介绍升级 OpenJDK 的相关内容,ZGC 原理等会专门撰文介绍。 升级依赖 依赖升级不是 KPI,也不涉及需求交付。所以,大多数项目的依赖自从项目创建后,就很少升级。如果想比较顺利地将项目升级到 OpenJDK 21,那么,先将项目所用依赖做一个整体升级是一个事半功倍的操作。可以直接使用 Maven 命令来检查依赖可以升级的情况:
mvn versions:display-dependency-updates 执行该命令后,会有如下类似输出:
# 检查依赖升级情况 $ mvn versions:display-dependency-updates # 此处省略一万个字 # @author: D瓜哥 · https://www.diguage.com [INFO] org.springframework:spring-aop ......... 5.3.33 -> 6.1.6 [INFO] org.springframework:spring-aspects ..... 5.3.33 -> 6.1.6 [INFO] org.springframework:spring-beans ....... 5.3.33 -> 6.1.6 [INFO] org.springframework:spring-context ..... 5.3.33 -> 6.1.6 [INFO] org.springframework:spring-core ........ 5.3.33 -> 6.1.6 [INFO] org.springframework:spring-jdbc ........ 5.3.33 -> 6.1.6 [INFO] org.springframework:spring-web ......... 5.3.33 -> 6.1.6 [INFO] org.mybatis:mybatis-2-spring ............ 1.1.0 -> 1.2.0 [INFO] org.mybatis:mybatis-spring .............. 2.1.1 -> 2.1.2 [INFO] org.junit.jupiter:junit-jupiter ........ 5.9.3 -> 5.10.2 [INFO] org.junit.jupiter:junit-jupiter-api .... 5.9.3 -> 5.10.2
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