[インデックス 18269] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http
パッケージにおいて、環境変数のルックアップ処理を最適化するものです。特にWindows環境での os.Getenv
の呼び出しが高コストであるという問題に対処するため、プロキシ関連の環境変数(HTTP_PROXY
および NO_PROXY
)の値を一度だけ取得し、キャッシュするメカニズムを導入しています。これにより、アプリケーションの起動時やプロキシ設定の評価時に発生するパフォーマンスオーバーヘッドを削減します。
コミット
commit 4deead7645fbb7302e0e86594445268085ded330
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Thu Jan 16 10:25:45 2014 -0800
net/http: cache transport environment lookup
Apparently this is expensive on Windows.
Fixes #7020
R=golang-codereviews, alex.brainman, mattn.jp, dvyukov
CC=golang-codereviews
https://golang.org/cl/52840043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4deead7645fbb7302e0e86594445268085ded330
元コミット内容
net/http: cache transport environment lookup
Apparently this is expensive on Windows.
Fixes #7020
変更の背景
このコミットの主な背景は、GoプログラムがWindows上で環境変数を頻繁に参照する際のパフォーマンス問題です。特に net/http
パッケージがHTTPプロキシ設定のために HTTP_PROXY
や NO_PROXY
といった環境変数を参照する際、これらのルックアップが繰り返されると、Windowsの os.Getenv
の実装が原因で顕著な遅延が発生することが報告されていました。
具体的には、Go issue #7020 (https://github.com/golang/go/issues/7020) でこの問題が議論されており、Windows環境での os.Getenv
の呼び出しが、他のOSと比較して非常に遅いことが指摘されていました。これは、Windowsの環境変数取得APIが、プロセス環境ブロックのロックや文字列のコピーなど、比較的重い処理を伴うためと考えられます。
net/http
パッケージの Transport
は、HTTPリクエストごとにプロキシ設定を評価する可能性があり、そのたびに環境変数を参照すると、特に多数のHTTPリクエストを処理するアプリケーションにおいて、パフォーマンスのボトルネックとなることが懸念されました。このコミットは、このパフォーマンス問題を解決し、Windows環境での net/http
の効率を向上させることを目的としています。
前提知識の解説
1. net/http
パッケージと Transport
Go言語の net/http
パッケージは、HTTPクライアントとサーバーの実装を提供します。クライアント側では、http.Client
がHTTPリクエストの送信を担当し、その内部で http.Transport
が実際のネットワーク接続、プロキシ処理、TLSハンドシェイクなどを管理します。
http.Transport
は、HTTPリクエストを送信する際に、環境変数 HTTP_PROXY
や NO_PROXY
を参照してプロキシ設定を自動的に適用する機能を持っています。
2. 環境変数と os.Getenv
環境変数は、オペレーティングシステムが提供するキーと値のペアで、プロセス間で情報を共有したり、アプリケーションの動作を設定したりするために使用されます。Go言語では、os
パッケージの os.Getenv(key string) string
関数を使って環境変数の値を取得します。
3. sync.Once
sync.Once
はGo言語の sync
パッケージで提供されるユーティリティで、特定の関数が一度だけ実行されることを保証します。これは、初期化処理やリソースのセットアップなど、一度だけ実行すればよい処理に非常に役立ちます。
sync.Once
の Do(f func())
メソッドは、f
が一度だけ実行されるようにします。複数のゴルーチンが同時に Do
を呼び出しても、f
は一度だけ実行され、他のゴルーチンは f
の完了を待ちます。
4. プロキシ設定の環境変数 (HTTP_PROXY
, NO_PROXY
)
HTTP_PROXY
: HTTPリクエストに使用するプロキシサーバーのURLを指定します。例えば、http://proxy.example.com:8080
のように設定されます。NO_PROXY
: プロキシを使用しないホスト名のリストを指定します。カンマ区切りで複数のホスト名を指定できます。例えば、localhost,127.0.0.1,.example.com
のように設定されます。この環境変数は、特定のドメインやIPアドレスへのアクセス時にプロキシをバイパスするために使用されます。
5. Windowsにおける環境変数取得のパフォーマンス特性
Windowsオペレーティングシステムでは、環境変数の取得は、Unix系OSと比較して相対的に高コストになることがあります。これは、Windowsの内部的な環境変数管理メカニズムが、プロセス環境ブロックのロックや文字列のエンコーディング変換、メモリコピーなどを伴うためです。特に、大文字・小文字を区別しない検索(例: HTTP_PROXY
と http_proxy
の両方をチェックする)を行う場合、複数回の os.Getenv
呼び出しが必要となり、そのコストが累積されることでパフォーマンスに影響を与える可能性があります。
技術的詳細
このコミットは、net/http
パッケージ内のプロキシ関連の環境変数ルックアップを最適化するために、sync.Once
を利用したキャッシュメカニズムを導入しています。
-
envOnce
構造体の導入:envOnce
という新しい構造体が定義されました。type envOnce struct { names []string // 検索する環境変数名のリスト (例: {"HTTP_PROXY", "http_proxy"}) once sync.Once // 一度だけ初期化を実行するためのsync.Onceインスタンス val string // キャッシュされた環境変数の値 }
この構造体は、複数の環境変数名(大文字・小文字のバリエーションなど)を試行し、最初に見つかった非空の値をキャッシュするために使用されます。
-
envOnce.Get()
メソッド: このメソッドは、envOnce
インスタンスのval
フィールドにキャッシュされた環境変数の値を返します。func (e *envOnce) Get() string { e.once.Do(e.init) // init() 関数を一度だけ実行することを保証 return e.val }
e.once.Do(e.init)
を呼び出すことで、e.init()
関数はenvOnce
インスタンスのライフタイム中に一度だけ実行されることが保証されます。これにより、環境変数のルックアップ処理が初回アクセス時のみ行われ、それ以降はキャッシュされた値が返されるため、繰り返しの高コストなos.Getenv
呼び出しが回避されます。 -
envOnce.init()
メソッド: このメソッドは、実際に環境変数をルックアップし、envOnce
インスタンスのval
フィールドに値を設定します。func (e *envOnce) init() { for _, n := range e.names { // 定義された環境変数名を順に試す e.val = os.Getenv(n) // 環境変数の値を取得 if e.val != "" { return // 値が見つかったら終了 } } }
init()
はnames
スライスに指定された環境変数名(例:HTTP_PROXY
とhttp_proxy
)を順番に試行し、最初に見つかった非空の値をe.val
に格納します。 -
グローバルな
envOnce
インスタンス:httpProxyEnv
とnoProxyEnv
という2つのグローバルなenvOnce
インスタンスが定義され、それぞれHTTP_PROXY
/http_proxy
とNO_PROXY
/no_proxy
の環境変数をキャッシュするために使用されます。var ( httpProxyEnv = &envOnce{ names: []string{"HTTP_PROXY", "http_proxy"}, } noProxyEnv = &envOnce{ names: []string{"NO_PROXY", "no_proxy"}, } )
-
既存関数の変更:
ProxyFromEnvironment
関数は、getenvEitherCase("HTTP_PROXY")
の代わりにhttpProxyEnv.Get()
を使用するように変更されました。useProxy
関数は、getenvEitherCase("NO_PROXY")
の代わりにnoProxyEnv.Get()
を使用するように変更されました。getenvEitherCase
関数は不要になったため削除されました。
-
テストサポートの追加:
export_test.go
にResetCachedEnvironment()
関数が追加されました。この関数は、httpProxyEnv.reset()
とnoProxyEnv.reset()
を呼び出します。envOnce.reset()
メソッドは、テストのためにsync.Once
インスタンスをリセットし、キャッシュされた値をクリアします。これにより、テストケース間で環境変数のキャッシュが影響し合わないようにできます。transport_test.go
のTestProxyFromEnvironment
では、各テストケースの開始時にResetCachedEnvironment()
が呼び出されるようになりました。
この変更により、HTTP_PROXY
や NO_PROXY
の環境変数の値は、アプリケーションの実行中に一度だけ os.Getenv
を通じて取得され、それ以降はキャッシュされた値が使用されるため、特にWindows環境でのパフォーマンスが大幅に改善されます。
コアとなるコードの変更箇所
src/pkg/net/http/transport.go
getenvEitherCase
関数が削除されました。envOnce
構造体、envOnce.Get()
メソッド、envOnce.init()
メソッド、envOnce.reset()
メソッドが追加されました。httpProxyEnv
とnoProxyEnv
というグローバルなenvOnce
変数が追加されました。ProxyFromEnvironment
関数内でgetenvEitherCase("HTTP_PROXY")
の呼び出しがhttpProxyEnv.Get()
に変更されました。useProxy
関数内でgetenvEitherCase("NO_PROXY")
の呼び出しがnoProxyEnv.Get()
に変更されました。
--- a/src/pkg/net/http/transport.go
+++ b/src/pkg/net/http/transport.go
@@ -99,7 +99,7 @@ type Transport struct {
// A nil URL and nil error are returned if no proxy is defined in the
// environment, or a proxy should not be used for the given request.
func ProxyFromEnvironment(req *Request) (*url.URL, error) {
- proxy := getenvEitherCase("HTTP_PROXY")
+ proxy := httpProxyEnv.Get()
if proxy == "" {
return nil, nil
}
@@ -243,11 +243,42 @@ func (t *Transport) CancelRequest(req *Request) {
// Private implementation past this point.
//
-func getenvEitherCase(k string) string {
- if v := os.Getenv(strings.ToUpper(k)); v != "" {
- return v
+var (
+ httpProxyEnv = &envOnce{
+ names: []string{"HTTP_PROXY", "http_proxy"},
}
- return os.Getenv(strings.ToLower(k))
+ noProxyEnv = &envOnce{
+ names: []string{"NO_PROXY", "no_proxy"},
+ }
+)
+
+// envOnce looks up an environment variable (optionally by multiple
+// names) once. It mitigates expensive lookups on some platforms
+// (e.g. Windows).
+type envOnce struct {
+ names []string
+ once sync.Once
+ val string
+}
+
+func (e *envOnce) Get() string {
+ e.once.Do(e.init)
+ return e.val
+}
+
+func (e *envOnce) init() {
+ for _, n := range e.names {
+ e.val = os.Getenv(n)
+ if e.val != "" {
+ return
+ }
+ }
+}
+
+// reset is used by tests
+func (e *envOnce) reset() {
+ e.once = sync.Once{}
+ e.val = ""
}
func (t *Transport) connectMethodForRequest(treq *transportRequest) (*connectMethod, error) {
@@ -550,7 +581,7 @@ func useProxy(addr string) bool {
}
}
- no_proxy := getenvEitherCase("NO_PROXY")
+ no_proxy := noProxyEnv.Get()
if no_proxy == "*" {
return false
}
src/pkg/net/http/export_test.go
- テスト用に
ResetCachedEnvironment()
関数が追加されました。
--- a/src/pkg/net/http/export_test.go
+++ b/src/pkg/net/http/export_test.go
@@ -63,4 +63,9 @@ func NewTestTimeoutHandler(handler Handler, ch <-chan time.Time) Handler {
return &timeoutHandler{handler, f, ""}\n }\n \n+func ResetCachedEnvironment() {\n+\thttpProxyEnv.reset()\n+\tnoProxyEnv.reset()\n+}\n+\n var DefaultUserAgent = defaultUserAgent
src/pkg/net/http/transport_test.go
TestProxyFromEnvironment
関数内でResetCachedEnvironment()
が呼び出されるようになりました。
--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -1566,6 +1566,7 @@ func TestProxyFromEnvironment(t *testing.T) {
for _, tt := range proxyFromEnvTests {
\tos.Setenv("HTTP_PROXY", tt.env)\n \tos.Setenv("NO_PROXY", tt.noenv)\n+\t\tResetCachedEnvironment()\n \t\treqURL := tt.req\n \t\tif reqURL == "" {\n \t\t\treqURL = "http://example.com"\n```
## コアとなるコードの解説
このコミットの核心は、`envOnce` 構造体と `sync.Once` を利用して、環境変数のルックアップを一度だけ実行し、その結果をキャッシュする点にあります。
1. **`envOnce` 構造体**:
`names` フィールドには、検索対象となる環境変数名のリスト(例: `HTTP_PROXY` と `http_proxy`)が格納されます。これは、OSによっては環境変数名の大文字・小文字が区別されない場合があるため、両方のケースを考慮するためです。
`once` フィールドは `sync.Once` 型で、`init()` メソッドが一度だけ実行されることを保証します。
`val` フィールドは、`init()` メソッドによって取得された環境変数の値をキャッシュするために使用されます。
2. **`envOnce.Get()` メソッド**:
このメソッドが呼び出されると、まず `e.once.Do(e.init)` が実行されます。`sync.Once` の性質により、`e.init()` はこの `envOnce` インスタンスのライフタイム中に一度だけ実行されます。
`e.init()` が完了すると、`e.val` には環境変数の値が格納されているため、`Get()` はそのキャッシュされた値を返します。これにより、2回目以降の `Get()` 呼び出しでは、高コストな `os.Getenv` の呼び出しがスキップされ、高速にキャッシュされた値が返されます。
3. **`envOnce.init()` メソッド**:
このメソッドは、`envOnce.Get()` から `sync.Once` を介して呼び出されます。
`for` ループで `e.names` に含まれる各環境変数名を順番に `os.Getenv` で検索します。
最初に空でない値が見つかった場合、その値が `e.val` に格納され、関数は即座に `return` します。これにより、複数の環境変数名が指定されていても、最初に見つかった有効な値のみが使用されます。
4. **`httpProxyEnv` と `noProxyEnv`**:
これらは `net/http` パッケージ内でグローバルに定義された `envOnce` インスタンスです。
`httpProxyEnv` は `HTTP_PROXY` と `http_proxy` を、`noProxyEnv` は `NO_PROXY` と `no_proxy` をそれぞれキャッシュします。
`net/http` の内部でプロキシ設定が必要になった際に、これらの `envOnce` インスタンスの `Get()` メソッドが呼び出され、効率的に環境変数の値が取得されます。
5. **テストにおける `ResetCachedEnvironment()`**:
`sync.Once` は一度実行されるとリセットされないため、テストにおいて異なる環境変数設定を試す際に問題となる可能性があります。`envOnce.reset()` メソッドは、`e.once = sync.Once{}` とすることで、`sync.Once` インスタンスを新しいものに置き換え、`e.val` をクリアします。これにより、次の `Get()` 呼び出しで `init()` が再度実行され、新しい環境変数の値が取得されるようになります。
`TestProxyFromEnvironment` で `ResetCachedEnvironment()` を呼び出すことで、各テストケースが独立して環境変数を設定し、その効果を検証できるようになっています。
この設計により、Goの `net/http` パッケージは、特にWindows環境での環境変数ルックアップのパフォーマンス問題を効果的に解決し、より効率的なプロキシ処理を実現しています。
## 関連リンク
* Go issue #7020: `os.Getenv` is slow on Windows: [https://github.com/golang/go/issues/7020](https://github.com/golang/go/issues/7020)
* Go CL 52840043: `net/http`: cache transport environment lookup: [https://golang.org/cl/52840043](https://golang.org/cl/52840043)
## 参考にした情報源リンク
* Go issue #7020 (上記と同じ)
* Go CL 52840043 (上記と同じ)
* Go言語の `sync.Once` ドキュメント: [https://pkg.go.dev/sync#Once](https://pkg.go.dev/sync#Once)
* Go言語の `os.Getenv` ドキュメント: [https://pkg.go.dev/os#Getenv](https://pkg.go.dev/os#Getenv)
* HTTPプロキシ環境変数に関する一般的な情報 (例: `HTTP_PROXY`, `NO_PROXY`)