概要
先日リリースされた1.11でソケットオプションを設定できるようになりました。
これによってLinux 3.9から導入されたSO_REUSEPORT
という、同じポートでbindすることが可能になる機能が利用可能になります。
環境
何が嬉しい?
一言で言うとGraceful Restartが可能になるという点です。
通常サーバプロセスを再起動するとその瞬間はリクエストを捌けなくなります。
Rolling updateのような事ができる環境であればいいですが、そうではない場合
- a) Listenしているsocketのfile descriptorの複製
- b)
SO_REUSEPORT
を使う
といった手段でデプロイ時に一時的に古いプロセスを残したまま新しいプロセスを起動し、起動完了後に古いプロセスを落とすといった形で実現する必要があります。
ただし a
, b
のような手段でないと bind error で新しいプロセスの方は起動できません。通常使用中のポートは別プロセスで利用できないからです。
今回リリースされたgolang 1.11では b
を自前でサクッと対応できるようになります。
これまでの対応方法
以前は具体的には以下のような対応方法でした。
a. Listenしているsocketのfile descriptorの複製
以下のようにソケットマネージャのCircusを利用したケースや、
ライブラリを使ったり
以下のように自前で頑張って実装するなどがあります
Restarting a Go Program Without Downtime | Teleport
b. SO_REUSEPORT
を使う
以下のようなライブラリが対応してます。
1.11からの対応方法
golang1.11では前述のようなライブラリ等が不要になります。
では実際に簡単なサンプルを紹介します。
golang自体のテストコードを参考に書いているので、中の人もこんな感じに使う想定だと思います。
gist.github.com
説明
こちらのコミットで分かるように、ListenConfig
にControl
というフィールドが追加されます。
この関数のポイントとして
RawConn
を作ってからbindするまでに実行される- 引数に
syscall.RawConn
がある socket()
などソケット周りのメソッドの引数に追加されている
があります。
そしてこの関数の引数のsyscall.RawConn
のControl()
の引数にはfile descriptorが渡されるので、それに対して
operr = unix.SetsockoptInt(int(s), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
のようにSetsockoptInt(fd, level, opt int, value int)
でソケットオプションを設定していきます。
検証
では先ほどのコードをビルドしてみます。
1つめのmain実行
$ ./main
curlしてみます。
$ curl localhost:8080/hello Hello, World
普通にレスポンスが返ります。
2つめのmain実行
$ ./main
特にエラーなく起動できます。通常のソケットであれば
panic: listen tcp4 :8080: bind: address already in use
というエラーが発生します。
デプロイのように1つめを落としてcurl(次のリクエスト)をしてみると
$ curl localhost:8080/hello Hello, World
ちゃんとレスポンスが返ります。
Gracefulなデプロイをするには
古いバイナリold_bin
とデプロイしたい新しいバイナリnew_bin
があるとして、以下の手順を取ります。
old_bin
が起動中old_bin
のPIDを取得old_bin
をnew_bin
で上書きnew_bin
を起動old_bin
のプロセスをPIDを元にkill
この手順を踏めば再起動時も新しいリクエストをさばくことができます。
またold_bin
を終了する際も、以前書いた
を実装していれば実行中リクエストを破棄することなく完了できます。
注意点
検証中に気になった点を以下にまとめていきます。
Darwinだとリクエストが偏る
検証環境は
の2種類で検証しましたが、Linuxの方はある程度分散されたのですがMacの方では同じサーバの方にしかリクエストが行きませんでした。
ab
で検証してみたのですが、Macでは先ほどの例だと1つめを落とさない限りずっと1つめにリクエストが行きました。
またab
の途中で落とすとMacはab
の実行自体止まってしまいました。
Linuxの方は片方を落としても、落ちてない方にリクエストが流れていき問題ありませんでした。
古いバイナリがSO_REUSEPORT
に対応していない場合、bind error
SO_REUSEPORT
に未対応の古いバイナリが起動中であっても対応済みの方がうまく回避してくれる、というわけではなく普通に以下のエラーがでました。
$ ./main panic: listen tcp4 :3000: bind: address already in use goroutine 1 [running]: main.main() /home/ubuntu/test/main.go:21 +0x148
なので使う時は起動中のプロセスもSO_REUSEPORT
に対応している必要があります。
supervisor経由で起動するとbind error
SO_REUSEPORT
対応済みのバイナリをsupervisorで起動している状態で、同じバイナリを起動しようとしたところこちらもbind error
が発生しました。
こちらは現状原因が分かっていません。。
まとめ
golang 1.11からSO_REUSEPORT
が使えるようになりました。
またこれ以外にもTCP_FASTOPEN
などこれまで使えなかったソケット周りの機能が色々使えるようになります。
ソース
- Restarting a Go Program Without Downtime | Teleport
- net: make some way to set socket options other than using File{Listener,Conn,PacketConn} · Issue #9661 · golang/go · GitHub
- Why do we need conn and rawConn ? : golang
- proposal: syscall: add Conn and RawConn interfaces · Issue #19435 · golang/go · GitHub