Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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パケットを定期的に送信することで、これらのデバイスに接続がアクティブであることを伝え、タイムアウトを防ぎます。
  • 動作:
    1. TCP Keep-Aliveが有効な接続で、一定時間データが送信されないと、カーネルがKeep-Aliveプローブパケットを送信します。
    2. 相手側が応答すれば、接続はアクティブであると判断され、タイマーがリセットされます。
    3. 相手側が応答しない場合、カーネルは何度か再試行します。
    4. 再試行しても応答がない場合、カーネルは接続がデッドであると判断し、アプリケーションにエラーを通知して接続を閉じます。
  • 設定: 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())を自動的に継承します。これにより、tcpKeepAliveListenernet.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メソッドの動作は以下の通りです。

  1. まず、埋め込まれた*net.TCPListenerAcceptTCP()メソッドを呼び出して、新しいTCP接続(*net.TCPConn型)を受け入れます。AcceptTCP()は、Accept()net.Connを返すのに対し、より具体的な*net.TCPConn型を返します。
  2. エラーが発生した場合は、そのままエラーを返します。
  3. 接続が正常に受け入れられた場合、受け入れた*net.TCPConnインスタンス(tc)に対して以下のメソッドを呼び出します。
    • tc.SetKeepAlive(true): この呼び出しにより、新しく確立されたTCP接続に対してTCP Keep-Alive機能が有効になります。
    • tc.SetKeepAlivePeriod(3 * time.Minute): Keep-Aliveプローブを送信する間隔を3分に設定します。これは、接続がアイドル状態になってから最初のプローブが送信されるまでの時間であり、その後のプローブ間隔もこれに準じます(OSの実装による)。この値は、一般的なネットワーク環境でのデッド接続の検出と、不要なネットワークトラフィックのバランスを考慮して選択されています。
  4. Keep-Alive設定が完了した*net.TCPConnnet.Connインターフェースとして返します。

このtcpKeepAliveListenerは、http.ListenAndServehttp.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ファイルに集中しています。

  1. 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()に渡しています。

  2. 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で取得したリスナーlntcpKeepAliveListenerでラップし、その後にtls.NewListenerに渡しています。

  3. 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.ListenAndServehttp.ListenAndServeTLSがこの新しい型を使用してTCP接続を受け入れるようにした点です。

tcpKeepAliveListener構造体

type tcpKeepAliveListener struct {
	*net.TCPListener
}

この構造体は、*net.TCPListenerを匿名フィールドとして埋め込んでいます。Goの埋め込みの原則により、tcpKeepAliveListener*net.TCPListenerのすべてのメソッドを自動的に「継承」します。これにより、tcpKeepAliveListenernet.Listenerインターフェース(Accept(), Addr(), Close()メソッドを持つ)を実装していると見なされます。

tcpKeepAliveListenerAcceptメソッド

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()メソッドをオーバーライドしています。

  1. ln.AcceptTCP(): 埋め込まれた*net.TCPListenerAcceptTCP()メソッドを呼び出します。これは、新しいTCP接続を待ち受け、接続が確立されると*net.TCPConn型のインスタンスを返します。AcceptTCP()を使用することで、返される接続が確実に*net.TCPConn型であることが保証され、TCP固有のメソッドにアクセスできるようになります。
  2. エラーハンドリング: AcceptTCP()がエラーを返した場合、そのエラーをそのまま呼び出し元に返します。
  3. tc.SetKeepAlive(true): 新しく受け入れられたTCP接続(tc)に対して、TCP Keep-Alive機能を有効にします。これにより、OSのTCPスタックが、この接続がアイドル状態になったときに定期的にKeep-Aliveプローブパケットを送信するようになります。
  4. tc.SetKeepAlivePeriod(3 * time.Minute): Keep-Aliveプローブを送信する間隔を3分に設定します。これは、接続がアイドル状態になってから最初のプローブが送信されるまでの時間であり、その後のプローブ間隔もこれに準じます。この値は、一般的なシナリオでデッド接続を効率的に検出しつつ、ネットワークトラフィックを過度に増やさないようにするためのバランスの取れた選択です。
  5. return tc, nil: Keep-Aliveが設定された*net.TCPConnインスタンスをnet.Connインターフェースとして返します。

ListenAndServeListenAndServeTLSでの利用

変更されたListenAndServeListenAndServeTLS関数は、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.TCPListenertcpKeepAliveListener構造体の匿名フィールドとして初期化し、srv.Serve()に渡しています。

これにより、srv.Serve()がこのtcpKeepAliveListenerAccept()メソッドを呼び出すたびに、新しく確立されるすべてのTCP接続に対して自動的にTCP Keep-Aliveが有効になり、3分の期間が設定されることになります。この変更は、net/httpパッケージのAPIを変更することなく、サーバーの堅牢性を大幅に向上させています。

関連リンク

参考にした情報源リンク