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

[インデックス 15987] ファイルの概要

このコミットは、Go言語の標準ライブラリである net/http パッケージにおけるサーバーのパフォーマンス最適化を目的としています。具体的には、HTTPレスポンスのステータスライン(例: HTTP/1.1 200 OK)の生成方法を改善し、リクエストあたりのメモリ割り当てを2つ削減しています。これにより、サーバーの処理速度向上とガベージコレクション(GC)負荷の軽減が図られています。

コミット

commit 42a840860f60b3e66c7bcd755795d193935a05cc
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Mar 28 13:07:14 2013 -0700

    net/http: remove two more server allocations per-request
    
    benchmark                                   old ns/op    new ns/op    delta
    BenchmarkServerFakeConnWithKeepAliveLite        11031        10689   -3.10%
    
    benchmark                                  old allocs   new allocs    delta
    BenchmarkServerFakeConnWithKeepAliveLite           23           21   -8.70%
    
    benchmark                                   old bytes    new bytes    delta
    BenchmarkServerFakeConnWithKeepAliveLite         1668         1626   -2.52%
    
    R=golang-dev, gri
    CC=golang-dev
    https://golang.org/cl/8110044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/42a840860f60b3e66c7bcd755795d193935a05cc

元コミット内容

net/http: remove two more server allocations per-request

このコミットは、Goの net/http パッケージにおいて、HTTPサーバーが各リクエストを処理する際に発生するメモリ割り当てをさらに2つ削減することを目的としています。ベンチマーク結果は、操作あたりのナノ秒(ns/op)、割り当て数(allocs)、および割り当てバイト数(bytes)のすべてにおいて改善を示しており、特に割り当て数が8.70%削減されたことが強調されています。

変更の背景

Goの net/http パッケージは、Webサーバーを構築するための基盤として広く利用されています。高性能なWebサービスでは、ミリ秒単位のレイテンシや、大量のリクエストを効率的に処理する能力が求められます。このような環境では、わずかなメモリ割り当ても積み重なると、ガベージコレクション(GC)の頻度と時間が大幅に増加し、アプリケーション全体のパフォーマンスに悪影響を及ぼします。

このコミットの背景には、Goの標準ライブラリが提供するHTTPサーバーの効率を最大化し、可能な限りメモリフットプリントとGCオーバーヘッドを削減するという継続的な取り組みがあります。特に、リクエストごとに繰り返し生成される文字列のようなオブジェクトは、小さなものであっても累積的な影響が大きいため、最適化の主要なターゲットとなります。HTTPレスポンスのステータスライン(例: "HTTP/1.1 200 OK\r\n")は、ほとんどのリクエストで同じ内容が使用されるため、これをキャッシュすることで、毎回文字列を構築する際のメモリ割り当てを避けることができます。

前提知識の解説

1. HTTPステータスライン (Status-Line)

HTTPレスポンスの最初の行は「ステータスライン」と呼ばれ、以下の形式で構成されます。

HTTP-Version SP Status-Code SP Reason-Phrase CRLF

例: HTTP/1.1 200 OK

  • HTTP-Version: 使用されているHTTPプロトコルのバージョン(例: HTTP/1.1)。
  • Status-Code: 3桁の整数で、リクエストの結果を示す(例: 200)。
  • Reason-Phrase: ステータスコードの短いテキストによる説明(例: OK)。
  • CRLF: キャリッジリターンとラインフィード(改行)。

このステータスラインは、各HTTPレスポンスで送信されるため、その生成効率はサーバーのパフォーマンスに直結します。

2. Goにおけるメモリ割り当てとガベージコレクション (GC)

Goはガベージコレクタを持つ言語です。プログラムが実行中に新しいオブジェクト(文字列、構造体、スライスなど)を作成すると、ヒープメモリにそのための領域が割り当てられます。これらの割り当てられたオブジェクトが不要になった場合、ガベージコレクタがそれらを検出し、メモリを解放します。

  • メモリ割り当て (Allocations): 新しいオブジェクトが作成されるたびに発生します。頻繁な小さな割り当ては、GCの負担を増大させます。
  • ガベージコレクション (GC): 不要なメモリを自動的に解放するプロセスです。GCが実行される間、アプリケーションの実行が一時停止(ストップ・ザ・ワールド)することがあり、これがレイテンシの原因となります。割り当ての数を減らすことは、GCの頻度と作業量を減らし、結果としてアプリケーションのパフォーマンスを向上させます。

3. 文字列の結合とメモリ割り当て

Goにおいて、複数の文字列を + 演算子で結合すると、通常、新しい文字列が作成され、そのためのメモリがヒープに割り当てられます。例えば、"a" + "b" + "c" は、中間的に "ab" という文字列が作成され、その後 "abc" が作成されるため、複数のメモリ割り当てが発生する可能性があります。

4. キャッシュの利用

