How we optimized our Akka application using Datadog’s Continuous Profiler (새 탭에서 열림)
Datadog은 Akka 프레임워크 기반 서비스에서 예상치 못한 20~30%의 CPU 과점유 문제를 발견했으며, 이는 성능 프로파일링 도구를 통해 `ForkJoinPool` 내부의 비효율성 때문인 것으로 밝혀졌습니다. 불규칙한 작업 부하를 가진 액터가 기본 디스패처에서 실행될 때, 스레드의 빈번한 생성과 정지(`park`/`unpark`)가 반복되면서 과도한 네이티브 호출 오버헤드가 발생한 것이 원인이었습니다. 이를 안정적인 작업 흐름을 가진 전용 디스패처로 분리함으로써 시스템 전반의 CPU 효율을 획기적으로 개선할 수 있었습니다. **성능 병목 현상의 발견과 프로파일링** * 로그 파싱 알고리즘을 최적화했음에도 불구하고 CPU 사용량이 줄어들지 않고 오히려 성능이 저하되는 기현상이 발생했습니다. * Datadog Continuous Profiler의 플레임 그래프 분석 결과, 예상 밖으로 `ForkJoinPool.scan()` 및 `Unsafe.park()` 메서드에서 전체 CPU의 상당 부분이 소모되고 있었습니다. * 상세 분석 결과, 특정 작업 풀(Work pool)이 아닌 Akka의 기본 디스패처(Default Dispatcher) 소속 스레드들이 대부분의 CPU 시간을 점유하고 있었으며, 이는 주로 지표를 보고하는 `LatencyReportActor`와 연관되어 있었습니다. **ForkJoinPool 오버헤드의 근본 원인** * `ForkJoinPool`은 대기 중인 작업량에 따라 내부 워커 스레드를 동적으로 추가하거나 `Unsafe.park()`/`unpark()`를 호출하여 스레드를 일시 중지 및 재개합니다. * 문제가 된 액터는 매초 수백 개의 이벤트를 순식간에 처리한 뒤 다음 배치가 올 때까지 대기하는 불규칙한(Irregular) 작업 흐름을 가지고 있었습니다. * 이로 인해 매초 가용한 모든 코어(예: 32코어)만큼의 스레드가 동시에 깨어났다가 작업 완료 후 즉시 잠드는 과정이 반복되었고, 이 과정에서 발생하는 네이티브 호출 비용이 실제 작업 비용보다 커지는 결과가 초래되었습니다. **디스패처 분리를 통한 문제 해결** * 불규칙한 부하를 가진 액터를 기본 디스패처에서 분리하여, 부하가 일정하게 유지되는 메인 "work" 디스패처로 이동시키는 단 한 줄의 설정 변경을 적용했습니다. * 수정 후 서비스 전체 CPU 사용량이 평균 30% 감소했으며, 프로파일링 결과 `ForkJoinPool.scan()`에서 소모되던 시간이 거의 사라진 것을 확인했습니다. * 기본 디스패처의 스레드 풀 크기가 32개에서 2개로 줄어들며 스레드 관리 효율이 극대화되었습니다. **안정적인 성능 유지를 위한 권장 사항** * `ForkJoinPool.scan()` 메서드의 CPU 점유율이 10~15%를 초과한다면 스레드 풀 설정과 부하의 안정성을 반드시 점검해야 합니다. * Akka 사용 시 액터 인스턴스 수를 제한하거나 스레드 풀의 최대 크기를 적절히 설정하여 무분별한 스레드 확장을 방지해야 합니다. * 작업 큐를 활용해 급격한 작업 스파이크를 완만하게 조절하거나, 사용되는 스레드 풀의 수를 줄여 부하 밀집도를 높임으로써 활성 스레드 수를 일정하게 유지하는 것이 중요합니다.