[インデックス 18294] ファイルの概要
このコミットは、Go言語の標準ライブラリである expvar
パッケージに対する変更です。expvar
パッケージは、実行中のGoプログラムの内部状態をHTTP経由で公開するためのシンプルなメカニズムを提供します。これにより、プログラムのメトリクスやデバッグ情報を簡単に監視できます。具体的には、expvar
は Var
インターフェースを実装する様々な型の変数を公開し、それらの値をJSON形式で /debug/vars
エンドポイントから取得できるようにします。
コミット
このコミットは、expvar
パッケージにおける2つの主要な問題を解決します。一つは、Map
型の変数や公開されているトップレベルの変数の出力順序が不定であることによる視認性の問題、もう一つは、Map
内の Int
や Float
型の値を同時に更新しようとした際に発生する可能性のある競合状態(レースコンディション)です。このコミットにより、出力されるマップのキーがソートされるようになり、また、マップ内の数値型変数の更新における競合状態が修正されました。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cfb9cf0f0679b07583b8667487c516b1f77f2292
元コミット内容
expvar: sort maps, fix race
It's pretty distracting to use expvar with the output of both
the top-level map and map values jumping around randomly.
Also fixes a potential race where multiple clients trying to
increment a map int or float key at the same time could lose
updates.
R=golang-codereviews, couchmoney
CC=golang-codereviews
https://golang.org/cl/54320043
変更の背景
このコミットが行われた背景には、expvar
パッケージの利用における実用上の課題と、潜在的なバグの修正があります。
-
マップの出力順序の不定性: Go言語の
map
は、その設計上、要素のイテレーション順序が保証されません。これは、マップがハッシュテーブルとして実装されており、要素の格納順序が内部のハッシュ関数やメモリ配置に依存するためです。expvar
パッケージが公開するMap
型の変数や、Publish
関数で登録されるトップレベルの変数 (vars
) は、内部的にmap[string]Var
を使用しています。このため、/debug/vars
エンドポイントにアクセスするたびに、JSON出力におけるキーの順序がランダムに変わってしまうという問題がありました。これは、特に監視ツールや人間がデバッグ情報を読み取る際に、視認性を著しく低下させ、比較や分析を困難にしていました。ユーザーは安定した出力順序を求めていました。 -
マップ内の数値型変数更新における競合状態:
expvar
のMap
型には、Add
(Int用) およびAddFloat
(Float用) メソッドがあります。これらのメソッドは、マップ内の既存のInt
またはFloat
変数の値をアトミックに増減させることを目的としています。しかし、元の実装では、これらのメソッドがマップに新しいキーを追加する際に、sync.RWMutex
のロックの取得と解放のタイミングに問題がありました。具体的には、RLock
(読み取りロック) を取得した状態でキーの存在チェックを行い、キーが存在しない場合にRUnlock
してからLock
(書き込みロック) を取得して新しい変数をマップに追加していました。この短い期間に、別のゴルーチンが同じキーに対してAdd
やAddFloat
を呼び出すと、両方のゴルーチンが新しい変数を生成し、最終的に一方の更新が失われる(上書きされる)可能性がありました。これは典型的な競合状態であり、データの整合性を損なう深刻なバグでした。
これらの問題に対処するため、マップのキーをソートして出力する機能と、数値型変数の更新における競合状態を解消する修正が導入されました。
前提知識の解説
1. expvar
パッケージ
expvar
パッケージは、Goプログラムの内部状態をHTTP経由で公開するためのシンプルなメカニズムを提供します。
Var
インターフェース:expvar
パッケージで公開できるすべての変数は、String() string
メソッドを持つVar
インターフェースを実装する必要があります。これにより、変数の文字列表現を取得できます。Int
,Float
,String
,Map
,Func
:expvar
パッケージは、int64
、float64
、string
、map[string]Var
、およびカスタム関数を公開するための具体的な型 (Int
,Float
,String
,Map
,Func
) を提供します。Publish(name string, v Var)
: この関数は、指定された名前でVar
インターフェースを実装する変数を公開します。公開された変数は、デフォルトで/debug/vars
HTTPエンドポイントからJSON形式でアクセスできるようになります。/debug/vars
:expvar
パッケージをインポートすると、自動的に/debug/vars
というHTTPエンドポイントが登録されます。このエンドポイントにアクセスすると、公開されているすべての変数の現在の値がJSON形式で返されます。
2. Go言語における map
のイテレーション順序
Go言語の map
は、キーと値のペアを格納するための組み込みのデータ構造です。重要な特性として、map
のイテレーション順序は保証されません。つまり、for key, value := range myMap
のようにマップをループするたびに、キーと値が返される順序は異なる可能性があります。これは、マップがハッシュテーブルとして実装されており、要素の格納順序が内部のハッシュ関数やメモリ配置に依存するためです。この不定性は、意図的な設計であり、開発者が特定の順序に依存しないように促すとともに、マップの実装を最適化するための柔軟性を提供します。しかし、expvar
のように外部に情報を公開する際には、この不定性が問題となることがあります。
3. 競合状態 (Race Condition) と sync.RWMutex
- 競合状態: 複数のゴルーチンが共有リソース(メモリ上の変数など)に同時にアクセスし、少なくとも1つのゴルーチンがそのリソースを変更する場合に発生する可能性があります。アクセスするタイミングによって結果が非決定的に変わるため、プログラムの動作が予測不能になり、バグの原因となります。
sync.RWMutex
: Go言語のsync
パッケージが提供する読み書きミューテックスです。Lock()
/Unlock()
: 排他ロック。一度に1つのゴルーチンのみがリソースにアクセスできます。書き込み操作に適しています。RLock()
/RUnlock()
: 読み取りロック。複数のゴルーチンが同時にリソースを読み取ることができますが、書き込み操作はブロックされます。読み取り操作が多い場合にパフォーマンスを向上させます。RWMutex
は、読み取り操作が頻繁に行われ、書き込み操作が比較的少ない場合に特に有効です。読み取りロック中に書き込みロックを要求すると、すべての読み取りロックが解放されるまで書き込みロックは待機します。
このコミットでは、Map
の Add
および AddFloat
メソッドにおける競合状態を解決するために、sync.RWMutex
の適切な使用が鍵となります。
技術的詳細
このコミットの技術的詳細は、主に以下の2点に集約されます。
-
マップのキーのソート:
expvar.Map
型にkeys []string
フィールドが追加されました。これは、マップm
に格納されているキーのソート済みリストを保持するためのものです。- トップレベルの公開変数
vars
(これもmap[string]Var
型) についても同様に、varKeys []string
が追加されました。 - 新しいキーがマップに追加されるたびに、
updateKeys()
メソッド(Map
型の場合)またはPublish
関数(トップレベルの変数vars
の場合)内で、sort.Strings()
を使用してkeys
またはvarKeys
スライスがソートされます。これにより、常にソートされたキーのリストが利用可能になります。 Map.String()
メソッドやMap.Do()
メソッド、およびトップレベルのDo()
関数は、マップm
を直接イテレートする代わりに、このソートされたkeys
またはvarKeys
スライスを使用してキーを順番に処理するように変更されました。これにより、出力されるJSONの順序が常に安定します。
-
競合状態の修正:
Map.Add()
およびMap.AddFloat()
メソッドにおける競合状態は、主に新しいInt
またはFloat
変数をマップに追加するロジックにありました。- 元のコードでは、まず読み取りロック (
RLock
) を取得してキーの存在を確認し、存在しない場合に読み取りロックを解放 (RUnlock
) してから書き込みロック (Lock
) を取得して新しい変数を追加していました。このRUnlock()
とLock()
の間の短い期間に、別のゴルーチンが同じキーに対して操作を行うと、競合が発生しました。 - 修正後のコードでは、
RLock
でキーが存在しないことが確認された場合、すぐにRUnlock
するのではなく、書き込みロック (Lock
) を取得した状態で再度キーの存在を確認するように変更されました。この「二重チェックロック (Double-Checked Locking)」パターンにより、最初のチェックでキーが存在しないと判断された後、書き込みロックを取得した安全なコンテキスト内で最終的な確認を行うことで、複数のゴルーチンが同時に新しい変数を生成して上書きし合うことを防ぎます。もし書き込みロックを取得した時点で既に別のゴルーチンが変数を追加していれば、その既存の変数を使用し、そうでなければ新しい変数を安全に追加します。 - 新しい変数が追加された際には、忘れずに
updateKeys()
を呼び出して、ソート済みキーリストを更新します。
これらの変更により、expvar
パッケージはより予測可能で堅牢な動作を提供するようになりました。
コアとなるコードの変更箇所
src/pkg/expvar/expvar.go
-
Int
,Float
,String
型のフィールド順序変更:Int
struct:i int64
とmu sync.RWMutex
の順序がmu sync.RWMutex
とi int64
に変更。Float
struct:f float64
とmu sync.RWMutex
の順序がmu sync.RWMutex
とf float64
に変更。String
struct:s string
とmu sync.RWMutex
の順序がmu sync.RWMutex
とs string
に変更。- 解説: これは機能的な変更ではなく、慣習的な変更です。ミューテックスを構造体の先頭に置くことで、アライメントや可読性が向上する場合があります。
-
Map
型へのkeys
フィールド追加:// Map is a string-to-Var map variable that satisfies the Var interface. type Map struct { mu sync.RWMutex m map[string]Var keys []string // sorted }
- 解説: マップのキーをソートして保持するためのスライスが追加されました。
-
Map.String()
メソッドの変更:--- a/src/pkg/expvar/expvar.go +++ b/src/pkg/expvar/expvar.go @@ -106,13 +108,13 @@ func (v *Map) String() string { var b bytes.Buffer fmt.Fprintf(&b, "{") first := true - for key, val := range v.m { + v.Do(func(kv KeyValue) { if !first { fmt.Fprintf(&b, ", ") } - fmt.Fprintf(&b, "\"%s\": %v", key, val) + fmt.Fprintf(&b, "\"%s\": %v", kv.Key, kv.Value) first = false - } + }) fmt.Fprintf(&b, "}") return b.String() }
- 解説:
v.m
を直接イテレートする代わりに、v.Do()
メソッド(後述)を呼び出すように変更されました。これにより、ソートされた順序でキーと値が処理されるようになります。
- 解説:
-
Map.updateKeys()
メソッドの追加:// updateKeys updates the sorted list of keys in v.keys. // must be called with v.mu held. func (v *Map) updateKeys() { if len(v.m) == len(v.keys) { // No new key. return } v.keys = v.keys[:0] for k := range v.m { v.keys = append(v.keys, k) } sort.Strings(v.keys) }
- 解説:
Map
に新しいキーが追加された際に、v.keys
スライスを更新し、ソートするためのヘルパー関数です。v.mu
(書き込みロック) が保持されている状態で呼び出されることを前提としています。
- 解説:
-
Map.Set()
メソッドでのupdateKeys()
呼び出し:--- a/src/pkg/expvar/expvar.go +++ b/src/pkg/expvar/expvar.go @@ -129,6 +148,7 @@ func (v *Map) Set(key string, av Var) { v.mu.Lock() defer v.mu.Unlock() v.m[key] = av + v.updateKeys() }
- 解説:
Set
メソッドで新しいキーが追加されたり、既存のキーの値が更新されたりする際に、updateKeys()
が呼び出され、キーリストがソートされます。
- 解説:
-
Map.Add()
およびMap.AddFloat()
メソッドにおける競合状態の修正とupdateKeys()
呼び出し:--- a/src/pkg/expvar/expvar.go +++ b/src/pkg/expvar/expvar.go @@ -139,9 +158,11 @@ func (v *Map) Add(key string, delta int64) { if !ok { // check again under the write lock v.mu.Lock() - if _, ok = v.m[key]; !ok { + av, ok = v.m[key] + if !ok { av = new(Int) v.m[key] = av + v.updateKeys() } v.mu.Unlock() } @@ -162,9 +181,11 @@ func (v *Map) AddFloat(key string, delta float64) { if !ok { // check again under the write lock v.mu.Lock() - if _, ok = v.m[key]; !ok { + av, ok = v.m[key] + if !ok { av = new(Float) v.m[key] = av + v.updateKeys() } v.mu.Unlock() }
- 解説:
RLock
でキーが存在しない場合に、Lock
を取得した後に再度キーの存在を確認する「二重チェックロック」パターンが導入されました。これにより、新しいInt
またはFloat
変数をマップに追加する際の競合状態が解消されます。また、新しい変数が追加された際にupdateKeys()
が呼び出され、キーリストが更新されます。
- 解説:
-
Map.Do()
メソッドの変更:--- a/src/pkg/expvar/expvar.go +++ b/src/pkg/expvar/expvar.go @@ -181,15 +202,15 @@ func (v *Map) Do(f func(KeyValue)) { func (v *Map) Do(f func(KeyValue)) { v.mu.RLock() defer v.mu.RUnlock() - for k, v := range v.m { - f(KeyValue{k, v}) + for _, k := range v.keys { + f(KeyValue{k, v.m[k]}) } }
- 解説:
v.m
を直接イテレートする代わりに、ソートされたv.keys
スライスをイテレートするように変更されました。これにより、Do
メソッドを介してマップの要素を処理する際に、常にソートされた順序が保証されます。
- 解説:
-
トップレベルの公開変数
vars
のvarKeys
フィールド追加:--- a/src/pkg/expvar/expvar.go +++ b/src/pkg/expvar/expvar.go @@ -215,8 +236,9 @@ func (f Func) String() string { // All published variables. var ( - mutex sync.RWMutex - vars map[string]Var = make(map[string]Var) + mutex sync.RWMutex + vars = make(map[string]Var) + varKeys []string // sorted )
- 解説: トップレベルで公開される変数のキーをソートして保持するためのスライスが追加されました。
-
Publish()
関数でのvarKeys
更新とソート:--- a/src/pkg/expvar/expvar.go +++ b/src/pkg/expvar/expvar.go @@ -229,6 +251,8 @@ func Publish(name string, v Var) { log.Panicln("Reuse of exported var name:", name) } vars[name] = v + varKeys = append(varKeys, name) + sort.Strings(varKeys) }
- 解説: 新しい変数が
Publish
されるたびに、varKeys
にキーが追加され、sort.Strings()
でソートされます。
- 解説: 新しい変数が
-
トップレベルの
Do()
関数でのvarKeys
利用:--- a/src/pkg/expvar/expvar.go +++ b/src/pkg/expvar/expvar.go @@ -270,8 +294,8 @@ func NewString(name string) *String { func Do(f func(KeyValue)) { mutex.RLock() defer mutex.RUnlock() - for k, v := range vars { - f(KeyValue{k, v}) + for _, k := range varKeys { + f(KeyValue{k, vars[k]}) } }
- 解説: トップレベルの
vars
マップを直接イテレートする代わりに、ソートされたvarKeys
スライスをイテレートするように変更されました。
- 解説: トップレベルの
src/pkg/expvar/expvar_test.go
-
RemoveAll()
関数でのvarKeys
リセット:--- a/src/pkg/expvar/expvar_test.go +++ b/src/pkg/expvar/expvar_test.go @@ -15,6 +18,7 @@ func RemoveAll() { mutex.Lock() defer mutex.Unlock() vars = make(map[string]Var) + varKeys = nil }
- 解説: テストのクリーンアップ時に、
varKeys
もリセットされるように変更されました。
- 解説: テストのクリーンアップ時に、
-
TestHandler()
テスト関数の追加:func TestHandler(t *testing.T) { RemoveAll() m := NewMap("map1") m.Add("a", 1) m.Add("z", 2) m2 := NewMap("map2") for i := 0; i < 9; i++ { m2.Add(strconv.Itoa(i), int64(i)) } rr := httptest.NewRecorder() rr.Body = new(bytes.Buffer) expvarHandler(rr, nil) want := `{\n"map1": {"a": 1, "z": 2},\n"map2": {"0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8}\n}\n` if got := rr.Body.String(); got != want { t.Errorf("HTTP handler wrote:\n%s\nWant:\n%s", got, want) } }
- 解説:
expvar
のHTTPハンドラ (expvarHandler
) が、マップのキーを正しくソートしてJSONを出力するかどうかを検証するためのテストが追加されました。map1
とmap2
という2つのマップを作成し、それぞれにキーを追加した後、期待されるソートされたJSON出力と比較しています。
- 解説:
コアとなるコードの解説
マップのソート機能
expvar
パッケージの Map
型と、グローバルな公開変数マップ vars
の両方に、キーのソート機能が導入されました。
-
Map
型のkeys
フィールドとupdateKeys()
メソッド:Map
struct にkeys []string
が追加されたことで、Map
インスタンスは自身のキーのソート済みリストを内部に保持できるようになりました。updateKeys()
メソッドは、Map
の内部マップm
に変更があった際に呼び出され、v.keys
スライスをクリアし、m
のすべてのキーを再収集してsort.Strings()
でソートします。このメソッドは、Map.Set()
,Map.Add()
,Map.AddFloat()
の中で、新しいキーが追加される可能性のあるパスで呼び出されます。Map.String()
とMap.Do()
メソッドは、従来のfor key, val := range v.m
のようにマップを直接イテレートする代わりに、for _, k := range v.keys
のようにソートされたv.keys
スライスをイテレートするように変更されました。これにより、expvar
の出力やDo
メソッドで処理される要素の順序が常にアルファベット順に保証されます。
-
グローバルな
vars
マップのvarKeys
とPublish()
:expvar
パッケージ全体で公開されている変数を管理するvars
マップ (map[string]Var
) についても、同様にvarKeys []string
というソート済みキーリストが導入されました。Publish(name string, v Var)
関数が呼び出され、新しい変数が公開されるたびに、name
がvarKeys
に追加され、sort.Strings(varKeys)
によってソートされます。- トップレベルの
Do(f func(KeyValue))
関数も、vars
マップを直接イテレートする代わりに、ソートされたvarKeys
を使用してf
を呼び出すように変更されました。これにより、/debug/vars
エンドポイントから取得されるJSON出力のトップレベルのキーも常にソートされた順序になります。
このソート機能の導入により、expvar
の出力が安定し、人間やツールによる解析が格段に容易になりました。
競合状態の修正
Map.Add()
および Map.AddFloat()
メソッドにおける競合状態は、新しい Int
または Float
変数をマップに追加するロジックの改善によって解決されました。
-
元の問題:
// Map.Add (simplified) v.mu.RLock() av, ok := v.m[key] // Read lock for existence check v.mu.RUnlock() // Release read lock if !ok { v.mu.Lock() // Acquire write lock if _, ok = v.m[key]; !ok { // Re-check (but not fully safe if another goroutine just added it) av = new(Int) v.m[key] = av } v.mu.Unlock() } // ... increment av
このコードでは、
RUnlock()
とLock()
の間に別のゴルーチンが同じキーを追加してしまうと、両方のゴルーチンが新しいInt
(またはFloat
) を作成し、最終的に一方のInt
がマップに設定され、もう一方のInt
はガベージコレクションされ、そのゴルーチンが行った更新が失われる可能性がありました。 -
修正後のロジック (二重チェックロック):
// Map.Add (simplified, after fix) v.mu.RLock() av, ok := v.m[key] v.mu.RUnlock() if !ok { v.mu.Lock() // Acquire write lock av, ok = v.m[key] // Re-check under write lock (this is the crucial part) if !ok { // If still not found, then it's safe to add av = new(Int) v.m[key] = av v.updateKeys() // Update sorted keys } v.mu.Unlock() } // ... increment av
修正後のコードでは、最初の
RLock
でキーが存在しないと判断された後、すぐにv.mu.Lock()
を取得します。そして、書き込みロックが保持されている安全な状態で、再度v.m[key]
をチェックします。- もしこの時点で
ok
がtrue
になっていれば、それは別のゴルーチンが既にこのキーを追加したことを意味します。この場合、既存のav
を使用して処理を続行します。 - もし
ok
がまだfalse
であれば、このゴルーチンが最初にこのキーを追加する権利を得たことになります。ここで新しいInt
(またはFloat
) を作成し、マップに追加します。 この「二重チェックロック」パターンにより、複数のゴルーチンが同時に新しい変数を生成して上書きし合うという競合状態が確実に防止されます。
- もしこの時点で
これらの変更は、expvar
パッケージの堅牢性と使いやすさを大幅に向上させました。
関連リンク
- Go言語
expvar
パッケージのドキュメント: https://pkg.go.dev/expvar - Go言語
sync
パッケージのドキュメント: https://pkg.go.dev/sync - Go言語
sort
パッケージのドキュメント: https://pkg.go.dev/sort - Go言語におけるマップのイテレーション順序に関する公式ブログ記事 (Go 1.0.3リリースノートの一部): https://go.dev/doc/go1.0.3#map (「Map iteration order is not specified」の項目を参照)
参考にした情報源リンク
- Go言語の公式ドキュメント (
expvar
,sync
,sort
パッケージ) - Go言語のマップのイテレーション順序に関する一般的な知識
- 競合状態とミューテックスに関する一般的なプログラミングの知識
- Go言語のコードレビューシステム (Gerrit) の変更セット: https://golang.org/cl/54320043 (元のコミットメッセージに記載されているリンク)