k8s-pod重启时生成dump文件

k8s环境,有个服务频繁重启,经过排查日志和事件,确认是由于OOM导致服务重启,为了方便研发定位OOM的具体原因,需要在OOM发生时自动生成内存快照(Heap Dump),以供后续研发分析。

方法一:JVM参数

JVM是提供了一些参数,能在发生OOM时自动生成Heap Dump:

  • -XX:+HeapDumpOnOutOfMemoryError
  • -XX:HeapDumpPath=/app/oom/java_heapdump.hprof

然而,目前存在问题:

  1. 多次重启会导致heap dump文件覆盖,例如,服务发生了两次OOM,第二次生成的heap dump会覆盖第一次的。
  2. dump文件保存在容器内,如果没有把目录挂载到宿主机的话,容器重启文件丢失。

所以如果只是单次分析OOM的原因,可以使用JVM的参数,在java启动参数里加上 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/oom/java_heapdump.hprof,再把容器内的/app/oom/目录挂载到node上即可。

1
2
3
4
5
6
7
8
volumes:
- name: dump-file
hostPath:
path: /opt/dump/oom
type: ''
volumeMounts:
- name: dump-file
mountPath: /app/oom

方法二:JVM参数+脚本

为了解决以上文件覆盖以及文件丢失的问题,可以写一个脚本,在发生OOM时把dump文件传到sso或者minio,在这里使用自己部署的minio,具体方法如下:

部署minio服务端

这里使用docker-compose来部署:

docker-compose.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
version: '3'
services:
minio:
image: registry.cn-shenzhen.aliyuncs.com/hxlk8s/minio:RELEASE.2023-03-20T20-16-18Z
hostname: "minio"
ports:
- 9000:9000 # api 端口
- 9001:9001 # 控制台端口
environment:
MINIO_ACCESS_KEY: admin #管理后台用户名
MINIO_SECRET_KEY: admin123 #管理后台密码,最小8个字符
volumes:
- ./data:/data #映射当前目录下的data目录至容器内/data目录
- ./config:/root/.minio/ #映射配置目录
command: server --console-address ':9001' /data #指定容器中的目录 /data
privileged: true
restart: always

启动:

1
docker-compose up -d

使用NGINX反向代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server {
listen 8080;
server_name minio.qifu.com;

location / {
proxy_pass http://10.168.2.236:9001;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

访问web界面:

编写shell脚本

编写shell脚本,在pod发生OOM时,执行脚本,把dump文件上传到minio,并告警到企微群:

oom.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

MINIO_URL="http://10.168.2.236:9000"
MINIO_USER="oomdump"
MINIO_PASSWORD="0TYyt2p7.U12454"
SW_AGENT_NAME=$(cat /app/config/bootstrap.yml | grep name | head -n 1 | cut -d : -f 2 | xargs)
MINIO_DUMPS_PATH=minio/oomdumps/test/$SW_AGENT_NAME/dump-$(date +'%Y-%m-%d_%H%M%S').hprof


mc alias set minio $MINIO_URL $MINIO_USER $MINIO_PASSWORD

mc cp /app/dump.hprof $MINIO_DUMPS_PATH
wx_alert(){
echo "发送OOM告警中!"
msg="## <font color=red size=22>【JVM OOM告警】</font> \n -----\n **应用**: *${1}* \n **环境**: *test* \n **时间**: *$(date '+%Y-%m-%d %H:%M:%S')* \n 测试环境的【${1}】发生OOM,相关dump文件已上传到minio的【'${2}'】路径下,请及时查看!! \n <@chenmingchang>"
webHookUrl="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=********"
content='{"msgtype": "markdown","markdown": {"content": "'$msg'","mentioned_mobile_list":['$3']}}'
curl -H 'Content-Type: application/json' -d "$content" $webHookUrl
echo "成功发送OOM告警!"
}
wx_alert "$SW_AGENT_NAME" "$MINIO_DUMPS_PATH"

添加JVM参数

启动应用时需要添加以下参数:

1
2
3
4
-XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=/opt/run/1.hprof
-XX:+ExitOnOutOfMemoryError
-XX:OnOutOfMemoryError=/opt/run/oom.sh

参数解释:

1
2
3
4
5
6
7
-XX:+HeapDumpOnOutOfMemoryError : 当发生内存溢出错误(Out of Memory Error)时,将会生成一个堆转储文件(Heap Dump)。堆转储文件是一个二进制文件,记录了 JVM 堆中对象的详细信息,可以用于分析内存使用情况和调试。使用该选项启用堆转储功能。

-XX:HeapDumpPath=/opt/run/1.hprof : 指定堆转储文件的保存路径和文件名。在这个示例中,堆转储文件将保存在 /opt/run 目录下,并命名为 1.hprof 。

-XX:+ExitOnOutOfMemoryError : 当发生内存溢出错误时,JVM 将直接退出。通常情况下,JVM 在发生内存溢出错误后会尝试继续执行程序,但可能会导致未定义行为或进一步的错误。使用该选项可以使 JVM 在出现内存溢出错误时立即退出。

-XX:OnOutOfMemoryError=/opt/run/oom.sh : 当发生内存溢出错误时,执行指定的 shell 脚本。在这个示例中,指定的脚本是 /opt/run/oom.sh ,可以根据需要自定义这个脚本。这个特性可以让在内存溢出错误发生时执行一些自定义的操作,例如发送通知、记录日志等。

下载minio客户端

1
2
3
wget https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc
mv mc /usr/local/sbin/

测试

让开发写个死循环去请求,触发OOM:

告警:

dump文件:

方法三:preStop钩子

为了解决k8s重启时无法导出heap dump和二次覆盖的问题,我们可以通过配置preStop钩子,在容器停止前生成内存快照。相关命令如下:

获取进程ID为10的程序的堆栈信息:

1
jstack -F 10 >> /logs/thread.dump

生成堆内存快照:

1
jmap -dump:format=b,file=/usr/src/logs/dump.hprof 10

通过这样的优化,既能避免heap dump被覆盖,又能在k8s重启时生成有用的内存快照,帮助排查问题。

命令:

1
jstack -F $(jps |grep -v Jps | awk '{print $1}') | tee -a /usr/src/logs/thread.dump && jmap -dump:format=b,file=/usr/src/logs/$(date +'%Y-%m-%d_%H%M%S').hprof $(jps |grep -v Jps | awk '{print $1}')"

pod的yaml文件配置

加上以下配置:

1
2
3
4
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "jstack -F $(jps |grep -v Jps | awk '{print $1}') | tee -a /usr/src/logs/thread.dump && jmap -dump:format=b,file=/usr/src/logs/$(date +'%Y-%m-%d_%H%M%S').hprof $(jps |grep -v Jps | awk '{print $1}')"]

验证

手动执行kubectl delete pod,可以查看到生成的文件:

两个命令的区别:

Thank you for your accept. mua!
-------------本文结束感谢您的阅读-------------