Carpe Diem

備忘録

Golang 1.11 で導入された ListenConfig を使って SO_REUSEPORT を利用する

概要

先日リリースされた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を利用したケースや、

ライブラリを使ったり

github.com

以下のように自前で頑張って実装するなどがあります

Gracefully Restarting a Go Program Without Downtime

b. SO_REUSEPORTを使う

以下のようなライブラリが対応してます。

github.com

1.11からの対応方法

golang1.11では前述のようなライブラリ等が不要になります。
では実際に簡単なサンプルを紹介します。
golang自体のテストコードを参考に書いているので、中の人もこんな感じに使う想定だと思います。 gist.github.com

説明

github.com

こちらのコミットで分かるように、ListenConfigControlというフィールドが追加されます。
この関数のポイントとして

  • RawConnを作ってからbindするまでに実行される
  • 引数にsyscall.RawConnがある
  • socket()などソケット周りのメソッドの引数に追加されている

があります。

そしてこの関数の引数のsyscall.RawConnControl()の引数には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があるとして、以下の手順を取ります。

  1. old_binが起動中
  2. old_binのPIDを取得
  3. old_binnew_binで上書き
  4. new_binを起動
  5. old_binのプロセスをPIDを元にkill

この手順を踏めば再起動時も新しいリクエストをさばくことができます。
またold_binを終了する際も、以前書いた

christina04.hatenablog.com

を実装していれば実行中リクエストを破棄することなく完了できます。

注意点

検証中に気になった点を以下にまとめていきます。

Darwinだとリクエストが偏る

検証環境は

の2種類で検証しましたが、Linuxの方はある程度分散されたのですがMacの方では同じサーバの方にしかリクエストが行きませんでした。
abで検証してみたのですが、Macでは先ほどの例だと1つめを落とさない限りずっと1つめにリクエストが行きました。
またabの途中で落とすとMacabの実行自体止まってしまいました。
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などこれまで使えなかったソケット周りの機能が色々使えるようになります。

ソース