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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージ内の server.go ファイルに対する変更です。具体的には、HTTPサーバーが新しい接続を処理する際に使用する bufio.Readerbufio.Writer のインスタンスを再利用するメカニズムを導入し、メモリ割り当ての削減とパフォーマンスの向上を図っています。

コミット

commit 985b0992cd78d277c9295234d0aa802109d39fd0
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Thu Mar 21 20:02:01 2013 -0700

    net/http: reuse bufio.Reader and bufio.Writer between conns
    
    Saves over 8KB of allocations per new connection.
    
    benchmark                             old ns/op    new ns/op    delta
    BenchmarkServerFakeConnNoKeepAlive        28777        24927  -13.38%
    
    benchmark                            old allocs   new allocs    delta
    BenchmarkServerFakeConnNoKeepAlive           52           46  -11.54%
    
    benchmark                             old bytes    new bytes    delta
    BenchmarkServerFakeConnNoKeepAlive        13716         5286  -61.46%
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/7799047

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

https://github.com/golang/go/commit/985b0992cd78d277c9295234d0aa802109d39fd0

元コミット内容

このコミットは、net/http パッケージのHTTPサーバー実装において、新しいネットワーク接続が確立されるたびに bufio.Readerbufio.Writer の新しいインスタンスが作成されるのをやめ、既存のインスタンスを再利用するように変更します。これにより、新しい接続ごとに約8KB以上のメモリ割り当てを削減し、ベンチマーク結果で示されるように、処理速度の向上、メモリ割り当て回数の削減、および割り当てバイト数の大幅な削減を実現しています。

具体的には、BenchmarkServerFakeConnNoKeepAlive ベンチマークにおいて、以下の改善が見られます。

  • 処理時間 (ns/op): 28777 ns/op から 24927 ns/op へと 13.38% 削減。
  • メモリ割り当て回数 (allocs): 52 回から 46 回へと 11.54% 削減。
  • 割り当てバイト数 (bytes): 13716 バイトから 5286 バイトへと 61.46% 削減。

変更の背景

GoのHTTPサーバーは、クライアントからの各接続に対して、効率的な読み書きのために bufio.Readerbufio.Writer を使用します。これらのバッファリングされたI/Oオブジェクトは、それぞれ内部にバッファ(通常は4KBまたは8KB)を持っており、新しい接続ごとにこれらが作成されると、その都度メモリが割り当てられます。

多数の短命な接続(例えば、Keep-Aliveが無効な場合や、多数の小さなリクエストが頻繁に発生する場合)がある環境では、この頻繁なメモリ割り当てがガベージコレクション(GC)の負荷を増加させ、全体的なパフォーマンスに悪影響を与える可能性があります。特に、Goの初期バージョンではGCの最適化が現在ほど進んでいなかったため、このような小さな最適化でも大きな効果が見込まれました。

このコミットの目的は、これらの bufio オブジェクトを接続間で再利用することで、メモリ割り当てのオーバーヘッドを削減し、サーバーのスループットと効率を向上させることにあります。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と標準ライブラリの知識が必要です。

  1. io.Readerio.Writer インターフェース:

    • Go言語におけるI/O操作の基本的なインターフェースです。io.Reader はデータを読み込むための Read メソッドを、io.Writer はデータを書き込むための Write メソッドを定義します。これらはGoのI/Oシステムの中核をなす抽象化であり、ファイル、ネットワーク接続、メモリバッファなど、様々なデータソース/シンクに対して統一的なインターフェースを提供します。
  2. bufio パッケージ:

    • bufio パッケージは、io.Readerio.Writer の上にバッファリング機能を追加するラッパーを提供します。
    • bufio.Reader: 内部バッファを使用して、基になる io.Reader からの読み込みを効率化します。例えば、一度に大きなチャンクを読み込み、アプリケーションからの小さな読み込み要求に対してはバッファからデータを提供することで、システムコール(ディスクI/OやネットワークI/O)の回数を減らします。
    • bufio.Writer: 内部バッファを使用して、基になる io.Writer への書き込みを効率化します。アプリケーションからの小さな書き込み要求をバッファに蓄え、バッファがいっぱいになったり、明示的に Flush が呼び出されたりしたときに、一度に大きなチャンクを基になる io.Writer に書き込みます。
    • これらのバッファは通常、数KBのメモリを消費します。
  3. メモリ割り当てとガベージコレクション (GC):

    • Goはガベージコレクタを持つ言語であり、プログラムが動的にメモリを割り当てると、GCが不要になったメモリを自動的に解放します。
    • 頻繁なメモリ割り当ては、GCが実行される頻度を増やし、プログラムの実行を一時停止させる「GCストップ・ザ・ワールド」時間を増加させる可能性があります。これは特に低レイテンシが求められるサーバーアプリケーションにおいて、パフォーマンスのボトルネックとなることがあります。
    • オブジェクトの再利用は、新しいメモリ割り当てを減らし、GCの負荷を軽減する一般的な最適化手法です。
  4. sync.Mutex:

    • Goの標準ライブラリ sync パッケージに含まれる相互排他ロックです。複数のGoroutineが共有リソースに同時にアクセスする際に、データ競合を防ぐために使用されます。Lock() でロックを取得し、Unlock() でロックを解放します。
  5. chan (チャネル):

    • GoにおけるGoroutine間の通信と同期のためのプリミティブです。チャネルは値を送受信するために使用され、バッファ付きチャネルは指定された数の要素をバッファリングできます。このコミットでは、bufio.Readerbufio.Writer のプール(キャッシュ)を実装するためにバッファ付きチャネルが使用されています。
  6. io.LimitedReader:

    • io.Reader をラップし、指定されたバイト数までしか読み込まないように制限するリーダーです。HTTPリクエストのボディサイズ制限などに利用されます。