キャッシュは、頻繁にアクセスされるデータを一時的に保存し、その後のアクセスを高速化する手法です。一度計算または生成されたデータをメモリに保持しておくことで、同じデータを再度必要としたときに再計算・再生成するコストを削減できます。

5. sync.RWMutex

sync.RWMutex はGoの標準ライブラリが提供する読み書きロック(Read-Write Mutex)です。

  • 読み取りロック (RLock/RUnlock): 複数のゴルーチンが同時に読み取りロックを取得できます。データが読み取り専用でアクセスされる場合に利用します。
  • 書き込みロック (Lock/Unlock): 一度に1つのゴルーチンのみが書き込みロックを取得できます。書き込みロックが取得されている間は、他の読み取りロックや書き込みロックは取得できません。 これは、共有データ構造(この場合は statusLines マップ)への並行アクセスを安全に管理するために使用されます。読み取り操作が書き込み操作よりもはるかに多い場合に特に有効です。

技術的詳細

このコミットの主要な変更点は、HTTPレスポンスのステータスライン文字列を生成する際に、毎回新しい文字列を構築するのではなく、キャッシュを利用するようにしたことです。

以前の実装では、chunkWriter.writeHeader 関数内で、HTTPプロトコルバージョン、ステータスコード、および理由フレーズを結合してステータスライン文字列を動的に生成していました。この文字列結合は、リクエストごとに新しい文字列オブジェクトのメモリ割り当てを伴いました。

新しい実装では、以下の要素が導入されています。

  1. statusLines キャッシュ:

    • var statusLines = make(map[int]string): グローバルな map[int]string 型の変数 statusLines が導入されました。これは、生成されたステータスライン文字列をキャッシュするために使用されます。
    • キーは int 型で、HTTPプロトコルバージョン(HTTP/1.0かHTTP/1.1か)とステータスコードを区別するために工夫されています。HTTP/1.1の場合はステータスコードをそのままキーとし、HTTP/1.0の場合はステータスコードを負の値にしてキーとすることで、同じステータスコードでもプロトコルバージョンが異なる場合に別のエントリとして扱われます。
    • var statusMu sync.RWMutex: statusLines マップへの並行アクセスを安全にするために、読み書きロック statusMu が導入されました。
  2. statusLine ヘルパー関数:

    • func statusLine(req *Request, code int) string: 新しいヘルパー関数 statusLine が追加されました。この関数が、実際のステータスライン文字列の取得とキャッシュ管理を担当します。
    • 高速パス (Fast path): まず、statusLines キャッシュを読み取りロック (statusMu.RLock()) を使って参照し、目的のステータスラインが既にキャッシュされているかを確認します。キャッシュにあれば、その文字列を直接返します。これにより、メモリ割り当てと文字列構築のコストを回避できます。
    • 低速パス (Slow path): キャッシュに存在しない場合、ステータスライン文字列を構築します。この際、strconv.Itoa でステータスコードを文字列に変換し、statusText マップから理由フレーズを取得します。構築された文字列は、書き込みロック (statusMu.Lock()) を使って statusLines キャッシュに保存されます。これにより、次回同じステータスラインが必要になったときに高速パスを利用できるようになります。
    • if ok { statusMu.Lock(); defer statusMu.Unlock(); statusLines[key] = line }: キャッシュへの書き込みは、statusText に対応する理由フレーズが存在する場合(つまり、既知の標準的なステータスコードの場合)にのみ行われます。これにより、カスタムのステータスコードやエラーメッセージがキャッシュを不必要に膨らませるのを防ぎます。

この変更により、ほとんどのリクエストではキャッシュされた文字列が再利用されるため、リクエストごとに発生していたステータスライン文字列の構築とそれに伴うメモリ割り当てが削減されます。コミットメッセージのベンチマーク結果が示すように、これにより allocs が2つ削減され、全体的なパフォーマンスが向上しています。

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

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -763,8 +763,40 @@ func (cw *chunkWriter) writeHeader(p []byte) {
 		cw.header.Set("Connection", "close")
 	}
 
