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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおけるパフォーマンス改善を目的としています。具体的には、HTTP GETリクエストにおいて、ボディが0バイトである場合に io.LimitedReader を不必要に割り当てることを避け、既存の eofReader グローバル変数を利用することで、メモリ割り当てと処理時間を削減しています。

コミット

commit 468851f1d57eb5cd3ec0ec6d3ce306ea5749090b
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Apr 3 10:31:12 2013 -0700

    net/http: don't allocate 0-byte io.LimitedReaders for GET requests
    
    Save an allocation per GET request and don't call io.LimitedReader(r, 0)
    just to read 0 bytes. There's already an eofReader global variable
    for when we just want a non-nil io.Reader to immediately EOF.
    
    (Sorry, I know Rob told me to stop, but I was bored on the plane and
    wrote this before I received the recent "please, really stop" email.)
    
    benchmark                         old ns/op    new ns/op    delta
    BenchmarkServerHandlerTypeLen         13888        13279   -4.39%
    BenchmarkServerHandlerNoLen           12912        12229   -5.29%
    BenchmarkServerHandlerNoType          13348        12632   -5.36%
    BenchmarkServerHandlerNoHeader        10911        10261   -5.96%
    
    benchmark                        old allocs   new allocs    delta
    BenchmarkServerHandlerTypeLen            20           19   -5.00%
    BenchmarkServerHandlerNoLen              18           17   -5.56%
    BenchmarkServerHandlerNoType             18           17   -5.56%
    BenchmarkServerHandlerNoHeader           13           12   -7.69%
    
    benchmark                         old bytes    new bytes    delta
    BenchmarkServerHandlerTypeLen          1913         1878   -1.83%
    BenchmarkServerHandlerNoLen            1878         1843   -1.86%
    BenchmarkServerHandlerNoType           1878         1844   -1.81%
    BenchmarkServerHandlerNoHeader         1085         1051   -3.13%
    
    Fixes #5188
    
    R=golang-dev, adg, r
    CC=golang-dev
    https://golang.org/cl/8297044

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

https://github.com/golang/go/commit/468851f1d57eb5cd3ec0ec6d3ce306ea5749090b

元コミット内容

このコミットは、net/http パッケージにおいて、GETリクエストに対する0バイトの io.LimitedReader の割り当てを停止することを目的としています。これにより、GETリクエストごとの割り当てを1つ削減し、0バイトを読み取るためだけに io.LimitedReader(r, 0) を呼び出すことを避けます。代わりに、すぐにEOF(End Of File)を返す非nilの io.Reader が必要な場合に利用できる eofReader グローバル変数が既に存在します。

コミットメッセージには、ベンチマーク結果も含まれており、ns/op (操作あたりのナノ秒)、allocs (割り当て数)、bytes (割り当てバイト数) の各指標で改善が見られることが示されています。

  • BenchmarkServerHandlerTypeLen: -4.39% (ns/op), -5.00% (allocs), -1.83% (bytes)
  • BenchmarkServerHandlerNoLen: -5.29% (ns/op), -5.56% (allocs), -1.86% (bytes)
  • BenchmarkServerHandlerNoType: -5.36% (ns/op), -5.56% (allocs), -1.81% (bytes)
  • BenchmarkServerHandlerNoHeader: -5.96% (ns/op), -7.69% (allocs), -3.13% (bytes)

この変更は、Issue #5188 を修正するものです。

変更の背景

この変更の背景には、Go言語の net/http パッケージにおけるHTTPリクエストボディの処理効率の改善があります。特に、HTTP GETリクエストのように通常ボディを持たないリクエストや、Content-Length: 0 が指定されたリクエストにおいて、不必要なメモリ割り当てが発生しているという問題がありました。

Goの io.LimitedReader は、指定されたバイト数だけを読み取る io.Reader のラッパーです。HTTP/1.1の仕様では、GETリクエストはメッセージボディを持つべきではありませんが、一部のクライアントやプロキシが誤ってボディを送信する可能性も考慮し、サーバー側ではボディの存在を処理できる必要があります。しかし、ボディが0バイトであることが明確な場合でも、以前の実装では io.LimitedReader(r, 0) のように io.LimitedReader のインスタンスを生成していました。

io.LimitedReader(r, 0) は、確かに0バイトを読み取るとすぐにEOFを返しますが、このオブジェクト自体を生成する際にメモリ割り当てが発生します。HTTPサーバーは大量のリクエストを処理するため、このような小さな割り当てであっても、リクエストごとに発生すると全体としてパフォーマンスに影響を与え、ガベージコレクションの頻度を増加させる可能性があります。

