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

[インデックス 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の主要な機能であるパイプライン処理のサポートが実際には使用されていなかったため、これを使用しないことでコードがよりシンプルになります。この変更は、ClientConnServerConnなどの関連する構造体を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.Readerbufio.Writer: Go言語のbufioパッケージが提供するバッファリングされたI/O操作のための型です。bufio.Readerio.Readerをラップして読み込みをバッファリングし、bufio.Writerio.Writerをラップして書き込みをバッファリングします。これにより、小さな読み書き操作が多数発生する場合でも、システムコールを減らし、効率的なI/Oを実現します。
  • io.ReadCloser: io.Readerio.Closerインターフェースを組み合わせたものです。データを読み込む機能と、リソースをクローズする機能を提供します。HTTPレスポンスボディなど、読み込み後にクローズする必要があるストリームによく使用されます。

技術的詳細

このコミットの核心は、http.Transportが内部でClientConnという抽象化を使用していたのをやめ、代わりにbufio.Readerbufio.Writerを直接利用するように変更した点にあります。

以前のTransportpersistConn構造体は、HTTP接続を管理するためにClientConnのインスタンスを保持していました。ClientConnは、HTTPパイプライン処理を抽象化し、リクエストの書き込みとレスポンスの読み込みを管理する役割を担っていました。しかし、コミットメッセージが示すように、TransportClientConnのパイプライン機能を実際には利用していませんでした。つまり、ClientConnは単にbufio.Readerbufio.Writerをラップしているだけであり、その抽象化が不要な複雑さを生み出していました。

この変更により、以下の点が改善されます。

  1. コードの簡素化: ClientConnという中間層がなくなることで、persistConnが直接bufio.Readerbufio.Writerを操作するようになり、コードのパスが短縮され、理解しやすくなります。
  2. 不要な抽象化の削除: 使用されていないパイプライン機能のための抽象化を削除することで、コードベースがスリム化され、将来的なメンテナンスが容易になります。
  3. 「http diet」への貢献: この変更は、net/httpパッケージの全体的なリファクタリング計画の一部です。ClientConnServerConnといった、より低レベルの接続管理に関する構造体を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ファイルに集中しています。

  1. persistConn構造体の変更:

    • - cc *ClientConn が削除されました。
    • + bw *bufio.Writer // to conn が追加されました。
    • br *bufio.Reader のコメントが // from conn に変更されました。
  2. getConn関数の変更:

    • pconn.cc = NewClientConn(conn, pconn.br) の行が削除されました。
    • pconn.bw = bufio.NewWriter(pconn.conn) の行が追加されました。
  3. 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デコードのエラーハンドリングが微調整されました。
  4. roundTrip関数の変更:

    • err = pc.cc.Write(req.Request) の呼び出しが err = req.Request.write(pc.bw, pc.isProxy, req.extra) に変更されました。これにより、ClientConnを介さずに直接bufio.Writerにリクエストを書き込むようになりました。
    • pc.bw.Flush() が追加されました。これは、バッファリングされた書き込みが確実に行われるようにするために重要です。
  5. close関数の変更:

    • pc.cc.Close() の呼び出しが削除されました。

コアとなるコードの解説

このコミットの最も重要な変更は、persistConnがHTTPリクエスト/レスポンスの読み書きをどのように行うかという点です。

変更前: persistConnClientConnのインスタンス(pc.cc)を保持し、リクエストの書き込みやレスポンスの読み込みといった低レベルのプロトコル処理をClientConnに委譲していました。例えば、リクエストの書き込みはpc.cc.Write(req.Request)を介して行われ、レスポンスの読み込みはpc.cc.readUsing(...)を介して行われていました。

変更後: ClientConnが削除されたため、persistConnは直接net.Connから取得したbufio.Readerpc.br)と新しく追加されたbufio.Writerpc.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言語の公式ドキュメント
  • Go言語のnet/httpパッケージのソースコード
  • HTTP/1.1の仕様 (RFC 2616, 特にパイプラインに関するセクション)
  • HTTP/2の仕様 (RFC 7540)
  • Go言語のコミット履歴と関連する議論(Gerritのチェンジリストコメントなど)