2015-02-09: haproxy の優雅な再起動

tl;dr haproxy -sf による再起動では SO_REUSEPORT が使えないと瞬断が発生する。SO_REUSEPORT は Linux 3.9+ か、CentOS, RHEL 6 では最新のカーネルに上げると利用できる。


haproxy は自分自身の設定を reload するみたいな便利な機能はない。 そのかわりに、 -sf オプションへ既存の pid を渡して新しく起動してあげると、入れ替わってくれる機能がある。

なんだけど、なぜか手元の環境だと ECONNREFUSED とかが発生するタイミングがあったので調べた。環境によってはならないこともある。

まず、最近の Linux には SO_REUSEPORT がある。sockopt で SO_REUSEPORT をつけていると同じ port に対して複数の fd が bind する事が許容される (The SO_REUSEPORT socket option [LWN.net]) オプションで、haproxy は勝手に利用してくれる

haproxy は起動するとまず必要なポートの bind を試みる。SO_REUSEPORT が利用できない場合は初回の bind は成功しない。そのため、新 haproxy は SIGTTOU を旧 haproxy へ送信、tcp_pause_listener 関数を実行させる。一定時間ごとに bind を再試行し、成功したタイミングで SIGUSR1 を旧 haproxy へ送信する。

一方 SO_REUSEPORT が利用できる環境では、新 haproxy の初回 bind はあっさり成功する。そのまま旧 haproxy へは SIGUSR1 が送信される。これによってダウンタイム無しに haproxy の入れ替えが完了する。この場合 SIGTTOU は送信されない。

どちらの場合も既存のコネクションは触られないため維持される。SIGUSR1 を受け取った旧 haproxy は、既存のコネクションを全て処理した段階で終了する。

そして SO_REUSEPORT が利用できない場合、 SIGTTOU → bind (再試行) → SIGUSR1 の間に新規の接続に対する ECONNREFUSED が発生する模様。このへんの処理は src/haproxy.c:L1554-1577 にある。

実際、再現しない環境でも手で SO_REUSEPORT の利用箇所をコメントアウトしてビルドすると再現した。

対応としては、新しい Linux を使う (Linux 3.9 以降)。しかし実は SO_REUSEPORT は最近の RHEL 6 (CentOS 6 では 2.6.32-417.el6?) で手に入る最新のカーネルへバックポートされている模様。なので RHEL 6, CentOS 6 をご利用の方々もどうにかなる気はする。

もう一個としてはhaproxy を立ち上げる親プロセス側であらかじめ bind した fd を持っておき、それを haproxy に渡す方法。bind fd@30 とかすると haproxy 側から利用できる。

楽しくなってちゃんと真面目に書いた実装がこちら。いちおう、bind に失敗した時や haproxy -c に失敗した時は死なないようになってたりします。 https://github.com/sorah/sandbox/tree/master/ruby/haproxy-master


あとこれは未だに良く分かってないので TCP_LISTEN な fd に対する shutdown(2) の挙動・このコードの意味を教えてください。とりあえず accept(2) できなくなるというのは軽くカーネル読んで確認しました。

追記: @pandax381 さんが教えてくださいました!

(ただし Linux のばあい)

(記事タイトルの元ネタ: nginxの優雅な再起動 )

Published at 2015-02-09 23:59:58 +0900