[インデックス 10229] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet/http
パッケージ内のTransport
構造体からClientConn
の利用を廃止する変更です。これにより、HTTPクライアントの接続管理ロジックが簡素化され、将来的なnet/http
パッケージの再編("http diet")に向けた準備が行われています。
コミット
commit 53493a22fe7fa4f66a04728ea9835f69c04f5341
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Nov 3 12:35:56 2011 -0700
http: don't use ClientConn in Transport
ClientConn's main feature (pipelining support)
wasn't being used anyway. Ends up simpler to
just not use it.
This is prep for the http diet, moving ClientConn,
ServerConn, etc into http/httputil.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5305088
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/53493a22fe7fa4f66a04728ea9835f69c04f5341
元コミット内容
このコミットの目的は、http.Transport
内でClientConn
を使用しないようにすることです。コミットメッセージによると、ClientConn
の主要な機能であるパイプライン処理のサポートが実際には使用されていなかったため、これを使用しないことでコードがよりシンプルになります。この変更は、ClientConn
やServerConn
などの関連する構造体をhttp/httputil
パッケージに移動させるという、より広範な「http diet」と呼ばれるnet/http
パッケージの再編作業の一環として行われています。
変更の背景
Go言語のnet/http
パッケージは、ウェブアプリケーション開発において非常に重要な役割を担っています。初期の設計では、HTTP/1.1のパイプライン処理をサポートするためにClientConn
のような抽象化が導入されていました。HTTPパイプラインは、単一のTCP接続上で複数のHTTPリクエストを連続して送信し、それらのレスポンスを順不同で受け取ることを可能にする技術です。これにより、ネットワークのラウンドトリップタイム(RTT)を削減し、パフォーマンスを向上させることが期待されていました。
しかし、実際の運用ではHTTPパイプラインにはいくつかの課題がありました。例えば、中間プロキシやサーバーの実装によってはパイプラインが正しく機能しない場合があること、また、リクエストの順序が保証されないため、アプリケーション側での複雑な状態管理が必要になることなどです。さらに、HTTP/2の登場により、パイプラインよりもはるかに効率的な多重化(multiplexing)が提供されるようになり、HTTP/1.1のパイプラインの重要性は相対的に低下しました。
このような背景から、Goのnet/http
パッケージの設計者は、実際に利用されていない、あるいは複雑さを増すだけの抽象化を削除し、コードベースを簡素化する方針を採りました。このコミットは、その「http diet」と呼ばれる大規模なリファクタリングの一環であり、ClientConn
が提供するパイプライン機能がTransport
内で活用されていなかったため、その依存関係を解消することでコードの保守性と理解しやすさを向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
- Go言語の
net/http
パッケージ: Go言語でHTTPクライアントおよびサーバーを構築するための標準ライブラリです。 http.Transport
:net/http
パッケージにおけるHTTPクライアントの主要なコンポーネントの一つです。これは、HTTPリクエストの送信、接続の確立と再利用(コネクションプーリング)、プロキシの処理、TLSハンドシェイクなどを担当します。Transport
は、複数のHTTPリクエストに対して単一のTCP接続を再利用することで、オーバーヘッドを削減し、パフォーマンスを向上させます。- HTTP/1.1 パイプライン: HTTP/1.1の機能の一つで、クライアントがサーバーからのレスポンスを待たずに、同じTCP接続上で複数のHTTPリクエストを連続して送信できる仕組みです。これにより、ネットワークの遅延を隠蔽し、通信効率を高めることが期待されました。ただし、サーバーはリクエストを受け取った順序でレスポンスを返す必要があり、途中でエラーが発生すると後続のリクエストもブロックされる「ヘッドオブラインブロッキング(HOL blocking)」の問題がありました。
bufio.Reader
とbufio.Writer
: Go言語のbufio
パッケージが提供するバッファリングされたI/O操作のための型です。bufio.Reader
はio.Reader
をラップして読み込みをバッファリングし、bufio.Writer
はio.Writer
をラップして書き込みをバッファリングします。これにより、小さな読み書き操作が多数発生する場合でも、システムコールを減らし、効率的なI/Oを実現します。io.ReadCloser
:io.Reader
とio.Closer
インターフェースを組み合わせたものです。データを読み込む機能と、リソースをクローズする機能を提供します。HTTPレスポンスボディなど、読み込み後にクローズする必要があるストリームによく使用されます。
技術的詳細
このコミットの核心は、http.Transport
が内部でClientConn
という抽象化を使用していたのをやめ、代わりにbufio.Reader
とbufio.Writer
を直接利用するように変更した点にあります。
以前のTransport
のpersistConn
構造体は、HTTP接続を管理するためにClientConn
のインスタンスを保持していました。ClientConn
は、HTTPパイプライン処理を抽象化し、リクエストの書き込みとレスポンスの読み込みを管理する役割を担っていました。しかし、コミットメッセージが示すように、Transport
はClientConn
のパイプライン機能を実際には利用していませんでした。つまり、ClientConn
は単にbufio.Reader
とbufio.Writer
をラップしているだけであり、その抽象化が不要な複雑さを生み出していました。
この変更により、以下の点が改善されます。
- コードの簡素化:
ClientConn
という中間層がなくなることで、persistConn
が直接bufio.Reader
とbufio.Writer
を操作するようになり、コードのパスが短縮され、理解しやすくなります。 - 不要な抽象化の削除: 使用されていないパイプライン機能のための抽象化を削除することで、コードベースがスリム化され、将来的なメンテナンスが容易になります。
- 「http diet」への貢献: この変更は、
net/http
パッケージの全体的なリファクタリング計画の一部です。ClientConn
やServerConn
といった、より低レベルの接続管理に関する構造体をhttp/httputil
のようなユーティリティパッケージに移動させることで、net/http
のコアパッケージはより高レベルのHTTPプロトコル処理に集中できるようになります。
具体的には、persistConn
構造体から*ClientConn
フィールドが削除され、代わりに*bufio.Writer
フィールドが追加されました。これにより、リクエストの書き込みはClientConn.Write
メソッドを介するのではなく、persistConn
が直接保持するbufio.Writer
に対して行われるようになります。同様に、レスポンスの読み込みもClientConn.readUsing
メソッドを介するのではなく、persistConn
が保持するbufio.Reader
から直接行われます。
また、以前はClientConn
が管理していたlastbody
(前のレスポンスボディ)の状態管理も、persistConn.readLoop
内で直接行われるようになりました。これは、ClientConn
が持つ状態の一部がpersistConn
に引き継がれたことを意味します。
コアとなるコードの変更箇所
変更はsrc/pkg/http/transport.go
ファイルに集中しています。
-
persistConn
構造体の変更:- cc *ClientConn
が削除されました。+ bw *bufio.Writer // to conn
が追加されました。br *bufio.Reader
のコメントが// from conn
に変更されました。
-
getConn
関数の変更:pconn.cc = NewClientConn(conn, pconn.br)
の行が削除されました。pconn.bw = bufio.NewWriter(pconn.conn)
の行が追加されました。
-
readLoop
関数の変更:resp, err := pc.cc.readUsing(...)
の呼び出しがresp, err := ReadResponse(pc.br, rc.req)
に変更されました。これにより、ClientConn
を介さずに直接bufio.Reader
からレスポンスを読み込むようになりました。lastbody
の管理がpc.cc.lastbody
からreadLoop
内のローカル変数lastbody
に移行し、関連するロック操作 (pc.cc.lk.Lock()
,pc.cc.lastbody = nil
,pc.cc.lk.Unlock()
) が削除されました。- gzipデコードのエラーハンドリングが微調整されました。
-
roundTrip
関数の変更:err = pc.cc.Write(req.Request)
の呼び出しがerr = req.Request.write(pc.bw, pc.isProxy, req.extra)
に変更されました。これにより、ClientConn
を介さずに直接bufio.Writer
にリクエストを書き込むようになりました。pc.bw.Flush()
が追加されました。これは、バッファリングされた書き込みが確実に行われるようにするために重要です。
-
close
関数の変更:pc.cc.Close()
の呼び出しが削除されました。
コアとなるコードの解説
このコミットの最も重要な変更は、persistConn
がHTTPリクエスト/レスポンスの読み書きをどのように行うかという点です。
変更前:
persistConn
はClientConn
のインスタンス(pc.cc
)を保持し、リクエストの書き込みやレスポンスの読み込みといった低レベルのプロトコル処理をClientConn
に委譲していました。例えば、リクエストの書き込みはpc.cc.Write(req.Request)
を介して行われ、レスポンスの読み込みはpc.cc.readUsing(...)
を介して行われていました。
変更後:
ClientConn
が削除されたため、persistConn
は直接net.Conn
から取得したbufio.Reader
(pc.br
)と新しく追加されたbufio.Writer
(pc.bw
)を使用してI/O操作を行います。
-
リクエストの書き込み: 変更前:
err = pc.cc.Write(req.Request)
変更後:err = req.Request.write(pc.bw, pc.isProxy, req.extra)
これは、Request
構造体自身のwrite
メソッドを呼び出し、その出力先としてpersistConn
が持つbufio.Writer
を直接指定しています。これにより、ClientConn
を介するオーバーヘッドがなくなります。また、pc.bw.Flush()
が追加されたことで、バッファリングされたデータがすぐにネットワークに送信されることが保証されます。 -
レスポンスの読み込み: 変更前:
resp, err := pc.cc.readUsing(rc.req, func(buf *bufio.Reader, forReq *Request) (*Response, error) { ... })
変更後:resp, err := ReadResponse(pc.br, rc.req)
ReadResponse
関数は、bufio.Reader
から直接HTTPレスポンスを読み込むための関数です。この変更により、ClientConn
が提供していたパイプライン関連のロジック(実際には使われていなかった)を迂回し、より直接的にレスポンスを処理できるようになります。 -
lastbody
の管理: HTTPレスポンスボディはストリームであり、完全に読み込まれるかクローズされるまで、基になる接続をブロックする可能性があります。persistConn
は、次のリクエストを送信する前に、前のレスポンスボディが完全に読み込まれたか、またはクローズされたことを確認する必要があります。 変更前は、このlastbody
の状態はClientConn
が管理していました。 変更後は、persistConn.readLoop
内でlastbody io.ReadCloser
というローカル変数が導入され、readLoop
自身がこの状態を管理するようになりました。これにより、ClientConn
の依存関係が完全に排除されます。
これらの変更は、net/http
パッケージの内部実装を簡素化し、不要な抽象化を取り除くことで、より効率的で保守しやすいコードベースを目指すものです。
関連リンク
- Go言語の
net/http
パッケージのドキュメント: https://pkg.go.dev/net/http - Go言語の
bufio
パッケージのドキュメント: https://pkg.go.dev/bufio - Go言語の
io
パッケージのドキュメント: https://pkg.go.dev/io - このコミットが参照しているGoのコードレビューシステム (Gerrit) のチェンジリスト: https://golang.org/cl/5305088
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語の
net/http
パッケージのソースコード - HTTP/1.1の仕様 (RFC 2616, 特にパイプラインに関するセクション)
- HTTP/2の仕様 (RFC 7540)
- Go言語のコミット履歴と関連する議論(Gerritのチェンジリストコメントなど)