Issue #5188 は、この特定のパフォーマンス問題、すなわち0バイトのボディに対して io.LimitedReader が割り当てられることによるオーバーヘッドを指摘していました。このコミットは、この問題を解決し、サーバーの効率を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とHTTPの基礎知識が必要です。

  1. io.Reader インターフェース: Go言語における基本的なI/O操作のインターフェースです。Read([]byte) (n int, err error) メソッドを持ち、データを読み取るための抽象化を提供します。ファイル、ネットワーク接続、メモリ上のバッファなど、様々なデータソースからデータを読み取る際に利用されます。

  2. io.LimitedReader: io.LimitedReader は、既存の io.Reader をラップし、指定されたバイト数(N)までしか読み取らないように制限する構造体です。Read メソッドが呼び出されるたびに、内部の N が減少し、N が0になるとそれ以上読み取らずに io.EOF を返します。 io.LimitedReader(r, 0) の場合、N が最初から0であるため、Read が呼び出されるとすぐに io.EOF を返します。

  3. io.EOF (End Of File): io.ReaderRead メソッドが、それ以上読み取るデータがないことを示すために返すエラーです。

  4. eofReader: Goの net/http パッケージ内部で定義されている、すぐに io.EOF を返す特別な io.Reader のインスタンスです。これは、0バイトのボディを表現するために、新しい io.LimitedReader を毎回割り当てる代わりに再利用できるシングルトン(またはそれに近い)オブジェクトとして機能します。これにより、不要なメモリ割り当てを避けることができます。

  5. HTTP GET リクエスト: HTTPメソッドの一つで、指定されたリソースの表現を要求するために使用されます。GETリクエストは、通常、リクエストボディを持ちません。HTTP/1.1の仕様では、GETリクエストのメッセージボディは意味を持たないとされています。

  6. メモリ割り当てとガベージコレクション (GC): Go言語はガベージコレクタを持つ言語です。プログラムが新しいオブジェクトを作成するたびに、メモリが割り当てられます。これらのオブジェクトが不要になった場合、ガベージコレクタがそれらを検出し、メモリを解放します。メモリ割り当ての頻度が高いと、ガベージコレクタがより頻繁に実行され、プログラムの実行が一時的に停止(ストップ・ザ・ワールド)する時間が長くなり、全体的なパフォーマンスが低下する可能性があります。不要なメモリ割り当てを削減することは、GCの負荷を軽減し、アプリケーションのスループットを向上させる上で重要です。

  7. bufio.Reader: バッファリングされたI/Oを提供する io.Reader の実装です。これにより、基になる io.Reader からの読み取り回数を減らし、パフォーマンスを向上させることができます。

  8. ioutil.Discard: io.Writer の一種で、書き込まれたすべてのデータを破棄します。io.Copy(ioutil.Discard, reader) のように使用すると、reader からのデータをすべて読み捨て、EOFに到達するまで消費するために使われます。これは、HTTPリクエストボディを完全に読み切る必要があるが、その内容には興味がない場合に特に有用です。

技術的詳細

このコミットの技術的な核心は、HTTPリクエストのボディ処理における最適化です。

以前の実装では、HTTPリクエストのボディの長さが0である場合(例えば、GETリクエストや Content-Length: 0 のヘッダを持つリクエスト)、io.LimitedReader(r, 0) を使用してボディを表現していました。 io.LimitedReader は、以下のような構造を持っています(簡略化された概念):

type LimitedReader struct {
    R io.Reader // underlying reader
    N int64     // max bytes remaining
}

func (l *LimitedReader) Read(p []byte) (n int, err error) {
    if l.N <= 0 {
        return 0, io.EOF
    }
    // ... (実際の読み取りロジック)
}

io.LimitedReader(r, 0) を呼び出すと、LimitedReader の新しいインスタンスがヒープに割り当てられ、その N フィールドが0に設定されます。このインスタンスは、Read メソッドが呼び出されるとすぐに io.EOF を返します。機能的には正しいですが、この LimitedReader オブジェクト自体を生成するためのメモリ割り当てが、リクエストごとに発生していました。

Goの net/http パッケージには、既に eofReader というグローバル変数(またはパッケージレベルの変数)が存在します。これは、Read メソッドが常に (0, io.EOF) を返すように実装された io.Reader のインスタンスです。

