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

[インデックス 11877] ファイルの概要

このコミットは、Go言語の net/http パッケージにおいて、net.ListenerAccept() メソッドが一時的なエラーを返した場合に、CPUを過剰に消費する「スピン」状態に陥るのを防ぐための修正です。具体的には、一時的なネットワークエラーが発生した際に、指数関数的バックオフ(exponential backoff)戦略を用いて再試行間隔を徐々に長くすることで、リソースの無駄な消費を抑え、システムの安定性を向上させています。

コミット

commit 913abfee3bd25af5d80b3b9079d22f8e296d94c8
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Feb 14 15:04:29 2012 +1100

    net/http: don't spin on temporary accept failure
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5658049

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/913abfee3bd25af5d80b3b9079d22f8e296d94c8

元コミット内容

net/http パッケージにおいて、Accept() メソッドが一時的なエラーを返した場合に、無限ループで再試行し続けることによるCPUの過剰な消費を防ぐ。

変更の背景

Goの net/http パッケージの Server.Serve() メソッドは、net.Listener から新しい接続を受け入れるために Accept() メソッドをループ内で呼び出します。しかし、ネットワークの一時的な問題(例: ファイルディスクリプタの枯渇、一時的なネットワークの切断、OSのバッファリングの問題など)により、Accept() が一時的なエラー(net.Error インターフェースの Temporary() メソッドが true を返すエラー)を返すことがあります。

このコミット以前は、Accept() が一時的なエラーを返した場合、Serve() メソッドはすぐに次のループイテレーションに入り、再度 Accept() を呼び出していました。これにより、エラーが継続する間、CPUを100%近く消費する「ビジーループ(busy-loop)」または「スピン(spin)」状態に陥る可能性がありました。これは、システムリソースの無駄な消費だけでなく、他のプロセスへの影響や、サーバーの応答性低下を引き起こす原因となります。

この問題に対処するため、一時的なエラーが発生した際に、すぐに再試行するのではなく、一定時間待機してから再試行するメカニズムを導入する必要がありました。

前提知識の解説

  • net.Listener: Go言語の net パッケージで提供されるインターフェースで、ネットワーク接続をリッスン(待ち受け)するための抽象化です。TCP/IPソケットなどの具体的なネットワークエンドポイントを表します。
  • Accept() メソッド: net.Listener インターフェースの主要なメソッドの一つで、新しい着信接続をブロックして待ち受け、接続が確立されると net.Conn インターフェースを実装するオブジェクトとエラーを返します。
  • net.Error インターフェース: net パッケージで定義されているエラーインターフェースで、ネットワーク関連のエラーに追加情報を提供します。
    • Temporary() メソッド: net.Error インターフェースの一部で、エラーが一時的なものであるかどうかを示すブール値を返します。true の場合、同じ操作を後で再試行すると成功する可能性があることを意味します。
  • 指数関数的バックオフ (Exponential Backoff): ネットワークプログラミングや分散システムで広く使われる戦略です。リソースへのアクセスや操作が失敗した場合に、次の再試行までの待機時間を指数関数的に増加させることで、システムへの負荷を軽減し、リソースが回復するまでの時間を稼ぎます。これにより、失敗した操作を繰り返し試行することによるリソースの枯渇や、ネットワークの輻輳(ふくそう)を防ぎます。通常、最大待機時間が設定され、それを超えることはありません。
  • time.Sleep(): Go言語の time パッケージで提供される関数で、指定された期間だけ現在のゴルーチンをスリープ(一時停止)させます。

技術的詳細

このコミットでは、Server.Serve() メソッド内の Accept() ループに、指数関数的バックオフのロジックが追加されています。

  1. tempDelay 変数の導入: time.Duration 型の tempDelay 変数が導入され、一時的なエラーが発生した際の次の Accept() 呼び出しまでの待機時間を管理します。初期値は 0 です。
  2. 一時的なエラーの検出: l.Accept() がエラーを返した場合、そのエラーが net.Error 型にキャスト可能であり、かつ Temporary() メソッドが true を返す場合に、一時的なエラーとして扱われます。
  3. バックオフ時間の計算:
    • tempDelay0 の場合(最初の一時エラー)、tempDelay5 * time.Millisecond に設定されます。
    • tempDelay0 でない場合(連続する一時エラー)、tempDelaytempDelay *= 2 によって2倍になります。
    • 最大待機時間 (max) は 1 * time.Second に設定されており、tempDelay がこの max を超えた場合、tempDelaymax に制限されます。これにより、待機時間が無限に長くなるのを防ぎます。
  4. エラーログとスリープ: 計算された tempDelay を用いて、log.Printf でエラーメッセージと次の再試行までの待機時間がログに出力され、その後 time.Sleep(tempDelay) によってゴルーチンが指定された時間だけスリープします。
  5. ループの継続: continue ステートメントにより、ループの次のイテレーションに進み、再度 Accept() が試行されます。
  6. 成功時のリセット: Accept() がエラーなく成功した場合、tempDelay0 にリセットされます。これにより、一時的なエラーが解消された後、すぐに通常の動作に戻ることができます。