+	io.WriteString(w.conn.buf, statusLine(w.req, code))
+	cw.header.Write(w.conn.buf)
+	w.conn.buf.Write(crlf)
+}
+
+// statusLines is a cache of Status-Line strings, keyed by code (for
+// HTTP/1.1) or negative code (for HTTP/1.0). This is faster than a
+// map keyed by struct of two fields. This map's max size is bounded
+// by 2*len(statusText), two protocol types for each known official
+// status code in the statusText map.
+var (
+	statusMu    sync.RWMutex
+	statusLines = make(map[int]string)
+)
+
+// statusLine returns a response Status-Line (RFC 2616 Section 6.1)
+// for the given request and response status code.
+func statusLine(req *Request, code int) string {
+	// Fast path:
+	key := code
+	proto11 := req.ProtoAtLeast(1, 1)
+	if !proto11 {
+		key = -key
+	}
+	statusMu.RLock()
+	line, ok := statusLines[key]
+	statusMu.RUnlock()
+	if ok {
+		return line
+	}
+
+	// Slow path:
 	proto := "HTTP/1.0"
-	if w.req.ProtoAtLeast(1, 1) {
+	if proto11 {
 		proto = "HTTP/1.1"
 	}
 	codestring := strconv.Itoa(code)
@@ -772,9 +804,13 @@ func (cw *chunkWriter) writeHeader(p []byte) {
 	if !ok {
 		text = "status code " + codestring
 	}
-	io.WriteString(w.conn.buf, proto+" "+codestring+" "+text+"\r\n")
-	cw.header.Write(w.conn.buf)
-	w.conn.buf.Write(crlf)
+	line = proto + " " + codestring + " " + text + "\r\n"
+	if ok {
+		statusMu.Lock()
+		defer statusMu.Unlock()
+		statusLines[key] = line
+	}
+	return line
 }
 
 // bodyAllowed returns true if a Write is allowed for this response type.

コアとなるコードの解説

変更前 (src/pkg/net/http/server.go の一部)

// ...
io.WriteString(w.conn.buf, proto+" "+codestring+" "+text+"\r\n")
cw.header.Write(w.conn.buf)
w.conn.buf.Write(crlf)
// ...

変更前は、chunkWriter.writeHeader 関数内で、HTTPステータスライン(例: HTTP/1.1 200 OK\r\n)を構成する文字列が、proto, codestring, text を結合することで直接生成されていました。この文字列結合操作は、リクエストごとに新しい文字列オブジェクトをヒープに割り当てる原因となっていました。

変更後 (src/pkg/net/http/server.go の一部)

// ...
io.WriteString(w.conn.buf, statusLine(w.req, code)) // 新しいヘルパー関数を呼び出す
cw.header.Write(w.conn.buf)
w.conn.buf.Write(crlf)

// statusLines is a cache of Status-Line strings, keyed by code (for
// HTTP/1.1) or negative code (for HTTP/1.0). This is faster than a
// map keyed by struct of two fields. This map's max size is bounded
// by 2*len(statusText), two protocol types for each known official
// status code in the statusText map.
var (
	statusMu    sync.RWMutex
	statusLines = make(map[int]string)
)

// statusLine returns a response Status-Line (RFC 2616 Section 6.1)
// for the given request and response status code.
func statusLine(req *Request, code int) string {
	// Fast path:
	key := code
	proto11 := req.ProtoAtLeast(1, 1)
	if !proto11 {
		key = -key // HTTP/1.0の場合はキーを負の値にする
	}
	statusMu.RLock() // 読み取りロック
	line, ok := statusLines[key]
	statusMu.RUnlock() // 読み取りロック解除
	if ok {
		return line // キャッシュヒット
	}

	// Slow path:
	proto := "HTTP/1.0"
	if proto11 {
		proto = "HTTP/1.1"
	}
	codestring := strconv.Itoa(code)
	text, ok := statusText[code]
	if !ok {
		text = "status code " + codestring
	}
	line = proto + " " + codestring + " " + text + "\r\n" // 文字列構築
	if ok { // 既知のステータスコードの場合のみキャッシュ
		statusMu.Lock() // 書き込みロック
		defer statusMu.Unlock() // 関数終了時に書き込みロック解除
		statusLines[key] = line
	}
	return line
}

変更後、chunkWriter.writeHeader は直接文字列を構築する代わりに、新しく導入された statusLine ヘルパー関数を呼び出すようになりました。

statusLine 関数は以下のロジックで動作します。

  1. キャッシュキーの生成: HTTPプロトコルバージョン(HTTP/1.0かHTTP/1.1か)とステータスコードを組み合わせた整数 key を生成します。HTTP/1.0の場合はステータスコードを負の値にすることで、HTTP/1.1の場合と区別します。
  2. キャッシュの参照 (Fast path): statusMu.RLock() を使用して読み取りロックを取得し、statusLines マップから key に対応するステータスライン文字列を検索します。見つかれば、それを返して処理を終了します。これにより、文字列構築とメモリ割り当てを回避できます。
  3. 文字列の構築とキャッシュ (Slow path): キャッシュに存在しない場合、proto, codestring, text を結合して新しいステータスライン文字列 line を構築します。
  4. キャッシュへの追加: 構築された line は、statusText に対応する理由フレーズが存在する場合(つまり、標準的なステータスコードの場合)にのみ、statusMu.Lock() を使用して書き込みロックを取得し、statusLines マップに保存されます。これにより、同じステータスラインが次回必要になったときにキャッシュから取得できるようになります。

この変更により、HTTPステータスラインの生成におけるメモリ割り当てが大幅に削減され、特に多数のリクエストを処理するサーバーにおいて、パフォーマンスの向上が期待できます。

関連リンク

参考にした情報源リンク