Quarkus gRPC 堆未满却 OOMKilled,检查旧版 gzip 解压路径
Java堆与容器总内存分开判断,核对旧Vert.x gRPC解压流关闭修复、BOM组合、实际压缩与持续负载结果。
Quarkus 的 gRPC 客户端持续接收数据,Java 堆明显低于 -Xmx,容器却被标记为 OOMKilled,进程 RSS 随传输增长。除了核对容器的总内存限制,还应确认实际使用的 gRPC 实现、压缩方式和依赖版本。
这里关注一条已经有版本化修复的旧路径:Vert.x gRPC 解码压缩消息时没有及时关闭解压流,涉及 BridgeMessageDecoder.decode、Codec$Gzip.decompress 和 JDK 的 Inflater。不能仅凭堆小或退出码 137,就认定当前应用遇到了同一问题。
把堆、RSS 和容器限额放到同一时间段
-Xmx 只限制 Java 堆,进程还使用线程栈、代码缓存、元空间及其他本地内存。容器限制也不是单独针对 Java 堆;物理机有余量,不代表受 cgroup 限制的进程可以继续分配。
记录同一次运行的容器 ID、终止原因、退出时间和实际限制,结合内核或 cgroup 记录。只有 137 而没有 OOM 证据时,先对照退出码与 OOMKilled 的区别。重启后的当前指标可能已不属于失败那一次运行。
cgroup v2 中可检查对应组的 memory.current、memory.max、可用时的 memory.peak,以及 memory.events。其中 memory.events 包含层级事件,memory.events.local 才是本组事件,需要确认正在读取的是哪一层。采样间隔中的低值也可能错过峰值。
RSS、Java 堆使用量和 cgroup 计费内存并不相等。尤其不要把 Native Memory Tracking 的 reserved 虚拟地址量直接当成实际驻留内存。
NMT 正常不能排除解压相关的本地分配
HotSpot NMT 并不跟踪所有第三方本地代码和 JDK 类库分配,因此它没有显示对应增长,不能作为这条路径不存在的证明。
NMT 需要在 JVM 启动时启用。若计划在隔离环境观察,可使用启动参数 -XX:NativeMemoryTracking=summary;启用有开销,已有进程也不能靠 jcmd 临时补开。随后用同一运行的基线比较,下面的 <pid> 替换为目标 JVM 的进程号,执行身份也须有相应访问权限:
jcmd <pid> VM.native_memory baseline
jcmd <pid> VM.native_memory summary.diff scale=MB
这两步只提供 JVM 能跟踪到的内存变化。进一步使用采样器检查分配调用栈时,应在受控环境评估权限和开销,不要在生产里反复制造 OOM 来验证猜测。
核对正式依赖中的解压流关闭修复
Vert.x gRPC 4.4.5 和 4.5.0 的这段解码代码直接把解压流交给解析器,没有在该分支关闭它;4.5.1 改为 try-with-resources,确保解析结束或异常时关闭。4.5.1 的正式发布说明列有 gzip inflater 泄漏修复。
Quarkus 3.4.3 的 BOM 使用 Vert.x 4.4.5,3.7.0 的 BOM 使用 4.5.1。这说明两组具体的管理版本,不表示 3.7.0 是最早含修复的 Quarkus 版本,也不是建议今天统一升级到这个历史版本。
先查看应用最终解析并装入的 vertx-grpc-common 版本,确认没有额外覆盖依赖。再按当前项目支持的 Quarkus 发布线选择兼容升级,使 BOM 和扩展一起保持一致;只替换一个 JAR 可能引入其他不匹配。
还要确认客户端真的收到了 gzip 消息。服务端配置允许压缩、请求头声明接受压缩,都不等于每次实际传输已经压缩。若协议和带宽允许,可在隔离环境临时关闭相应传输的压缩,比较相同数据与负载下的内存走势;同时保留原配置以恢复。这是定位分支的对照,不能保证所有 gRPC 内存增长都会因此消失。
升级验收看持续负载与完整处理结果
修复后,用代表性数据量、压缩方式和处理速度观察完整任务,核对实际依赖、进程 RSS、容器峰值和终止记录,以及数据是否全部处理。接收端积压、批量读取或其他本地分配仍需独立检查。
只扩大 -Xmx 可能挤占原本留给本地内存的余量;只提高容器限制,也可能延后暴露增长。保存原镜像、BOM 和压缩配置,回退时一起恢复相关组合,并保留任务幂等与检查点,避免失败重跑时重复写入数据。


