K8s에서 Java Heap Dump 저장하고 내려받기 개선

오종성
SSG TECH BLOG
Published in
13 min readJul 14, 2023

안녕하세요. Technical Architect 팀에서는 on-premise Kubernetes 를 운영하고 있습니다. 이번 글에서는 Kubernetes 환경에서 동작하는 JVM 애플리케이션에 OOM 이 발생했을 때, 덤프 파일을 쉽게 수집하고 내려받게 작업한 내용을 소개해 보려고 합니다.

배경

덤프 파일은 서비스의 안정성 및 성능 분석에 중요하기 때문에 원한다면 언제든지 내려 받아서 분석할 수 있어야 합니다.

물리 WAS 서버에서 서비스되는 JVM 애플리케이션에 OOM 이 발생하는 경우를 생각해보겠습니다. OOM 이 발생하면 덤프 파일이 생성됩니다. 그러면 해당 서버에 접근 권한을 가진 사용자가 접속해서 덤프 파일을 내려받고, 분석하는 방식으로 작업합니다. 물리서버의 디스크 용량이 허락하는 한 덤프 파일은 계속 해당 서버에 남아 있을 수 있고 언제든지 내려받아 분석할 수 있습니다.

위와 같은 방식을 Kubernetes 환경에서 동작하는 JVM 애플리케이션에 적용하면, 덤프 파일은 POD 에 생성됩니다. 그리고 나서 kubectl cp 명령으로 덤프 파일을 내려받아서 분석하게 됩니다.

kubectl cp command

kubectl cp example-pod-name:heap-dump-path/heap-dump-name.hprof local-directory-path

그런데, POD 는 물리서버와 다르게 덤프파일을 영구히 보관하지 않습니다. Kubernetes 환경에서 동작하는 JVM 애플리케이션은 대부분 무상태(stateless) 서비스입니다. 그러다 보니 볼륨을 사용하지 않거나, ephemeral volume(임시 볼륨) 중 emptyDir 을 사용하고 있습니다.

임시 볼륨은 이름에서 알 수 있듯이 그 수명을 POD 와 함께합니다. 그래서 POD 안의 컨테이너에서 파일을 생성했지만, POD 가 충돌나거나 재시작되면 해당 파일을 잃게 됩니다. 그리고 어떤 이유로든 노드에서 POD 가 제거되면 데이터가 영구적으로 삭제됩니다.

emptyDir

이 문제를 해결하기 위해서 PV (persistent volume) 을 적용해서 덤프파일을 POD 생명주기에서 벗어나 오래 보관하고, 생성된 덤프 파일을 내려받는 작업도 편하게 개선하기로 결정했습니다.

요구사항 및 용어 정리

요구사항 정리

  1. PV (persistent volume)를 마운트해서 덤프 파일이 사라지는 문제 해결
  2. 명령줄 도구(kubectl) 를 이용해서 내려받는 과정 없이 Web UI 에서 손쉽게 내려받을 수 있게 개선

Volume

ephemeral volume

PV (persistent volume)

명령줄 도구 (kubectl)

PV 선택

1번 요구 사항을 만족하기 위해서 PV (persistent volume) 을 적용해야 했습니다. PV 종류는 다양한데, 이번 작업에 고려한 종류는 다음 두 가지 입니다.

hostpath

POD 가 동작하는 쿠버네티스 클러스터의 노드(물리서버)의 로컬 파일시스템의 파일 및 폴더를 POD 가 사용할 수 있는 볼륨으로 제공해줍니다.

POD 가 사라지거나 재시작해도 노드에 데이터가 남아 있습니다. 그러나 POD 가 재시작해서 새로운 노드에서 서비스될 경우, 새로운 노드의 hostPath 를 사용하기 때문에 이전 노드에서 생성한 파일에 접근할 수 없습니다.

hostPath

nfs

nfs 를 사용하면 사내 NAS 를 POD 가 사용할 수 있는 볼륨으로 제공할 수 있습니다. hostPath 와는 달리 어떤 노드에서 서비스되더라도 같은 볼륨을 바라보게 됩니다. 즉, 데이터를 POD 간에 공유할 수 있게 됩니다.

nfs

hostPath 의 단점은 POD 가 다른 노드로 옮겨가면 이전 노드에서 생성한 파일을 읽을 수가 없다는 점입니다. 그런데 덤프 파일은 한번 쓰고나면 끝이고 다시 읽지 않기 때문에 문제가 되지 않습니다.

