[インデックス 18209] ファイルの概要
このコミットは、Goの標準ライブラリであるnet/http
パッケージのListenAndServe
およびListenAndServeTLS
関数において、TCP Keep-Alive機能を有効にする変更を導入しています。これにより、クライアントが予期せず接続を切断した場合(例: ノートPCを閉じる、ネットワークが切断されるなど)に、サーバー側でTCP接続がリークする(解放されずに残り続ける)問題を軽減することを目的としています。
コミット
commit d6bce32a3607222075734bf4363ca3fea02ea1e5
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Jan 9 15:05:09 2014 -0800
net/http: use TCP keep-alives for ListenAndServe and ListenAndServeTLS
Our default behavior for the common cases shouldn't lead to
leaked TCP connections (e.g. from people closing laptops) when
their Go servers are exposed to the open Internet without a
proxy in front.
Too many users on golang-nuts have learned this the hard way.
No API change. Only ListenAndServe and ListenAndServeTLS are
updated.
R=golang-codereviews, cespare, gobot, rsc, minux.ma
CC=golang-codereviews
https://golang.org/cl/48300043
---
src/pkg/net/http/server.go | 30 ++++++++++++++++++++++++------
1 file changed, 24 insertions(+), 6 deletions(-)
diff --git a/src/pkg/net/http/server.go b/src/pkg/net/http/server.go
index 7ebd8575f3..a56aa3df31 100644
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -1608,11 +1608,11 @@ func (srv *Server) ListenAndServe() error {
if addr == "" {
addr = ":http"
}
- l, e := net.Listen("tcp", addr)
- if e != nil {
- return e
+ ln, err := net.Listen("tcp", addr)
+ if err != nil {
+ return err
}
- return srv.Serve(l)
+ return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
// Serve accepts incoming connections on the Listener l, creating a
@@ -1742,12 +1742,12 @@ func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error {
return err
}
- conn, err := net.Listen("tcp", addr)
+ ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
- tlsListener := tls.NewListener(conn, config)
+ tlsListener := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, config)
return srv.Serve(tlsListener)
}
@@ -1837,6 +1837,24 @@ func (tw *timeoutWriter) WriteHeader(code int) {
tw.w.WriteHeader(code)
}
+// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
+// connections. It's used by ListenAndServe and ListenAndServeTLS so
+// dead TCP connections (e.g. closing laptop mid-download) eventually
+// go away.
+type tcpKeepAliveListener struct {
+ *net.TCPListener
+}
+
+func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
+ tc, err := ln.AcceptTCP()
+ if err != nil {
+ return
+ }
+ tc.SetKeepAlive(true)
+ tc.SetKeepAlivePeriod(3 * time.Minute)
+ return tc, nil
+}
+
// globalOptionsHandler responds to "OPTIONS *\" requests.
type globalOptionsHandler struct{}
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d6bce32a3607222075734bf4363ca3fea02ea1e5
元コミット内容
このコミットは、Goのnet/http
パッケージにおけるListenAndServe
およびListenAndServeTLS
関数が、デフォルトでTCP Keep-Aliveを使用するように変更します。これにより、クライアントが予期せず接続を閉じた場合(例: ノートPCを閉じるなど)に発生するTCP接続のリークを防ぎます。この変更はAPIの変更を伴わず、既存の動作に影響を与えずに接続の健全性を向上させます。
変更の背景
GoのHTTPサーバーは、特にプロキシを介さずに直接インターネットに公開される場合、クライアントが突然接続を切断すると、サーバー側に「デッド」なTCP接続が残り続ける問題に直面していました。これは、クライアントが正常なTCP FIN/ACKシーケンスを完了せずに接続を終了した場合に発生します。例えば、ユーザーがノートPCを閉じる、ネットワークケーブルを抜く、Wi-Fiの範囲外に出るなどの状況がこれに該当します。
このようなデッドな接続は、サーバーのリソース(ファイルディスクリプタ、メモリなど)を消費し続け、最終的にはサーバーのパフォーマンス低下や、新しい接続を受け入れられなくなるなどの問題を引き起こす可能性があります。Goコミュニティのメーリングリスト「golang-nuts」では、多くのユーザーがこの問題に遭遇し、解決策を模索していました。
このコミットは、このような一般的なシナリオにおいて、GoのHTTPサーバーがより堅牢に動作するように、デフォルトでTCP Keep-Aliveを有効にすることで、この長年の課題に対処することを目的としています。
前提知識の解説
TCP (Transmission Control Protocol)
TCPは、インターネット上で信頼性の高いデータ転送を提供するプロトコルです。接続指向であり、データの順序保証、再送制御、フロー制御、輻輳制御などの機能を提供します。クライアントとサーバー間で通信を開始する際には「3ウェイハンドシェイク」を行い、通信終了時には「4ウェイハンドシェイク」を行って接続を正常に閉じます。
TCP Keep-Alive
TCP Keep-Aliveは、アイドル状態のTCP接続がまだ有効であるかどうかを確認するために使用されるメカニズムです。これは、データが長時間流れていない接続に対して、小さなプローブパケットを定期的に送信することで機能します。
- 目的:
- デッド接続の検出: ネットワーク障害やクライアントの突然の切断により、相手側が応答しなくなった接続を検出します。これにより、サーバーは不要なリソースを解放できます。
- NAT/ファイアウォールのタイムアウト防止: 一部のNATデバイスやファイアウォールは、一定時間アイドル状態の接続をタイムアウトさせて切断します。Keep-Aliveパケットを定期的に送信することで、これらのデバイスに接続がアクティブであることを伝え、タイムアウトを防ぎます。
- 動作:
- TCP Keep-Aliveが有効な接続で、一定時間データが送信されないと、カーネルがKeep-Aliveプローブパケットを送信します。
- 相手側が応答すれば、接続はアクティブであると判断され、タイマーがリセットされます。
- 相手側が応答しない場合、カーネルは何度か再試行します。
- 再試行しても応答がない場合、カーネルは接続がデッドであると判断し、アプリケーションにエラーを通知して接続を閉じます。
- 設定: Keep-Aliveの動作は、以下のパラメータで制御されます。
TCP_KEEPALIVE
(またはSO_KEEPALIVE
): Keep-Alive機能を有効/無効にするフラグ。TCP_KEEPIDLE
: 接続がアイドル状態になってから最初のKeep-Aliveプローブを送信するまでの時間。TCP_KEEPINTVL
: Keep-Aliveプローブの再送間隔。TCP_KEEPCNT
: 応答がない場合にプローブを再送する最大回数。
Goのnet/http
パッケージ
Goのnet/http
パッケージは、HTTPクライアントとサーバーを実装するための標準ライブラリです。
http.Server
: HTTPサーバーの構成と動作を制御する構造体です。http.ListenAndServe(addr string, handler Handler)
: 指定されたアドレスでHTTPサーバーを起動し、リクエストを処理します。内部的にはnet.Listen("tcp", addr)
でTCPリスナーを作成し、そのリスナーを使ってhttp.Server.Serve()
を呼び出します。http.ListenAndServeTLS(addr, certFile, keyFile string, handler Handler)
:ListenAndServe
と同様ですが、TLS(HTTPS)接続を処理します。net.Listener
インターフェース: ネットワーク接続を受け入れるための汎用インターフェースです。Accept()
メソッドを持ち、新しい接続が確立されるたびにnet.Conn
を返します。net.Conn
インターフェース: ネットワーク接続を表す汎用インターフェースです。Read()
、Write()
、Close()
などのメソッドを持ちます。TCP接続の場合、具体的な型は*net.TCPConn
となり、これにはSetKeepAlive()
やSetKeepAlivePeriod()
などのTCP固有のメソッドがあります。
技術的詳細
このコミットの核心は、net/http
パッケージが内部的に使用するTCPリスナーをラップし、そのリスナーが受け入れるすべての新しいTCP接続に対してTCP Keep-Aliveを自動的に有効にすることです。
具体的には、tcpKeepAliveListener
という新しい構造体が導入されています。
type tcpKeepAliveListener struct {
*net.TCPListener
}
この構造体は、*net.TCPListener
を埋め込んでいます。Goの埋め込み(embedding)機能により、tcpKeepAliveListener
は*net.TCPListener
のすべてのメソッド(例: Addr()
, Close()
)を自動的に継承します。これにより、tcpKeepAliveListener
はnet.Listener
インターフェースを満たすことができます。
しかし、net.Listener
インターフェースの最も重要なメソッドであるAccept()
は、tcpKeepAliveListener
によってオーバーライドされています。
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
tc, err := ln.AcceptTCP()
if err != nil {
return
}
tc.SetKeepAlive(true)
tc.SetKeepAlivePeriod(3 * time.Minute)
return tc, nil
}
このカスタムAccept
メソッドの動作は以下の通りです。
- まず、埋め込まれた
*net.TCPListener
のAcceptTCP()
メソッドを呼び出して、新しいTCP接続(*net.TCPConn
型)を受け入れます。AcceptTCP()
は、Accept()
がnet.Conn
を返すのに対し、より具体的な*net.TCPConn
型を返します。 - エラーが発生した場合は、そのままエラーを返します。
- 接続が正常に受け入れられた場合、受け入れた
*net.TCPConn
インスタンス(tc
)に対して以下のメソッドを呼び出します。tc.SetKeepAlive(true)
: この呼び出しにより、新しく確立されたTCP接続に対してTCP Keep-Alive機能が有効になります。tc.SetKeepAlivePeriod(3 * time.Minute)
: Keep-Aliveプローブを送信する間隔を3分に設定します。これは、接続がアイドル状態になってから最初のプローブが送信されるまでの時間であり、その後のプローブ間隔もこれに準じます(OSの実装による)。この値は、一般的なネットワーク環境でのデッド接続の検出と、不要なネットワークトラフィックのバランスを考慮して選択されています。
- Keep-Alive設定が完了した
*net.TCPConn
をnet.Conn
インターフェースとして返します。
このtcpKeepAliveListener
は、http.ListenAndServe
とhttp.ListenAndServeTLS
の内部で、net.Listen
が返す元の*net.TCPListener
をラップするために使用されます。
http.ListenAndServe
では、net.Listen("tcp", addr)
が返すnet.Listener
を*net.TCPListener
に型アサートし、それをtcpKeepAliveListener
でラップしてsrv.Serve()
に渡します。http.ListenAndServeTLS
でも同様に、net.Listen("tcp", addr)
が返すリスナーをtcpKeepAliveListener
でラップし、さらにtls.NewListener
でTLSリスナーとしてラップしてsrv.Serve()
に渡します。
この変更により、ListenAndServe
およびListenAndServeTLS
を使用するすべてのHTTPサーバーは、追加の設定なしに自動的にTCP Keep-Aliveの恩恵を受けることができます。これは、Goの「デフォルトで正しいことをする」という設計哲学に沿ったものです。
コアとなるコードの変更箇所
変更はsrc/pkg/net/http/server.go
ファイルに集中しています。
-
ListenAndServe
関数の変更:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -1608,11 +1608,11 @@ func (srv *Server) ListenAndServe() error { if addr == "" { addr = ":http" } - l, e := net.Listen("tcp", addr) - if e != nil { - return e + ln, err := net.Listen("tcp", addr) + if err != nil { + return err } - return srv.Serve(l) + return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) }
net.Listen
で取得したリスナーln
を、tcpKeepAliveListener{ln.(*net.TCPListener)}
でラップしてからsrv.Serve()
に渡しています。 -
ListenAndServeTLS
関数の変更:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -1742,12 +1742,12 @@ func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error { return err } - conn, err := net.Listen("tcp", addr) + ln, err := net.Listen("tcp", addr) if err != nil { return err } - tlsListener := tls.NewListener(conn, config) + tlsListener := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, config}) return srv.Serve(tlsListener) }
こちらも同様に、
net.Listen
で取得したリスナーln
をtcpKeepAliveListener
でラップし、その後にtls.NewListener
に渡しています。 -
tcpKeepAliveListener
構造体とAccept
メソッドの追加:--- a/src/pkg/net/http/server.go +++ b/src/pkg/net/http/server.go @@ -1837,6 +1837,24 @@ func (tw *timeoutWriter) WriteHeader(code int) { tw.w.WriteHeader(code) } +// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted +// connections. It's used by ListenAndServe and ListenAndServeTLS so +// dead TCP connections (e.g. closing laptop mid-download) eventually +// go away. +type tcpKeepAliveListener struct { + *net.TCPListener +} + +func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { + tc, err := ln.AcceptTCP() + if err != nil { + return + } + tc.SetKeepAlive(true) + tc.SetKeepAlivePeriod(3 * time.Minute) + return tc, nil +} + // globalOptionsHandler responds to "OPTIONS *\" requests. type globalOptionsHandler struct{}
ファイルの末尾近くに、
tcpKeepAliveListener
型とそのAccept
メソッドの定義が追加されています。
コアとなるコードの解説
このコミットの主要な変更は、net/http/server.go
ファイルに新しい型tcpKeepAliveListener
を導入し、http.ListenAndServe
とhttp.ListenAndServeTLS
がこの新しい型を使用してTCP接続を受け入れるようにした点です。
tcpKeepAliveListener
構造体
type tcpKeepAliveListener struct {
*net.TCPListener
}
この構造体は、*net.TCPListener
を匿名フィールドとして埋め込んでいます。Goの埋め込みの原則により、tcpKeepAliveListener
は*net.TCPListener
のすべてのメソッドを自動的に「継承」します。これにより、tcpKeepAliveListener
はnet.Listener
インターフェース(Accept()
, Addr()
, Close()
メソッドを持つ)を実装していると見なされます。
tcpKeepAliveListener
のAccept
メソッド
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
tc, err := ln.AcceptTCP()
if err != nil {
return
}
tc.SetKeepAlive(true)
tc.SetKeepAlivePeriod(3 * time.Minute)
return tc, nil
}
このメソッドは、net.Listener
インターフェースのAccept()
メソッドをオーバーライドしています。
ln.AcceptTCP()
: 埋め込まれた*net.TCPListener
のAcceptTCP()
メソッドを呼び出します。これは、新しいTCP接続を待ち受け、接続が確立されると*net.TCPConn
型のインスタンスを返します。AcceptTCP()
を使用することで、返される接続が確実に*net.TCPConn
型であることが保証され、TCP固有のメソッドにアクセスできるようになります。- エラーハンドリング:
AcceptTCP()
がエラーを返した場合、そのエラーをそのまま呼び出し元に返します。 tc.SetKeepAlive(true)
: 新しく受け入れられたTCP接続(tc
)に対して、TCP Keep-Alive機能を有効にします。これにより、OSのTCPスタックが、この接続がアイドル状態になったときに定期的にKeep-Aliveプローブパケットを送信するようになります。tc.SetKeepAlivePeriod(3 * time.Minute)
: Keep-Aliveプローブを送信する間隔を3分に設定します。これは、接続がアイドル状態になってから最初のプローブが送信されるまでの時間であり、その後のプローブ間隔もこれに準じます。この値は、一般的なシナリオでデッド接続を効率的に検出しつつ、ネットワークトラフィックを過度に増やさないようにするためのバランスの取れた選択です。return tc, nil
: Keep-Aliveが設定された*net.TCPConn
インスタンスをnet.Conn
インターフェースとして返します。
ListenAndServe
とListenAndServeTLS
での利用
変更されたListenAndServe
とListenAndServeTLS
関数は、net.Listen
が返す通常の*net.TCPListener
を直接srv.Serve()
に渡す代わりに、この新しく定義されたtcpKeepAliveListener
でラップしてから渡します。
例えば、ListenAndServe
では:
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
ここで、ln.(*net.TCPListener)
は、net.Listen
が返すnet.Listener
インターフェースの具体的な型が*net.TCPListener
であることを型アサートしています。そして、この*net.TCPListener
をtcpKeepAliveListener
構造体の匿名フィールドとして初期化し、srv.Serve()
に渡しています。
これにより、srv.Serve()
がこのtcpKeepAliveListener
のAccept()
メソッドを呼び出すたびに、新しく確立されるすべてのTCP接続に対して自動的にTCP Keep-Aliveが有効になり、3分の期間が設定されることになります。この変更は、net/http
パッケージのAPIを変更することなく、サーバーの堅牢性を大幅に向上させています。
関連リンク
- Go Code Review: https://golang.org/cl/48300043
- Go
net/http
パッケージドキュメント: https://pkg.go.dev/net/http - Go
net
パッケージドキュメント: https://pkg.go.dev/net - Go
net.TCPConn
ドキュメント: https://pkg.go.dev/net#TCPConn
参考にした情報源リンク
- TCP Keepalive HOWTO: https://tldp.org/HOWTO/TCP-Keepalive-HOWTO/index.html
- Wikipedia - TCP Keepalive: https://en.wikipedia.org/wiki/TCP_keepalive
- golang-nutsメーリングリスト (一般的な議論の場): https://groups.google.com/g/golang-nuts
- Goの埋め込み (Embedding) について: https://go.dev/tour/methods/15
- Goのインターフェースについて: https://go.dev/tour/methods/10
- Goの型アサーションについて: https://go.dev/tour/methods/14