본문 바로가기
Java

Java - K8S를 활용한 JVM WarmUp

by 오늘부터개발시작 2023. 6. 19.

문제점

K8S 환경에서 운영 중인 스프링 어플리케이션을 자주 배포 하다 보니, 배포 직후 속도가 느려진다는 문제점을 찾았다. 자바 어플리케이션은 최초 실행됐을 때 속도가 느리다는 특징이 있다. JVM은 컴파일된 바이트코드(클래스파일)를 필요할 때마다 클래스로더를 통해 JVM에 로딩해서 사용하고, 인터프리터 방식을 사용하기 때문에 인터프리터가 최적화(JIT 컴파일러) 될 시간이 필요하다. 배포 후 사용하다 보면 웬만한 클래스들은 로드가 완료되고, JIT 컴파일러도 동작하면서 속도가 점차 개선이 된다. 하지만 서비스 업데이트, 패치 등의 이유로 배포할 때마다 다시 속도가 느려지게 된다. 어떻게 이 문제를 해결할 수 있을까?

 

JVM WarmUp

JVM 웜업은 위에서 말한 JVM의 특징이자 단점을 최소화시키기 위한 전략이다. 어플리케이션 재시작 시 중요한 기능과 관련된 클래스를 JVM에 미리 로드하고, JIT 컴파일러에 캐싱해서 성능을 최적화시킨다.

그렇다면 어떻게 어플리케이션이 시작할 때 JVM을 웜업 시킬 수 있을까? 어플리케이션 시작 직후 로직을 처리해 주는 @PostContruct 어노테이션을 사용해야 할까? 아니다. 만약 단순히 @PostConstruct를 사용하면, 트래픽이 유입되고 있는 시점에 웜업이 여전히 진행 중일 수도 있다. 그렇다면 서비스에 트래픽이 들어오기 전에 JVM 웜업을 처리할 방법이 필요한데, 어떻게 해결할 수 있을까?

 

K8S 활용

바로 K8S의 startupProbe와 readinessProbe를 활용하면 해결할 수 있다. startupProbe는 K8S의 Pod이 시작되면, 최초 1번 정상적으로 Pod가 실행됐는지 설정한 API로 헬스 체크 요청을 보낸다. 이때 설정해 놓은 헬스 체크용 API에 JVM 웜업 로직도 함께 실행되도록 하면, @PostConstruct 어노테이션을 사용할 필요 없이 웜업 로직을 실행할 수 있고, 최초 1번만 웜업 로직이 실행되도록 보장할 수 있다.

 

트래픽 유입 시점 제어는 readinessProbe를 활용하면 된다. readinessProbe도 설정한 API로의 헬스 체크 결과를 바탕으로 Pod로의 트래픽 유입을 제어한다. Pod 시작 시 readinessProbe가 설정되어 있는 경우, readinessProbe가 헬스 체크를 하고 K8S에 정상 응답을 줘야 트래픽이 유입이 시작된다. readinessProbe는 startupProbe가 완료된 이후에만 실행되기 때문에, startupProbe에서 JVM 웜업이 완료되고 나서 트래픽이 유입 되는 것을 보장할 수 있다. 참고로 startupProbe와 readinessProbe는 각각 다른 헬스 체크 API를 설정할 수 있으니, 웜업이 또 실행될거라는 걱정은 하지 않아도 된다.

 

이렇게 K8S를 활용해서 JVM 웜업을 설정하고 Pod 리스타트를 해보니, 웜업이 된 기능들은 응답 속도가 최적화된 것을 확인할 수 있었다.

 

아래 코드들은 K8S, Spring 설정하는 방법의 예시이다.

 

K8S Deployment 설정

...
startupProbe:
  httpGet:
    path: /api/health-startup
    port: 8080
  initialDelaySeconds: 15
  timeoutSeconds: 30
readinessProbe:
  httpGet:
    path: /api/health-check
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 30
...

 

Java Spring 컨트롤러 설정

@GetMapping(value = "/api/health-startup")
public ResponseEntity healthCheckStartup() {
    for (int i = 0; i < 필요한 횟수 만큼 반복; i++) {
        // JVM WarmUP 하고 싶은 기능들
    }
    return ResponseEntity.status(HttpStatus.OK);
}

@GetMapping(value = "/api/health-check")
public ResponseEntity healthCheck() {
    return ResponseEntity.status(HttpStatus.OK);
}