그러나 hostPath 에서는 덤프파일을 내려받기 위해서 Web UI 지원도 힘들뿐만 아니라, 덤프파일이 생성된 노드(물리서버)가 어디인지 찾아야 한다는 문제가 있습니다.

반면에 nfs 를 사용하면 NAS 특정 폴더에 덤프파일이 생성되서 hostPath 방식보다 덤프파일을 한 곳에 모아두어 관리하기 쉬운 장점이 있습니다. 그리고 Web UI 지원이 안되서 직접 내려받기를 한다해도, 이미 개발자는 다른 작업때문에 NAS 서버 접근 권한을 가지고 있는 경우가 많습니다.

그래서 기존방식과 유사하고, hostPath 보다 적응하기도 쉬운 nfs 방식을 사용하기로 결정했습니다.

손쉬운 내려받기

2번 요구사항은 NAS 서버에 직접 접근하거나 Web UI 를 사용해서 내려받는 것을 고려했습니다. 그러나 사내에 개발자가 사용할 수 있는 NAS Web UI 가 존재하지 않았습니다. 게다가 덤프 파일을 위한 NAS 가 기존에 개발자가 접근 권한을 가지고 있는 NAS 서버와 달라서 다른 대안이 필요했습니다.

MinIO (Open Source object storage)

MinIO는 오픈소스로 제공되는 오브젝트 스토리지 서버이며 AWS S3와의 호환되는 클라우드 스토리지를 구축할 수 있는 도구입니다.

MinIO 는 Technical Architect 팀에서 on-premise Kubernetes 를 운영하기 위해서 이미 구축해서 사용 중이였고, Web UI 로 파일을 편하게 내려받을 수 있기 때문에 대안으로 선정했습니다.

그러면, PV 에 존재하는 덤프파일을 MinIO 로 옮기는 작업이 필요했습니다.

MinIO 자체를 PV 로 사용하면 따로 옮기는 작업이 필요없어지기 때문에 방법이 없나 찾아봤습니다. 그러나 PV 유형에서 object storage 를 지원하지 않아서 MinIO 를 POD 에 직접 마운트 할 수 없었습니다. S3Fuse 나 goofys 같은 오픈소스가 존재하지만, 성능이 느리거나 아직 stable 버전이 존재하지 않아서 사용하기 부담스러웠습니다.

그래서 1, 2 번 요구사항을 모두 만족하기 위해서는 PV 에 생성된 덤프 파일을 MinIO 로 옮겨줄 무언가가 필요했습니다.

Dump Collector

폴더에 생성된 파일을 지속적으로 MinIO 로 옮기는 작업만 주기적으로 실행할 dump collector 를 만들기로 했습니다. 간단한 기능이라고 생각했는데, 생각보다 요구조건이 까다로웠습니다.

요구조건

  1. 원본 폴더와 대상 폴더를 비교해서 대상 폴더에 존재하지 않는 원본 파일을 대상 폴더로 옮긴다.
  2. 원본 폴더에는 없고 대상 폴더에 존재하는 파일은 신경쓰지 않는다. (원본 폴더의 디스크 용량 관리로 삭제했기 때문에)
  3. 업로드가 실패하면 재시도 한다. (재시도 간격, 횟수 조정 가능하게)
  4. 한번 옮긴 파일은 대상 폴더에서 삭제해도 다시 옮기지 않는다. (내려받고 더 이상 필요없어서 삭제했거나 용량 관리로 삭제했기 때문에)

요구조건을 정리하고 보니, 직접 개발하기보다는 오픈소스 중에 사용할 수 있는게 없는지 찾아보게 되었습니다. 그래서 Rclone 이라는 Open-source Command Line Tool 을 사용하게 되었습니다.

rclone copy 명령에는 다음과 같은 기능이 포함되어 있어서 걱정없이 덤프 파일을 옮길 수 있습니다.

  • 대상 폴더로 데이터 복제
  • 원본 폴더에서 데이터가 삭제되도 대상 폴더의 데이터를 삭제하지 않음
  • 한번 옮긴 데이터는 다시 복제하지 않음
  • 업로드가 실패해도 일정 간격으로 3번 재시도 한다 (커스텀 가능)

그래서 아래와 비슷한 shell script 을 실행하는 것만으로도 요구조건을 충족시킬 수 있어서, 매우 간단하게 서비스를 만들 수 있게 되었습니다.

#!/usr/bin/env bash

while true; do
echo "syncing /java_dumps to $MinIO-bucket"
.rclone.conf copy /java_dumps remote:$MinIO-bucket
sleep 60s
done

