Puma 6 Keep-Alive 버그의 원인
Puma 6 및 이전 버전에서는 스레드가 모두 사용 중일 때 새로운 Keep-Alive 요청이 “새치기”하는 문제가 있었습니다. 이는 동일한 Keep-Alive 연결에서 다음 요청(예: C, D)이 처리 대기 중인 다른 요청(예: B)보다 먼저 처리되는 현상을 초래했습니다. 이로 인해 특정 Keep-Alive 연결이 Puma 스레드를 독점하여 다른 요청의 지연을 야기했습니다. 이러한 동작은 특히 Puma가 스레드 수보다 많은 요청으로 “과부하” 상태일 때 발생했습니다.
“Fast Inline Optimization”의 부작용
이러한 새치기 현상은 의도된 것이 아니라, 파이프라인 연결을 위한 “Fast Inline Optimization”의 부산물이었습니다. 이 최적화는 뮤텍스 처리와 같은 오버헤드를 줄이기 위해 파이프라인 요청을 동일한 스레드에서 순서대로 처리하도록 하는 루프를 추가했습니다. 문제는 이 코드 경로가 Keep-Alive 연결에도 재사용되면서, 나중에 도착한 Keep-Alive 요청까지 마치 동시에 도착한 파이프라인 요청처럼 처리하여 불공정한 순서를 만들었다는 점입니다. max_fast_inline 설정으로 최대 10개의 요청으로 제한되었지만, 본질적인 문제는 해결되지 않았습니다.
Puma 7의 해결책: 공정한 스케줄링 및 부하 분산
Puma 7은 이 문제를 해결하기 위해 두 가지 주요 목표를 설정했습니다.
-
요청 처리 순서의 공정성: 소켓의
accept호출을 통해 들어온 요청이든, 기존 Keep-Alive 연결을 통해 들어온 요청이든, 수신된 순서대로 처리되어야 합니다. -
부하의 균등 분배: 서버 내 모든 Puma 프로세스(워커)에 요청 부하가 균등하게 분배되어야 합니다. Puma 7은 파이프라인 요청을 더 이상 단일 스레드가 독점하지 않고, 다음 요청이 도착한 것처럼 큐의 맨 뒤에 배치하여 다른 요청과 인터리빙(interleaving)되도록 했습니다. 또한, Puma 6의
wait_until_not_full최적화(스레드 여유가 없을 때accept루프를 블로킹)를 제거하고,accept루프가 절대 완전히 블로킹되지 않도록 변경했습니다. 대신,wait_for_less_busy_worker라는 기존 sleep-sort 전략을 확장하여, 더 바쁜 프로세스가 더 오래 슬립하도록 비례적으로 계산함으로써 워커 간 부하를 균등하게 분배합니다. 이러한 변경으로 Puma 7은 Keep-Alive 요청 처리의 공정성을 확보하고 서버 과부하 시에도 지연 시간이 공정하게 분배되도록 합니다.