09|容器中如何调试 Java 程序

1,229 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情

背景

我们当前的项目还比较新,所以在生产环境的容器镜像中,安装了 JDK 方便出现问题时直接在线上的某个 Pod 上进行调试,比如经常会用到 jstack 和 Arthas,但是在调试过程中发现了一个问题,当在容器中执行 jstack 1时会报出如下错误:

$ jstack 1
    
output:
1: Unable to get pid of LinuxThreads manager thread

我们使用的容器镜像为 Alpine, 通过查找资料得出的结论是:在 Alpine 镜像中,如果 Java 进程是 1 号进程,则不能使用 jstack 等工具对其进行调试。

解决方案1:笨方法

要知道容器的本质是宿主机上的一个进程,虽然 Java 进程在容器中的进程号为 1,但是它在宿主机上的进程号肯定不为 1,那找到它在宿主机上的进程号,然后再用 jstack 对其调试不就可以了吗?

找到容器中进程对应宿主机中的进程号

$ kubectl describe pod <podName> -n <namespace>

通过使用 kubectl 的 describe 命令可以查看 pod 的详细信息,显示信息中的 Node 字段就是 Pod 所在的节点 IP,Container ID 字段是容器名称,只需复制前 8 位如:b509f75c 到宿主机上执行:docker top b509f75c就看以看到容器中进程,对应宿主机进程号是 PID 字段的值。

$ docker top b509f75c
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                1242                1216                99                  Apr02               ?                   3-01:07:17

使用 jstack 进行调试

登录到具体的宿主机上,执行:jstack 1242就能打印出 Java 进程的堆栈信息了。

注意在某些宿主机上执行 jstack 命令需要先安装如下包依赖:

$ yum -y install java && yum -y install java-17-amazon-corretto-devel.x86_64

解决方案2:使用 Tini 做为 init 进程

虽然容器中理想状态下只启动一个进程,但是在实际生产环境中,很多原本就构建于虚拟机上的应用天生就是多进程的,所以对于容器内多进程、管理僵尸进程、处理程序的信号量以及上文说到 java 进程为 1 号进程时,无法使用 jstack 进行调式的问题是亟待解决的。

tini 是一款容器内的 init 程序,可以代为管理容器内的其它进程,在实际环境中使用较多,建议调研使用。

tini 的使用也是相当简单,只需要在 Dockerfile 中以 ENTRYPOINT 的方式引入即可,如:

# Add Tini
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]

# Run your program under Tini
CMD ["/your/program", "-and", "-its", "arguments"]
# or docker run your-image /your/program ...

总结:

通常解决一个问题有多种方法,有的解决路径比较长,相对麻烦,有的则可以简单的解决问题,所以针对一个问题,还是应该多优化调研,寻找更优解。