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

[インデックス 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_PROXYNO_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_PROXYNO_PROXY を参照してプロキシ設定を自動的に適用する機能を持っています。

2. 環境変数と os.Getenv

環境変数は、オペレーティングシステムが提供するキーと値のペアで、プロセス間で情報を共有したり、アプリケーションの動作を設定したりするために使用されます。Go言語では、os パッケージの os.Getenv(key string) string 関数を使って環境変数の値を取得します。

3. sync.Once

sync.Once はGo言語の sync パッケージで提供されるユーティリティで、特定の関数が一度だけ実行されることを保証します。これは、初期化処理やリソースのセットアップなど、一度だけ実行すればよい処理に非常に役立ちます。

sync.OnceDo(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_PROXYhttp_proxy の両方をチェックする)を行う場合、複数回の os.Getenv 呼び出しが必要となり、そのコストが累積されることでパフォーマンスに影響を与える可能性があります。

技術的詳細

このコミットは、net/http パッケージ内のプロキシ関連の環境変数ルックアップを最適化するために、sync.Once を利用したキャッシュメカニズムを導入しています。

  1. envOnce 構造体の導入: envOnce という新しい構造体が定義されました。

    type envOnce struct {
        names []string // 検索する環境変数名のリスト (例: {"HTTP_PROXY", "http_proxy"})
        once  sync.Once // 一度だけ初期化を実行するためのsync.Onceインスタンス
        val   string   // キャッシュされた環境変数の値
    }
    

    この構造体は、複数の環境変数名(大文字・小文字のバリエーションなど)を試行し、最初に見つかった非空の値をキャッシュするために使用されます。

  2. 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 呼び出しが回避されます。

  3. 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_PROXYhttp_proxy)を順番に試行し、最初に見つかった非空の値を e.val に格納します。

  4. グローバルな envOnce インスタンス: httpProxyEnvnoProxyEnv という2つのグローバルな envOnce インスタンスが定義され、それぞれ HTTP_PROXY/http_proxyNO_PROXY/no_proxy の環境変数をキャッシュするために使用されます。

    var (
        httpProxyEnv = &envOnce{
            names: []string{"HTTP_PROXY", "http_proxy"},
        }
        noProxyEnv = &envOnce{
            names: []string{"NO_PROXY", "no_proxy"},
        }
    )
    
  5. 既存関数の変更:

    • ProxyFromEnvironment 関数は、getenvEitherCase("HTTP_PROXY") の代わりに httpProxyEnv.Get() を使用するように変更されました。
    • useProxy 関数は、getenvEitherCase("NO_PROXY") の代わりに noProxyEnv.Get() を使用するように変更されました。
    • getenvEitherCase 関数は不要になったため削除されました。
  6. テストサポートの追加:

    • export_test.goResetCachedEnvironment() 関数が追加されました。この関数は、httpProxyEnv.reset()noProxyEnv.reset() を呼び出します。
    • envOnce.reset() メソッドは、テストのために sync.Once インスタンスをリセットし、キャッシュされた値をクリアします。これにより、テストケース間で環境変数のキャッシュが影響し合わないようにできます。
    • transport_test.goTestProxyFromEnvironment では、各テストケースの開始時に ResetCachedEnvironment() が呼び出されるようになりました。

この変更により、HTTP_PROXYNO_PROXY の環境変数の値は、アプリケーションの実行中に一度だけ os.Getenv を通じて取得され、それ以降はキャッシュされた値が使用されるため、特にWindows環境でのパフォーマンスが大幅に改善されます。

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

src/pkg/net/http/transport.go

  • getenvEitherCase 関数が削除されました。
  • envOnce 構造体、envOnce.Get() メソッド、envOnce.init() メソッド、envOnce.reset() メソッドが追加されました。
  • httpProxyEnvnoProxyEnv というグローバルな 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`)