시작하며
내가 운영하는 마이데이터 서비스 아키텍처 앞단에는 API Gateway Solution이 붙어있다. Broadcom에서 제공하는 제품인데, 어느 순간부터 솔루션에 FullGC time alert이 발생하기 시작했다.
FullGC가 일어난다는 것은 stop-the-world가 발생한다는 의미이고, stop-the-world가 발생하면 GC를 실행하는 스레드를 제외한 나머지는 동작을 멈추기 때문에 서비스에 굉장히 크리티컬하다. 이 글에서는 이 FullGC 알람을 지우기 위해 시도한 과정을 정리한다.
방법 1. Heap Memory 증설
- 원인 분석 : 솔루션 단에서 Heap 메모리를 이전보다 많이 사용하고 있었다. 인증을 태우거나, 토큰을 발급하거나, 특정 Kafka Broker topic에 전송하기 위한 로그를 producing 하는 등 추가되는 작업들로 인해 Old Generation으로 넘어가는 오브젝트가 이전보다 늘고 있다고 판단되었다. 정확하게는 APM에서 이전 Gateway들의 GC stat을 모두 저장하고 있지 못했으므로, Old 영역에 어느 정도 속도로 쌓였는지 과거 데이터는 확인할 수 없었다.
- 액션 : 따라서 우선적으로 JVM Heap 메모리를 증설했다. 기존 4GB로 잡혀있던 Heap Size를 두 배로 늘렸고, OS 영역은 그대로 유지하기 위해 Heap 메모리 비율 조정뿐 아니라 메모리 자체를 두 배로 증설했다. 장기적으로 늘어나는 서비스 유저를 생각하면 필요한 작업이라는 판단에서였다.
- 결과 : 결과는 원하는 방향과 완전히 반대로 나왔다. Heap 메모리가 늘어남에 따라 Old-Generation 영역도 같이 늘어났고, FullGC가 일어나는 빈도는 줄었지만 그만큼 FullGC가 한 번 수행되는 시간이 길어졌다. 결국 stop-the-world가 발생할 확률이 더욱 높아졌다.
방법 2. GC algorithm 변경
- 원인 분석 : 결과적으로 FullGC의 횟수를 줄일 것인가, FullGC의 수행 시간을 줄일 것인가를 두고 Heap Size를 최적화하는 방법인데, 이 방법으로는 운영 상태의 Gateway들을 지속적으로 재기동하며 모니터링해야 했고, 추후 솔루션에서 수행하는 작업에 변화가 있다면 그때마다 같은 작업을 반복해야 하는 어려움이 있었다.
- 액션 : 논의 끝에 솔루션의 JVM이 사용 중인 GC 알고리즘을 Parallel GC → G1GC로 변경하기로 했다.
- 결과 : G1GC로 바뀌면서 전체적인 Response Time은 ms 단위로 살짝 올라갔지만 FullGC 횟수가 확연히 줄어들었다.
Parallel GC vs G1GC
사실 이번 포스팅에서 정리하고 싶었던 것은 이 부분이다. 먼저 GC가 가지고 있는 철학은 다음과 같다.
weak generational hypothesis
- 대부분의 객체는 금방 Unreachable 상태가 된다.
- 오래된 객체에서 젊은 객체로의 참조는 매우 드물게 발생한다.
Parallel GC는 여러 스레드를 사용해 GC를 병렬로 수행하여 처리량(throughput)을 극대화하는 데 초점이 있다. 반면 G1GC(Garbage First)는 Heap을 동일한 크기의 여러 region으로 나누고, garbage가 가장 많은 region부터 회수하여 예측 가능한 짧은 정지 시간(pause time)을 목표로 한다. 위 사례처럼 stop-the-world로 인한 응답성 저하가 문제일 때는, 처리량을 약간 손해 보더라도 정지 시간을 줄여주는 G1GC가 더 적합했다.
정리하며
이번 작업을 통해 단순히 Heap 메모리를 늘리는 것이 항상 정답은 아니며, 문제의 성격(FullGC 빈도 vs 수행 시간)에 따라 GC 알고리즘 선택이 더 본질적인 해결책이 될 수 있다는 것을 확인했다. 다음 포스팅에서는 G1GC의 동작 원리를 조금 더 자세히 정리해보려 한다. 👋