성능 저하의 미스터리 및 오진
애플리케이션은 부하가 없음에도 불구하고 응답 시간이 지속적으로 증가하는 현상을 겪었습니다. 서버의 CPU와 메모리 사용률은 낮았지만, 레이턴시 그래프는 급격히 상승했습니다. 초기 진단에서는 다음과 같은 일반적인 성능 개선 방안을 시도했습니다.
-
YJIT 활성화: 부분적인 개선은 있었으나 근본적인 해결책은 아니었습니다.
-
GC 튜닝: 미미한 성능 향상에 그쳤습니다.
-
Redis 캐싱: 정상적으로 작동하고 있었으며, 문제의 원인이 아니었습니다.
-
인프라 증설: 이미 충분히 오버프로비저닝된 상태였습니다.
이러한 노력에도 불구하고 레이턴시 스파이크, 타임아웃, 사용자 불만은 지속되었습니다.
문제의 핵심: 스레드 고갈
Datadog APM 추적 중 ActiveRecord::ConnectionTimeoutError 발생 횟수가 증가하는 것을 발견했습니다. 이는 데이터베이스 연결 풀 내 스레드 고갈을 시사하는 결정적인 단서였습니다.
-
Rails 기본 설정: ActiveRecord의 기본 연결 풀 크기는 5였습니다.
-
Puma 설정: Puma는 최대 16개의 스레드를 사용하도록 구성되어 있었습니다.
즉, 최대 16개의 스레드가 단 5개의 사용 가능한 데이터베이스 연결을 기다리는 상황이 발생했으며, 대부분의 스레드는 유휴 상태로 대기하고 있었습니다. 이러한 상황에서는 아무리 캐싱이나 GC 최적화를 해도 성능 병목 현상을 해결할 수 없었습니다.
해결책 및 결과
문제 해결을 위해 config/database.yml 파일에서 pool 설정을 Puma의 최대 스레드 수와 일치하도록 조정했습니다.
yaml
# config/database.yml
production:
adapter: postgresql
pool: 16
timeout: 5000
이후 Puma를 동일한 스레드 설정으로 재시작했습니다 (puma -t 4:16).
단 하나의 설정 변경으로 인해 백로그가 거의 즉시 해소되었습니다.
-
평균 레이턴시: 650ms에서 220ms로 급감했습니다.
-
CPU 활용률: 정상적으로 상승하며 애플리케이션이 활성화되었습니다.
추가 프로파일링 결과, 연결 타임아웃은 더 이상 발생하지 않았고 APM 추적에서도 스레드가 유휴 상태로 대기하는 대신 활성화된 상태를 유지하는 것이 확인되었습니다. 이는 복잡한 코드 변경이나 프레임워크 교체 없이, 기본적인 관찰과 정확한 진단을 통해 얻어진 효과적인 해결책이었습니다.