tl;dr haproxy -sf
による再起動では SO_REUSEPORT が使えないと瞬断が発生する。SO_REUSEPORT は Linux 3.9+ か、CentOS, RHEL 6 では最新のカーネルに上げると利用できる。
haproxy は自分自身の設定を reload するみたいな便利な機能はない。
そのかわりに、 -sf
オプションへ既存の pid を渡して新しく起動してあげると、入れ替わってくれる機能がある。
-
http://linux.die.net/man/1/haproxy
Send FINISH signal to the pids in pidlist after startup. The processes which receive this signal will wait for all sessions to finish before exiting. This option must be specified last, followed by any number of PIDs. Technically speaking, SIGTTOU and SIGUSR1 are sent.
なんだけど、なぜか手元の環境だと 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) できなくなるというのは軽くカーネル読んで確認しました。
これ何やってるかいまいち分からない (listen している socket に対する shutdown(2)) ので誰か解説を… https://t.co/6MQuj8k5ZX
— そらは (@sora_h) February 9, 2015
追記: @pandax381 さんが教えてくださいました!
@sora_h すでに解決済みかもしれませんが… listen socket に対して shutdown(2) しているのは、後に resume_listener() にて再度 listen(2) するためじゃないですかね。close(2) だとディスクリプタ解放されてしまうので
— みやび (@pandax381) February 10, 2015
@sora_h 再度 listen して shutdown(SHUT_RD) した時点で backlog に溜まっている ESTABLISHED だけど未acceptなコネクションにRSTが送られて、以降はディスクリプタは有効だけど SYN に対して RST 返す動きになるかと
— みやび (@pandax381) February 10, 2015
(ただし Linux のばあい)
(記事タイトルの元ネタ: nginxの優雅な再起動 )