본문에서는 Rails 애플리케이션의 Puma max_threads를 최적화하기 위한 구체적인 절차와 도구를 소개합니다.
GVL 상태 및 perfm Gem
Ruby GVL의 존재로 인해 스레드는 세 가지 상태 중 하나에 있을 수 있습니다:
-
Running: GVL을 소유하고 Ruby 코드를 실행 중.
-
Idle: I/O 작업을 수행 중이므로 GVL을 필요로 하지 않음.
-
Stalled: GVL을 원하지만 대기 큐에서 기다리는 중.
perfmGem은 Ruby 3.2+에서 사용 가능한 GVL 계측 API를 활용하여 각 스레드가 이 상태에서 보내는 시간을 측정합니다. 이는 Rack 미들웨어로 작동하여 지표를 수집하고 저장하며,Perfm::GvlMetricsAnalyzer클래스를 통해 보고서를 생성합니다.
perfm을 이용한 I/O 비율 측정
perfm Gem을 Gemfile에 추가하고 bin/rails generate perfm:install로 마이그레이션을 생성한 후, config/initializers/perfm.rb에 설정을 추가합니다.
ruby
Perfm.configure do |config|
config.enabled = true
config.monitor_gvl = true
config.storage = :local
end
Perfm.setup!
프로덕션 환경에 배포 후 약 2만 건의 요청 데이터를 수집한 뒤, monitor_gvl을 false로 설정하여 데이터 수집을 중지합니다. Rails 콘솔에서 Perfm::GvlMetricsAnalyzer를 사용하여 I/O 비율을 분석할 수 있습니다. 예시로 NeetoCal 애플리케이션은 45%의 I/O 비율을 보였습니다.
암달의 법칙을 통한 이론적 스레드 수 계산
암달의 법칙은 병렬화 가능한 부분(p)과 스레드 수(N)를 기반으로 이론적인 최대 속도 향상을 예측합니다. I/O 비율 45%(p=0.45)를 적용하면, 스레드 수가 4개일 때 이전 대비 성능 향상이 5% 미만으로 떨어져, 이론적으로 4개의 스레드가 합리적인 값임을 시사합니다.
정체 시간(Stall Time)을 이용한 스레드 수 검증
이론적으로 도출된 RAILS_MAX_THREADS 값을 실제 환경에 적용하고 GVL 정체 시간(stall time)을 측정하여 검증합니다. GVL 정체는 스레드가 GVL을 기다리는 시간으로, 75ms 미만이 권장됩니다. perfm 분석 결과를 통해 평균 정체 시간(average_stall_ms)을 확인할 수 있습니다.
-
puma_max_threads: 4설정 시, NeetoCal의 평균 정체 시간은 110.24ms로 높게 나타났습니다. -
puma_max_threads: 3으로 감소시키자, 평균 정체 시간은 79.38ms로 75ms에 가까워졌습니다. 이는RAILS_MAX_THREADS에 3을 최종 값으로 결정할 수 있음을 의미합니다. 스레드 수를 더 줄이면 정체 시간은 감소하지만, 애플리케이션의 동시성을 제한하게 되므로 트레이드오프를 고려해야 합니다.
GVL 정체 감소를 위한 권장 사항
-
N+1 쿼리 제거
-
오래 실행되는 쿼리 최적화
-
인라인 서드파티 API 호출을 백그라운드 작업으로 이동
-
무거운 계산 작업을 백그라운드 작업으로 이동 I/O 작업이 많은 프록시 애플리케이션의 경우, Falcon과 같은 서버로 전환하는 것을 고려할 수 있습니다.