4. HAProxyの停止と再起動
HAProxyはグレースフル停止と強制停止をサポートしています。強制停止は単純で、haproxyプロセスにSIGTERMシグナルを送ると即座に終了し、確立済みの接続はすべて閉じられます。グレースフル停止は、haproxyプロセスにSIGUSR1シグナルを送ると開始されます。リッスンポートのバインドだけを解除し、既存の接続が閉じられるまで処理を続けます。最後の接続が閉じられると、プロセスが終了します。
サービス管理スクリプトの"stop"や"restart"アクションでは、強制停止が使われます。新しいプロセスで新しい設定を途切れなく読み込もうとする"reload"アクションでは、グレースフル停止が使われます。
これらのシグナルは、リロードや再起動の際に新しいhaproxyプロセス自身から送ることもできます。これにより、絶対に必要な場合に限り、可能な限り遅い時点で送信されます。この動作を行うのが、それぞれ"-st"(強制停止)と"-sf"(グレースフル停止)オプションです。
マスターワーカーモードでは、設定のリロードのために新しいhaproxyプロセスを起動する必要はありません。マスタープロセスはSIGUSR2シグナルを受け取ると、-sfパラメーターにワーカーのPIDを続けて指定し、自身を再実行します。その後、設定ファイルを解析し、新しいワーカーをforkします。
これらのシグナルの使われ方をよりよく理解するには、再起動の仕組み全体を理解することが重要です。
まず、既存のhaproxyプロセスが動作しています。管理者は"/etc/init.d/haproxy reload"など、システム固有のコマンドを使って、新しい設定ファイルの適用を指示します。すると、次の処理が行われます。最初に、サービススクリプト(/etc/init.d/haproxyなど)が"haproxy -c"で設定ファイルを正しく解析できることを確認します。その後、この設定ファイルを使い、"-st"または"-sf"を指定してhaproxyの起動を試みます。
次に、HAProxyはすべてのリッスンポートへのバインドを試みます。致命的なエラー(システムにアドレスが存在しない、権限がないなど)が発生すると、エラーで終了します。ポートがすでに使用されているためにソケットのバインドに失敗した場合は、まず"-st"または"-sf"のPIDリストで指定されたすべてのPIDにSIGTTOUシグナルを送ります。これが"pause"シグナルです。既存のすべてのhaproxyプロセスに対し、一時的にポートでのリッスンを停止して、新しいプロセスが再度バインドを試せるよう指示します。この間も、古いプロセスは既存の接続を処理し続けます。それでもバインドに失敗する場合(別のデーモンとポートを共有している場合など)は、新しいプロセスが古いプロセスにSIGTTINシグナルを送り、何も起きなかったかのように動作を再開するよう指示します。古いプロセスはポートでのリッスンを再開し、接続を受け付け続けます。この仕組みはシステムに依存し、OSによってはマルチプロセスモードでサポートされない場合があります。
新しいプロセスがすべてのポートに正常にバインドできた場合は、すべてのプロセスにSIGTERM("-st"の場合の強制停止)またはSIGUSR1("-sf"の場合のグレースフル停止)を送ります。これにより、新しいプロセスが処理を引き継ぎ、古いプロセスは即座に、または処理を終えた時点で終了しなければならないことを通知します。
この一連の処理には、それぞれ数ミリ秒の短い期間が2つあり、高負荷時にはその間に少数の接続失敗が発生する可能性がある点に注意が必要です。一般的に観測される失敗率は、新規接続が毎秒10000件ある場合、リロード操作中に約1件です。つまり、新規接続を毎秒30000件処理している高負荷のサイトでは、リロードのたびに約3件の接続失敗が発生する可能性があります。これが起きるのは、次の2つの状況です。
-
古いプロセスが存在するために新しいプロセスがバインドできない場合は、まずSIGTTOU+SIGTTINの手順を実行する必要があります。フロントエンドが数十個ある場合、通常は約1ミリ秒かかり、この間、一部のポートは古いプロセスのバインドが解除され、新しいプロセスにもまだバインドされていない状態になります。SO_REUSEPORTソケットオプションをサポートするシステムでは、古いプロセスにバインド解除を求めずに新しいプロセスがバインドできるため、HAProxyはこの問題を回避できます。ほとんどのBSDシステムは、非常に古くからこれをサポートしています。Linuxではバージョン2.0でサポートされていましたが、2.2頃に削除されました。当時もいくつかのパッチは流通していました。カーネル3.9で再導入されたため、上記よりも高い接続失敗率が見られる場合は、カーネルが3.9以降であるか、該当するパッチがバックポートされているか(その可能性は低いですが)を確認してください。
-
古いプロセスがリッスンポートを閉じる際、ソケットのバックログに残っていた保留中の接続を、カーネルが必ずしも再分配するとは限りません。高負荷時には、ソケットが閉じられる直前にSYNパケットが届き、クライアントにRSTパケットが送られることがあります。1件の切断も許されない重要な環境では、リロード中にファイアウォールルールでSYNパケットをブロックしてクライアントに再送させ、この問題に対処することがあります。ほかのリッスンキューを調べてこのRSTを回避できるシステムもあるため、この動作は完全にシステムに依存します。もう1つは、閉じられる直前にSYN_RECV状態にあったローカルソケットに、クライアントからACKが届く場合です。このACKによってRSTパケットが送られますが、haproxyプロセスはまだこの接続を認識していません。このケースの回避はより困難ですが、プロセスを再起動する約1秒前に上記のファイアウォールのフィルタールールを適用すると、うまく対処できます。
大多数のユーザーでは、競合状態を引き起こすほど負荷が高くないため、このような切断は発生しません。また、トラフィックの多いユーザーでも、少なくともSO_REUSEPORTがシステムで正しくサポートされていれば、失敗率は通常のばらつきの範囲に十分収まります。