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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージにおけるリクエスト書き込みのバッファリングに関する最適化とバグ修正を目的としています。具体的には、http.Requestwrite メソッドにおいて、書き込み先(io.Writer)が既にバッファリングされている場合に、bufio.NewWriter を使ってさらにバッファリングを行うことを避けるように変更されています。これにより、特にリバースプロキシの実装において発生していた、書き込み粒度が4KB未満にできないという問題が解決されます。

コミット

commit f38df4e8790969180aee6b5889305f41539a8693
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Jan 9 10:33:46 2013 -0800

    net/http: don't buffer request writing if dest is already buffered
    
    The old code made it impossible to implement a reverse proxy
    with anything less than 4k write granularity to the backends.
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/7060059

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

https://github.com/golang/go/commit/f38df4e8790969180aee6b5889305f41539a8693

元コミット内容

net/http: don't buffer request writing if dest is already buffered

このコミットの目的は、net/http パッケージにおいて、リクエストの書き込み先が既にバッファリングされている場合、二重にバッファリングを行わないようにすることです。以前のコードでは、この二重バッファリングが原因で、バックエンドへの書き込み粒度が4KB未満にできないという問題があり、リバースプロキシの実装を困難にしていました。

変更の背景

Goの net/http パッケージは、HTTPクライアントとサーバーの実装に広く利用されています。このコミットが修正しようとしている問題は、特にリバースプロキシのような、HTTPリクエストを別のサーバーに転送するアプリケーションにおいて顕著でした。

以前の http.Request.write メソッドの実装では、常に bufio.NewWriter(w) を呼び出して、与えられた io.Writer をバッファリングしていました。bufio.NewWriter は、デフォルトで4KBのバッファサイズを持つ bufio.Writer を作成します。

この挙動は、以下のような問題を引き起こしていました。

  1. 二重バッファリング: もし w が既に bufio.Writer のようなバッファリングされた io.Writer であった場合、bufio.NewWriter(w) はその上にさらにバッファリング層を追加することになります。これは冗長であり、パフォーマンスの低下やメモリの無駄遣いにつながる可能性があります。
  2. 書き込み粒度の強制: より深刻な問題は、リバースプロキシのシナリオで発生しました。リバースプロキシは、クライアントからのリクエストボディを読み込み、それをバックエンドサーバーに転送します。この際、リクエストボディが非常に大きい場合や、ストリーミングで転送したい場合など、細かな粒度でデータを書き込みたいことがあります。しかし、bufio.NewWriter が強制的に4KBのバッファを導入するため、データは4KB単位でしかフラッシュされず、バックエンドへの書き込みが遅延したり、リアルタイム性が損なわれたりする可能性がありました。特に、小さなリクエストボディや、チャンク転送エンコーディングを使用している場合に、この4KBの書き込み粒度がボトルネックとなることがありました。

このコミットは、この「4KBの書き込み粒度」という制約を取り除き、リバースプロキシがより効率的かつ柔軟に動作できるようにすることを目的としています。