또 다시 PV 선택

dump collector 를 만들고 보니, PV 는 hostPath 방식이든 nfs 방식이든 무엇을 사용해도 괜찮아 보였습니다. 둘 다 폴더 경로는 동일하니 dump collector 가 작업하는데는 문제가 없기 때문입니다.

hostPath

앞서 언급했듯이 hostPath 방식은 각 노드의 지정된 폴더를 덤프 파일 저장소로 사용하는 방법입니다. POD 는 어떤 노드에서든 서비스 될 수 있기 때문에 모든 노드에 똑같은 폴더를 만들고, hostPath 로 마운트해서 덤프 파일을 쓰게 해야 합니다. 그러면 dump collector 는 모든 노드의 덤프 폴더를 지켜보다가 파일이 생성되면 MinIO 로 옮기게 됩니다.

여기서 dump collector 는 어떻게 모든 노드의 덤프 폴더를 지켜볼까요? 노드(물리서버) 목록을 가지고 있다가 순회하면서 체크해야 할까요?

Kubernetes 환경에서는 각 노드마다 서비스가 존재하게 끔 배포할 수 있는 방법이 있습니다. 바로 DaemonSet 방식입니다.

DaemonSet 방식은 dump collector 를 각 노드마다 하나씩 1:1로 배포합니다. 그래서 노드가 늘어날수록 Dump Collector 의 개수도 똑같이 늘어납니다.

그러나 JVM 애플리케이션에서 OOM 이 발생하고 덤프 파일이 생성되는 빈도를 생각해보면, 쓸데없이 dump collector 를 많이 배포한다는 생각이 들었습니다.

nfs

앞서 언급했듯이 nfs 방식을 사용하면, NAS 를 PV로 사용할 수 있습니다. 그래서 POD 와 dump collector 가 서로 다른 노드에서 서비스되도 같은 PV 를 사용합니다. 그렇기 때문에 hostPath 를 사용할 때처럼 dump collector 를 각 노드마다 배포할 필요가 없습니다.

게다가 비록 dump collector 가 필요하는 리소스가 작다하더라도, 각 노드마다 배포하지 않기 때문에 hostPath 방식보다 리소스를 아낄 수 있습니다. 그래서 nfs 방식으로 결정했습니다.

서비스에 적용하기

jvm Option 추가

-XX:+HeapDumpOnOutOfMemoryError 
-XX:+ExitOnOutOfMemoryError
-XX:HeapDumpPath=/java_dumps/${SPRING_PROFILES_ACTIVE}-${MY_POD_NAME}-deployedTime$(date +-%Y-%m-%d-%H-%M-%S).hprof

-XX:+HeapDumpOnOutOfMemoryError : OOM 발생시, 힙덤프 생성

-XX:+ExitOnOutOfMemoryError : OOM 이 발생하면, 인스턴스 재시작

-XX:HeapDumpPath : 힙덤프 생성 폴더 및 파일명 설정 (검색 용이하게)

POD 에 볼륨 마운트

pvc 설정

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: java-dump-pvc
labels:
pvc: java-dump
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10G
storageClassName: ''
volumeMode: Filesystem
volumeName: java-dump-pvc

pod 설정

apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: java-service
name: your-service-name
spec:
replicas: 1
template:
metadata:
labels:
app: java-service
spec:
containers:
- image: java-service:latest
name: java-service
volumeMounts:
- mountPath: /java_dumps
name: java-dump
volumes:
- name: java-dump
persistentVolumeClaim:
claimName: java-dump-pvc

이제 POD 에는 /java_dumps 폴더가 마운트됩니다. 덤프 파일은 앞서 설정한 jvm 옵션에 의해서 /java_dumps 폴더에 생성되고, dump-collector 가 이를 읽어서 MinIO 로 전송합니다.

MinIO

개발자는 덤프 분석이 필요할 경우, MinIO UI 에서 편하게 받을 수 있게 되었습니다.

마무리

이번 글에서는 Kubernetes 환경에서 동작하는 JVM 애플리케이션에 OOM 이 발생했을 때, 덤프 파일을 쉽게 수집하고 내려받게 작업한 내용을 살펴봤습니다. 서비스 개선을 위해서 덤프 파일 분석이 필요할 때, 파일이 없는 상황을 막으려고 실제로 작업한 내용을 기반으로 작성했습니다. 이 글이 정답은 아니지만, 비슷한 문제로 고민하고 있는 분들께 도움이 되길 바랍니다. 읽어주셔서 감사합니다.

--

--