http.ListenAndServe() を使ったサーバーをプロダクションに投入していたのですが、海外からのアクセスが多くなったころにリソースリークが発覚しました。
ListenAndServeのソースを見るとこうなっています。
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
addr, handler 以外は http.Server のnil値がそのまま使われている事がわかります。この構造体にはいくつかのタイムアウト値がありまが、nil値で初期化されるとタイムアウトなしの状態になってしまいます。
// ReadTimeout is the maximum duration for reading the entire
// request, including the body.
//
// Because ReadTimeout does not let Handlers make per-request
// decisions on each request body's acceptable deadline or
// upload rate, most users will prefer to use
// ReadHeaderTimeout. It is valid to use them both.
ReadTimeout time.Duration
// ReadHeaderTimeout is the amount of time allowed to read
// request headers. The connection's read deadline is reset
// after reading the headers and the Handler can decide what
// is considered too slow for the body.
ReadHeaderTimeout time.Duration // Go 1.8
// WriteTimeout is the maximum duration before timing out
// writes of the response. It is reset whenever a new
// request's header is read. Like ReadTimeout, it does not
// let Handlers make decisions on a per-request basis.
WriteTimeout time.Duration
// IdleTimeout is the maximum amount of time to wait for the
// next request when keep-alives are enabled. If IdleTimeout
// is zero, the value of ReadTimeout is used. If both are
// zero, ReadHeaderTimeout is used.
IdleTimeout time.Duration // Go 1.8
今回問題になっていたのはTCPハンドシェイクをしたあと全くリクエストを送ってこないクライアントでした。 ReadTimeout も IdleTimeout も設定していなかったのでずっとTCP接続がESTABLISHEDのままじわじわ溜まって行きました。接続数に伴って利用している fd 数も増え続け、いつかは上限に到達します。
上記のドキュメントにあるとおり、ReadTimeout と WriteTimeout は http handler 側で制御することができますが、 ReadHeaderTimeout だけは絶対に設定しておきましょう。 IdleTimeout は nil 値でも ReadTimeout か ReadHeaderTimeout と同じ値になるので、 ReadHeaderTimeout を設定してあればずっとたまり続けることは無いはずです。