[インデックス 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 failureAccept()が一時的なエラーを返した際に、次に再試行するまでの待機時間を保持するための変数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 = 0Accept()がエラーなく成功した場合、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パッケージのソースコード