// eofReader is an io.Reader that always returns 0, io.EOF.
var eofReader = &eofReaderT{}

type eofReaderT struct{}

func (eofReaderT) Read(b []byte) (n int, err error) { return 0, io.EOF }

この eofReader は一度だけ初期化され、その後は何度でも再利用できます。したがって、0バイトのボディを表現するために io.LimitedReader(r, 0) を毎回新しく割り当てる代わりに、既存の eofReader を参照するだけで済みます。これにより、新しいオブジェクトの割り当てが不要になり、ヒープの使用量が削減され、ガベージコレクションの頻度が低下し、結果として全体的なパフォーマンスが向上します。

コミットメッセージに示されているベンチマーク結果は、この最適化が実際に効果をもたらしたことを明確に示しています。特に allocs (割り当て数) の削減は、この変更の直接的な効果を反映しています。

また、body.Close() メソッドの変更も重要です。以前は、b.Reader == eofReader の場合でも io.Copy(ioutil.Discard, b) を呼び出していました。io.Copy は、たとえリーダーがすぐにEOFを返すとしても、関数呼び出しのオーバーヘッドや、場合によっては内部的なバッファリング処理などが発生する可能性があります。新しいコードでは、b.Reader == eofReader の場合は io.Copy をスキップすることで、この不要な処理をさらに削減しています。これにより、Close メソッドの実行効率も向上しています。

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

変更は src/pkg/net/http/transfer.go ファイルで行われています。

