[インデックス 16180] ファイルの概要
このコミットは、Go言語のランタイムの安定性と堅牢性を継続的にテストするための新しいストレスプログラムの導入を目的としています。具体的には、test/stress
ディレクトリ配下に maps.go
、parsego.go
、runstress.go
の3つの新しいファイルが追加され、Goランタイムの様々な側面(ガベージコレクション、マップ操作、チャネル、ネットワーク、Goコードのパースなど)に負荷をかけ続けるように設計されています。このプログラムは「永遠に実行され、決して終了しない」ことを前提としており、ランタイムの潜在的なバグやリソースリークを検出することを目的としています。
コミット
commit b3809cae5e89af31d618b07843267a17cff28999
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Apr 15 11:50:14 2013 -0700
test/stress: start of a runtime stress program
Runs forever, stressing the runtime in various ways.
It should never terminate.
R=golang-dev, r, minux.ma
CC=golang-dev
https://golang.org/cl/8583047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b3809cae5e89af31d618b07843267a17cff28999
元コミット内容
test/stress: start of a runtime stress program
Runs forever, stressing the runtime in various ways.
It should never terminate.
R=golang-dev, r, minux.ma
CC=golang-dev
https://golang.org/cl/8583047
変更の背景
Go言語は、並行処理と効率的なランタイムを特徴とするプログラミング言語です。ランタイムは、ガベージコレクタ、スケジューラ、メモリ管理など、Goプログラムの実行を支える重要なコンポーネントです。これらのコンポーネントは複雑であり、特に長時間の実行や高負荷下での安定性を確保することは非常に重要です。
このコミットの背景には、Goランタイムの潜在的なバグ、リソースリーク、デッドロック、またはパフォーマンスの劣化を、通常のテストスイートでは見つけにくいシナリオで発見するという目的があります。継続的に様々な操作を実行し、ランタイムにストレスをかけることで、これらの問題が顕在化する可能性が高まります。特に、ガベージコレクションの効率性、マップ操作の並行安全性、チャネルの挙動、ネットワークI/Oの安定性などが重点的にテストされます。
この種のストレスプログラムは、ソフトウェアの信頼性を高めるための一般的な手法であり、Go言語のようなシステムプログラミング言語においては、その基盤となるランタイムの堅牢性が極めて重要であるため、このようなテストの導入は必然的なものと言えます。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の概念と関連技術についての知識が役立ちます。
- Goランタイム (Go Runtime): Goプログラムの実行を管理するシステム。ガベージコレクタ、ゴルーチン(軽量スレッド)スケジューラ、メモリ割り当て、チャネル通信などが含まれます。Goプログラムのパフォーマンスと安定性に直接影響します。
- ガベージコレクション (Garbage Collection, GC): プログラムが不要になったメモリを自動的に解放するプロセス。GoのGCは並行的に動作し、プログラムの実行を一時停止する時間を最小限に抑えるように設計されていますが、GCに過度な負荷がかかるとパフォーマンスに影響を与えたり、稀にバグを引き起こしたりする可能性があります。
- ゴルーチン (Goroutine): Go言語における軽量な並行実行単位。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行することも可能です。Goランタイムのスケジューラによって管理されます。
- チャネル (Channel): ゴルーチン間で値を安全に送受信するための通信メカニズム。チャネルはGoにおける並行処理の主要な構成要素であり、デッドロックや競合状態を防ぐために重要です。
- マップ (Map): キーと値のペアを格納するGoの組み込みデータ構造。並行アクセスに対する安全性は保証されておらず、複数のゴルーチンから同時に書き込みを行うとパニック(ランタイムエラー)が発生する可能性があります。このストレスプログラムでは、マップの並行アクセスに対するランタイムの挙動を間接的にテストしている可能性があります。
go/parser
パッケージ: Goのソースコードを解析し、抽象構文木 (AST: Abstract Syntax Tree) を生成するためのパッケージ。Goのツール(go fmt
、go vet
など)やIDEなどで利用されます。ソースコードのパースはCPUとメモリを消費する操作であり、大量のコードを繰り返しパースすることでランタイムに負荷をかけることができます。net/http/httptest
パッケージ: HTTPサーバーやクライアントのテストを容易にするためのパッケージ。テスト用のHTTPサーバーをインメモリで起動し、実際のネットワーク通信なしにHTTPリクエスト/レスポンスのテストを行うことができます。os/exec
パッケージ: 外部コマンドを実行するためのパッケージ。シェルコマンドなどをGoプログラムから呼び出す際に使用されます。外部プロセスの起動と終了はシステムリソースを消費するため、これを繰り返すことでランタイムに負荷をかけることができます。sync.WaitGroup
: 複数のゴルーチンの完了を待つための同期プリミティブ。並行処理において、特定の処理がすべて完了するまで待機する場合に利用されます。runtime.Gosched()
: 現在のゴルーチンを一時停止し、他のゴルーチンにCPUを譲るようにスケジューラにヒントを与える関数。これにより、他のゴルーチンが実行される機会を得て、デッドロックの回避や公平なスケジューリングに役立つことがあります。ストレステストでは、意図的にゴルーチンの切り替えを促し、スケジューラの挙動をテストする目的で使われることがあります。
技術的詳細
このストレスプログラムは、main
関数から複数のゴルーチンを起動し、それぞれがGoランタイムの異なる側面を継続的にストレスする設計になっています。
runstress.go
がメインのエントリポイントであり、コマンドライン引数によってどのストレス機能を有効にするかを制御できます。デフォルトでは、すべてのストレス機能が有効になります。
-
stressMaps()
(maps.go):MapType
インターフェースとMap
インターフェースを定義し、様々なマップ型を抽象化しています。- 現在の実装では
intMapType
(map[int][]byte) のみがテスト対象です。 stressMapType
関数は、マップにアイテムを追加 (AddItem
)、削除 (DelItem
)、取得 (GetItem
)、全要素をイテレート (RangeAll
) する操作を繰り返します。- マップのサイズが10000に達するまでアイテムを追加し、その後すべて削除するというサイクルを繰り返します。
AddItem
では、ランダムなキーとランダムなサイズのバイトスライスを値としてマップに追加します。これにより、メモリ割り当てとGCに負荷がかかります。GetItem
では、マップからランダムなアイテムを取得し、その値のバイトスライスの一部をdeadcafe
というバイト列で上書きします。これは、メモリの書き込みパターンを多様化し、潜在的なメモリ破損を検出するのに役立つ可能性があります。runtime.Gosched()
を呼び出すことで、マップ操作中にゴルーチンの切り替えを促し、並行処理下でのマップの挙動をテストします。numGets
(10) のゴルーチンを起動してGetItem
またはRangeAll
を並行して実行し、sync.WaitGroup
で完了を待ちます。これにより、マップへの並行読み取りアクセスをシミュレートします。
-
stressParseGo()
(parsego.go):- Goの標準ライブラリのソースコードを繰り返しパースすることで、CPUとメモリに負荷をかけます。
pkgroot
はGOROOT/src/pkg/
を指し、Goの標準パッケージのソースコードが格納されている場所です。packages
変数には、パース対象となるGo標準パッケージのパスがハードコードされています。parseDir
関数は、指定されたディレクトリ内のGoファイルをパースし、go/ast.Package
構造体を返します。isGoFile
やisPkgFile
といったヘルパー関数で、パース対象のファイルをフィルタリングしています。特に、テストファイル (_test.go
) や、パッケージ名がディレクトリ名と一致しないファイルは除外されます。parser.ParseDir
をparser.ParseComments
オプション付きで呼び出すことで、コメントもパース対象に含め、より完全なASTを構築します。- この処理は無限ループで実行され、大量のGoソースコードを繰り返しパースすることで、メモリ割り当て、GC、CPU使用率に継続的な負荷をかけます。
-
stressNet()
(runstress.go):net/http/httptest
を使用してインメモリのHTTPサーバーを起動します。- このサーバーは、リクエストで指定されたサイズのバイト列をレスポンスとして返します。
dialStress
ゴルーチンは、ランダムなタイムアウトでサーバーへのTCP接続を繰り返し確立し、短時間で切断します。これにより、エフェメラルポートの枯渇やネットワークスタックのストレスをテストします。- メインのループでは、ランダムなサイズのHTTP GETリクエストをサーバーに送信し、レスポンスボディを
ioutil.Discard
にコピーして読み捨てます。これにより、HTTPクライアント、ネットワークI/O、メモリ割り当て、GCに負荷をかけます。 log.Fatalf
を使用して、HTTPリクエストやレスポンスの検証に失敗した場合にプログラムを終了させます。
-
stressExec()
(runstress.go):os/exec.Command
を使用して/bin/sh -c "echo ...; exit ..."
というコマンドを繰り返し実行します。- コマンドの終了コードと出力はランダムに生成され、検証されます。
gate := make(chan bool, 10)
を使用して、同時に実行される外部コマンドの数を10に制限しています。これにより、システムリソースを過度に消費することなく、外部プロセスの起動と終了を継続的にテストします。- 外部プロセスの起動と終了は、OSとのインタラクション、パイプの作成、メモリ割り当てなど、Goランタイムに様々な負荷をかけます。
-
stressChannels()
(runstress.go):- 「スレッドリング」パターンを実装しています。これは、複数のゴルーチンがチャネルを介して値を順に渡していく古典的な並行処理のベンチマークです。
threadRing(bufsize int)
関数は、N
(100) 個のゴルーチンを連鎖的に起動し、それぞれが前のゴルーチンからチャネルで値を受け取り、デクリメントして次のゴルーチンに渡します。bufsize
はチャネルのバッファサイズを制御し、バッファなし (0
) とバッファあり (1
) の両方でテストします。- このテストは、チャネルの作成、送信、受信、クローズといった操作を繰り返し行い、Goランタイムのチャネル実装とゴルーチンスケジューラに負荷をかけます。特に、バッファなしチャネルは同期的な通信を強制するため、ゴルーチンのスケジューリングが頻繁に発生し、スケジューラに高い負荷がかかります。
これらのストレス機能はすべて main
関数から独立したゴルーチンとして起動され、無限ループで実行されます。これにより、Goランタイムは継続的に様々な種類の負荷にさらされ、長期的な安定性やリソース管理の健全性が検証されます。
コアとなるコードの変更箇所
このコミットでは、以下の3つの新しいファイルが test/stress/
ディレクトリに追加されています。
-
test/stress/maps.go
:- マップ操作(追加、削除、取得、イテレーション)をストレスするためのロジックが含まれています。
MapType
およびMap
インターフェースを定義し、具体的なマップ実装(intMap
)を提供します。stressMapType
関数がマップに対する一連の操作を実行します。stressMaps
関数がstressMapType
を無限ループで呼び出します。
-
test/stress/parsego.go
:- Goソースコードのパースをストレスするためのロジックが含まれています。
isGoFile
,isPkgFile
,pkgName
,parseDir
などのヘルパー関数を定義します。stressParseGo
関数がGOROOT
内のGo標準パッケージを繰り返しパースします。- パース対象のパッケージリスト
packages
が定義されています。
-
test/stress/runstress.go
:- ストレスプログラムのメインエントリポイントです。
- コマンドラインフラグ (
-maps
,-exec
,-chan
,-net
,-parsego
) を定義し、どのストレス機能を有効にするかを制御します。 dialStress
,stressNet
,doAnExec
,stressExec
,ringf
,threadRing
,stressChannels
といった各ストレス機能の実装が含まれています。main
関数で、有効なストレス機能をそれぞれ新しいゴルーチンとして起動し、無限に実行させます。
コアとなるコードの解説
test/stress/maps.go
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"math/rand"
"runtime"
"sync"
)
// MapType インターフェース: 異なるマップ型を抽象化
type MapType interface {
NewMap() Map
}
// Map インターフェース: マップの基本的な操作を定義
type Map interface {
AddItem()
DelItem()
Len() int
GetItem()
RangeAll()
}
// stressMapType: 特定のMapTypeに対してストレス操作を実行
func stressMapType(mt MapType, done func()) {
defer done()
m := mt.NewMap()
for m.Len() < 10000 { // マップサイズが10000に達するまで
// ... (AddItem, DelItem, GetItem, RangeAll の呼び出し)
var wg sync.WaitGroup
const numGets = 10
wg.Add(numGets)
for i := 0; i < numGets; i++ {
go func(i int) { // 複数のゴルーチンで並行読み取り
if i&1 == 0 {
m.GetItem()
} else {
m.RangeAll()
}
wg.Done()
}(i)
}
wg.Wait() // 並行読み取りの完了を待つ
}
for m.Len() > 0 { // マップが空になるまで削除
m.DelItem()
}
}
// intMapType: intキーと[]byte値を持つマップの具体的な実装
type intMapType struct{}
func (intMapType) NewMap() Map {
return make(intMap)
}
type intMap map[int][]byte
func (m intMap) AddItem() {
s0 := len(m)
for len(m) == s0 { // 新しいキーが生成されるまでループ
key := rand.Intn(s0 + 1)
m[key] = make([]byte, rand.Intn(64<<10)) // ランダムサイズのバイトスライスを値として追加
}
}
func (m intMap) DelItem() {
for k := range m { // 最初のキーを削除
delete(m, k)
return
}
}
func (m intMap) GetItem() {
key := rand.Intn(len(m))
if s, ok := m[key]; ok {
copy(s, deadcafe) // 値の一部を上書き
}
}
func (m intMap) Len() int { return len(m) }
func (m intMap) RangeAll() {
for _ = range m { // 全要素をイテレート
}
}
// stressMaps: マップストレスのメインループ
func stressMaps() {
for { // 無限ループ
var wg sync.WaitGroup
for _, mt := range mapTypes() { // 現在はintMapTypeのみ
wg.Add(1)
go stressMapType(mt, wg.Done) // 各マップ型をゴルーチンでストレス
}
wg.Wait() // すべてのマップストレスゴルーチンの完了を待つ
}
}
maps.go
は、Goのマップデータ構造に特化したストレステストを提供します。AddItem
でランダムなサイズのバイトスライスを値として追加することで、メモリ割り当てとガベージコレクションに負荷をかけます。GetItem
で値の一部を上書きする操作は、メモリの整合性やGCの挙動を間接的にテストします。複数のゴルーチンからの並行読み取りは、マップの内部ロックメカニズムや並行アクセス時のランタイムの安定性を検証します。
test/stress/parsego.go
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"go/ast"
"go/parser"
"go/token"
"os"
"path"
"runtime"
"strings"
)
// isGoFile: Goファイルかどうかを判定
func isGoFile(dir os.FileInfo) bool {
return !dir.IsDir() &&
!strings.HasPrefix(dir.Name(), ".") &&
path.Ext(dir.Name()) == ".go"
}
// isPkgFile: パッケージファイル(テストファイル以外)かどうかを判定
func isPkgFile(dir os.FileInfo) bool {
return isGoFile(dir) &&
!strings.HasSuffix(dir.Name(), "_test.go")
}
// pkgName: ファイルのパッケージ名を取得
func pkgName(filename string) string {
file, err := parser.ParseFile(token.NewFileSet(), filename, nil, parser.PackageClauseOnly)
if err != nil || file == nil {
return ""
}
return file.Name.Name
}
// parseDir: ディレクトリ内のGoファイルをパース
func parseDir(dirpath string) map[string]*ast.Package {
_, pkgname := path.Split(dirpath)
filter := func(d os.FileInfo) bool {
if isPkgFile(d) {
name := pkgName(dirpath + "/" + d.Name())
return name == pkgname // ディレクトリ名とパッケージ名が一致するファイルのみパース
}
return false
}
pkgs, err := parser.ParseDir(token.NewFileSet(), dirpath, filter, parser.ParseComments)
if err != nil {
println("parse", dirpath, err.Error())
panic("go ParseDir fail: " + err.Error())
}
return pkgs
}
// stressParseGo: Goソースコードのパースをストレス
func stressParseGo() {
pkgroot := runtime.GOROOT() + "/src/pkg/" // Go標準ライブラリのソースパス
for { // 無限ループ
m := make(map[string]map[string]*ast.Package)
for _, pkg := range packages { // 定義されたパッケージリストをパース
m[pkg] = parseDir(pkgroot + pkg)
Println("parsed go package", pkg)
}
}
}
// packages: パース対象のGo標準パッケージのリスト
var packages = []string{
// ... (大量のGo標準パッケージパス)
}
parsego.go
は、Goの標準ライブラリのソースコードを繰り返しパースすることで、CPUとメモリに負荷をかけます。go/parser
パッケージは、Goコンパイラやツールチェーンの基盤となる部分であり、その安定性をテストすることは重要です。大量のファイルを繰り返しパースすることで、メモリ割り当て、ガベージコレクション、CPU使用率に継続的なストレスを与え、潜在的なメモリリークやパフォーマンスの劣化を検出します。
test/stress/runstress.go
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"net/http/httptest"
"os/exec"
"strconv"
"time"
)
// コマンドラインフラグの定義
var (
v = flag.Bool("v", false, "verbose")
doMaps = flag.Bool("maps", true, "stress maps")
doExec = flag.Bool("exec", true, "stress exec")
doChan = flag.Bool("chan", true, "stress channels")
doNet = flag.Bool("net", true, "stress networking")
doParseGo = flag.Bool("parsego", true, "stress parsing Go (generates garbage)")
)
// Println: verboseモードでのみログ出力
func Println(a ...interface{}) {
if *v {
log.Println(a...)
}
}
// dialStress: ネットワークダイヤルをストレス
func dialStress(a net.Addr) {
for { // 無限ループ
d := net.Dialer{Timeout: time.Duration(rand.Intn(1e9))}
c, err := d.Dial("tcp", a.String())
if err == nil {
// ... (接続クローズのゴルーチン)
}
time.Sleep(250 * time.Millisecond) // エフェメラルポート枯渇防止
}
}
// stressNet: ネットワークI/Oをストレス
func stressNet() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
size, _ := strconv.Atoi(r.FormValue("size"))
w.Write(make([]byte, size)) // 指定されたサイズのバイト列を返す
}))
go dialStress(ts.Listener.Addr()) // ダイヤルストレスを並行実行
for { // 無限ループ
size := rand.Intn(128 << 10)
res, err := http.Get(fmt.Sprintf("%s/?size=%d", ts.URL, size))
// ... (エラーチェックとレスポンスボディの読み捨て)
res.Body.Close()
}
}
// doAnExec: 外部コマンドを1回実行
func doAnExec() {
exit := rand.Intn(2)
wantOutput := fmt.Sprintf("output-%d", rand.Intn(1e9))
cmd := exec.Command("/bin/sh", "-c", fmt.Sprintf("echo %s; exit %d", wantOutput, exit))
out, err := cmd.CombinedOutput()
// ... (出力と終了コードの検証)
}
// stressExec: 外部コマンド実行をストレス
func stressExec() {
gate := make(chan bool, 10) // 同時実行数を制限
for { // 無限ループ
gate <- true // 実行許可を取得
go func() {
doAnExec()
<-gate // 実行完了を通知
}()
}
}
// ringf: スレッドリングの各ノード関数
func ringf(in <-chan int, out chan<- int, donec chan<- bool) {
for {
n := <-in
if n == 0 {
donec <- true
return
}
out <- n - 1
}
}
// threadRing: スレッドリングベンチマークを実行
func threadRing(bufsize int) {
const N = 100
donec := make(chan bool)
one := make(chan int, bufsize)
var in, out chan int = nil, one
for i := 1; i <= N-1; i++ {
in, out = out, make(chan int, bufsize)
go ringf(in, out, donec) // N-1個のゴルーチンを連鎖的に起動
}
go ringf(out, one, donec) // 最後のゴルーチンを最初のチャネルに接続
one <- N // 最初の値を送信
<-donec // 完了を待つ
}
// stressChannels: チャネル操作をストレス
func stressChannels() {
for { // 無限ループ
threadRing(0) // バッファなしチャネル
threadRing(1) // バッファありチャネル
}
}
// main: プログラムのエントリポイント
func main() {
flag.Parse()
for want, f := range map[*bool]func(){
doMaps: stressMaps,
doNet: stressNet,
doExec: stressExec,
doChan: stressChannels,
doParseGo: stressParseGo,
} {
if *want {
go f() // 各ストレス機能をゴルーチンとして起動
}
}
select {} // メインゴルーチンをブロックし、他のゴルーチンが実行され続けるようにする
}
runstress.go
は、ストレスプログラム全体のオーケストレーションを行います。main
関数で各ストレス機能を独立したゴルーチンとして起動し、select {}
でメインゴルーチンをブロックすることで、これらのストレスゴルーチンが継続的に実行されるようにします。
stressNet
は httptest
を利用してHTTP通信をシミュレートし、ネットワークスタックとHTTPクライアント/サーバーの実装に負荷をかけます。
stressExec
は外部コマンドの繰り返し実行を通じて、プロセス管理とシステムコールにストレスを与えます。
stressChannels
はスレッドリングベンチマークを通じて、Goのチャネルとゴルーチンスケジューラの効率性と堅牢性をテストします。特に、バッファなしチャネルはゴルーチンの頻繁なコンテキストスイッチを引き起こし、スケジューラに高い負荷をかけます。
これらのファイルが連携して、Goランタイムの様々な側面を継続的にテストし、長期的な安定性とリソース管理の健全性を検証する包括的なストレスプログラムを構成しています。
関連リンク
- Go CL 8583047: https://golang.org/cl/8583047
参考にした情報源リンク
- Go言語公式ドキュメント: https://golang.org/doc/
- Go言語のガベージコレクション: https://go.dev/doc/gc-guide
- Go言語の並行処理 (Goroutines and Channels): https://go.dev/tour/concurrency/1
go/parser
パッケージ: https://pkg.go.dev/go/parsernet/http/httptest
パッケージ: https://pkg.go.dev/net/http/httptestos/exec
パッケージ: https://pkg.go.dev/os/execsync.WaitGroup
パッケージ: https://pkg.go.dev/sync#WaitGroupruntime.Gosched()
関数: https://pkg.go.dev/runtime#Gosched