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

[インデックス 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
	// ...
}

idleConnidleConnChのキーの型が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.goproxy_test.goのテストコードも、新しいconnectMethodKey構造体に対応するように修正されています。

  • IdleConnKeysForTesting(): キャッシュキーを文字列として返すために、connectMethodKeyString()メソッドを呼び出すように変更。
  • IdleConnCountForTesting(): 引数として受け取る文字列キーを、connectMethodKeyString()メソッドの結果と比較するように変更。
  • TestCacheKeys(): connectMethod.key()が返すconnectMethodKeyString()メソッドの結果を期待値と比較するように変更。

これらの変更は、内部的なキーの型変更が外部に公開されるテストインターフェースに影響を与えないように、またはテストが新しい内部実装と整合するように行われています。

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

このコミットにおける主要なコード変更は、以下のファイルと箇所に集中しています。

  1. src/pkg/net/http/transport.go:

    • Transport構造体内のidleConnidleConnChマップのキーの型を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に対応するように変更。
  2. src/pkg/net/http/export_test.go:

    • IdleConnKeysForTesting()関数内で、t.idleConnマップのキーをイテレートする際に、key.String()を呼び出して文字列として返すように変更。
    • IdleConnCountForTesting()関数内で、引数のcacheKey文字列とt.idleConnマップのキー(connectMethodKey型)のString()メソッドの結果を比較するように変更。
  3. src/pkg/net/http/proxy_test.go:

    • TestCacheKeys()関数内で、cm.key()が返すconnectMethodKeyString()メソッドの結果を期待値と比較するように変更。

これらの変更により、Transportのアイドルコネクションキャッシュのキー管理が、文字列ベースから構造体ベースへと完全に移行されました。

コアとなるコードの解説

connectMethodKeyの導入とconnectMethod.key()の変更

最も重要な変更は、connectMethodKey構造体の導入と、connectMethodkey()メソッドの振る舞いの変更です。

以前は、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構造体内のidleConnidleConnChという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言語のmapのキーの要件に関する公式ドキュメントやブログ記事
  • Go言語のガベージコレクションに関する情報源
  • net/httpパッケージのドキュメントとソースコード
  • Go言語のハッシュ関数に関する情報源(特に文字列ハッシュの最適化について)