[インデックス 14833] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおけるリクエスト書き込みのバッファリングに関する最適化とバグ修正を目的としています。具体的には、http.Request
の write
メソッドにおいて、書き込み先(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
を作成します。
この挙動は、以下のような問題を引き起こしていました。
- 二重バッファリング: もし
w
が既にbufio.Writer
のようなバッファリングされたio.Writer
であった場合、bufio.NewWriter(w)
はその上にさらにバッファリング層を追加することになります。これは冗長であり、パフォーマンスの低下やメモリの無駄遣いにつながる可能性があります。 - 書き込み粒度の強制: より深刻な問題は、リバースプロキシのシナリオで発生しました。リバースプロキシは、クライアントからのリクエストボディを読み込み、それをバックエンドサーバーに転送します。この際、リクエストボディが非常に大きい場合や、ストリーミングで転送したい場合など、細かな粒度でデータを書き込みたいことがあります。しかし、
bufio.NewWriter
が強制的に4KBのバッファを導入するため、データは4KB単位でしかフラッシュされず、バックエンドへの書き込みが遅延したり、リアルタイム性が損なわれたりする可能性がありました。特に、小さなリクエストボディや、チャンク転送エンコーディングを使用している場合に、この4KBの書き込み粒度がボトルネックとなることがありました。
このコミットは、この「4KBの書き込み粒度」という制約を取り除き、リバースプロキシがより効率的かつ柔軟に動作できるようにすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の基本的な概念とHTTPの知識が必要です。
-
io.Writer
インターフェース:- Go言語における基本的なI/Oインターフェースの一つで、データを書き込むための抽象化を提供します。
Write(p []byte) (n int, err error)
メソッドを持ち、バイトスライスp
を書き込み、書き込んだバイト数n
とエラーerr
を返します。- ファイル、ネットワーク接続、メモリバッファなど、様々な書き込み先がこのインターフェースを実装しています。
-
io.ByteWriter
インターフェース:io.Writer
とは異なり、単一のバイトを書き込むためのインターフェースです。WriteByte(c byte) error
メソッドを持ちます。- このインターフェースは、
bufio.Writer
のようなバッファリングされたライターが内部的に利用することがあります。
-
bufio.Writer
:bufio
パッケージは、I/O操作をバッファリングすることで効率を向上させるための機能を提供します。bufio.Writer
は、内部にバッファを持ち、データが書き込まれるとまずそのバッファに蓄積します。バッファが満杯になるか、Flush()
メソッドが明示的に呼び出されるか、またはClose()
されるまで、実際の基底のio.Writer
への書き込みは行われません。bufio.NewWriter(w io.Writer)
は、指定されたio.Writer
の上に新しいbufio.Writer
を作成します。デフォルトのバッファサイズは4KBです。- バッファリングは、特に小さな書き込みが頻繁に行われる場合に、システムコール(OSへのI/O要求)の回数を減らし、パフォーマンスを向上させる効果があります。
-
HTTPリバースプロキシ:
- クライアントからのリクエストを受け取り、それを一つ以上のバックエンドサーバーに転送し、バックエンドからのレスポンスをクライアントに返すサーバーです。
- ロードバランシング、SSL終端、キャッシュ、セキュリティ強化などの目的で利用されます。
- リバースプロキシでは、クライアントからのリクエストボディを効率的にバックエンドに転送することが重要です。
-
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
を作成する前に条件分岐が追加されています。
-
if _, ok := w.(io.ByteWriter); !ok { ... }
- この行は、渡された
io.Writer
(w
) がio.ByteWriter
インターフェースを実装しているかどうかをチェックしています。 io.ByteWriter
は、WriteByte(c byte) error
メソッドを持つインターフェースです。bufio.Writer
はio.ByteWriter
を実装しています。したがって、この条件は「w
が既にbufio.Writer
のような、バイト単位の書き込みをサポートするバッファリングされたライターであるか」を間接的にチェックしています。- もし
w
がio.ByteWriter
を実装していない場合(つまり、まだバッファリングされていない可能性が高い場合)、bufio.NewWriter(w)
を呼び出して新しいbufio.Writer
(bw
) を作成し、以降の書き込みに使用するw
をこのbw
に置き換えます。 - もし
w
がio.ByteWriter
を実装している場合(既にバッファリングされていると判断される場合)、新しいbufio.Writer
は作成されず、元のw
がそのまま使用されます。
- この行は、渡された
-
fmt.Fprintf(w, ...)
やreq.Header.WriteSubset(w, ...)
など、すべての書き込み操作は、bw
が作成された場合はbw
に、作成されなかった場合は元のw
に直接行われるように変更されています。 -
最後に、
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.go
の Request.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のインターフェースの柔軟性を活用して、より賢いバッファリング戦略を導入しています。
-
io.ByteWriter
を用いた既存バッファリングの検出:if _, ok := w.(io.ByteWriter); !ok { ... }
の行が最も重要です。- このコードは、型アサーション
w.(io.ByteWriter)
を使用して、w
がio.ByteWriter
インターフェースを実装しているかどうかをチェックしています。 bufio.Writer
はio.ByteWriter
を実装しています。したがって、このチェックは、w
が既にbufio.Writer
のようなバッファリングされたライターであるかどうかを効率的に判断する手段となります。- もし
w
がio.ByteWriter
を実装していない場合(!ok
がtrue
)、それはw
がまだバッファリングされていない、あるいはバイト単位の書き込みを効率的にサポートしていない可能性が高いことを意味します。この場合にのみ、新しいbufio.Writer
(bw
) が作成され、w
はこのbw
に置き換えられます。 - これにより、
bytes.Buffer
のような、既に内部で効率的なバッファリングを行っているio.Writer
や、呼び出し元が意図的にバッファリングを行っているio.Writer
に対して、不必要なbufio.Writer
の追加を避けることができます。
-
書き込み先の動的な切り替え:
w = bw
の行により、以降のfmt.Fprintf(w, ...)
やio.WriteString(w, ...)
などのすべての書き込み操作は、新しく作成されたbufio.Writer
(bw
) に対して行われるようになります。- もし
bw
が作成されなかった場合、w
は元のio.Writer
のままであり、書き込みは直接そのio.Writer
に対して行われます。
-
条件付きの
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 CL 7060059: https://golang.org/cl/7060059
- Go
net/http
パッケージドキュメント: https://pkg.go.dev/net/http - Go
bufio
パッケージドキュメント: https://pkg.go.dev/bufio - Go
io
パッケージドキュメント: https://pkg.go.dev/io
参考にした情報源リンク
- Go言語の公式ドキュメント (上記「関連リンク」に記載)
- Go言語のソースコード (上記「コアとなるコードの変更箇所」に記載)
bufio.Writer
のデフォルトバッファサイズに関する一般的なGoの議論- Goにおけるリバースプロキシの実装パターンに関する情報