[インデックス 18382] ファイルの概要
このコミットは、Go言語の標準ライブラリnet/http
パッケージ内のTransport
構造体におけるアイドルコネクションキャッシュのキーとして使用されるデータ型を、従来の文字列から構造体へと変更するものです。これにより、ガベージコレクション(GC)の負荷を軽減し、メモリ割り当てを最適化することを目的としています。
コミット
commit ae8251b0aa946177877f61b45a96e90319dce1ff
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Jan 30 09:57:04 2014 +0100
net/http: use a struct instead of a string in transport conn cache key
The Transport's idle connection cache is keyed by a string,
for pre-Go 1.0 reasons. Ever since Go has been able to use
structs as map keys, there's been a TODO in the code to use
structs instead of allocating strings. This change does that.
Saves 3 allocatins and ~100 bytes of garbage per client
request. But because string hashing is so fast these days
(thanks, Keith), the performance is a wash: what we gain
on GC and not allocating, we lose in slower hashing. (hashing
structs of strings is slower than 1 string)
This seems a bit faster usually, but I've also seen it be a
bit slower. But at least it's how I've wanted it now, and it
the allocation improvements are consistent.
LGTM=adg
R=adg
CC=golang-codereviews
https://golang.org/cl/58260043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ae8251b0aa946177877f61b45a96e90319dce1ff
元コミット内容
net/http: use a struct instead of a string in transport conn cache key
Transport
のアイドルコネクションキャッシュは、Go 1.0以前の理由により文字列をキーとしていました。Goが構造体をマップキーとして使用できるようになって以来、文字列の割り当てを避けて構造体を使用するというTODOコメントがコード内に存在していました。この変更はそのTODOを解消するものです。
これにより、クライアントリクエストごとに3つの割り当てと約100バイトのガベージを削減します。しかし、最近の文字列ハッシュの高速化(Keithに感謝)により、パフォーマンスは相殺されます。GCと割り当て削減で得られるものは、ハッシュの遅さで失われます(文字列の構造体をハッシュする方が1つの文字列よりも遅いため)。
これは通常少し速いように見えますが、少し遅くなることもありました。しかし、少なくともこれで望んでいた形になり、割り当ての改善は一貫しています。
変更の背景
この変更の主な背景は、net/http.Transport
が内部で管理するアイドルコネクションキャッシュのキーとして、歴史的な理由から文字列を使用していた点にあります。Go言語の初期バージョンでは、マップのキーとして使用できる型に制限があり、構造体を直接キーとして使用することはできませんでした。そのため、コネクションを識別するための情報(プロキシ情報、スキーム、アドレスなど)を結合して一つの文字列を生成し、それをキーとして利用していました。
しかし、Go言語の進化に伴い、構造体をマップのキーとして直接使用できるようになりました(構造体のフィールドがすべて比較可能な型である場合)。この機能が導入されて以来、net/http
のコードベースには、文字列の代わりに構造体をキーとして使用すべきだというTODOコメントが存在していました。
文字列をキーとして使用することの欠点は、キーを生成するたびに新しい文字列が割り当てられ、それがガベージコレクションの対象となることです。特にHTTPリクエストが頻繁に行われるようなアプリケーションでは、この小さな割り当てが積み重なり、GCの負荷を増大させる可能性があります。コミットメッセージにあるように、「クライアントリクエストごとに3つの割り当てと約100バイトのガベージ」が発生していました。これは、大量の同時接続や高頻度なリクエスト処理を行うサーバーアプリケーションにおいて、無視できないオーバーヘッドとなり得ます。
このコミットは、このTODOを解消し、より効率的なキー管理を実現することで、GCの負荷を軽減し、全体的なシステムパフォーマンスの向上を目指しました。
前提知識の解説
Go言語のmap
とキーの要件
Go言語のmap
(ハッシュマップ)は、キーと値のペアを格納するためのデータ構造です。map
のキーとして使用できる型には制約があり、その型は「比較可能(comparable)」でなければなりません。比較可能な型とは、==
演算子や!=
演算子で比較できる型を指します。
具体的には、以下の型が比較可能です。
- ブール型
- 数値型(整数型、浮動小数点型、複素数型)
- 文字列型
- ポインタ型
- チャネル型
- インターフェース型(動的な型と値が比較可能な場合)
- 構造体型(すべてのフィールドが比較可能な型である場合)
- 配列型(要素の型が比較可能な型である場合)
スライス、マップ、関数は比較可能ではないため、map
のキーとして直接使用することはできません。
このコミットの文脈では、Goが構造体をマップキーとして使用できるようになったことが重要です。これにより、複数の要素(プロキシ、スキーム、アドレスなど)をまとめた構造体を直接キーとして利用できるようになり、文字列への変換とそれに伴うメモリ割り当てを回避できるようになりました。
Go言語のガベージコレクション(GC)とメモリ割り当て
Go言語は自動メモリ管理(ガベージコレクション)を採用しています。プログラムがメモリを割り当てると、GCは不要になったメモリを自動的に解放します。GCの効率は、アプリケーションのパフォーマンスに大きな影響を与えます。
- メモリ割り当て(Allocation): プログラムが新しいオブジェクトを作成するたびに、ヒープメモリが割り当てられます。この割り当て操作自体にもコストがかかります。
- ガベージ(Garbage): 不要になったメモリ領域やオブジェクトは「ガベージ」と呼ばれます。
- ガベージコレクション(GC): GCは、ガベージを識別し、そのメモリを再利用可能にするプロセスです。GCが実行されると、一時的にプログラムの実行が停止したり(ストップ・ザ・ワールド)、CPUリソースを消費したりするため、GCの頻度や実行時間がパフォーマンスのボトルネックになることがあります。
このコミットで「3つの割り当てと約100バイトのガベージを削減」とあるのは、HTTPリクエストごとに発生していた小さなメモリ割り当てが減ることで、GCの頻度や負荷が軽減され、結果としてアプリケーション全体の応答性やスループットが向上する可能性があることを意味します。
net/http.Transport
のアイドルコネクションキャッシュ
net/http.Transport
は、HTTPクライアントがネットワークリソース(TCPコネクションなど)を管理するための主要な構造体です。特に、HTTP/1.1のKeep-Alive機能を利用して、一度確立したTCPコネクションを再利用することで、コネクション確立のオーバーヘッド(TCPハンドシェイク、TLSハンドシェイクなど)を削減し、パフォーマンスを向上させます。
この再利用可能なコネクションは「アイドルコネクションキャッシュ」に格納されます。クライアントがHTTPリクエストを送信する際、Transport
はまずキャッシュ内に適切なアイドルコネクションがあるかを探します。見つかればそれを再利用し、なければ新しいコネクションを確立します。
キャッシュのキーは、どのコネクションがどの宛先(プロキシ、スキーム、アドレス)に対応するかを識別するために使用されます。このコミット以前は、このキーが文字列として生成されていました。
文字列と構造体のハッシュ性能
Goのmap
は内部的にハッシュテーブルとして実装されており、キーのハッシュ値を計算してバケットを決定します。ハッシュ計算の速度は、マップ操作(挿入、検索、削除)のパフォーマンスに直接影響します。
- 文字列のハッシュ: Goの文字列ハッシュ関数は非常に最適化されており、一般的に高速です。コミットメッセージにも「string hashing is so fast these days (thanks, Keith)」とあるように、Goの文字列ハッシュは高性能です。
- 構造体のハッシュ: 構造体をマップキーとして使用する場合、Goランタイムは構造体の各フィールドのハッシュ値を計算し、それらを組み合わせて最終的なハッシュ値を生成します。このプロセスは、特に構造体内に複数の文字列フィールドが含まれる場合、単一の文字列をハッシュするよりも計算コストが高くなる可能性があります。
このコミットでは、GCの負荷軽減というメリットがある一方で、構造体ハッシュの計算コストが文字列ハッシュよりも高くなるというトレードオフが指摘されています。結果として「パフォーマンスは相殺される」と述べられており、GCの改善とハッシュの遅延がバランスを取り合う形になっています。
技術的詳細
このコミットの核心は、net/http.Transport
がアイドルコネクションを管理するために使用するマップのキーの型を、string
から新しく定義されたconnectMethodKey
構造体に変更することです。
connectMethodKey
構造体の導入
変更前は、connectMethod
構造体のString()
メソッドがコネクションの識別子となる文字列を生成し、これがマップのキーとして使われていました。
// 変更前 (transport.go)
type connectMethod struct {
proxyURL *url.URL // nil if no proxy
targetScheme string // "http" or "https"
targetAddr string // "host:port"
}
func (ck *connectMethod) key() string {
return ck.String() // TODO: use a struct type instead
}
func (ck *connectMethod) String() string {
proxyStr := ""
targetAddr := ck.targetAddr
if ck.proxyURL != nil {
proxyStr = ck.proxyURL.String()
if ck.targetScheme == "http" {
targetAddr = ""
}
}
return strings.Join([]string{proxyStr, ck.targetScheme, targetAddr}, "|")
}
このコミットでは、connectMethodKey
という新しい構造体が導入されました。
// 変更後 (transport.go)
// connectMethodKey is the map key version of connectMethod, with a
// stringified proxy URL (or the empty string) instead of a pointer to
// a URL.
type connectMethodKey struct {
proxy, scheme, addr string
}
func (cm *connectMethod) key() connectMethodKey {
proxyStr := ""
targetAddr := cm.targetAddr
if cm.proxyURL != nil {
proxyStr = cm.proxyURL.String()
if cm.targetScheme == "http" {
targetAddr = ""
}
}
return connectMethodKey{
proxy: proxyStr,
scheme: cm.targetScheme,
addr: targetAddr,
}
}
func (k connectMethodKey) String() string {
// Only used by tests.
return fmt.Sprintf("%s|%s|%s", k.proxy, k.scheme, k.addr)
}
connectMethodKey
は、proxy
, scheme
, addr
という3つの文字列フィールドを持ちます。これらのフィールドはすべて比較可能な型であるため、connectMethodKey
構造体自体も比較可能となり、Goのmap
のキーとして直接使用できるようになります。
connectMethod.key()
メソッドは、もはや文字列を生成するのではなく、connectMethodKey
構造体のインスタンスを返します。これにより、strings.Join
による文字列の結合と、それに伴う新しい文字列の割り当てが不要になります。
Transport
構造体の変更
net/http.Transport
構造体内のアイドルコネクションキャッシュを保持するマップの型定義が変更されました。
// 変更前 (transport.go)
type Transport struct {
idleMu sync.Mutex
idleConn map[string][]*persistConn
idleConnCh map[string]chan *persistConn
// ...
}
// 変更後 (transport.go)
type Transport struct {
idleMu sync.Mutex
idleConn map[connectMethodKey][]*persistConn
idleConnCh map[connectMethodKey]chan *persistConn
// ...
}
idleConn
とidleConnCh
のキーの型がstring
からconnectMethodKey
に変更されました。これにより、マップ操作(make
, アクセス、削除)が新しい構造体キーを使用するようになります。
メモリ割り当てとGCへの影響
この変更の最大のメリットは、メモリ割り当ての削減です。以前は、各HTTPリクエストでコネクションキャッシュのキーとして使用される文字列が動的に生成されていました。この文字列は、proxyStr
, targetScheme
, targetAddr
という3つの文字列を結合して作られていました。
proxyStr
の生成(proxyURL.String()
)strings.Join
による新しい文字列の生成
これらの操作は、それぞれメモリ割り当てを伴います。コミットメッセージにある「3 allocatins and ~100 bytes of garbage per client request」は、これらの文字列生成と結合に関連する割り当てを指していると考えられます。
connectMethodKey
構造体を直接キーとして使用することで、これらの文字列の動的な結合と割り当てが不要になります。構造体自体はスタックに割り当てられるか、ヒープに割り当てられたとしても、そのライフサイクルはマップのエントリと直接結びつくため、一時的な文字列オブジェクトの生成と破棄によるGCの負荷が軽減されます。
パフォーマンスのトレードオフ
コミットメッセージでは、この変更がGCの負荷を軽減する一方で、パフォーマンスが「相殺される」と述べられています。これは、Goの文字列ハッシュが非常に高速であるのに対し、複数の文字列フィールドを持つ構造体のハッシュ計算は、単一の文字列のハッシュよりも時間がかかる可能性があるためです。
Goランタイムは、構造体をマップキーとして使用する場合、その構造体の各フィールドのハッシュ値を計算し、それらを組み合わせて最終的なハッシュ値を生成します。connectMethodKey
には3つの文字列フィールドがあるため、それぞれの文字列のハッシュ計算とそれらの結合が必要になります。これが、単一の結合済み文字列をハッシュするよりも遅くなる原因です。
結果として、GCの負荷軽減によるメリットと、ハッシュ計算のオーバーヘッドによるデメリットが相殺され、全体的なパフォーマンスは劇的に向上するわけではないが、メモリ割り当ての改善という一貫したメリットが得られる、という結論になっています。これは、パフォーマンスの最適化においては常にトレードオフが存在することを示す良い例です。
テストコードの変更
export_test.go
とproxy_test.go
のテストコードも、新しいconnectMethodKey
構造体に対応するように修正されています。
IdleConnKeysForTesting()
: キャッシュキーを文字列として返すために、connectMethodKey
のString()
メソッドを呼び出すように変更。IdleConnCountForTesting()
: 引数として受け取る文字列キーを、connectMethodKey
のString()
メソッドの結果と比較するように変更。TestCacheKeys()
:connectMethod.key()
が返すconnectMethodKey
のString()
メソッドの結果を期待値と比較するように変更。
これらの変更は、内部的なキーの型変更が外部に公開されるテストインターフェースに影響を与えないように、またはテストが新しい内部実装と整合するように行われています。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、以下のファイルと箇所に集中しています。
-
src/pkg/net/http/transport.go
:Transport
構造体内のidleConn
とidleConnCh
マップのキーの型をstring
からconnectMethodKey
に変更。--- a/src/pkg/net/http/transport.go +++ b/src/pkg/net/http/transport.go @@ -41,8 +41,8 @@ const DefaultMaxIdleConnsPerHost = 2 // Transport can also cache connections for future re-use. type Transport struct { idleMu sync.Mutex - idleConn map[string][]*persistConn - idleConnCh map[string]chan *persistConn + idleConn map[connectMethodKey][]*persistConn + idleConnCh map[connectMethodKey]chan *persistConn reqMu sync.Mutex reqConn map[*Request]*persistConn altMu sync.RWMutex
connectMethodKey
構造体の新規定義。--- a/src/pkg/net/http/transport.go +++ b/src/pkg/net/http/transport.go @@ -634,20 +628,20 @@ type connectMethod struct { targetAddr string // Not used if proxy + http targetScheme (4th example in table) } -func (ck *connectMethod) key() string { - return ck.String() // TODO: use a struct type instead -} - -func (ck *connectMethod) String() string { +func (cm *connectMethod) key() connectMethodKey { proxyStr := "" - targetAddr := ck.targetAddr - if ck.proxyURL != nil { - proxyStr = ck.proxyURL.String() - if ck.targetScheme == "http" { + targetAddr := cm.targetAddr + if cm.proxyURL != nil { + proxyStr = cm.proxyURL.String() + if cm.targetScheme == "http" { targetAddr = "" } } - return strings.Join([]string{proxyStr, ck.targetScheme, targetAddr}, "|") + return connectMethodKey{ + proxy: proxyStr, + scheme: cm.targetScheme, + addr: targetAddr, + } } // addr returns the first hop "host:port" to which we need to TCP connect. @@ -668,11 +662,23 @@ func (cm *connectMethod) tlsHost() string { return h } +// connectMethodKey is the map key version of connectMethod, with a +// stringified proxy URL (or the empty string) instead of a pointer to +// a URL. +type connectMethodKey struct { + proxy, scheme, addr string +} + +func (k connectMethodKey) String() string { + // Only used by tests. + return fmt.Sprintf("%s|%s|%s", k.proxy, k.scheme, k.addr) +} + // persistConn wraps a connection, usually a persistent one // (but may be used for non-keep-alive requests as well) type persistConn struct { t *Transport - cacheKey string // its connectMethod.String() + cacheKey connectMethodKey conn net.Conn closed bool // whether conn has been closed br *bufio.Reader // from conn
connectMethod.key()
メソッドの戻り値をstring
からconnectMethodKey
に変更し、内部でconnectMethodKey
を構築して返すように変更。persistConn
構造体のcacheKey
フィールドの型をstring
からconnectMethodKey
に変更。putIdleConn
,getIdleConnCh
,getIdleConn
,getConn
,dialConn
など、connectMethod
を引数にとる関数や、マップキーとしてkey
を使用する箇所で、引数の型やマップの初期化、アクセス方法を新しいconnectMethodKey
に対応するように変更。
-
src/pkg/net/http/export_test.go
:IdleConnKeysForTesting()
関数内で、t.idleConn
マップのキーをイテレートする際に、key.String()
を呼び出して文字列として返すように変更。IdleConnCountForTesting()
関数内で、引数のcacheKey
文字列とt.idleConn
マップのキー(connectMethodKey
型)のString()
メソッドの結果を比較するように変更。
-
src/pkg/net/http/proxy_test.go
:TestCacheKeys()
関数内で、cm.key()
が返すconnectMethodKey
のString()
メソッドの結果を期待値と比較するように変更。
これらの変更により、Transport
のアイドルコネクションキャッシュのキー管理が、文字列ベースから構造体ベースへと完全に移行されました。
コアとなるコードの解説
connectMethodKey
の導入とconnectMethod.key()
の変更
最も重要な変更は、connectMethodKey
構造体の導入と、connectMethod
のkey()
メソッドの振る舞いの変更です。
以前は、connectMethod.key()
はconnectMethod
の情報を結合した文字列を返していました。この文字列は、strings.Join
を使用して生成され、そのたびに新しい文字列オブジェクトがヒープに割り当てられていました。
新しい実装では、connectMethodKey
という構造体が定義され、connectMethod.key()
はこの構造体のインスタンスを返します。
type connectMethodKey struct {
proxy, scheme, addr string
}
func (cm *connectMethod) key() connectMethodKey {
proxyStr := ""
targetAddr := cm.targetAddr
if cm.proxyURL != nil {
proxyStr = cm.proxyURL.String()
if cm.targetScheme == "http" {
targetAddr = ""
}
}
return connectMethodKey{
proxy: proxyStr,
scheme: cm.targetScheme,
addr: targetAddr,
}
}
この変更により、以下のメリットが生まれます。
- メモリ割り当ての削減:
strings.Join
による一時的な文字列の割り当てが不要になります。connectMethodKey
構造体自体は、マップのキーとして使用される際に、そのフィールドが直接マップの内部構造にコピーされるか、あるいはスタックに割り当てられた後、ヒープ上のマップエントリにコピーされるため、GCの対象となる一時オブジェクトの数が減ります。 - 型安全性と可読性: コネクションの識別情報が単一の文字列ではなく、意味のあるフィールドを持つ構造体として表現されるため、コードの可読性と型安全性が向上します。
Transport
マップの型変更
Transport
構造体内のidleConn
とidleConnCh
という2つのマップのキーの型がstring
からconnectMethodKey
に変更されました。
idleConn map[connectMethodKey][]*persistConn
idleConnCh map[connectMethodKey]chan *persistConn
これにより、これらのマップへのアクセス(挿入、検索、削除)は、connectMethodKey
型の値を使用して行われるようになります。Goランタイムは、connectMethodKey
構造体の各フィールド(proxy
, scheme
, addr
)のハッシュ値を計算し、それらを組み合わせてマップのキーとして使用します。
persistConn.cacheKey
の型変更
persistConn
構造体は、個々の永続的なコネクションをラップするものです。この構造体内のcacheKey
フィールドも、コネクションがどのキャッシュキーに対応するかを識別するために使用されます。このフィールドの型もstring
からconnectMethodKey
に変更されました。
type persistConn struct {
t *Transport
cacheKey connectMethodKey // its connectMethod.String()
conn net.Conn
// ...
}
これにより、persistConn
が保持するキー情報も、文字列ではなく構造体として管理されるようになり、一貫性が保たれます。
関数シグネチャの変更と内部ロジックの調整
putIdleConn
, getIdleConnCh
, getIdleConn
, getConn
, dialConn
といった、アイドルコネクションの管理に関わる関数群のシグネチャも変更されました。これらの関数は以前は*connectMethod
ポインタを引数にとり、内部でcm.key()
を呼び出して文字列キーを取得していました。変更後は、connectMethod
の値を直接引数にとるようになり、内部でcm.key()
を呼び出してconnectMethodKey
を取得します。
例えば、getIdleConn
関数の変更は以下のようになります。
// 変更前
func (t *Transport) getIdleConn(cm *connectMethod) (pconn *persistConn) {
key := cm.key() // ここで文字列キーを生成
// ...
}
// 変更後
func (t *Transport) getIdleConn(cm connectMethod) (pconn *persistConn) {
key := cm.key() // ここでconnectMethodKey構造体を生成
// ...
}
これらの変更は、Transport
の内部で一貫してconnectMethodKey
構造体を使用するようにするためのものです。全体として、このコミットはnet/http
パッケージの内部実装を、Go言語の進化(構造体をマップキーとして使用できるようになったこと)に合わせて近代化し、メモリ効率を向上させることを目的としています。
関連リンク
- Go CL 58260043: https://golang.org/cl/58260043
参考にした情報源リンク
- Go言語の
map
のキーの要件に関する公式ドキュメントやブログ記事 - Go言語のガベージコレクションに関する情報源
net/http
パッケージのドキュメントとソースコード- Go言語のハッシュ関数に関する情報源(特に文字列ハッシュの最適化について)