Puma는 rails server
명령을 통해 또는 bundle exec puma
명령으로 직접 실행될 수 있으며, 두 경우 모두 Puma::Launcher
를 통해 시작됩니다. Launcher
는 Puma의 진입점이며, 서버의 실행 모드를 결정하는 Runner
인스턴스를 초기화합니다.
Puma의 실행 모드
### 1. 단일 모드 (Single Mode)
기본 모드인 단일 모드는 단일 OS 프로세스 내에서 하나의 서버와 하나의 스레드 풀을 운영합니다. Single#run
메서드는 애플리케이션을 메모리에 로드하고 소켓을 바인딩한 후, Puma::Server
인스턴스를 시작합니다.
Puma::Server
는 다음의 내부 스레드를 관리합니다:
* Reactor 스레드: queue_requests
설정(기본 활성화)에 따라 미완료 HTTP 요청을 버퍼링합니다. 워커 스레드가 연결을 수락한 후 요청이 불완전하면 Reactor 스레드에 넘겨주어 백그라운드에서 데이터를 기다리게 함으로써 워커 스레드의 가용성을 높이고 Slow Client Attack을 방지합니다.
* Reaper 스레드: 주기적으로 비정상 종료된(dead) 스레드를 감지하여 종료하고 새로운 스레드를 생성하여 스레드 풀의 안정성을 유지합니다.
* Trimmer 스레드: 서버 부하에 따라 스레드 풀의 크기를 동적으로 조절하며, 유휴 상태의 스레드를 최소 설정치까지 줄여 리소스를 최적화합니다.
요청 처리는 IO.select
를 사용하여 소켓의 읽기 가능 상태를 감지하며, Puma 5부터 GitLab의 기여로 도입된 워커 로드 밸런싱 기능은 바쁜 워커에 미세한 지연(0.005초)을 주어 유휴 워커가 요청을 먼저 처리하도록 유도하여 전체적인 처리량을 개선합니다.
2. 클러스터 모드 (Cluster Mode)
클러스터 모드는 다수의 워커 프로세스를 운영하며, 각 워커는 독립적인 서버와 스레드 풀을 가집니다. Puma::Cluster
클래스는 마스터 프로세스 역할을 하며, Process.fork
를 사용하여 워커를 생성하고 관리합니다.
클러스터 모드의 핵심 기능:
* preload_app
: 워커를 fork하기 전에 마스터 프로세스에서 애플리케이션을 메모리에 로드합니다. 이는 Copy-on-Write(CoW) OS 최적화를 활용하여, Rails 설정이나 초기화 코드와 같이 한 번 로드된 후 변경되지 않는 ‘불변 객체’의 메모리를 워커 간에 공유하게 하여 메모리 사용량을 크게 절감합니다. 이 기능을 사용하려면 애플리케이션이 fork-safe
해야 합니다.
* IPC (Inter-Process Communication): Puma는 IO.pipe
를 활용하여 마스터 프로세스와 워커 프로세스 간의 단방향 통신을 구현합니다. 워커는 파이프를 통해 자신의 상태를 마스터에게 보고하며, 마스터는 이를 통해 워커의 생존 여부 및 상태를 모니터링합니다.
마스터 프로세스는 워커로부터의 메시지를 기다리는 메인 루프를 실행하며, 워커의 부팅, 종료, 외부 시그널 수신 등 다양한 이벤트를 처리하여 클러스터 전체의 안정적인 운영을 보장합니다.