Skip to content

Java 8 Alpine 容器中 jstack 与 Arthas 失败:一次 JVM 诊断能力补齐

这个问题最初并不是因为 Alpine 跑 Java 业务有异常。事实上,后续签名复现实验里 Alpine + en_US.UTF-8 的接口验签是成功的;这里遇到的是另一条独立问题:JVM 诊断能力不可用。

真正的触发点是一次 CPU 问题排查:服务运行正常,但需要进一步查看 JVM 线程时,才发现容器里的诊断能力并没有想象中完整。

背景:Java 应用 CPU 过高

当时一个 Java 应用 CPU 占用较高,需要先查看线程信息。

服务使用的基础镜像是:

dockerfile
FROM openjdk:8-jdk-alpine

ENV LANG="en_US.UTF-8"
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

WORKDIR /app
COPY /target/user-app.jar /app/user-app.jar
EXPOSE 8003
ENTRYPOINT ["java","-jar","/app/user-app.jar"]

既然镜像名称里已经是 jdk,第一反应自然是进入容器,用 jps 找进程,再用 jstack 看线程栈。

jps 正常,jstack 却失败

进入容器后:

bash
/app # jps
1 user-app.jar
170 Jps

jps 能够正常识别 PID 为 1 的 Java 进程。

但继续执行:

bash
/app # jstack 1 > a.log
1: Unable to get pid of LinuxThreads manager thread

这时问题已经不是“镜像里没装 JDK 工具”。

jpsjstack 都存在,Java 进程也能被 jps 看见,但 jstack 无法取得需要的线程信息。

再用 Arthas 验证

为了排除只是 jstack 命令自身的问题,又尝试使用 Arthas attach 到同一个 Java 进程。

Arthas 同样无法正常 attach,并出现相同方向的错误。

这一步很重要,因为它把问题从:

text
jstack 某个命令坏了?

进一步转成:

text
这个 Java 8 + Alpine 运行环境下,JVM attach / 诊断能力本身存在兼容问题?

开始关注 Alpine 本身

Alpine 和常见 Debian / Ubuntu 镜像有一个很基础的差异:Alpine 使用 musl libc,而 Debian / Ubuntu 通常使用 glibc。

当时使用的又是 Java 8 时代的 OpenJDK Alpine 镜像,因此开始怀疑这不是业务代码问题,而是旧版本 JDK 诊断工具与 Alpine/musl 组合的兼容性问题。

这个错误并不是孤例。Docker OpenJDK 项目早期也有人在 java:openjdk-8u92-jdk-alpine 中对 PID 1 执行 jmap 时得到同样的:

text
Unable to get pid of LinuxThreads manager thread

而切换到非 Alpine 的 OpenJDK 8 镜像后恢复正常。

因此这里更适合把结论限定为:

在这组 Java 8 + OpenJDK Alpine 镜像中,JVM Attach / Serviceability 工具存在实际兼容性问题;不应该泛化为“所有 Alpine、所有 JDK 版本都不能使用 jstack”。

换到 Debian 验证

最终把基础镜像换成 Debian 系列:

dockerfile
FROM openjdk:8u322-jdk-bullseye

重新运行服务后再次验证:

text
jstack → 正常
Arthas → 正常

这才完成了这个问题的闭环。

也就是说,当时切换 Debian 的目的非常明确:

不是为了修复 Java 应用运行问题,而是为了让测试和生产环境重新具备可用的 JVM 诊断能力。

为什么我最终没有继续折腾 Alpine

理论上还可以继续研究特定 Alpine 版本、musl、JDK 构建方式和 attach 实现之间的兼容细节。

但从业务系统维护角度,当时真正需要的能力很简单:

text
CPU 高 → 能 jstack
线程问题 → 能 attach Arthas
内存问题 → 能继续使用 JDK 诊断工具

既然换到 Debian JDK 镜像后这些能力都恢复了,就没有必要为了更小的基础镜像继续承担额外诊断成本。

这是一种工程取舍,而不是说 Alpine 不能运行 Java。

后续:这个改动又引出了另一个问题

有意思的是,这次“为了补齐 JVM 诊断能力”而做的基础镜像切换,后来又引出了一个完全不同的生产问题:多个第三方接口的签名开始失败。

那次问题最终又追到了 Docker 运行环境与 String#getBytes() 默认字符集。

详见:

一次 Docker 基础镜像切换后的签名异常:从生产回滚到默认字符集

结论

这次排查里最值得保留的不是“Alpine 一定不能用于生产”,而是几个更具体的判断:

  1. JDK 镜像里存在诊断命令,不代表这些命令在当前基础镜像和 JVM 组合下就一定可用。
  2. 业务运行正常和现场可诊断是两种不同能力。 真正遇到 CPU、线程或内存问题时,后者才会暴露出来。
  3. Java 8 Alpine 的旧镜像组合需要特别验证 Attach / Serviceability 工具。 不要只因为 jps 能看到进程,就认为 jstackjmap、Arthas 一定也能工作。
  4. 基础镜像选择应该把维护和诊断成本一起算进去。 镜像体积不是唯一指标。

参考