在 K8S 中 Java OOM dump 文件存储方案

2021-05-27 18:10:24 浏览数 (1)

本文试图解决在 k8s 环境下 java 内存溢出时候 dump 文件的存储问题。

问题

在容器中运行 java 应用,通过类似如下命令行启动程序:

代码语言:txt复制
java -Xms1536m -Xmx1536m 
    -XX: HeapDumpOnOutOfMemoryError 
    -XX:HeapDumpPath=/dumper 
    -jar /app/oom-sims-1.0-SNAPSHOT.jar 2000

应用运行过程中,如果内存超过 1536M,会触发 java 的内存溢出,这个时候 java 会把内存 dump 成为文件 /dumper/java_pid1.hprof。过程完成之后,java 进程退出,容器会被 k8s 重启。

在这个过程中,会有如下几个“棘手”的问题:

  • 在 yaml 配置中 dump 的文件名无法修改,当 再次 dump 的时候,会发现文件已经存在,dump 会直接报错。
  • dump 文件存储问题,这个文件不能存在容器中,因为重启之后会丢,只能想办法存到主机上,但集群服务器多了,想拿到这个文件也不太容易。
  • 使用分布式的网络存储,通过 PV 绑定到集群可以解决文件寻找的问题,但文件很大,网络存储较慢,有时候没有存完,容器被 liveness 等探针重启。通过网络存储亦有文件名重复问题。

方案

下述方案使用腾讯云产品实现。

1、 将cos 作为存储介质,直接绑定到集群。当发现 java_pid1.hprof 生成后,使用 scf 触发器修改文件名即可。

2、 写一个脚本,监视 java_pid1.hprof 文件,并进行操作。此脚本部署在同 pod,作为应用的 sidecar 运行。

下面重点讨论第二种方案。

实现步骤:

  • 绑定 node 的临时文件夹到容器的 /dumper 目录
  • 监视 /dumper 文件夹,发现 java_pid1.hprof 保存完成后进行 改名,压缩,上传 cos,删除操作。

这个脚本的完整实现如下:

代码语言:txt复制
package main

import (
	"bufio"
	"compress/gzip"
	"context"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"time"

	"github.com/fsnotify/fsnotify"
	"github.com/tencentyun/cos-go-sdk-v5"
)

func main() {
	COS_SECRETID := os.Getenv("COS_SECRETID")
	COS_SECRETKEY := os.Getenv("COS_SECRETKEY")
	COS_BUCKETURL := os.Getenv("COS_BUCKETURL")
	COS_DUMPER_ROOT := os.Getenv("COS_DUMPER_ROOT")
	APP_NAME := os.Getenv("APP_NAME")
	DUMPER_ROOT := os.Getenv("DUMPER_ROOT") 
	dumpFileName := DUMPER_ROOT   "java_pid1.hprof"

	u, _ := url.Parse(COS_BUCKETURL)
	b := &cos.BaseURL{BucketURL: u}
	c := cos.NewClient(b, &http.Client{
		//设置超时时间
		Timeout: 3600 * time.Second,
		Transport: &cos.AuthorizationTransport{
			SecretID:  COS_SECRETID,
			SecretKey: COS_SECRETKEY,
		},
	})

	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Fatal(err)
	}
	defer watcher.Close()

	done := make(chan bool)

	log.Println("COS 地址:", COS_BUCKETURL)
	log.Println("COS 存储目录:", COS_DUMPER_ROOT)
	log.Println("进程开启,开始监控:", DUMPER_ROOT)
	go func() {
		for {
			select {
			case event, ok := <-watcher.Events:
				if !ok {
					return
				}

				if event.Op&fsnotify.Create == fsnotify.Create {
					if event.Name == dumpFileName {
						log.Println("检测到 dump 文件:", event.Name)
						current_time := time.Now().Local()
						newFileName := dumpFileName   "-"   current_time.Format("2006-01-02-15-04-05")   "."   APP_NAME
						zipFileName := newFileName   ".gz"

						//死循环,根据文件大小判断是否 dump 完成
						var postSize int64 = 0
						for {
							time.Sleep(2 * time.Second)
							fileinfo, _ := os.Stat(event.Name)
							currSize := fileinfo.Size()
							log.Println("当前文件大小:", currSize)
							if currSize == postSize {
								break
							}
							postSize = currSize
						}
						log.Println("最终文件大小:", postSize)
                        
						// 改名
						os.Rename(dumpFileName, newFileName)
						log.Println("文件 dump 完成,改名,开始压缩:", newFileName)

						//压缩
						compress(newFileName, zipFileName)
						log.Println("文件压缩完成,准备上传:", zipFileName)

						//上传
						upload(zipFileName, COS_DUMPER_ROOT, c)

						//删除
						os.Remove(newFileName)
						log.Println("文件删除:", newFileName)
						os.Remove(zipFileName)
						log.Println("文件删除:", zipFileName)
					}
				}

			case err, ok := <-watcher.Errors:
				if !ok {
					return
				}
				log.Println("error:", err)
			}
		}
	}()

	err = watcher.Add(DUMPER_ROOT)
	if err != nil {
		log.Fatal(err)
	}
	<-done
}

