Understanding Real-World Concurrency Bugs in Go¶
Abstract¶
Go の並行処理に関するバグの研究
- Go のマルチスレッドプログラミングの並行化メカニズム
メッセージパッシング
共有メモリ
Go で実装されたメジャーなプロダクト(Docker, Kebernetes, etcd, CockroachDB, gRCP, BoltDB)が調査対象
171 個の並行処理に関するバグがあった
調査するために Bug 検出器を使って検証/評価した
1 Introduction¶
- バグの原因
- 共有メモリ
traditional(Mutex, RWMutex)
WaitGroup
- メッセージパッシングの誤用
チャネル
- バグの種類
- ブロッキングバグ
ゴルーチンがブロックされてしまうバグ
- ノンブロッキングバグ
処理自体は進むが意図しない動作をするバグ
- ブロッキングバグの約 58 %はメッセージパッシングが原因
どのゴルーチンも Read していないチャネルにメッセージを送信
...
バグの例
タイムアウトすると time.After(timeout)
からチャネルが return されてメインのルーチンは抜けるけど、ゴルーチンは残っている。後にゴルーチンはチャネルに書き込もうとするが、チャネルを受信するルーチンがいないので、チャネルへの書き込みがブロックされる。ゴルーチンリークにつながる。バッファ付きチャネルにすることで結果をチャネルに書き込むことができる。
func finishReq(timeout time.Duration) r ob {
- ch := make(chan ob)
+ ch := make(chan ob, 1)
go func() {
result := fn()
ch <- result // block
}()
select {
case result = <-ch:
return result
case <-time.After(timeout):
return nil
}
}
2 Background and Applications¶
Go は静的型付けの言語で並行プログラミング用に設計されている
スレッドモデル、スレッド間の通信方法、スレッド同期メカニズム、Go の並行処理のメカニズムの説明
6 つのアプリケーションの紹介
2.1 Goroutine¶
Go は並行処理のメカニズムとしてゴルーチンという概念を用いる
ゴルーチンは軽量のユーザーランドのスレッド
Go のランタイムライブラリが管理
OS のスレッドとは M:N でマップ
- Goroutine の説明
ゴルーチンは匿名関数の実行をサポート
- 呼び出し元と変数を共有することができる
データ競合をもたらす
2.3 Synchronization with Message Passing¶
- チャネル(
chan
) Goによって導入された新しい並行性プリミティブ
ゴルーチン間のデータと状態の送受信
nil チャネルへのデータ送信またはデータの受信は、ゴルーチンをブロックする
close されたチャネルを再度 close すると実行時パニックを起こす可能性がある
- チャネル(
select
ゴルーチンの複数チャネルの待機
- 複数のケースが有効な場合は非決定的(ランダムに決まる)
並行バグを引き起こしうる
context
ゴルーチン間でリクエストデータやメタデータを伝播
Pipe
Reader
とWriter
間のストリームデータのやり取り新しいタイプの並行バグを生み出しうる
2.4 Go Applications¶
Go の人気と採用が増えている話
4 Bug Study Methodology¶
- 並行バグの分類
- GitHub のコミットログに対してキーワードを用いて検索
race
deadlock
synchronizataion
concurrency
Lock
mutex
atomic
conpete
context
once
goroutine leak
検索でフィルター後、手動でコミットを特定し調査
- バグの分類
ブロッキングバグ
ノンブロッキングバグ
- 従来の研究はバグをデッドロックバグと非デッドロックバグに分類
ブロッキングバグはデッドロックバグを包括していて、より広い概念
従来の分類方法を拡張している
分類 |
メッセージパッシング |
共有メモリ |
合計 |
---|---|---|---|
ブロッキングバグ |
49 |
36 |
85 |
ノンブロッキングバグ |
17 |
69 |
86 |
合計 |
66 |
105 |
171 |
5 Blocking Bugs¶
ブロッキングバグに関する調査結果
5.1 Root Causes of Blocking Bugs¶
- ブロッキングバグの原因を共有メモリとメッセージパッシングの観点で分類
約 42 %は共有メモリの保護エラーが原因
約 58 %はメッセージパッシングのエラーが原因
共有メモリプリミティブはメッセージパッシングプリミティブよりも頻繁に使用される
メッセージパッシングはブロッキングバグを引き起こしやすくなる
5.1.2 Misuse of Message Passing¶
コンテキストオブジェクトを別のコンテキストオブジェクトで上書きしてしまうことで、古いオブジェクトを使っているゴルーチンにメッセージを送信/closeすることできなくなる
-hctx, hcancel := context.WithCancel(ctx)
+var hctx context.Context
+var hcancel context.CancelFunc
if timeout > 0 {
hctx, hcancel = context.WithTimeout(ctx, timeout)
+} else {
+ hctx, hcancel = context.WithCancel(ctx)
}
- チャネルを使うことの考慮不足によるブロッキングバグ
関数の実行順序(
gorotine1()
=>goroutine2()
)によっては永久にブロックされるselect
のdefault
を使うことでFixした
func gorotine1() {
m.Lock()
// goroutine2() がまだチャネルをReadしていないと、リクエストが書き込めないためブロックされる
- ch <- request
+ select {
+ case ch <- request:
// default を用いることで書き込めない場合は何も処理せずアンロックする
+ default:
+ }
m.Unlock()
}
func goroutine2() {
for {
// ブロックされる
m.Lock()
m.Unlock()
request <-ch
}
}
5.2 Fixes of Blocking Bugs¶
5.3 Detection of Blocking Bugs¶
6 Non-Blocking Bugs¶
ノンブロッキングバグの調査
6.1 Root Causes of Non-blocking Bugs¶
ノンブロッキングバグも、ブロッキングバグと同様に、共有メモリとメッセージパッシングに関するバグに分類
6.1.2 Errors during Message Passing¶
メッセージパッシングでエラーが発生するとノンブロッキングバグが発生する
ノンブロッキングバグの約 20 %を占める
- select {
- case <- c.closed:
// 複数のゴルーチンが同時に close すると panic が起こる
// Once.Do を用いることで確実に一度だけ close することができる
- default:
+ Once.Do(func() {
close(c.closed)
+ })
- }
select
の非決定的選択による不具合チャネルが close されたとしてもタイミングによっては重い処理 f() が再度実行されてしまう
ticker := time.NewTicker()
for {
+ select {
+ case <-stopCh:
+ return
+ default:
+ }
f()
select {
case <-stopCh:
return
case <-ticker:
}
}
ライブラリの中でチャネルを使用されることによるノンブロッキングバグ
- 開発者は dur が 0 より大きい、または
ctx.Done()
が呼ばれた場合のみ関数から return することを意図している Fix前は dur が 0 以下の場合に処理をすり抜けてしまう、というバグ
- 開発者は dur が 0 より大きい、または
-timer := time.NewTimer(0)
+var timeout <- chan time.Time
if dur > 0 {
- timer = time.NewTimer(dur)
+ timeout = time.NewTimer(dur).C
}
select {
-case <- timer.C:
+case <- timeout:
case <- ctx.done():
return nil
}
6.2 Fixes of Non-Blocking Bugs¶
6.3 Detection of Non-Blocking Bugs¶
7 Discussion and Future Work¶
スレッド間の通信としてメッセージパッシングを推奨
- 共有メモリを使う場合としてバグが減るかどうかは不明
メッセージパッシングと Go の同期メカニズムを理解する必要がある
ブロッキングバグの検出には、従来の静的解析やデッドロック検出が役に立つ
筆者らの検出器は、バグを学習して未知のバグを検出することができる
9 Conclusion¶
ブロッキングバグとノンブロッキングバグの観点から実プロダクトの並行処理のバグを研究した最初の研究
筆者らは Go の並行バグの理解を深め、より注意されることを期待している