前提知識の解説

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

  1. io.Writer インターフェース:

    • Go言語における基本的なI/Oインターフェースの一つで、データを書き込むための抽象化を提供します。
    • Write(p []byte) (n int, err error) メソッドを持ち、バイトスライス p を書き込み、書き込んだバイト数 n とエラー err を返します。
    • ファイル、ネットワーク接続、メモリバッファなど、様々な書き込み先がこのインターフェースを実装しています。
  2. io.ByteWriter インターフェース:

    • io.Writer とは異なり、単一のバイトを書き込むためのインターフェースです。
    • WriteByte(c byte) error メソッドを持ちます。
    • このインターフェースは、bufio.Writer のようなバッファリングされたライターが内部的に利用することがあります。
  3. bufio.Writer:

    • bufio パッケージは、I/O操作をバッファリングすることで効率を向上させるための機能を提供します。
    • bufio.Writer は、内部にバッファを持ち、データが書き込まれるとまずそのバッファに蓄積します。バッファが満杯になるか、Flush() メソッドが明示的に呼び出されるか、または Close() されるまで、実際の基底の io.Writer への書き込みは行われません。
    • bufio.NewWriter(w io.Writer) は、指定された io.Writer の上に新しい bufio.Writer を作成します。デフォルトのバッファサイズは4KBです。
    • バッファリングは、特に小さな書き込みが頻繁に行われる場合に、システムコール(OSへのI/O要求)の回数を減らし、パフォーマンスを向上させる効果があります。
  4. HTTPリバースプロキシ:

    • クライアントからのリクエストを受け取り、それを一つ以上のバックエンドサーバーに転送し、バックエンドからのレスポンスをクライアントに返すサーバーです。
    • ロードバランシング、SSL終端、キャッシュ、セキュリティ強化などの目的で利用されます。
    • リバースプロキシでは、クライアントからのリクエストボディを効率的にバックエンドに転送することが重要です。
  5. GoのHTTPリクエストの構造 (http.Request):

    • net/http パッケージの Request 型は、HTTPリクエストを表します。
    • Request.Write(w io.Writer) メソッドは、このリクエストを io.Writer に書き込むために使用されます。これには、リクエストライン、ヘッダー、そしてリクエストボディが含まれます。

技術的詳細

このコミットの核心は、src/pkg/net/http/request.go 内の Request.write メソッドの変更にあります。

変更前:

func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header) error {
    // ...
    bw := bufio.NewWriter(w) // 常に新しいbufio.Writerを作成
    fmt.Fprintf(bw, "%s %s HTTP/1.1\\r\\n", valueOrDefault(req.Method, "GET"), ruri)
    // ...
    // すべての書き込みはbwに対して行われる
    // ...
    return bw.Flush() // 最後にフラッシュ
}

変更前は、Request.write メソッドが呼び出されるたびに、引数として渡された io.Writer (w) をラップする形で、常に新しい bufio.Writer (bw) が作成されていました。そして、HTTPリクエストの各部分(リクエストライン、ヘッダー、ボディ)はすべてこの bw を通じて書き込まれていました。最後に bw.Flush() が呼び出され、バッファリングされたデータが基底の w に書き出されます。

変更後:

func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header) error {
    // ...
    var bw *bufio.Writer
    if _, ok := w.(io.ByteWriter); !ok { // wがio.ByteWriterを実装していない場合のみ
        bw = bufio.NewWriter(w)
        w = bw // 以降の書き込みはbw(または元のw)に対して行われる
    }

    fmt.Fprintf(w, "%s %s HTTP/1.1\\r\\n", valueOrDefault(req.Method, "GET"), ruri)
    // ...
    // すべての書き込みはwに対して行われる (wは元のwか、新しく作成されたbwのいずれか)
    // ...
    if bw != nil { // bwが作成された場合のみフラッシュ
        return bw.Flush()
    }
    return nil
}

変更後のコードでは、bufio.Writer を作成する前に条件分岐が追加されています。

  1. if _, ok := w.(io.ByteWriter); !ok { ... }

    • この行は、渡された io.Writer (w) が io.ByteWriter インターフェースを実装しているかどうかをチェックしています。
    • io.ByteWriter は、WriteByte(c byte) error メソッドを持つインターフェースです。
    • bufio.Writerio.ByteWriter を実装しています。したがって、この条件は「w が既に bufio.Writer のような、バイト単位の書き込みをサポートするバッファリングされたライターであるか」を間接的にチェックしています。
    • もし wio.ByteWriter を実装していない場合(つまり、まだバッファリングされていない可能性が高い場合)、bufio.NewWriter(w) を呼び出して新しい bufio.Writer (bw) を作成し、以降の書き込みに使用する w をこの bw に置き換えます。
    • もし wio.ByteWriter を実装している場合(既にバッファリングされていると判断される場合)、新しい bufio.Writer は作成されず、元の w がそのまま使用されます。
  2. fmt.Fprintf(w, ...)req.Header.WriteSubset(w, ...) など、すべての書き込み操作は、bw が作成された場合は bw に、作成されなかった場合は元の w に直接行われるように変更されています。

  3. 最後に、if bw != nil { return bw.Flush() } という条件付きのフラッシュが追加されました。これは、bw が実際に作成された場合にのみ Flush() を呼び出すことを意味します。bw が作成されなかった場合は、元の w が直接使用されており、その w のフラッシュは呼び出し元に委ねられるか、あるいはその w がそもそもフラッシュを必要としない(例:bytes.Buffer)ことを想定しています。

