SpringBoot WarmUp

Chans·2023년 3월 10일
1

MSA

목록 보기
1/1

문제

서버가 실행된 후, 혹은 서버의 요청이 없는 첫 번째 요청 또는 새로운 요청이 발생하면 응답 시간이 매우 느리다.
Spring boot의 cold start 문제

원인 파악을 위해서는 JVM 흐름을 알아야 한다.

JVM 흐름
1. Bootstrap Class Loading : Java 코드와 java.lang.Object 와 같은 필수 클래스를 메모리에 로드함
2. Extension Class Loading : java.ext.dirs 경로에 있는 모든 JAR 파일을 로드함(개발자가 수동으로 JAR을 추가하는 경우)
3. Application Class Loading : 애플리케이션 클래스 경로에 있는 모든 클래스를 로드함

이때 중요한 것은 이러한 초기화 프로세스가 지연 로딩(LAZY LOADING) 방식을 기반으로 한다는 것이다.
클래스 로딩이 완료되면 모든 중요한 클래스(프로세스 시작 시 사용)가 JVM 캐시(네이티브 코드)로 푸시되어 런타임 중에 더 빠르게 엑세스할 수 있다.
warm up 을 미리 하지 않으면 초기 요청들은 순단에 가까운 장애가 발생한다.

Java 기반 웹 애플리케이션의 첫 번째 요청의 응답이 평균 응답 시간 보다 훨씬 느린 이유는 JVM 아키텍처가 지연 클래스 로딩과 JIT 컴파일로 이루어져 있기 때문이다.

JIT 컴파일러는 바이드 코드를 머신 코드로 변환하는 과정에서 머신 코드를 캐시에 저장하고 활용한다.
이를 통해 반복되는 변환 과정을 줄여 성능을 향상시키고 런타임 환경에 맞춰 코드를 최적화함으로써 성능을 보완하고 있다.

애플리케이션 시작 후 의도적으로 미리 로직을 실행하여 기계어가 캐시에 저장되고 최적화될 수 있도록 하는 warm up 절차가 필요하다.

구현

  1. 필요한 라이브러리를 등록한다.
    implementation "org.springframework.boot:spring-boot-starter-actuator:$springbootVersion"
    implementation "org.springframework.boot:spring-boot:$springbootVersion"

그리고 application 설정 파일에 아래 두개를 추가하면 /actuator/health 요청에 health 관련 정보들이 나온다. show-details 는 필수가 아니고 새롭게 만든 health indicator 를 확인하려면 필요하다.

management.endpoints.web.exposure.include: 'health'
management.endpoint.health.show-details: always

2-1. 간단히 구현하려면 아래 처럼 하나의 클래스를 선언하여 구현한다.

@Component
public class WarmUpListener implements ApplicationListener<ApplicationReadyEvent> {
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        // Warm up
    }
}

2-2. MSA처럼 여러 프로젝트에서 필요 시 공통으로 사용할 라이브러리 코드를 만들 시 아래처럼 만들어 jar를 참조하도록 한다.

새로운 config 파일을 만든다. ApplicationHealthIndicatorConfig 에서 warmer 이라는 빈을 생성하는데, ApplicationHealthIndicator객체가 spring에서 제공하는 확장포인트(AbstractHealthIndicator) 를 구현한다.

@Slf4j
@Configuration
@RequiredArgsConstructor
public class ApplicationHealthIndicatorConfig {

	private final ApplicationWarmerMediator warmer;

	@Bean
	ApplicationHealthIndicator applicationHealthIndicator(ApplicationContext context) {
		var configurableApplicationContext = (ConfigurableApplicationContext)context;
		return new ApplicationHealthIndicator(configurableApplicationContext, warmer);
	}
}

2-3. ApplicationHealthIndicator 내부 구현을 해보자. 생성자로 ApplicationWarmerChecker 를 주입받고 doHealthCheck 메서드에서 새로운 룰로 health check 가 이뤄진다. 그 룰의 정책과 검사는 ApplicationWarmerChecker 가 담당한다.

@Slf4j
public class ApplicationHealthIndicator extends AbstractHealthIndicator {

	private final ApplicationWarmerChecker warmer;

	public ApplicationHealthIndicator(ConfigurableApplicationContext context,
									  ApplicationWarmerMediator warmer) {
		this.warmer = warmer;
		context.addApplicationListener(this.warmer);
	}

	@Override
	protected void doHealthCheck(Health.Builder builder) {
		log.info("request from /actuator/health");
		if (this.warmer.checkWarmup()) {
			builder.up();
		} else {
			builder.outOfService();
		}
	}
}

2-4. ApplicationWarmerChecker 에서는 ApplicationListener 인터페이스를 구현체로써 onApplicationEvent 메서드를 오버라이딩한다(여기서는 ApplicationReadyEvent 를 사용했다). 그런데 이렇게 되면 trigger 포인트가 두곳이다. onApplicationEvent 메서드와 /actuator/health 요청하면 호출되는 checkWarmup 메서드다(doHealthCheck 메서드가 checkWarmup 메서드를 호출하므로). 때에 따라서는 동시에 올 수 도 있어서 checkWarmup 메서드를 synchronized 처리한다.

@Slf4j
@Component
@RequiredArgsConstructor
public class ApplicationWarmerChecker implements ApplicationListener<ApplicationReadyEvent> {
	private final Object lockObject = new Object();
	private final AtomicBoolean completed = new AtomicBoolean(false);

	private final List<ApplicationWarmer> applicationWarmers;

	public boolean checkWarmup() {
		if (!completed.get()) {
			this.execute();
		}
		return completed.get();
	}

	private void execute() {
		synchronized (this.lockObject) {
			log.info("application warmup start");
			Optional.ofNullable(applicationWarmers)
					.orElseGet(Collections::emptyList)
					.forEach(this::warmup);
			this.completed.set(true);
			log.info("application warmup completed");
		}
	}

	private void warmup(ApplicationWarmer warmer) {
		try {
			var className = warmer.getClass().getSimpleName();
			log.info("{} - warmup start", className);
			warmer.warmup();
			log.info("{} - warmup end", className);
		} catch (Exception ex) {
			log.warn("ApplicationWarmer is Failed - " + ex.getMessage());
		}
	}

	@Override
	public void onApplicationEvent(@NonNull ApplicationReadyEvent event) {
		checkWarmup();
	}
}

2-5. ApplicationWarmer Interface를 정의한다.

해당 jar를 참조하는 어플리케이션에서 ApplicationWarmer을 상속받아 필요한 warm up을 적용한다.
예를들어 cache가 필요한 항목/메인 화면에서 호출되는 빈번한 호출 또는 비즈니스 적으로 response가 오래 걸리는 작업 등

public interface ApplicationWarmer {
	void warmup();
}

0개의 댓글