JVM 输出 GC 日志导致 JVM 卡住

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: JVM 输出 GC 日志导致 JVM 卡住

image.png


本系列是 我TM人傻了 系列第七期[捂脸],往期精彩回顾:


最近,我们升级了 Java 17。后来,我们的 k8s 运维团队为了优化我们的应用日志采集,将我们所有 pod (你可以理解为一个 Java 微服务进程)的 JVM 日志都统一采集到同一个 AWS 的 EFS 服务(EFS 是 Elastic File System 的缩写,弹性块文件存储系统,底层是 NFS + S3 对象存储集群),我们对于 JVM 日志配置包括以下几个:

  1. GC日志:-Xlog:gc*=debug:file=${LOG_PATH}/gc%t.log:utctime,level,tags:filecount=50,filesize=100M
  2. JIT 编译日志:-Xlog:jit+compilation=info:file=${LOG_PATH}/jit_compile%t.log:utctime,level,tags:filecount=10,filesize=10M
  3. Safepoint 日志:-Xlog:safepoint=trace:file=${LOG_PATH}/safepoint%t.log:utctime,level,tags:filecount=10,filesize=10M
  4. 关闭堆栈省略:这个只会省略 JDK 内部的异常,比如 NullPointerException 这种的:-XX:-OmitStackTraceInFastThrow,我们应用已经对于大量报错的时候输出大量堆栈导致性能压力的优化,参考:https://zhuanlan.zhihu.com/p/428375711

JVM 统一日志配置请参考:https://zhuanlan.zhihu.com/p/111886882

在这样做之后,我们的应用出现这样一个奇怪的问题,这个问题有三种不同的现象,统一的表现是处于安全点的时间特别特别长


1.通过 safepoint 日志看出来,等待所有线程进入安全点的时间特别长(Reaching safepoint:25s多)


image.png


2.通过 safepoint 日志看出来,还有处于 safepoint 时间过长的,并且原因是 GC(At safepoint: 37s多)


image.png


查看 GC 日志,Heap before GC invocations 与输出堆结构的日志间隔了很久:


image.png



3.另一种处于 safepoint 时间过长的,原因也是 GC,但是间隔日志的地方不一样(29s多)

查看 GC 日志,输出堆结构的日志某些间隔了很久:


image.png


问题定位


首先,Java 应用线程整体处于 safepoint,这时候应用线程什么都做不了,所以依赖应用线程的监控即通过 JVM 外部监控,例如 spring actuator 暴露的 prometheus 接口,以及 Skywalking 插桩监控,是什么都看不到的,只会看到出于安全点时调用的这些方法时间特别长,但是并不是这些方法真的有瓶。

