前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >在 K8S 中 Java OOM dump 文件存储方案

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

原创
作者头像
谢正伟
发布2021-05-27 18:10:24
8.7K1
发布2021-05-27 18:10:24
举报
文章被收录于专栏:云原生研究云原生研究

本文试图解决在 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

参考:


原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题
  • 方案
  • 其他
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档