--- a/src/pkg/net/http/transfer.go
+++ b/src/pkg/net/http/transfer.go
@@ -328,12 +328,13 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {\
 	switch {
 	case chunked(t.TransferEncoding):
 		if noBodyExpected(t.RequestMethod) {
-			t.Body = &body{Reader: io.LimitReader(r, 0), closing: t.Close}
+			t.Body = &body{Reader: eofReader, closing: t.Close}
 		} else {
 			t.Body = &body{Reader: newChunkedReader(r), hdr: msg, r: r, closing: t.Close}
 		}
-	case realLength >= 0:
-		// TODO: limit the Content-Length. This is an easy DoS vector.
+	case realLength == 0:
+		t.Body = &body{Reader: eofReader, closing: t.Close}
+	case realLength > 0:
 		t.Body = &body{Reader: io.LimitReader(r, realLength), closing: t.Close}
 	default:
 		// realLength < 0, i.e. "Content-Length" not mentioned in header
@@ -342,7 +343,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {\
 		if t.Close {
 			t.Body = &body{Reader: r, closing: t.Close}
 		} else {
 			// Persistent connection (i.e. HTTP/1.1)
-			t.Body = &body{Reader: io.LimitReader(r, 0), closing: t.Close}
+			t.Body = &body{Reader: eofReader, closing: t.Close}
 		}
 	}
 
@@ -612,30 +613,26 @@ func (b *body) Close() error {\
 	if b.closed {
 		return nil
 	}
-	defer func() {\
-		b.closed = true
-	}()
-	if b.hdr == nil && b.closing {
-		// no trailer and closing the connection next.
-		// no point in reading to EOF.
-		return nil
-	}
-
-	// In a server request, don't continue reading from the client
-	// if we've already hit the maximum body size set by the
-	// handler. If this is set, that also means the TCP connection
-	// is about to be closed, so getting to the next HTTP request
-	// in the stream is not necessary.
-	if b.res != nil && b.res.requestBodyLimitHit {
-		return nil
-	}
-
-	// Fully consume the body, which will also lead to us reading
-	// the trailer headers after the body, if present.
-	if _, err := io.Copy(ioutil.Discard, b); err != nil {
-		return err
+	var err error
+	switch {
+	case b.hdr == nil && b.closing:
+		// no trailer and closing the connection next.
+		// no point in reading to EOF.
+	case b.res != nil && b.res.requestBodyLimitHit:
+		// In a server request, don't continue reading from the client
+		// if we've already hit the maximum body size set by the
+		// handler. If this is set, that also means the TCP connection
+		// is about to be closed, so getting to the next HTTP request
+		// in the stream is not necessary.
+	case b.Reader == eofReader:
+		// Nothing to read. No need to io.Copy from it.
+	default:
+		// Fully consume the body, which will also lead to us reading
+		// the trailer headers after the body, if present.
+		_, err = io.Copy(ioutil.Discard, b)
 	}
-	return nil
+	b.closed = true
+	return err
 }
 
 // parseContentLength trims whitespace from s and returns -1 if no value

コアとなるコードの解説

このコミットは、主に readTransfer 関数と body.Close メソッドの2箇所を変更しています。

readTransfer 関数内の変更

readTransfer 関数は、HTTPリクエストまたはレスポンスのボディを読み取るためのロジックをカプセル化しています。

  1. noBodyExpected(t.RequestMethod) のケース: これは、GETリクエストのようにボディが期待されない場合に該当します。

    • 変更前: t.Body = &body{Reader: io.LimitReader(r, 0), closing: t.Close} io.LimitReader(r, 0) を使用して、0バイトを読み取るリーダーを生成していました。これにより、新しい LimitedReader オブジェクトがヒープに割り当てられていました。
    • 変更後: t.Body = &body{Reader: eofReader, closing: t.Close} 既存の eofReader グローバル変数を使用するように変更されました。eofReader は常にEOFを返すため、機能的には io.LimitReader(r, 0) と同じですが、新しいオブジェクトの割り当てが不要になります。
  2. realLength の処理: Content-Length ヘッダが存在する場合の処理です。

    • 変更前: case realLength >= 0: のブロックで、io.LimitReader(r, realLength) を使用していました。 このロジックは、realLength が0の場合も含まれていました。
    • 変更後:
      • case realLength == 0: を新しく追加し、この場合は t.Body = &body{Reader: eofReader, closing: t.Close} を使用するようにしました。これにより、Content-Length: 0 の場合も eofReader が利用され、不要な割り当てが削減されます。
      • case realLength > 0: を追加し、realLength が正の値の場合のみ io.LimitReader(r, realLength) を使用するようにしました。
  3. 永続接続 (Persistent connection) のケース: Content-Length ヘッダがなく、かつ接続が永続的(HTTP/1.1など)な場合の処理です。

    • 変更前: t.Body = &body{Reader: io.LimitReader(r, 0), closing: t.Close} ここでも io.LimitReader(r, 0) が使用されていました。
    • 変更後: t.Body = &body{Reader: eofReader, closing: t.Close} 同様に eofReader を使用するように変更され、割り当てが削減されました。

これらの変更により、0バイトのボディを表現する際に、毎回新しい io.LimitedReader を割り当てる代わりに、既存の eofReader を再利用するようになり、メモリ割り当てが削減されます。

body.Close() メソッド内の変更

body.Close() メソッドは、HTTPリクエストボディの読み取りを完了し、必要に応じて残りのデータを消費するために呼び出されます。

  • 変更前: 複数の if 文と defer を使用していましたが、io.Copy(ioutil.Discard, b) の呼び出しは、b.Readerio.LimitedReader(r, 0) であっても実行される可能性がありました。io.Copy は、たとえすぐにEOFを返すリーダーであっても、関数呼び出しのオーバーヘッドや、場合によっては内部的なバッファリング処理などが発生する可能性があります。

  • 変更後: switch ステートメントを使用して、ボディを読み捨てる必要があるかどうかをより効率的に判断するように変更されました。 特に重要なのは、case b.Reader == eofReader: という新しい条件が追加された点です。この条件が真の場合、つまりボディが eofReader で表現されている場合、io.Copy(ioutil.Discard, b) の呼び出しが完全にスキップされます。これは、eofReader は既にEOFを返すと分かっているため、そこからデータを読み取ろうとするのは無駄だからです。 これにより、Close メソッドが呼び出された際の不要な処理が削減され、パフォーマンスがさらに向上します。

全体として、このコミットは、HTTPリクエストボディの処理パスにおいて、0バイトのボディを扱う際の不要なメモリ割り当てとCPUサイクルを削減することで、net/http パッケージの効率を向上させています。

関連リンク

  • Go Change-Id: https://golang.org/cl/8297044
  • Go Issue: https://golang.org/issue/5188

参考にした情報源リンク

  • Go言語の io パッケージのドキュメント (特に io.Reader, io.LimitedReader, io.EOF): https://pkg.go.dev/io
  • Go言語の net/http パッケージのドキュメント: https://pkg.go.dev/net/http
  • Go言語のガベージコレクションに関する一般的な情報
  • HTTP/1.1 仕様 (RFC 2616 または後続のRFC): 特にGETメソッドとメッセージボディに関するセクション。
  • Go言語のソースコード (特に src/pkg/net/http/transfer.go および src/pkg/io/io.go の関連部分)