사이트를 다시 로드할 때 약 40초간의 다운타임이 발생하고 있습니다. 제 기술 수준을 좀 벗어난 문제라 클로드(Claude)에게 도움을 요청했습니다.
클로드는 문제가 유니콘(unicorn) 런처가 구버전 마스터 프로세스에 잘못된 신호(SIGTERM 대신 SIGQUIT)를 보내고 있기 때문이라고 주장합니다. 이로 인해 새 마스터가 요청을 처리하기 전에 구버전 마스터가 종료됩니다.
수정 내용은 6줄의 업데이트로 알려졌지만, 먼저 여기서 건전성 검사(sanity check)를 하고 싶습니다.
클로드가 생성한 보고서 보기
요약
pitchfork 코드 경로에서 config/unicorn_launcher의 on_reload_pitchfork()는 워커 _프로세스_가 존재하는 즉시 나가는(outgoing) 마스터를 퇴역(retire)시킵니다. 이 과정에서 새 마스터가 요청을 처리할 수 있는지 확인하지 않으며, kill -s QUIT(그라운셜 종료) 대신 kill(SIGTERM, 즉시 종료)을 사용하여 퇴역시킵니다.
그 바로 위 유니콘 경로인 on_reload_unicorn()은 두 가지를 모두 올바르게 수행합니다 — curl $LOCAL_WEB을 실행하여 새 마스터를 워밍업하고, 그런 다음 QUIT 신호를 보냅니다.
단일 sv hup unicorn 명령의 관찰된 결과는 약 40초 동안 어떤 요청도 완료되지 않는 상태이며, 이는 새 세대가 Booted Rails를 로깅하는 시점에 정확히 종료됩니다.
환경
- Discourse
2026.8.0-latest.1, Rails 8.0.5.1, Ruby 3.4.10 - pitchfork 0.18.2,
RUN_PITCHFORK미설정 (따라서 pitchfork 분기가 활성화됨) - 스탠드얼론 컨테이너, nginx →
http://127.0.0.1:3000 - 2 vCPU / 4 GB 드롭렛; 앱 부팅에 약 40초 소요
코드
config/unicorn_launcher:
function on_reload_unicorn() {
...
while [ ... workers not up ... ]; do sleep 1; done
curl $LOCAL_WEB &>/dev/null # warm the new master
kill -s QUIT $UNICORN_PID # graceful
}
function on_reload_pitchfork() {
log "Reloading pitchfork ($UNICORN_PID)"
OLD_PID=$UNICORN_PID
pitchfork "${PITCHFORK_ARGS[@]}" &
UNICORN_PID=$!
count=0
while [ "$count" -lt 180 -a -z "$(pgrep -f -P $UNICORN_PID worker)" ]; do
log "Waiting for new pitchfork workers under $UNICORN_PID to start up..."
count=$((count + 1))
sleep 1
done
kill $OLD_PID 2>/dev/null # SIGTERM, and no warm-up first
}
pgrep -f -P $UNICORN_PID worker는 워커 프로세스가 _생성(spawn)_되는 즉시 반환됩니다. pitchfork에서는 워커가 애플리케이션 로딩이 완료되기 훨씬 전에 존재하므로, 루프가 조기에 종료되고 새 마스터가 여전히 부팅 중인 동안 구버전 마스터가 해체됩니다.
관찰된 동작
production.log, 리로드 1회 (타임스탬프 UTC):
23:16:49 Completed 200 OK in 95ms <- outgoing generation, warm
(44 seconds; not one Completed line)
23:17:33 Booted Rails 8.0.5.1 application in production environment
23:17:36 Completed 200 OK in 4128ms <- first render on the new generation, cold
23:17:39 Completed 200 OK in 200ms
해당 시간 동안 nginx를 통해 약 2초 간격으로 GET /를 폴링한 모니터는 다음과 같이 기록했습니다:
23:17:11 499 19.697s (client gave up; upstream never answered)
23:17:29 503 16.301s
23:17:31 502 1.448s
그리고 502 오류에 대한 nginx 오류 로그:
2026/08/14 23:17:31 [error] 70#70: *110675 recv() failed (104: Connection reset by peer)
while reading response header from upstream, upstream: "http://127.0.0.1:3000/"
모든 실패한 라인에서 upstream_response_time이 request_time과 같으므로 nginx가 병목이 아닙니다 — 요청은 응답할 수 없는 프로세스에 도달하고 있습니다. 23:17:31의 리셋은 큐에 연결이 남아 있는 구버전 마스터를 SIGTERM이 해체하는 것과 일치합니다.
이것은 재현 가능합니다: 한 번의 배포에서 두 번의 리로드는 각각 Booted Rails 라인에서 종료되는 동일한 약 40초의 윈도우를 생성했습니다.
리소스 압박이 아닙니다: dmesg에 OOM 킬이 없고, 스왑은 2048MB 중 141MB 사용, 로드 평균 0.07, 그리고 두 번의 부팅 모두 정상적으로 완료되었습니다.
권장 수정 사항
유니콘 분기를 모방하여, 구버전을 퇴역시키기 전에 새 마스터가 실제로 요청을 처리하는지 기다리고, 그라운셜하게 퇴역시킵니다:
count=0
while [ "$count" -lt 180 -a -z "$(pgrep -f -P $UNICORN_PID worker)" ]; do
log "Waiting for new pitchfork workers under $UNICORN_PID to start up..."
count=$((count + 1))
sleep 1
done
- kill $OLD_PID 2>/dev/null
+ count=0
+ until curl -sf -o /dev/null "$LOCAL_WEB" || [ "$count" -ge 180 ]; do
+ log "Waiting for the new pitchfork master to serve a request..."
+ count=$((count + 1))
+ sleep 1
+ done
+
+ kill -s QUIT $OLD_PID 2>/dev/null
TERM 대신 QUIT를 사용하는 부분은 모호하지 않습니다: pitchfork는 유니콘의 신호 시맨틱을 상속받으며, 여기서 QUIT는 드레인(drain)하고 TERM은 즉시 정지입니다.
PS: 이것은 제 첫 번째 AI 보조 보고서입니다. 예의를 어기고 있다면 알려주세요. ![]()