[우아한테크코스] 10월 18일 TIL
[Java] JVM Warm up
서버가 실행된 후 처음 시작될 때와 요청이 없는 상태로 1~2시간이 경과하면 응답이 매우 느린 문제가 발생했다.
이유가 무엇인지 찾아보니 아래와 같았다.
jvm이 시작되면
-
Bootstrap Class Loading:
JRE\lib \rt.jar
java와 Object 같은 필수 클래스 로딩 -
Extension Class Loading:
java.ext.dirs
모든 jar 파일 로딩 -
Application Class Loading:
application class path
어플리케이션 클래스 경로에 있는 클래스 로딩
이렇게가 로딩되는데 이들은 지연 로딩을 기반으로 한다.
클래스로딩이 완료되면 JVM cache에 클래스들이 푸시되고, 그러면 런타임 중에 더욱 빠르게 접근할 수 있다.
보통의 첫 요청이 느린 것은 Just-In-Time Compile과 lazy class loading 때문이다.
-
JIT Compiler란?
바이트코드를 cpu로 직접 보낼 수 있는 명령어로 바꾸는 프로그램으로 자바 런타임 환경에서 사용된다.
(컴파일 시점) java source -> compiler -> bytecode -> (런타임 시점) JIT compiler -> native code
바이트코드를 로드하고 컴파일 하는 데에 시간이 소요되어 application 실행 시나 캐싱되지 못해 새롭게 컴파일하는 경우 시작 지연이 발생한다.
아래는
optimized java
책에 소개된 튜닝법
어플리케이션이 동작하는 동안 메서드는 네이티브 캐시에 로드된다. 중요한 메서드를 Tiered Compilation 이라는 VM 인수를 설정해서 강제로 캐시에 로드시킬 수도 있다. 지연시간이 긴 메서드의 경우에는 사전에 캐시에 올려둘 필요가 있다.
첫 요청 뿐만 아니라 종종 응답이 느린 이유는 준비된 쓰레드 풀보다 요청이 많은 경우거나 jit 컴파일이 완전히 되지 않은 것을 예측!! 해볼 수 있다.(warmup 적용 후에도 발생할 수 있는 문제)
JVM
-
jvm architecture
-
jvm memory 구조
code cache 영역에 jit compiler가 데이터 저장, 자주 접근하는 컴파일된 코드 블록이 저장된다.
jconsole
을 터미널에 입력하면 코드캐치 정보를 볼 수 있다.-Dcom.sun.management.jmxremote.ssl=false
이 옵션 줘야 잘 뜸jar 파일 구동 시
-XX:+PrintCodeCache
옵션 주면 코드 캐시 결과가 마지막에 뜸 -
jvm cache
개선방법
따라서 개발자가 제어할 수 있는 이벤트 중 가장 마지막에 수행되는 ApplicationReadyEvent
를 이용해 자주 사용되는 메서드를 WarmUp 하도록 할 수 있었다.
SpringApplication.run() 수행 시 발생하는 코드
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);//스프링부트가 기본적으로 사용하는 리스너들 등록
listeners.starting();//starting과 관련된 이벤트를 호출(ApplicationStartingEvent)
try { ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);//arguments 가져오고
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);//환경정보 가져오고
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);//배너 찍고
context = createApplicationContext();//context 생성하고
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context);//스프링 내부에서 사용하는 구성정보를 들고오고
prepareContext(context, environment, listeners, applicationArguments, printedBanner);//가져온 것들로 컨텍스트 준비하고
refreshContext(context);//준비가 끝나면 리프레시
afterRefresh(context, applicationArguments);//리프레시 후에 뭔가 해야할 것들 호출하고
stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } listeners.started(context);//ApplicationStartedEvent 발생하는 곳
callRunners(context, applicationArguments);//ApplicationRunner, CommandLineRunner 실행해주는 곳
} catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);//ApplicationFailedEvent
throw new IllegalStateException(ex); }
try { listeners.running(context);//running과 관련된 이벤트
} catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context; }
출처: https://jeong-pro.tistory.com/206 [기본기를 쌓는 정아마추어 코딩블로그]
-
ApplicationWarmUp Bean 생성
@Component @Slf4j public class ApplicationWarmUp { @Scheduled(fixedDelay = 60 * 60 * 1000) @EventListener(ApplicationReadyEvent.class) public void warmUp() { log.info("application warm up 시작"); login(); } private void login() { LoginRequest loginRequest = new LoginRequest("pobi@woowa.com", "test1234"); TokenResponse tokenResponse = WebClient.builder() .baseUrl("http://localhost:8080") //"https://k8s.zzimkkong.com") .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) .build() .post() .uri("/api/managers/login/token") .bodyValue(loginRequest) .retrieve() .bodyToMono(TokenResponse.class) .blockOptional() .orElseThrow(IllegalAccessError::new); String accessToken = tokenResponse.getAccessToken(); log.info("로그인 warm up 성공"); createMap(accessToken); } }
-
Application에 EnabledScheduled 등록
@SpringBootApplication @EnableScheduling public class ZzimkkongApplication { public static void main(String[] args) { TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul")); SpringApplication.run(ZzimkkongApplication.class, args); } }
결과
기존 로컬에서 400ms -> 200ms 이하로 줄었고, 이후 postman 요청 결과 200ms와 유사한 응답 속도가 보여 성공적으로 개선되었다.
사실 이게 맞는진 모르겠지만 아래 현업에서도 적용한 것 같길래.. 효과적이라고 예상!