SV HUP Unicornのトラフィック減少

サイトのリロード時に約40秒のダウンタイムが発生しています。これは私の技術的な能力を超えているため、Claudeに助けを求めています。

Claudeによると、問題はユニコーン(Unicorn)ランチャーが古いマスタープロセスに誤ったシグナル(SIGQUITの代わりにSIGTERM)を送信していることです。これにより、新しいマスターがサービスを開始する前に古いマスターが強制終了してしまいます。

修正にはわずか6行の更新が必要とのことです。しかし、まずはここで健全性チェックを行いたいです。

Claudeが生成したレポートを見るにはクリック

概要

pitchforkコードパスにおいて、config/unicorn_launcheron_reload_pitchfork()関数は、新しいマスターがリクエストを処理できることを確認することなく、ワーカープロセスが存在し次第、古いマスターを退任させます。また、退任にはkill(SIGTERM、即時シャットダウン)が使用されており、kill -s QUIT(優雅なシャットダウン)ではありません。

その直上のユニコーンパスであるon_reload_unicorn()は、この両方を正しく処理しています。つまり、新しいマスターをウォームアップするためにcurl $LOCAL_WEBを発行し、その後QUITを送信します。

単一のsv hup unicornコマンド実行後の観測結果は、新しい世代がBooted Railsをログに記録するまで、約40秒間リクエストが完了しないというものでした。

環境

  • 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      # 新しいマスターをウォームアップ
  kill -s QUIT $UNICORN_PID        # 優雅な終了
}

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、ウォームアップなし
}

pgrep -f -P $UNICORN_PID workerは、ワーカープロセスが生成された時点で返ります。pitchforkでは、アプリケーションの読み込みが完了するずっと前にワーカーが存在するため、ループは早期に終了し、新しいマスターがまだ起動中の間に古いマスターが解体されます。

観測された挙動

production.log、1回のリロード(タイムスタンプはUTC):

23:16:49  Completed 200 OK in 95ms        <- 古い世代、ウォーム
          (44秒間、Completed行なし)
23:17:33  Booted Rails 8.0.5.1 application in production environment
23:17:36  Completed 200 OK in 4128ms      <- 新しい世代での最初のレンダリング、コールド
23:17:39  Completed 200 OK in 200ms

その間、nginx経由で約2秒ごとにGET /をポーリングするモニターは以下を記録しました:

23:17:11  499  19.697s   (クライアントが放棄; upstreamは応答せず)
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_timerequest_timeと等しいため、nginxがボトルネックではありません。リクエストは応答できないプロセスに到達しています。23:17:31でのリセットは、接続がまだキューイングされたままの古いマスターをSIGTERMが解体したことに一致します。

これは再現可能です。1回のデプロイでの2回のリロードは、それぞれ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の支援を受けて作成した初めてのレポートです。何か礼儀違反があれば教えてください。:sweat_smile: