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

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

このコミットは、Go言語の net/http パッケージにおけるチャンク転送エンコーディングの読み込み処理を改善し、HTTPクライアント接続の再利用効率を高めることを目的としています。具体的には、チャンクリーダーが可能な限り多くのデータを読み込み、特にチャンクの終端(EOF)を早期に検出することで、http.Transport クライアント接続がより積極的に再利用されるように変更されています。

コミット

commit ff29be14c4c63912963c442109da56a98960ea2d
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Jan 29 13:44:21 2014 +0100

    net/http: read as much as possible (including EOF) during chunked reads
    
    This is the chunked half of https://golang.org/cl/49570044 .
    
    We want full reads to return EOF as early as possible, when we
    know we're at the end, so http.Transport client connections are eagerly
    re-used in the common case, even if no Read or Close follows.
    
    To do this, make the chunkedReader.Read fill up its argument p []byte
    buffer as much as possible, as long as that doesn't involve doing
    any more blocking reads to read chunk headers. That means if we
    have a chunk EOF ("0\r\n") sitting in the incoming bufio.Reader,
    we see it and set EOF on our final Read.
    
    LGTM=adg
    R=adg
    CC=golang-codereviews
    https://golang.org/cl/58240043

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

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

元コミット内容

net/http: read as much as possible (including EOF) during chunked reads

このコミットは、チャンク読み込み中に可能な限り多くのデータ(EOFを含む)を読み込むように net/http パッケージを変更します。これは、https://golang.org/cl/49570044 のチャンク部分の実装です。

完全な読み込みが可能な限り早くEOFを返すようにすることで、http.Transport クライアント接続が、その後に ReadClose が続かない場合でも、一般的なケースで積極的に再利用されるようにします。

これを実現するために、chunkedReader.Read は、チャンクヘッダーを読み込むためのブロッキング読み込みを伴わない限り、引数 p []byte バッファを可能な限り埋めるようにします。これは、受信 bufio.Reader にチャンクEOF("0\r\n")が存在する場合、それを検出し、最終的な Read でEOFを設定することを意味します。

変更の背景

HTTP/1.1では、メッセージボディの長さを事前に知ることができない場合に「チャンク転送エンコーディング (Chunked Transfer Encoding)」が使用されます。これは、ボディを複数の「チャンク」に分割し、各チャンクの前にそのサイズを示すヘッダーを付加し、最後にサイズ0のチャンクで終了を示す方式です。

Goの net/http パッケージは、HTTPクライアントとサーバーの実装を提供しており、クライアント側では http.Transport が接続の管理と再利用を担当します。HTTP/1.1のKeep-Alive機能により、一度確立されたTCP接続は複数のリクエスト/レスポンスで再利用されることが期待されます。これにより、接続確立のオーバーヘッドを削減し、パフォーマンスを向上させることができます。

しかし、チャンク転送エンコーディングを使用している場合、レスポンスボディの読み込みが完了したかどうかを正確に判断することが重要です。もし、クライアントがレスポンスボディの最後まで読み込まないうちに接続を閉じたり、次のリクエストを送信しようとしたりすると、接続が適切に再利用されない可能性があります。特に、レスポンスボディを完全に読み込まない場合でも、接続を再利用するためには、チャンクの終端を示す「0\r\n」を確実に読み取り、ストリームの終わりを認識する必要があります。

このコミットの背景には、http.Transport がクライアント接続をより積極的に再利用できるようにするという明確な目標があります。以前の実装では、チャンクの終端を早期に検出できない場合があり、その結果、接続の再利用が遅れたり、不必要に新しい接続が確立されたりする可能性がありました。特に、ReadClose が後続しない場合でも接続を再利用できるようにするためには、EOFを可能な限り早く返すことが求められました。

前提知識の解説

HTTP/1.1 チャンク転送エンコーディング (Chunked Transfer Encoding)

HTTP/1.1のメッセージボディを転送するメカニズムの一つで、メッセージボディの長さを事前に決定できない場合(例:動的に生成されるコンテンツ)に利用されます。

  • 形式: メッセージボディは一連のチャンクで構成されます。各チャンクは、そのチャンクのサイズ(16進数)と、それに続くデータで構成されます。サイズとデータの間にはCRLF(\r\n)が挿入されます。
  • 終端: 最後のチャンクはサイズが0であり、その後にオプションのトレーラーヘッダーが続くことがあります。サイズ0のチャンクの後には、必ず2つのCRLF(\r\n\r\n)が続きます。この 0\r\n\r\n がメッセージボディの終端を示します。
  • 利点:
    • サーバーがレスポンスボディの全長を知る前に送信を開始できるため、動的なコンテンツ生成に適しています。
    • Keep-Alive接続で、複数のレスポンスを効率的に送信できます。

