상황
Grafana로 어플리케이션 메모리 사용량을 분석한 결과, 기동 후 메모리 사용량이 감소하지 않는 문제가 확인된다. 즉 GC가 정상적으로 동작하지 않은 것이다.
원인
해당 어플리케이션과 다른 어플리케이션에서 Heap dump를 통해 메트릭을 비교한 결과, metrics 수가 비정상적으로 많음을 확인했다. local에서 기동 후 metric을 확인한 결과, 기동 직후와 2분 후 메모리 사용량이 계속 증가하는 것을 확인했다.
micrometer로 metrics를 등록할 때 JVM 메모리를 지속적으로 사용하는 문제가 있었는데, 어플리케이션 내부적으로 batch job, batch step을 생성할 때 이름에 시간을 함께 붙여서 생성하는 구조로 개발이 되어 있었다.
spring batch metric들은 각각 name에 대해 신규 metric을 등록하게 되어, 높은 확률로 unique해져서 high cardinality가 발생한 것이다. JVM에서 관리하는 재사용되지 않는 metric이 생기면서 메모리 사용량이 증가한 것으로 보였다. 해당 가설을 뒷받침 하기 위해서 로컬 테스트를 진행하였다.
로컬 테스트(가설 검증)
도커로 로컬에 Prometheus 기동
프로메테우스 yaml 파일 생성
# global config
global:
scrape_interval: 15s
evaluation_interval: 15s
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets:
rule_files:
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
- job_name: "spring-actuator"
metrics_path: '/actuator/prometheus'
scrape_interval: 1s
static_configs:
- targets: [ 'host.docker.internal:8089' ]
도커 이미지 다운
docker pull prom/prometheus
도커 기동
docker run -d -p 9090:9090 -v {prometheus.yml 파일 경로}:/etc/prometheus/prometheus.yml --name prometheus prom/prometheus
도커로 로컬에 Grafana 기동
도커 이미지 다운
docker pull grafana/grafana
도커 기동
docker run -d -p 3000:3000 --name grafana grafana/grafana
도커와 프로메테우스 연결 후 스프링 어플리케이션 기동
결과
ConcurrentHashMap과 micrometer 라이브러리 클래스 사용량이 지속적으로 증가 io.micrometer.core.instrument.Meter$Id 의 경우 사용량이 10위권 까지 증가하는 모습을 보임
해결 방안
batch의 name을 재사용하도록 변경한다.
상기 동일한 방식으로 로컬 테스트 진행한 결과 io.micrometer.core.instrument.Meter$Id 사용량은 초기 스케줄러에서 배치 동작후 194번째까지 사용량이 증가후 그 이후의 변동폭이 적은것을 확인할 수 있었다.
참고 자료