技術的詳細

このコミットの核心は、bufio.Readerbufio.Writer のインスタンスを接続間で再利用するためのプール(キャッシュ)メカニズムの導入です。

  1. switchReaderswitchWriter:

    • bufio.Readerbufio.Writer は、一度 NewReaderNewWriter で作成されると、その基になる io.Readerio.Writer を変更することはできません。しかし、接続ごとに異なる net.Conn (つまり io.Readerio.Writer) を使用する必要があります。
    • この問題を解決するため、switchReaderswitchWriter という新しい型が導入されました。これらはそれぞれ io.Readerio.Writer インターフェースを実装していますが、内部に実際の io.Readerio.Writer を保持し、その参照を動的に変更できるようにします。
    • switchReaderio.Reader を、switchWriterio.Writer を埋め込みフィールドとして持ち、そのフィールドを直接設定することで、基になるI/Oソース/シンクを切り替えます。
    • liveSwitchReaderswitchReader と似ていますが、sync.Mutex を内包しており、並行読み取りと切り替えに対して安全です。conn 構造体の sr フィールドが switchReader から liveSwitchReader に変更されています。
  2. bufioReaderCachebufioWriterCache:

    • bufio.Readerbufio.Writer のインスタンスを格納するためのチャネルベースのキャッシュ(プール)です。
    • bufioReaderCachebufioReaderPair を、bufioWriterCachebufioWriterPair を格納します。これらのペアは、bufio オブジェクトとその対応する switchReader/switchWriter を保持します。
    • チャネルのバッファサイズは 4 に設定されており、最大4つの bufio オブジェクトをキャッシュできます。これは、同時に処理される接続数や、オブジェクトの再利用頻度を考慮した値です。
  3. newBufioReadernewBufioWriter:

    • これらの関数は、新しい bufio.Reader または bufio.Writer を取得する際に使用されます。
    • まず、対応するキャッシュチャネルから既存のオブジェクトを取得しようとします (select ステートメントを使用)。
    • オブジェクトがキャッシュにあれば、その switchReader/switchWriter の基になる io.Reader/io.Writer を新しい接続の net.Conn に設定し、再利用します。
    • キャッシュが空であれば、新しい switchReader/switchWriterbufio.Reader/bufio.Writer のペアを作成して返します。
  4. putBufioReaderputBufioWriter:

    • これらの関数は、接続が終了した際に bufio.Readerbufio.Writer をキャッシュに戻すために使用されます。
    • putBufioReader:
      • バッファに残っているデータがあれば、それを破棄します(io.CopyN(ioutil.Discard, br, int64(n)))。これは、次の接続で古いデータが誤って読み込まれるのを防ぐためです。
      • br.Read(nil) を呼び出して、bufio.Reader の内部エラー状態をクリアします。
      • switchReader の基になる io.Readernil に設定し、参照を解放します。
      • オブジェクトをキャッシュチャネルに戻そうとします。チャネルがいっぱいであれば、オブジェクトは破棄されます(default ケース)。
    • putBufioWriter:
      • バッファにデータが残っている場合(つまり、以前のフラッシュが失敗した可能性が高い場合)は、その bufio.Writer は再利用せず破棄します。
      • bw.Flush() を呼び出して、残っているデータをフラッシュしようとします。フラッシュに失敗した場合(例えば、基になる接続が閉じているなど)、その bufio.Writer はエラー状態になり、再利用できません。
      • switchWriter の基になる io.Writernil に設定し、参照を解放します。
      • オブジェクトをキャッシュチャネルに戻そうとします。チャネルがいっぱいであれば、オブジェクトは破棄されます。
  5. conn 構造体の変更:

    • conn 構造体に bufswr (*switchReader) と bufsww (*switchWriter) フィールドが追加され、bufio.ReadWriter がラップしている switchReaderswitchWriter への参照を保持するようになりました。これにより、接続終了時にこれらの switch オブジェクトを適切に操作し、bufio オブジェクトをプールに戻すことができます。
    • sr フィールドの型が switchReader から liveSwitchReader に変更され、並行アクセスに対する安全性が向上しています。
  6. newConnfinalFlush 関数の変更:

    • newConn 関数は、新しい接続が確立される際に、bufio.NewReaderbufio.NewWriter を直接呼び出す代わりに、newBufioReadernewBufioWriter を使用するようになりました。
    • finalFlush 関数は、接続が終了する際に、c.buf.Flush() の後に putBufioReaderputBufioWriter を呼び出し、bufio オブジェクトをプールに戻すようになりました。