func upload(fPath string, cosRoot string, client *cos.Client) {

	key := cosRoot   filepath.Base(fPath)
	_, err := client.Object.PutFromFile(context.Background(), key, fPath, nil)
	if err != nil {
		panic(err)
	}
	log.Println("上传完成:", key)
}

func compress(fPath string, fPathZip string) {
	f, _ := os.Open(fPath)
	reader := bufio.NewReader(f)
	content, _ := ioutil.ReadAll(reader)

	f, _ = os.Create(fPathZip)

	w := gzip.NewWriter(f)
	w.Write(content)
	w.Close()
}

将此文件打包镜像,并进行部署,部署 yaml 如下:

代码语言:txt复制
apiVersion: apps/v1
kind: Deployment
metadata:
  name: oom-sims
  namespace: oom-test
  labels:
    app: oom-sims
spec:
  replicas: 1
  selector:
    matchLabels:
      app: oom-sims
  template:
    metadata:
      labels:
        app: oom-sims
    spec:
      volumes:
        - name: heap-dumps
          emptyDir: {}
      containers:
        - name: oom-sims-container
          image: cloudbeer/oom-sims:1.0
          command:
          - java
          - -Xms512m
          - -Xmx1536m
          - -XX: HeapDumpOnOutOfMemoryError
          - -XX:HeapDumpPath=/dumper
          - -jar
          - /app/oom-sims-1.0-SNAPSHOT.jar
          args: ["2000"]
          resources:
            requests:
              memory: "3Gi"
              cpu: "500m"
            limits:
              memory: "3Gi"
              cpu: "500m"
          volumeMounts:
          - name: heap-dumps
            mountPath: /dumper
        - name: dumper
          image: cloudbeer/dumper:1.5
          env:
          - name: COS_SECRETID
            value: "YOUR_COS_SECRETID"  # 这个是你的 API 密钥
          - name: COS_SECRETKEY
            value: "YOUR_COS_SECRETKEY" # 这个是你的 API 密钥
          - name: COS_BUCKETURL
            value: "YOUR_COS_BUCKETURL" # 这个是 cos bucket 地址
          - name: COS_DUMPER_ROOT  
            value: "dumper/" # 这个是 cos 的 key 前缀 
          - name: APP_NAME 
            value: "oom-tester" # 应用名称,这个名称会附加在文件名后面
          - name: DUMPER_ROOT 
            value: "/dumper/" # 会监视这个文件夹下面的 java_pid1.hprof
          volumeMounts:
          - name: heap-dumps
            mountPath: /dumper

上面的脚本说明:

  • 在 pod 内部署了 2 个容器,一个是应用,一个是 dump 文件处理脚本。
  • 应用模拟了一个 OOM 的场景,调整 args 参数,会得到不同的 JVM 内存占用。
  • dump 脚本可以通过环境变量将腾讯云的 cos 参数指定进去。
  • cloudbeer/oom-sims:1.0 和 cloudbeer/dumper:1.5 这俩镜像均可直接测试使用。
  • dump 1.5G 的时间大约需要 3s 钟,如果你配置了 liveness 探针,请给 dump 留下足够的时间。
  • 使用此脚本的优点:本地存储速度会比较快,改名,压缩,上传过程也是异步进行,比较清爽。缺点:每个应用都要多配一个伴生容器。

其他

在配置容器的资源的时候,需要保证容器的 limits 内存配置要大于 jvm 的内存,如果配小了,会引发容器的 OOM,从而直接被杀死。

另外 java 的老版本会不支持容器的内存统计,实际占用内存会超过 Xmx 的设置,引发 容器的 OOM。需要保证 java 版本不低于如下版本:Java SE 8u131 和 openjdk 8u181

本文的源代码地址:https://github.com/cloudbeer/oom-sims

参考:


  • How to do a Java/JVM heap dump in Kubernetes
  • Java SE support for Docker CPU and memory limits
  • Sizing Kubernetes pods for JVM apps without fearing the OOM Killer

0 人点赞