[インデックス 11877] ファイルの概要
このコミットは、Go言語の net/http
パッケージにおいて、net.Listener
の Accept()
メソッドが一時的なエラーを返した場合に、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()
ループに、指数関数的バックオフのロジックが追加されています。
tempDelay
変数の導入:time.Duration
型のtempDelay
変数が導入され、一時的なエラーが発生した際の次のAccept()
呼び出しまでの待機時間を管理します。初期値は0
です。- 一時的なエラーの検出:
l.Accept()
がエラーを返した場合、そのエラーがnet.Error
型にキャスト可能であり、かつTemporary()
メソッドがtrue
を返す場合に、一時的なエラーとして扱われます。 - バックオフ時間の計算:
tempDelay
が0
の場合(最初の一時エラー)、tempDelay
は5 * time.Millisecond
に設定されます。tempDelay
が0
でない場合(連続する一時エラー)、tempDelay
はtempDelay *= 2
によって2倍になります。- 最大待機時間 (
max
) は1 * time.Second
に設定されており、tempDelay
がこのmax
を超えた場合、tempDelay
はmax
に制限されます。これにより、待機時間が無限に長くなるのを防ぎます。
- エラーログとスリープ: 計算された
tempDelay
を用いて、log.Printf
でエラーメッセージと次の再試行までの待機時間がログに出力され、その後time.Sleep(tempDelay)
によってゴルーチンが指定された時間だけスリープします。 - ループの継続:
continue
ステートメントにより、ループの次のイテレーションに進み、再度Accept()
が試行されます。 - 成功時のリセット:
Accept()
がエラーなく成功した場合、tempDelay
は0
にリセットされます。これにより、一時的なエラーが解消された後、すぐに通常の動作に戻ることができます。
この変更により、一時的なネットワークエラーが発生しても、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.Listener
のAccept()
メソッドを呼び出し、新しい接続 (rw
) を受け入れるか、エラー (e
) を受け取ります。
if e != nil { ... }
Accept()
がエラーを返した場合の処理ブロックです。
if ne, ok := e.(net.Error); ok && ne.Temporary() { ... }
- エラー
e
がnet.Error
インターフェースを実装しているか (ok
がtrue
)、かつそのエラーが一時的なものであるか (ne.Temporary()
がtrue
) をチェックします。この条件が満たされた場合、一時的なネットワークエラーと判断されます。
- エラー
if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 }
tempDelay
が0
の場合(最初の一時エラー)、待機時間を5ミリ秒
に設定します。tempDelay
が0
でない場合(連続する一時エラー)、待機時間を2倍にします。これが指数関数的バックオフの核心部分です。
if max := 1 * time.Second; tempDelay > max { tempDelay = max }
- 待機時間
tempDelay
が1秒
を超えないように上限を設定します。これにより、ネットワークの問題が長引いても、再試行間隔が無限に伸びることを防ぎます。
- 待機時間
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()
がエラーなく成功した場合、tempDelay
を0
にリセットします。これにより、次に一時的なエラーが発生した際には、バックオフが初期の5ミリ秒
から再開されます。
関連リンク
- Go issue: net/http: don't spin on temporary accept failure (このコミットの元となった可能性のあるissue)
- Go CL (Change List): https://golang.org/cl/5658049 (コミットメッセージに記載されている変更リストへのリンク)
参考にした情報源リンク
- GoDoc: net.Listener
- GoDoc: net.Error
- GoDoc: time.Sleep
- Exponential backoff - Wikipedia
- Go言語のnetパッケージにおけるエラーハンドリング - Qiita (一般的なGoのエラーハンドリングの参考)
- Goのnet/httpパッケージのAccept()ループとCPU使用率 - Stack Overflow (類似の問題に関する議論の可能性)
- Goのnet.Error.Temporary()の挙動について - Zenn (net.Error.Temporary()に関する詳細な解説の可能性)
- Goのnet/httpサーバーにおけるAcceptループの最適化 - Medium (Acceptループの最適化に関する記事の可能性)
- Goのnet/httpパッケージの内部実装 - Speaker Deck (net/httpの内部実装に関するプレゼンテーションの可能性)
- Goのnetパッケージのソースコード
- Goのnet/httpパッケージのソースコード