本系列是 我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多)



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



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



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

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

问题定位

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

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

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

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

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

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

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

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

针对现象三,通过查看 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/master/src/hotspot/share/logging/logFileStreamOutput.cpp

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

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

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

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

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

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

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

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

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

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

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

微信搜索“干货满满张哈希”关注公众号,加作者微信,每日一刷,轻松提升技术,斩获各种offer



我会经常发一些很好的各种框架的官方社区的新闻视频资料并加上个人翻译字幕到如下地址(也包括上面的公众号),欢迎关注:

JVM 输出 GC 日志导致 JVM 卡住,我 TM 人傻了的更多相关文章

  1. 升级到Spring 5.3.x之后,GC次数急剧增加,我TM人傻了

    最近我们项目升级到了 Spring Boot 2.4.6 + Spring Cloud 2020.0.x,通过我的另一系列即可看出:Spring Cloud 升级之路.但是升级后,我们发现 Young ...

  2. 获取异常信息里再出异常就找不到日志了,我TM人傻了

    本系列是 我TM人傻了 系列第三期[捂脸],往期精彩回顾: 升级到Spring 5.3.x之后,GC次数急剧增加,我TM人傻了 这个大表走索引字段查询的 SQL 怎么就成全扫描了,我TM人傻了 最近组 ...

  3. jvm的GC日志分析 [转]

      jvm的GC日志分析 标签: jvm内存javagc 2015-06-22 16:37 1566人阅读 评论(1) 收藏 举报  分类: Java(4)  JVM的GC日志的主要参数包括如下几个: ...

  4. JVM学习之Eclipse输出GC日志

    Java应用启动时,可以通过设置verbose参数来输出JVM的gc情况,命令如下:-verbose:gc或者-XX:+PrintGC在Eclipse中可以通过Run As|Run Configura ...

  5. 曹工杂谈:手把手带你读懂 JVM 的 gc 日志

    一.前言 今天下午本来在划水,突然看到微信联系人那一个红点点,看了下,应该是博客园的朋友.加了后,这位朋友问了我一个问题: 问我,这两块有什么关系? 看到这段 gc 日志,一瞬间脑子还有点懵,嗯,这个 ...

  6. jvm之gc日志

    ava GC日志可以通过 +PrintGCDetails开启 以ParallelGC为例 YoungGC日志解释如下 FullGC:

  7. JVM内存GC的骗局——JVM不抛出OOM但内存已经泄露

    概述 在日常测试中,我们会去重点观察java的内存使用情况,比如:进程会抛出OOM异常,不再接收新的请求:响应时间在固定时间段内变长,超时或者不响应,CPU使用率时常像过山车一样等.有时候JVM还会发 ...

  8. JVM OOM异常会导致JVM退出吗?

    出处:  https://mp.weixin.qq.com/s/8j8YTcr2qhVActLGzOqe7Q  https://blog.csdn.net/h2604396739/article/de ...

  9. 日志导致jvm内存溢出相关问题

    生产环境日志级别为info,请看如下这行代码: LOGGER.debug("the DTO info: {}", JSON.toJSONString(DTO)); 这段代码主要有两 ...

随机推荐

  1. Intel主板芯片组

    写这个的初衷还是由于linux内核本身就是硬件的抽象,如果你对硬件的相关发展,机制以及架构不了解,实际你也是看不懂linux内核代码以及看不懂linux很多命令输出的结果的,如果你看内核代码就会发现内 ...

  2. Rb(redis blaster),一个为 redis 实现 non-replicated 分片的 python 库

    Rb,redis blaster,是一个为 redis 实现非复制分片(non-replicated sharding)的库.它在 python redis 之上实现了一个自定义路由系统,允许您自动定 ...

  3. Unity实现A*寻路算法学习2.0

    二叉树存储路径节点 1.0中虽然实现了寻路的算法,但是使用List<>来保存节点性能并不够强 寻路算法学习1.0在这里:https://www.cnblogs.com/AlphaIcaru ...

  4. C++基础-2-引用

    2. 引用 2.1 引用的基本语法 1 #include<iostream> 2 using namespace std; 3 4 5 int main() { 6 7 //引用的基本语法 ...

  5. c++:-3

    上一节学习了C++的函数:c++:-2,本节学习C++的数组.指针和字符串 数组 定义和初始化 定义 例如:int a[10]; 表示a为整型数组,有10个元素:a[0]...a[9] 例如: int ...

  6. 深入了解tomcat中servlet的创建方式实现

    Tomcat如何创建Servlet? A.先到缓存中寻找有没有这个对象(Servlet是单实例的,只会创建一次) 如果没有: 1.通过反射去创建相应的对象(执行构造方法) 2.tomcat会把对象存放 ...

  7. viewport布局

    1.viewport实例 <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <h ...

  8. ansible的介绍与安装

    ansible是在远程主机上批量执行命令或者脚本的一个工具 epel源:一个第三方的yum源(阿里巴巴开源镜像站:https://opsx.alibaba.com/mirror?lang=zh-CN) ...

  9. UDP协议,多道技术,进程,同步与异步,阻塞与非阻塞

    UDP协议 简介 UDP叫做用户数据报协议,是OSI七层参考模型中传输层使用的协议,他提供的是不可靠传输,既它在传输过程 中不保证数据的完整性! 端口号 UDP使用IP地址和端口号进行标识,以此将数据 ...

  10. Python技法:实用运维脚本编写(进程/文件/目录操作)

    Python在很大程度上可以对shell脚本进行替代.笔者一般单行命令用shell,复杂点的多行操作就直接用Python了.这篇文章就归纳一下Python的一些实用脚本操作. 1. 执行外部程序或命令 ...