この変更により、一時的なネットワークエラーが発生しても、Serve() メソッドがCPUを無駄に消費し続けることなく、適切な間隔で再試行を行うようになり、サーバーの安定性と効率が向上します。

コアとなるコードの変更箇所

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -1007,15 +1007,26 @@ func (srv *Server) ListenAndServe() error {
 // then call srv.Handler to reply to them.
 func (srv *Server) Serve(l net.Listener) error {
 	defer l.Close()
+	var tempDelay time.Duration // how long to sleep on accept failure
 	for {
 		rw, e := l.Accept()
 		if e != nil {
 			if ne, ok := e.(net.Error); ok && ne.Temporary() {
-				log.Printf("http: Accept error: %v", e)
+				if tempDelay == 0 {
+					tempDelay = 5 * time.Millisecond
+				} else {
+					tempDelay *= 2
+				}
+				if max := 1 * time.Second; tempDelay > max {
+					tempDelay = max
+				}
+				log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay)
+				time.Sleep(tempDelay)
 				continue
 			}
 			return e
 		}
+		tempDelay = 0
 		if srv.ReadTimeout != 0 {
 			rw.SetReadDeadline(time.Now().Add(srv.ReadTimeout))
 		}

コアとなるコードの解説

  • var tempDelay time.Duration // how long to sleep on accept failure
    • Accept() が一時的なエラーを返した際に、次に再試行するまでの待機時間を保持するための変数 tempDelay を宣言しています。初期値は time.Duration のゼロ値である 0 です。
  • for { ... }
    • 無限ループで、新しい接続を継続的に待ち受けます。
  • rw, e := l.Accept()
    • net.ListenerAccept() メソッドを呼び出し、新しい接続 (rw) を受け入れるか、エラー (e) を受け取ります。
  • if e != nil { ... }
    • Accept() がエラーを返した場合の処理ブロックです。
  • if ne, ok := e.(net.Error); ok && ne.Temporary() { ... }
    • エラー enet.Error インターフェースを実装しているか (oktrue)、かつそのエラーが一時的なものであるか (ne.Temporary()true) をチェックします。この条件が満たされた場合、一時的なネットワークエラーと判断されます。
  • if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 }
    • tempDelay0 の場合(最初の一時エラー)、待機時間を 5ミリ秒 に設定します。
    • tempDelay0 でない場合(連続する一時エラー)、待機時間を2倍にします。これが指数関数的バックオフの核心部分です。
  • if max := 1 * time.Second; tempDelay > max { tempDelay = max }
    • 待機時間 tempDelay1秒 を超えないように上限を設定します。これにより、ネットワークの問題が長引いても、再試行間隔が無限に伸びることを防ぎます。
  • log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay)
    • 一時的なエラーが発生したことと、次に再試行するまでの待機時間をログに出力します。これにより、サーバー管理者は問題の発生とバックオフの動作を把握できます。
  • time.Sleep(tempDelay)
    • 計算された tempDelay の間、現在のゴルーチンをスリープさせます。これにより、CPUのビジーループを防ぎます。
  • continue
    • 現在のループイテレーションを終了し、for ループの次のイテレーションに進みます。これにより、スリープ後に再度 Accept() が試行されます。
  • return e
    • 一時的なエラーではない場合(例: リスナーがクローズされた、致命的なエラーなど)、Serve() メソッドはエラーを返して終了します。
  • tempDelay = 0
    • Accept() がエラーなく成功した場合、tempDelay0 にリセットします。これにより、次に一時的なエラーが発生した際には、バックオフが初期の 5ミリ秒 から再開されます。

関連リンク

参考にした情報源リンク