この変更により、net/http のリクエスト書き込みは、不必要な二重バッファリングを避け、特にリバースプロキシのようなシナリオで、基底の io.Writer が提供する書き込み粒度を尊重するようになります。これにより、リバースプロキシはより小さなチャンクでデータをバックエンドに転送できるようになり、効率とリアルタイム性が向上します。

また、request_test.go には、logWrites というカスタムの io.Writer 実装と、TestRequestWriteBufferedWriter という新しいテストケースが追加されています。このテストは、io.ByteWriter を実装しないカスタムライターに Request.Write を適用した場合に、bufio.Writer が適切に挿入され、書き込みがバッファリングされることを検証しています。

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

src/pkg/net/http/request.goRequest.write メソッドが主な変更箇所です。

--- a/src/pkg/net/http/request.go
+++ b/src/pkg/net/http/request.go
@@ -331,11 +331,20 @@ func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header) err
 	}
 	// TODO(bradfitz): escape at least newlines in ruri?
 
-	bw := bufio.NewWriter(w)
-	fmt.Fprintf(bw, "%s %s HTTP/1.1\\r\\n", valueOrDefault(req.Method, "GET"), ruri)
+	// Wrap the writer in a bufio Writer if it's not already buffered.
+	// Don't always call NewWriter, as that forces a bytes.Buffer
+	// and other small bufio Writers to have a minimum 4k buffer
+	// size.
+	var bw *bufio.Writer
+	if _, ok := w.(io.ByteWriter); !ok {
+		bw = bufio.NewWriter(w)
+		w = bw
+	}
+
+	fmt.Fprintf(w, "%s %s HTTP/1.1\\r\\n", valueOrDefault(req.Method, "GET"), ruri)
 
 	// Header lines
-	fmt.Fprintf(bw, "Host: %s\\r\\n", host)
+	fmt.Fprintf(w, "Host: %s\\r\\n", host)
 
 	// Use the defaultUserAgent unless the Header contains one, which
 	// may be blank to not send the header.
@@ -346,33 +355,36 @@ func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header) err
 		}
 	}
 	if userAgent != "" {
-		fmt.Fprintf(bw, "User-Agent: %s\\r\\n", userAgent)
+		fmt.Fprintf(w, "User-Agent: %s\\r\\n", userAgent)
 	}
 
 	// Process Body,ContentLength,Close,Trailer
 	if err != nil {
 		return err
 	}
-	err = tw.WriteHeader(bw)
+	err = tw.WriteHeader(w)
 	if err != nil {
 		return err
 	}
 
 	// TODO: split long values?  (If so, should share code with Conn.Write)
-	err = req.Header.WriteSubset(bw, reqWriteExcludeHeader)
+	err = req.Header.WriteSubset(w, reqWriteExcludeHeader)
 	if err != nil {
 		return err
 	}
 
 	if extraHeaders != nil {
-		err = extraHeaders.Write(bw)
+		err = extraHeaders.Write(w)
 		if err != nil {
 			return err
 		}
 	}
 
-	io.WriteString(bw, "\\r\\n")
+	io.WriteString(w, "\\r\\n")
 
 	// Write body and trailer
-	err = tw.WriteBody(bw)
+	err = tw.WriteBody(w)
 	if err != nil {
 		return err
 	}
 
-	return bw.Flush()
+	if bw != nil {
+		return bw.Flush()
+	}
+	return nil
 }