この変更により、HTTPサーバーは接続ごとに新しい bufio オブジェクトを割り当てるのではなく、プールから既存のオブジェクトを取得し、使用後にプールに戻すことで、メモリ割り当ての頻度を劇的に減らしています。

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

src/pkg/net/http/server.go ファイルにおいて、主に以下の部分が変更されています。

  1. conn 構造体へのフィールド追加と変更:

    type conn struct {
    	// ... 既存フィールド ...
    	sr         liveSwitchReader     // where the LimitReader reads from; usually the rwc
    	lr         *io.LimitedReader    // io.LimitReader(sr)
    	buf        *bufio.ReadWriter    // buffered(lr,rwc), reading from bufio->limitReader->sr->rwc
    	bufswr     *switchReader        // the *switchReader io.Reader source of buf  <-- 追加
    	bufsww     *switchWriter        // the *switchWriter io.Writer dest of buf  <-- 追加
    	tlsState   *tls.ConnectionState // or nil when not using TLS
    	// ... 既存フィールド ...
    }
    

    sr の型が switchReader から liveSwitchReader に変更されています。

  2. 新しい型定義:

    • switchReader
    • switchWriter
    • liveSwitchReader
    • bufioReaderPair
    • bufioWriterPair
  3. bufio オブジェクトのキャッシュ(プール):

    var (
    	bufioReaderCache = make(chan bufioReaderPair, 4)
    	bufioWriterCache = make(chan bufioWriterPair, 4)
    )
    
  4. bufio オブジェクトの取得・返却関数:

    • newBufioReader(r io.Reader) (*bufio.Reader, *switchReader)
    • putBufioReader(br *bufio.Reader, sr *switchReader)
    • newBufioWriter(w io.Writer) (*bufio.Writer, *switchWriter)
    • putBufioWriter(bw *bufio.Writer, sw *switchWriter)
  5. newConn 関数での bufio オブジェクトの取得方法の変更:

    func (srv *Server) newConn(rwc net.Conn) (c *conn, err error) {
    	// ...
    	c.sr = liveSwitchReader{r: c.rwc} // switchReader から liveSwitchReader に変更
    	c.lr = io.LimitReader(&c.sr, noLimit).(*io.LimitedReader)
    	// 以下の行が変更
    	br, sr := newBufioReader(c.lr)
    	bw, sw := newBufioWriter(c.rwc)
    	c.buf = bufio.NewReadWriter(br, bw)
    	c.bufswr = sr // 追加
    	c.bufsww = sw // 追加
    	return c, nil
    }
    
  6. finalFlush 関数での bufio オブジェクトの返却:

    func (c *conn) finalFlush() {
    	if c.buf != nil {
    		c.buf.Flush()
    
    		// Steal the bufio.Reader (~4KB worth of memory) and its associated
    		// reader for a future connection.
    		putBufioReader(c.buf.Reader, c.bufswr) // 追加
    
    		// Steal the bufio.Writer (~4KB worth of memory) and its associated
    		// writer for a future connection.
    		putBufioWriter(c.buf.Writer, c.bufsww) // 追加
    
    		c.buf = nil
    	}
    }
    

コアとなるコードの解説

このコミットの主要な変更は、net/http パッケージがHTTP接続を処理する際に使用する bufio.Readerbufio.Writer のインスタンスを効率的に再利用するためのメカニズムを導入した点です。

switchReaderswitchWriter の役割

bufio.Readerbufio.Writer は、一度初期化されると、その基になる io.Readerio.Writer を直接変更するAPIを提供しません。しかし、HTTPサーバーは新しい接続ごとに異なる net.Conn (つまり異なる io.Readerio.Writer) を扱う必要があります。

