不是的 JVM 暂停:一个战争故事

主要观点:在高性能计算中常寻找代码、算法或基础设施中的瓶颈,而作者最喜欢的“bug”并非这些,而是 JVM 垃圾收集器与服务器磁盘之间的隐形交互,导致每秒处理数百万请求的服务出现 15 秒以上的停顿。

关键信息:

  • 工作的大规模 Java 服务每秒处理数百万用户请求,系统设计用于高吞吐量,但受负载均衡器超时间歇性尖峰影响,部分服务器会停滞并停止接受新连接,行为与重磁盘 I/O 相关。
  • 从垃圾收集日志中找到“罪魁祸首”,年轻代垃圾收集暂停通常为数十或数百毫秒,但此处长达 15 秒多,差异在于 JVM 处于“Stop-the-World”状态时间长,而实际工作时间短,是垃圾收集器最后同步写入 GC 日志文件到磁盘时因磁盘争用而导致整个应用冻结。
  • 有两种解决方法,一是将 GC 日志路径改为基于内存的文件系统(tmpfs),避免磁盘 I/O,通过设置 JVM 内置日志轮换标志控制内存使用;二是使用 JVM 级别的异步 GC 日志功能,将日志写入内存缓冲区并由后台线程异步刷新到磁盘。
  • 在容器时代,应用直接日志到stdout/stderr仍可能导致阻塞,若日志代理阻塞,会使应用的write()操作阻塞,从而引发与之前类似的停顿。

重要细节:

  • 年轻代垃圾收集是“Stop-the-World”事件,JVM 会暂停所有应用线程进行内存操作。
  • 写入tmpfs的操作是内存到内存的复制,几乎瞬间完成,write()调用立即返回,结束 STW 暂停。
  • 使用-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=6 -XX:GCLogFileSize=20M设置控制 GC 日志的内存使用。
  • 异步 GC 日志功能通过async装饰器和-XX:AsyncLogBufferSize=100M设置,让 STW 线程将日志写入内存缓冲区后立即恢复应用线程,由后台线程异步刷新磁盘。
  • 在容器化环境中要注意stdout的阻塞问题,确保日志管道健壮且非阻塞。
阅读 56
0 条评论