また、src/pkg/net/http/request_test.go には、この変更の挙動を検証するための新しいテストケース TestRequestWriteBufferedWriter が追加されています。

--- a/src/pkg/net/http/request_test.go
+++ b/src/pkg/net/http/request_test.go
@@ -267,6 +267,36 @@ func TestNewRequestContentLength(t *testing.T) {
 	}
 }
 
+type logWrites struct {
+	t   *testing.T
+	dst *[]string
+}
+
+func (l logWrites) WriteByte(c byte) error {
+	l.t.Fatalf("unexpected WriteByte call")
+	return nil
+}
+
+func (l logWrites) Write(p []byte) (n int, err error) {
+	*l.dst = append(*l.dst, string(p))
+	return len(p), nil
+}
+
+func TestRequestWriteBufferedWriter(t *testing.T) {
+	got := []string{}
+	req, _ := NewRequest("GET", "http://foo.com/", nil)
+	req.Write(logWrites{t, &got})
+	want := []string{
+		"GET / HTTP/1.1\\r\\n",
+		"Host: foo.com\\r\\n",
+		"User-Agent: Go http package\\r\\n",
+		"\\r\\n",
+	}
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("Writes = %q\\n  Want = %q", got, want)
+	}
+}
+
 func testMissingFile(t *testing.T, req *Request) {
 	f, fh, err := req.FormFile("missing")
 	if f != nil {

コアとなるコードの解説

Request.write メソッドの変更は、Goのインターフェースの柔軟性を活用して、より賢いバッファリング戦略を導入しています。

  1. io.ByteWriter を用いた既存バッファリングの検出:

    • if _, ok := w.(io.ByteWriter); !ok { ... } の行が最も重要です。
    • このコードは、型アサーション w.(io.ByteWriter) を使用して、wio.ByteWriter インターフェースを実装しているかどうかをチェックしています。
    • bufio.Writerio.ByteWriter を実装しています。したがって、このチェックは、w が既に bufio.Writer のようなバッファリングされたライターであるかどうかを効率的に判断する手段となります。
    • もし wio.ByteWriter を実装していない場合(!oktrue)、それは w がまだバッファリングされていない、あるいはバイト単位の書き込みを効率的にサポートしていない可能性が高いことを意味します。この場合にのみ、新しい bufio.Writer (bw) が作成され、w はこの bw に置き換えられます。
    • これにより、bytes.Buffer のような、既に内部で効率的なバッファリングを行っている io.Writer や、呼び出し元が意図的にバッファリングを行っている io.Writer に対して、不必要な bufio.Writer の追加を避けることができます。
  2. 書き込み先の動的な切り替え:

    • w = bw の行により、以降の fmt.Fprintf(w, ...)io.WriteString(w, ...) などのすべての書き込み操作は、新しく作成された bufio.Writer (bw) に対して行われるようになります。
    • もし bw が作成されなかった場合、w は元の io.Writer のままであり、書き込みは直接その io.Writer に対して行われます。
  3. 条件付きの Flush() 呼び出し:

    • メソッドの最後で、if bw != nil { return bw.Flush() } という条件が追加されました。
    • これは、bufio.Writer (bw) が実際に作成された場合にのみ、そのバッファをフラッシュすることを保証します。
    • もし bw が作成されなかった場合(つまり、元の w が直接使用された場合)、Flush() は呼び出されません。これは、元の w がフラッシュを必要としない(例:bytes.Buffer)か、あるいはその w のフラッシュは呼び出し元が責任を持つべきである、という設計思想に基づいています。

この変更は、Goの net/http パッケージが、より汎用的で効率的なI/O処理を行うための重要な改善です。特に、リバースプロキシのような、低レベルのI/O制御がパフォーマンスに直結するアプリケーションにおいて、その効果は大きいです。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (上記「関連リンク」に記載)
  • Go言語のソースコード (上記「コアとなるコードの変更箇所」に記載)
  • bufio.Writer のデフォルトバッファサイズに関する一般的なGoの議論
  • Goにおけるリバースプロキシの実装パターンに関する情報