[インデックス 14454] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet/http
パッケージにおいて、HTTPサーバーがパニック(panic)発生時にスタックトレースを出力する方法を改善するものです。具体的には、runtime/debug.Stack
関数の使用をruntime.Stack
関数に切り替えることで、より効率的かつ直接的にスタックトレースを取得するように変更されています。これにより、パニック発生時のログ出力のパフォーマンスとメモリ使用量が改善されます。
コミット
- コミットハッシュ:
dd43bf807d2d571a7278e9d0755d6787800a0006
- Author: Dave Cheney dave@cheney.net
- Date: Thu Nov 22 08:18:45 2012 +1100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/dd43bf807d2d571a7278e9d0755d6787800a0006
元コミット内容
net/http: use runtime.Stack instead of runtime/debug.Stack
Fixes #4060.
2012/11/21 19:51:34 http: panic serving 127.0.0.1:47139: Kaaarn!
goroutine 7 [running]:
net/http.func·004(0x7f330807ffb0, 0x7f330807f100)
/home/dfc/go/src/pkg/net/http/server.go:615 +0xa7
----- stack segment boundary -----
main.(*httpHandler).ServeHTTP()
/home/dfc/src/httppanic.go:16 +0x53
net/http.(*conn).serve(0xc200090240, 0x0)
/home/dfc/go/src/pkg/net/http/server.go:695 +0x55d
created by net/http.(*Server).Serve
/home/dfc/go/src/pkg/net/http/server.go:1119 +0x36d
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/6846085
変更の背景
この変更は、Go言語のnet/http
パッケージにおいて、HTTPリクエスト処理中に発生したパニックを捕捉し、そのスタックトレースをログに出力する際の挙動を改善するために行われました。元の実装ではruntime/debug.Stack()
関数を使用していましたが、これにはいくつかの課題がありました。
コミットメッセージに記載されているFixes #4060
は、この変更が解決するIssueの番号を示しています。このIssueでは、runtime/debug.Stack()
が内部でbytes.Buffer
を使用しており、スタックトレースのサイズが大きくなると、そのバッファの再割り当てが頻繁に発生し、パフォーマンスのオーバーヘッドやメモリ使用量の増加につながる可能性が指摘されていました。特に、高負荷な環境下で頻繁にパニックが発生する場合、このオーバーヘッドは無視できないものとなります。
また、runtime/debug.Stack()
は、スタックトレースを文字列として返すため、一度文字列として生成されたものを再度log.Print
で出力するという二重の処理が必要でした。これは、より直接的なスタックトレースの取得方法があれば改善できる点でした。
このコミットは、これらの課題に対処し、より効率的で直接的なスタックトレースの取得方法であるruntime.Stack()
への切り替えを行うことで、net/http
サーバーの堅牢性とパフォーマンスを向上させることを目的としています。
前提知識の解説
Go言語のパニックとリカバリー
Go言語には、プログラムの異常終了を示す「パニック(panic)」というメカニズムがあります。これは、回復不可能なエラー(例: nilポインタ参照、配列の範囲外アクセス)が発生した場合に、プログラムの実行を停止させるために使用されます。パニックが発生すると、通常の実行フローは中断され、遅延関数(defer
)が実行され、最終的にプログラムがクラッシュします。
しかし、Goではrecover
関数とdefer
ステートメントを組み合わせることで、パニックを捕捉し、プログラムのクラッシュを防ぎ、正常な状態に回復させることができます。net/http
パッケージのサーバーは、HTTPリクエストのハンドラ内で発生したパニックがサーバー全体を停止させないように、このrecover
メカニズムを内部的に利用しています。パニックを捕捉した後、通常はエラーログを出力し、クライアントには適切なHTTPエラーレスポンスを返します。
スタックトレース
スタックトレース(Stack Trace)とは、プログラムが実行されている間に、どの関数がどの順序で呼び出されたかを示すリストです。パニックやエラーが発生した際にスタックトレースが出力されることで、問題が発生したコードの場所や、その問題に至るまでの関数の呼び出し履歴を特定するのに役立ちます。これはデバッグにおいて非常に重要な情報となります。
runtime
パッケージとruntime/debug
パッケージ
Go言語には、ランタイムシステムと対話するための2つの主要なパッケージがあります。
-
runtime
パッケージ: Goランタイムの低レベルな機能を提供します。これには、ゴルーチン(goroutine)の管理、メモリ割り当て、ガベージコレクション、そしてスタックトレースの取得など、Goプログラムの実行環境に関する基本的な情報や操作が含まれます。runtime.Stack()
関数は、このパッケージの一部であり、現在のゴルーチンのスタックトレースをバイトスライスとして直接取得します。 -
runtime/debug
パッケージ: デバッグ目的でより高レベルなランタイム情報を提供します。これには、スタックトレースのフォーマット済み文字列の取得(runtime/debug.Stack()
)、GC(ガベージコレクション)の統計情報、メモリプロファイルなどが含まれます。runtime/debug.Stack()
は、内部でruntime.Stack()
を呼び出し、その結果を整形して文字列として返すため、追加の処理オーバーヘッドが発生します。
bytes.Buffer
bytes.Buffer
は、Go言語で可変長のバイトシーケンスを扱うための構造体です。これは、バイトデータを効率的に構築したり、読み書きしたりする際に便利です。fmt.Fprintf
のような関数と組み合わせて、フォーマットされた文字列をバッファに書き込むことができます。しかし、バッファの容量が不足すると、内部的に新しい、より大きなメモリ領域を割り当ててデータをコピーするという再割り当て処理が発生します。これが頻繁に起こると、パフォーマンスに影響を与える可能性があります。
技術的詳細
このコミットの技術的な核心は、スタックトレースの取得方法をruntime/debug.Stack()
からruntime.Stack()
へ変更し、それに伴うログ出力の最適化です。
-
スタックトレース取得の効率化:
- 変更前:
debug.Stack()
を呼び出していました。この関数は、スタックトレースを内部でbytes.Buffer
に書き込み、最終的にstring
として返します。このプロセスには、バッファの動的な拡張と文字列への変換というオーバーヘッドが伴います。 - 変更後:
runtime.Stack(buf, false)
を呼び出しています。この関数は、引数として渡された[]byte
スライスに直接スタックトレースを書き込みます。これにより、中間的なbytes.Buffer
の使用や、文字列への変換が不要になります。事前に適切なサイズのバッファを確保することで、メモリの再割り当てを最小限に抑え、より効率的にスタックトレースを取得できます。
- 変更前:
-
メモリ割り当ての最適化:
- 変更前:
var buf bytes.Buffer
でbytes.Buffer
を宣言し、fmt.Fprintf
でメッセージを書き込み、debug.Stack()
の結果をbuf.Write
で追加していました。このbytes.Buffer
は、スタックトレースのサイズに応じて動的にメモリを再割り当てする可能性がありました。 - 変更後:
buf := make([]byte, size)
で固定サイズのバイトスライスを事前に確保しています。runtime.Stack
は、このスライスにスタックトレースを書き込み、実際に書き込まれたバイト数に基づいてスライスの長さを調整します(buf = buf[:runtime.Stack(buf, false)]
)。これにより、不要なメモリ割り当てやコピーが削減されます。const size = 4096
は、一般的なスタックトレースのサイズを考慮した妥当な初期バッファサイズとして選ばれています。
- 変更前:
-
ログ出力の簡素化と効率化:
- 変更前:
fmt.Fprintf
でメッセージをbytes.Buffer
に書き込み、debug.Stack()
の結果も同じバッファに書き込んだ後、log.Print(buf.String())
で最終的な文字列をログに出力していました。 - 変更後:
log.Printf("http: panic serving %v: %v\\n%s", c.remoteAddr, err, buf)
を使用しています。log.Printf
はフォーマット文字列と引数を受け取り、直接ログに出力します。runtime.Stack
から得られたバイトスライスbuf
は、%s
フォーマット指定子によって文字列として解釈され、効率的に出力されます。これにより、中間的な文字列結合やバッファ操作が減り、コードが簡潔になるとともに、ログ出力の効率が向上します。
- 変更前:
これらの変更は、特に高負荷なHTTPサーバーにおいて、パニック発生時のリソース消費を抑え、全体的なパフォーマンスと安定性を向上させることに貢献します。
コアとなるコードの変更箇所
diff --git a/src/pkg/net/http/server.go b/src/pkg/net/http/server.go
index 805e0737a9..3a4d61c213 100644
--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -11,7 +11,6 @@ package http
import (
"bufio"
- "bytes"
"crypto/tls"
"errors"
"fmt"
@@ -21,7 +20,7 @@ import (
"net"
"net/url"
"path"
- "runtime/debug"
+ "runtime"
"strconv"
"strings"
"sync"
@@ -610,10 +609,10 @@ func (c *conn) serve() {
return
}
- var buf bytes.Buffer
- fmt.Fprintf(&buf, "http: panic serving %v: %v\n", c.remoteAddr, err)
- buf.Write(debug.Stack())
- log.Print(buf.String())
+ const size = 4096
+ buf := make([]byte, size)
+ buf = buf[:runtime.Stack(buf, false)]
+ log.Printf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
if c.rwc != nil { // may be nil if connection hijacked
c.rwc.Close()
コアとなるコードの解説
このコミットにおける主要な変更は、src/pkg/net/http/server.go
ファイルのconn.serve()
メソッド内のパニックリカバリー処理部分に集中しています。
-
インポートの変更:
- "bytes"
:bytes.Buffer
を使用しなくなったため、bytes
パッケージのインポートが削除されました。- "runtime/debug"
:debug.Stack()
を使用しなくなったため、runtime/debug
パッケージのインポートが削除されました。+ "runtime"
:runtime.Stack()
を使用するために、runtime
パッケージが新しくインポートされました。
-
スタックトレース取得とログ出力ロジックの変更:
-
変更前:
var buf bytes.Buffer fmt.Fprintf(&buf, "http: panic serving %v: %v\n", c.remoteAddr, err) buf.Write(debug.Stack()) log.Print(buf.String())
このコードでは、まず
bytes.Buffer
型のbuf
を宣言しています。次に、fmt.Fprintf
を使ってパニックメッセージをbuf
に書き込み、その後にdebug.Stack()
が返すスタックトレースのバイトスライスをbuf.Write
で追記しています。最後に、buf.String()
でバッファの内容を文字列に変換し、log.Print
で出力しています。この一連の処理は、複数のメモリ割り当てとデータコピーを伴う可能性がありました。 -
変更後:
const size = 4096 buf := make([]byte, size) buf = buf[:runtime.Stack(buf, false)] log.Printf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
この新しいコードでは、まず
size
という定数で4096バイトのバッファサイズを定義しています。次に、make([]byte, size)
を使って、このサイズのバイトスライスbuf
を事前に確保します。runtime.Stack(buf, false)
は、現在のゴルーチンのスタックトレースをbuf
スライスに直接書き込みます。第2引数のfalse
は、すべてのゴルーチンのスタックトレースではなく、現在のゴルーチンのみのスタックトレースを取得することを示します。この関数は実際に書き込まれたバイト数を返すため、buf = buf[:runtime.Stack(buf, false)]
という行で、スライスの長さを実際に書き込まれたデータ量に調整しています。 最後に、log.Printf
を使って、フォーマットされたメッセージと、runtime.Stack
によって直接書き込まれたbuf
スライス(%s
で文字列として解釈される)を一度にログに出力しています。これにより、中間的なバッファや文字列変換のステップが不要になり、より効率的なログ出力が実現されています。
-
この変更により、パニック発生時のスタックトレース取得とログ出力のオーバーヘッドが削減され、net/http
サーバーのパフォーマンスと安定性が向上しました。
関連リンク
- Go Issue #4060: https://github.com/golang/go/issues/4060
- Gerrit Change-ID: https://golang.org/cl/6846085
参考にした情報源リンク
- Go言語の公式ドキュメント:
runtime
パッケージ: https://pkg.go.dev/runtimeruntime/debug
パッケージ: https://pkg.go.dev/runtime/debuglog
パッケージ: https://pkg.go.dev/logbytes
パッケージ: https://pkg.go.dev/bytes
- Go言語のパニックとリカバリーに関する一般的な情報源 (例: Go by Example - Panics): https://gobyexample.com/panics
- Go言語のスタックトレースに関するブログ記事や解説(一般的なGoのデバッグ手法に関する情報)
- Go言語の
net/http
パッケージの内部実装に関する情報(Goのソースコードリーディング) - Go言語のメモリ管理とパフォーマンスに関する一般的な情報