需要通过 JVM 内部线程的监控机制,例如 JVM 日志,以及 JFR(Java Flight Recording)来定位。还有就是通过 async_profiler (https://github.com/jvm-profiling-tools/async-profiler/),因为我们发现,在出问题的时候,进程本身的 CPU 占用(注意不是机器的,是这个进程的)也会激增:


image.png


但是非常奇怪的是,通过 async_profiler 查看 CPU 占用,发现出问题的时间段,除了:

并且在处于安全点的期间,日志也是被中断了一样,这是非常少见的,为什么这么说,请看下面分析:

针对现象一,等待所有线程进入 safepoint 时间特别长,这个一般会不断输出等待哪个线程没有进入安全点的日志,参考 JVM 源码:

https://github.com/openjdk/jdk/blob/master/src/hotspot/share/runtime/safepoint.cpp


image.png


但是现象一中我们并没有看到因为哪个线程导致进入 safepoint 时间过长。

针对现象二,通过 JFR,也没看出 GC 的哪个阶段耗时很长:


image.png


针对现象三,通过查看 JVM 源码发现,输出这两个间隔很大的日志的代码之间,没有做任何的事情,只是打日志。并且查看所有出异常的时间点,都是每个小时的 05 分左右询问运维知道在这个时间,会进行上一小时日志文件的移出与与 EFS 同步(我们一个小时生成一个日志文件),会有大量文件 IO(由于底层使用的是云服务,也许并不是磁盘,而是 EFS 这种 NFS 或者网络对象存储)。会不会是文件 IO 太大导致 JVM 日志输出堵住导致 JVM 卡住呢?

为啥 JVM 日志输出会导致 JVM 所有应用线程卡住,假设 JVM 某个线程输出日志卡住了,倘若没有处于 safepoint,那么不会卡住所有应用线程,只会卡住它自己。但是如果处于 safepoint,所有应用线程本身就被暂停了,如果这个时候某个 JVM 线程输出日志卡住,那么可能造成迟迟不能所有线程进入安全点,或者所有处于安全点时间过长。对应现象一,某个线程输出的是 JVM 日志而不是应用日志(输出应用日志一般是涉及文件 IO 原生调用,处于原生调用直接就算进入了安全点,不会有影响,请参考我的另一篇文章:JVM相关 - SafePoint 与 Stop The World 全解:


https://zhuanlan.zhihu.com/p/161710652),输出 JVM 日志卡住导致这个线程迟迟没有进入安全点。针对现象二三,都是 GC 线程输出 JVM 日志卡住导致 GC 迟迟不结束。

首先通过 JVM 源码确认下 JVM 日志输出卡住是否会阻塞 JVM。


JVM 输出 JVM 日志源码分析


我们使用的是 Java 17,Java 17 之前没有异步 JVM 日志输出。所以待会的源码分析请忽略异步日志的代码,这样就是 Java 17 前的日志输出:


https://github.com/openjdk/jdk/blob/mast


image.png


通过这里的代码可以看出,如果输出文件 IO 卡住,这里的 flush 是会卡住的。同时,会有短暂的 CPU 激增,因为刷入等待的策略应该是 CPU 空转等待一段时间之后进入阻塞。

那么我们换成异步日志怎么样?异步日志有哪些参数呢? JVM 异步日志是 Java 17 引入的,对应的 ISSUE 是:https://bugs.openjdk.org/browse/JDK-8229517,其中的关键,在于这两个参数:


image.png


通过 -Xlog:async 启用 JVM 异步日志,通过 -XX:AsyncLogBufferSize= 指定异步日志缓冲大小,这个大小默认是 2097152 即 2MB。异步日志的原理是:


image.png


修改参数为异步日志,问题大幅度缓解,但是并没完全解除,进一步定位


我们修改日志为异步日志,加入启动参数: -Xlog:async,-XX:AsyncLogBufferSize=4194304。之后观察,问题得到大幅度缓解:


er/src

image.png


但是还是在某一个实例上出现了一次问题,查看现象,与之前的不同了,通过 safepoint 日志看,是某个线程一直 running 不愿意不进入 safepoint


image.png


那么这个线程在干什么呢?通过 jstack 看一下这个线程是什么线程:


image.png


这是一个定时刷新微服务实例列表的线程,代码对于 WebFlux 的使用并不标准:


image.png


这样使用异步代码,可能带来 JIT 优化错误(正确的用法调用很频繁,这个错误用法调用也很频繁,导致 JIT C2 不断优化与去优化),查看 JFR 发现这段时间也有很多 JIT 去优化:


image.png


这样可能导致安全点缺失走到 IO 不断空转等待很久的问题,需要改成正确的用法:


image.png


修改好之后,迟迟不进入 safepoint 的问题消失。



/hotspot/share/logging/logFileStreamOutput.cpp





相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
26天前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
51 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
8天前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。
|
26天前
|
Arthas 监控 Java
JVM知识体系学习七:了解JVM常用命令行参数、GC日志详解、调优三大方面(JVM规划和预调优、优化JVM环境、JVM运行出现的各种问题)、Arthas
这篇文章全面介绍了JVM的命令行参数、GC日志分析以及性能调优的各个方面,包括监控工具使用和实际案例分析。
39 3
|
27天前
|
算法 Java
JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
本文详细介绍了JVM中的GC算法,包括年轻代的复制算法和老年代的标记-整理算法。复制算法适用于年轻代,因其高效且能避免内存碎片;标记-整理算法则用于老年代,虽然效率较低,但能有效解决内存碎片问题。文章还解释了这两种算法的具体过程及其优缺点,并简要提及了其他GC算法。
 JVM进阶调优系列(4)年轻代和老年代采用什么GC算法回收?
|
26天前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
36 4
|
3月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
28天前
|
存储 缓存 算法
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
JVM核心知识点整理(内存模型),收藏再看!
|
16天前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
42 10
|
15天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
25天前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
46 2

热门文章

最新文章