主要观点:在高性能计算中常寻找代码、算法或基础设施中的瓶颈,而作者最喜欢的“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的阻塞问题,确保日志管道健壮且非阻塞。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。