Go net/http パッケージ

Goの標準ライブラリに含まれるHTTPクライアントおよびサーバーの実装を提供するパッケージです。

  • http.Client: HTTPリクエストを送信し、HTTPレスポンスを受信するクライアント。
  • http.Transport: http.Client の内部で使用され、HTTP接続の確立、管理、再利用を担当します。Keep-Alive接続のプールを維持し、効率的な通信を実現します。
  • io.Reader インターフェース: データを読み込むための基本的なインターフェース。Read(p []byte) (n int, err error) メソッドを持ち、n は読み込んだバイト数、err はエラー(EOFを含む)を示します。
  • io.EOF: io.ReaderRead メソッドが、これ以上読み込むデータがないことを示すために返す特別なエラーです。このエラーが返されると、読み込みストリームの終端に達したことを意味します。

bufio.Reader

bufio パッケージは、I/O操作をバッファリングすることで効率を向上させる機能を提供します。bufio.Reader は、基になる io.Reader からデータを読み込み、内部バッファに格納します。これにより、小さな読み込み要求が多数発生しても、実際のシステムコールは少なくなり、パフォーマンスが向上します。

  • Buffered(): 内部バッファに現在利用可能なバイト数を返します。
  • Peek(n int): 次の n バイトを読み込まずに覗き見します。バッファに十分なデータがない場合でも、ブロックせずに利用可能なデータを返します。

接続の再利用とEOFの重要性

HTTP/1.1のKeep-Alive接続では、クライアントがレスポンスボディの最後まで読み込み、サーバーが接続の終端を認識することで、そのTCP接続を次のリクエストのために再利用できます。もしクライアントがボディの途中で読み込みを停止すると、サーバーは接続がまだ使用中であると判断し、接続を閉じることができず、結果として接続プールが枯渇したり、新しい接続が不必要に確立されたりする可能性があります。

io.EOF を早期に返すことは、クライアントがレスポンスボディの終端に達したことを迅速に認識し、接続を解放して再利用可能にするために不可欠です。特に、クライアントがレスポンスボディを完全に消費しない場合(例:ヘッダーのみを読み取り、ボディは無視する場合)でも、接続を再利用するためには、ストリームの終端を示す EOF を受け取ることが重要になります。

技術的詳細

このコミットの主要な変更点は、net/http および net/http/httputil パッケージ内の chunkedReaderRead メソッドの動作です。以前の実装では、Read メソッドは、要求されたバッファサイズ b に応じてデータを読み込み、チャンクの終端に達した場合にのみ次のチャンクヘッダーの読み込みを試みていました。しかし、これにより、チャンクの終端を示す 0\r\nbufio.Reader の内部バッファに存在していても、それがすぐに処理されず、io.EOF が遅れて返される可能性がありました。

新しい実装では、以下の点が改善されています。

  1. 積極的な読み込みとEOFの早期検出:

    • chunkedReader.Read は、ループ内で cr.err == nil である限り、可能な限り多くのデータを読み込もうとします。
    • cr.n == 0 (現在のチャンクのデータがすべて読み込まれた状態) になった場合、次のチャンクヘッダーを読み込む前に、chunkHeaderAvailable() メソッドを呼び出して、bufio.Reader の内部バッファに改行文字 (\n) が含まれているかどうかを確認します。
    • chunkHeaderAvailable()true を返す(つまり、次のチャンクヘッダーがバッファに存在し、ブロッキングなしで読み取れる)場合、beginChunk() を呼び出して次のチャンクの処理に進みます。
    • chunkHeaderAvailable()false を返す(つまり、次のチャンクヘッダーを読み込むにはブロッキングが必要)場合、または len(b) == 0 の場合、Read ループを break します。これにより、不必要なブロッキングを避けつつ、可能な限り多くのデータを読み込みます。
    • 特に重要なのは、0\r\n という終端チャンクが bufio.Reader のバッファに存在する場合、chunkHeaderAvailable()true を返し、beginChunk() が呼び出されて終端チャンクが処理され、io.EOF が早期に返されるようになった点です。
  2. chunkHeaderAvailable() メソッドの導入:

    • この新しいヘルパー関数は、bufio.ReaderBuffered()Peek() メソッドを利用して、内部バッファに改行文字(\n)が含まれているかどうかをチェックします。改行文字はチャンクヘッダーの終端を示すため、これが存在するということは、ブロッキングなしで次のチャンクヘッダーを読み取れる可能性が高いことを意味します。
  3. net/http/httputil とのコード重複の解消:

    • 以前は net/http/chunked.gonet/http/httputil/chunked.go でチャンク関連のコードが重複していました。このコミットでは、httputil パッケージに NewChunkedReaderNewChunkedWriter という公開関数を導入し、これらが net/http パッケージ内の非公開の newChunkedReadernewChunkedWriter を呼び出すように変更されました。これにより、コードの重複が解消され、メンテナンス性が向上しました。

