개요
카카오T의 계정 서비스에서 JVM Warm Up을 통해 어떻게 성능을 향상시켰는지 알아봅니다.
Intro
- 자바는 최초 인터프리터로 동작하는 이유에 성능이 컴파일 언어에 비해 부족합니다. 이를 해결하기 위해 자바에서는 적시에 기계어를 만드는 JIT(Just In Time) Compiler를 사용합니다.
- 머신 코드를 캐시에 저장해 반복되는 기계어 변환 과정을 줄이는 방식으로 동작합니다.
- 하지만 애플리케이션 시작 단계에서는 캐시된 내역이 없어 성능 이슈가 발생 가능합니다. 의도적으로 미리 최적화 단계를 위한 웜업이 필요합니다.
병목을 찾자
- 리소스는 여유가 있음에도 트래픽이 몰렸을 때 응답 지연이 발생했습니다. 대부분은 외부 시스템이 아닌 내부에 있었습니다. 개선 사항은 아래와 같습니다.
- 톰캣 스레드가 서버 시작 시 200개까지 늘어나는 것을 확인하여, 기존 톰캣 스레드의 개수를 10 → 256개로 수정했습니다.
- 기존 동작하던 웜업을 리얼 트래픽과 비슷하게 조정했습니다.
- Database Query → Localhost Api call
- 웜업 이전까진 400을 응답하고 완료되면 200을 반환해 트래픽이 유입할 수 있도록 변경했습니다.
- 자주 사용되는 + 대부분의 GET API를 포함시켰고 실제 응답과 유사하게 처리했습니다. APM에서 발견된 지연 대상 API도 추가했습니다.
Ideation
- Hotspot에서 Graal JIT으로 변경해보았지만, 효과가 없었습니다.
- Graal VM에서 제공하는 네이티브 바이너리를 생성해볼까 고민했지만, 코드 동작을 보장하기 어려워 선택하지 않았습니다.
- Redis 지연으로 인한 커넥션 풀을 도입했지만 해결되지 않았습니다.
- 실 트래픽처럼 Warm up count를 상당수 늘려서 해결했습니다.
JIT(Just in time) Internals
- JIT는 메서드 전체 단위로 컴파일을 실행합니다. 네이티브 코드로 변환한 뒤에는 추가적인 최적화 작업(Tiered Compilation)을 위해 프로파일링 데이터들을 수집합니다.
- Tiered Compilation은 크게 C1 컴파일러의 간략한 최적화와 C2 컴파일러 최대 최적화의 작업으로 나눠집니다.
- 둘 모두 컴파일된 네이티브 코드를 ‘코드 캐시’라는 내부 메모리에 적재해 활용하는 형태로 동작합니다.
- JVM은 인터프리터에 의해 특정 임계치만큼 해석되는 메서드들을 C1 컴파일러를 통해 최적화합니다. 이후 C2 컴파일러 임계치 설정만큼 더 많이 호출되는 경우 C2 컴파일러를 통해 최대 최적화를 실행합니다.