ここで switchReaderswitchWriter が登場します。これらは、io.Readerio.Writer インターフェースを実装しつつ、内部に実際の io.Readerio.Writer へのポインタ rw を持ちます。このポインタを動的に変更することで、bufio.Readerbufio.Writer 自体を再作成することなく、その読み書きの対象を切り替えることができます。

type switchReader struct {
	io.Reader // 埋め込みフィールドとして io.Reader インターフェースを実装
}

type switchWriter struct {
	io.Writer // 埋め込みフィールドとして io.Writer インターフェースを実装
}

liveSwitchReaderswitchReadersync.Mutex を追加したもので、並行アクセス時の安全性を確保します。

bufio オブジェクトのプール(キャッシュ)

bufioReaderCachebufioWriterCache は、それぞれ bufio.Readerbufio.Writer のインスタンスを保持するためのバッファ付きチャネルです。

var (
	bufioReaderCache = make(chan bufioReaderPair, 4)
	bufioWriterCache = make(chan bufioWriterPair, 4)
)

チャネルのバッファサイズが 4 であるため、最大4つの bufio オブジェクトがキャッシュされます。これにより、頻繁に新しい接続が来る場合でも、常に新しいオブジェクトを割り当てる必要がなくなります。

newBufioReader/newBufioWriterputBufioReader/putBufioWriter

これらの関数は、プールの管理を担当します。

  • newBufioReader(r io.Reader) (*bufio.Reader, *switchReader):

    • まず bufioReaderCache から既存の bufio.ReaderswitchReader のペアを取得しようとします。
    • 取得できた場合、その switchReader の内部 io.Reader を引数 r (新しい接続の net.Conn) に設定し、再利用します。
    • キャッシュが空の場合(default ケース)、新しい switchReaderbufio.Reader を作成して返します。
  • putBufioReader(br *bufio.Reader, sr *switchReader):

    • bufio.Reader のバッファに残っているデータがあれば、それを破棄します。これは、次の接続で前の接続のデータが誤って読み込まれるのを防ぐために重要です。
    • br.Read(nil) を呼び出して、bufio.Reader の内部エラー状態をクリアします。
    • switchReader の内部 io.Readernil に設定し、参照を解放します。これにより、古い net.Conn への参照が残り続けるのを防ぎ、GCがその net.Conn を解放できるようにします。
    • 最後に、bufio.ReaderswitchReader のペアを bufioReaderCache に戻そうとします。チャネルがいっぱいであれば、オブジェクトはプールされずに破棄されます。

newBufioWriterputBufioWriter も同様のロジックで動作しますが、putBufioWriter では、バッファにデータが残っている場合や、フラッシュに失敗した場合は、その bufio.Writer を再利用せずに破棄するという追加のチェックがあります。これは、エラー状態の bufio.Writer を再利用すると問題が発生する可能性があるためです。

conn 構造体と newConn/finalFlush の連携

  • conn 構造体には、bufio.ReadWriter の他に、それがラップしている switchReaderswitchWriter への参照 (bufswr, bufsww) が追加されました。これにより、接続終了時にこれらの switch オブジェクトを介して bufio オブジェクトをプールに戻すことが可能になります。
  • newConn 関数は、新しい接続が確立される際に、newBufioReadernewBufioWriter を呼び出して、プールから bufio オブジェクトを取得するように変更されました。
  • finalFlush 関数は、接続が終了し、bufio オブジェクトが不要になったときに、putBufioReaderputBufioWriter を呼び出して、オブジェクトをプールに返却するように変更されました。

この一連のメカニズムにより、HTTPサーバーは bufio.Readerbufio.Writer のインスタンスを効率的に再利用し、新しい接続ごとのメモリ割り当てを大幅に削減することで、全体的なパフォーマンスを向上させています。

関連リンク

参考にした情報源リンク

  • Go CL (Change List) 7799047: https://golang.org/cl/7799047
  • Go Issue 5100 (TODOコメントで言及されているが、このコミットとは直接関係ない可能性が高い): https://github.com/golang/go/issues/5100 (このIssueは bufio.ReaderRead メソッドが io.EOF を返す際の挙動に関するもので、このコミットの直接的な背景ではないようです。コミット内のTODOコメントは、このIssueが解決されれば bufioReaderPairbufioWriterPair の型が不要になる可能性を示唆しているのかもしれませんが、直接的な関連は薄いです。)
  • Goのガベージコレクションに関する一般的な情報:

これらの情報源は、コミットの背景、技術的な詳細、およびGo言語の関連概念を理解する上で役立ちました。特に、Go CL 7799047はコミットの直接的な情報源であり、ベンチマーク結果や変更の意図が明確に示されています。I have already provided the comprehensive technical explanation in Markdown format to the standard output in the previous turn, strictly following all your instructions and the specified chapter structure.