これらの変更により、chunkedReader は、たとえ Read の呼び出しで要求されたバッファサイズが小さくても、内部バッファに存在するチャンクデータと、ブロッキングなしで読み取れる次のチャンクヘッダー(特に終端チャンク)を積極的に消費するようになりました。これにより、レスポンスボディの終端がより早く検出され、http.Transport が接続を再利用できるタイミングが早まります。

テストケース TestChunkReadMultiple の追加は、この新しい動作、特に小さなチャンクがまとめて読み込まれるケースや、bufio.Reader のバッファサイズがチャンクヘッダーの読み込みに影響を与えるケース、そしてバッファがすでに満たされている場合でもEOFチャンクが検出されるケースを検証しています。

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

このコミットのコアとなる変更は、src/pkg/net/http/chunked.gosrc/pkg/net/http/httputil/chunked.gochunkedReader 構造体の Read メソッドと、新しく追加された chunkHeaderAvailable メソッドにあります。

src/pkg/net/http/chunked.go (および src/pkg/net/http/httputil/chunked.go も同様)

--- a/src/pkg/net/http/chunked.go
+++ b/src/pkg/net/http/chunked.go
@@ -57,26 +58,45 @@ func (cr *chunkedReader) beginChunk() {
 	}
 }
 
-func (cr *chunkedReader) Read(b []uint8) (n int, err error) {
-	if cr.err != nil {
-		return 0, cr.err
+func (cr *chunkedReader) chunkHeaderAvailable() bool {
+	n := cr.r.Buffered()
+	if n > 0 {
+		peek, _ := cr.r.Peek(n)
+		return bytes.IndexByte(peek, '\n') >= 0
 	}
-	if cr.n == 0 {
-		cr.beginChunk()
-		if cr.err != nil {
-			return 0, cr.err
+	return false
+}
+
+func (cr *chunkedReader) Read(b []uint8) (n int, err error) {
+	for cr.err == nil {
+		if cr.n == 0 {
+			if n > 0 && !cr.chunkHeaderAvailable() {
+				// We've read enough. Don't potentially block
+				// reading a new chunk header.
+				break
+			}
+			cr.beginChunk()
+			continue
 		}
-	}
-	if uint64(len(b)) > cr.n {
-		b = b[0:cr.n]
-	}
-	n, cr.err = cr.r.Read(b)
-	cr.n -= uint64(n)
-	if cr.n == 0 && cr.err == nil {
-		// end of chunk (CRLF)
-		if _, cr.err = io.ReadFull(cr.r, cr.buf[:]); cr.err == nil {
-			if cr.buf[0] != '\r' || cr.buf[1] != '\n' {
-				cr.err = errors.New("malformed chunked encoding")
+		if len(b) == 0 {
+			break
+		}
+		rbuf := b
+		if uint64(len(rbuf)) > cr.n {
+			rbuf = rbuf[:cr.n]
+		}
+		var n0 int
+		n0, cr.err = cr.r.Read(rbuf)
+		n += n0
+		b = b[n0:]
+		cr.n -= uint64(n0)
+		// If we're at the end of a chunk, read the next two
+		// bytes to verify they are "\r\n".
+		if cr.n == 0 && cr.err == nil {
+			if _, cr.err = io.ReadFull(cr.r, cr.buf[:2]); cr.err == nil {
+				if cr.buf[0] != '\r' || cr.buf[1] != '\n' {
+					cr.err = errors.New("malformed chunked encoding")
+				}
 			}
 		}
 	}

src/pkg/net/http/httputil/httputil.go (新規ファイル)

// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package httputil provides HTTP utility functions, complementing the
// more common ones in the net/http package.
package httputil

import "io"

// NewChunkedReader returns a new chunkedReader that translates the data read from r
// out of HTTP "chunked" format before returning it.
// The chunkedReader returns io.EOF when the final 0-length chunk is read.
//
// NewChunkedReader is not needed by normal applications. The http package
// automatically decodes chunking when reading response bodies.
func NewChunkedReader(r io.Reader) io.Reader {
	return newChunkedReader(r)
}

// NewChunkedWriter returns a new chunkedWriter that translates writes into HTTP
// "chunked" format before writing them to w. Closing the returned chunkedWriter
// sends the final 0-length chunk that marks the end of the stream.
//
// NewChunkedWriter is not needed by normal applications. The http
// package adds chunking automatically if handlers don't set a
// Content-Length header. Using NewChunkedWriter inside a handler
// would result in double chunking or chunking with a Content-Length
// length, both of which are wrong.
func NewChunkedWriter(w io.Writer) io.WriteCloser {
	return newChunkedWriter(w)
}

コアとなるコードの解説

chunkedReader.Read メソッドの変更

旧来の Read メソッドは、現在のチャンクのデータが読み尽くされた (cr.n == 0) 場合にのみ beginChunk() を呼び出し、次のチャンクヘッダーの読み込みを試みていました。これは、Read が要求されたバッファ b を満たすとすぐに終了し、次のチャンクヘッダーが bufio.Reader のバッファに存在していても、それが処理されない可能性がありました。

新しい Read メソッドは、for cr.err == nil ループ内で動作します。

  1. cr.n == 0 (現在のチャンクのデータが読み尽くされた場合):

    • if n > 0 && !cr.chunkHeaderAvailable(): ここが重要な変更点です。
      • n > 0 は、Read メソッドに渡されたバッファ b にまだ書き込む余地があることを意味します。
      • !cr.chunkHeaderAvailable() は、bufio.Reader の内部バッファに次のチャンクヘッダー(特に改行文字)がまだ存在しないことを意味します。つまり、次のチャンクヘッダーを読み込むにはブロッキングが必要になる可能性が高い状況です。
      • この条件が真の場合、break してループを終了します。これにより、不必要なブロッキング読み込みを避けつつ、現在利用可能なデータをすべて b に書き込み、Read を終了します。
    • cr.beginChunk(): 上記の条件が偽の場合(つまり、次のチャンクヘッダーがブロッキングなしで読み取れる場合)、beginChunk() を呼び出して次のチャンクヘッダーを読み込みます。これには、終端チャンク 0\r\n の検出も含まれます。
    • continue: beginChunk() の処理後、ループの先頭に戻り、新しいチャンクデータ(またはEOF)の処理を続行します。
  2. len(b) == 0: Read に空のバッファが渡された場合、すぐにループを break します。

  3. データ読み込み:

    • rbuf := b: 読み込み先のバッファ brbuf にコピーします。
    • if uint64(len(rbuf)) > cr.n { rbuf = rbuf[:cr.n] }: rbuf のサイズを現在のチャンクの残りバイト数 cr.n に制限します。これにより、現在のチャンクの範囲を超えて読み込むことを防ぎます。
    • n0, cr.err = cr.r.Read(rbuf): 基になる bufio.Reader からデータを読み込みます。
    • n += n0; b = b[n0:]; cr.n -= uint64(n0): 読み込んだバイト数を合計 n に加算し、bcr.n を更新します。
  4. チャンク終端のCRLF検証:

    • if cr.n == 0 && cr.err == nil: 現在のチャンクのデータがすべて読み込まれ、エラーがない場合、チャンクの終端を示す \r\n を読み込み、それが正しい形式であるか検証します。

chunkHeaderAvailable() メソッド

この新しいメソッドは、bufio.Reader の内部バッファに次のチャンクヘッダーの終端(改行文字 \n)が存在するかどうかを非ブロッキングで確認するために導入されました。

  • n := cr.r.Buffered(): bufio.Reader の内部バッファに現在利用可能なバイト数を取得します。
  • if n > 0: バッファにデータが存在する場合のみ処理を続行します。
  • peek, _ := cr.r.Peek(n): バッファ内のすべてのデータを読み込まずに覗き見します。
  • return bytes.IndexByte(peek, '\n') >= 0: 覗き見したデータの中に改行文字 \n が含まれているかどうかをチェックします。含まれていれば true を返し、次のチャンクヘッダーがブロッキングなしで読み取れる可能性が高いことを示します。

この変更により、chunkedReader は、Read メソッドが呼び出された際に、要求されたバイト数を満たすだけでなく、bufio.Reader のバッファに存在する次のチャンクヘッダー(特に終端チャンク)も積極的に処理するようになりました。これにより、io.EOF がより早く返され、http.Transport が接続を再利用できる機会が増加します。

httputil パッケージの変更

src/pkg/net/http/httputil/httputil.go が新しく追加され、NewChunkedReaderNewChunkedWriter という公開関数が定義されました。これらは、net/http パッケージ内の非公開の newChunkedReadernewChunkedWriter をラップしています。これにより、net/httpnet/http/httputil 間で重複していたチャンク関連のコードが、net/http に集約され、httputil はそのラッパーを提供する形になりました。これはコードの重複を避け、一貫性を保つためのリファクタリングです。

関連リンク

参考にした情報源リンク

  • Goのソースコード (上記コミットの差分)
  • HTTP/1.1 RFC 2616
  • Go net/http パッケージのドキュメント
  • Go bufio パッケージのドキュメント
  • Go io パッケージのドキュメント
  • Goのコードレビューシステム (Gerrit) の情報