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

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

このコミットは、Go言語の標準ライブラリであるnet/httpパッケージに、Linux環境でのsendfileシステムコールの利用を検証するためのテストを追加するものです。具体的には、HTTPサーバーが静的ファイルを配信する際に、カーネルレベルでの効率的なデータ転送メカニズムであるsendfileが適切に使用されていることを確認するためのテストケースが導入されました。

コミット

commit b8df36182d7321201d3985a4b3d8ca1c0faf63d2
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue Feb 14 09:34:52 2012 +1100

    net/http: add a Linux-only sendfile test
    
    I remembered that sendfile support was lacking a test.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5652079

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

https://github.com/golang/go/commit/b8df36182d7321201d3985a4b3d8ca1c0faf63d2

元コミット内容

このコミットは、Go言語のnet/httpパッケージに、Linux専用のsendfileテストを追加します。コミットメッセージによると、作者はsendfileのサポートにはテストが不足していることを思い出し、そのためにこのテストを追加したとのことです。

変更の背景

Go言語のnet/httpパッケージは、Webサーバー機能を提供する上で、静的ファイルの効率的な配信が重要な要素となります。多くのモダンなオペレーティングシステム、特にLinuxでは、sendfileのようなシステムコールを提供しており、これによりユーザー空間を介さずにカーネル空間内で直接ファイルデータをソケットに転送することが可能になります。これは「ゼロコピー」と呼ばれる技術であり、CPUオーバーヘッドの削減やメモリコピーの回数減少により、I/O性能を大幅に向上させることができます。

しかし、このような最適化が実際に機能していることを保証するためには、適切なテストが必要です。このコミットが作成された時点では、net/httpパッケージのsendfileサポートが期待通りに動作していることを検証するテストが存在しなかったため、作者はこれを追加する必要性を認識しました。テストがない場合、将来の変更によってsendfileの利用が意図せず無効になったり、パフォーマンス上の利点が失われたりするリスクがあります。このテストの追加は、net/httpパッケージの堅牢性とパフォーマンス保証を向上させることを目的としています。

前提知識の解説

Go言語のnet/httpパッケージ

net/httpパッケージは、Go言語の標準ライブラリの一部であり、HTTPクライアントとサーバーの実装を提供します。WebアプリケーションやAPIサーバーを構築する際に中心的な役割を果たし、ルーティング、ミドルウェア、静的ファイルの配信など、HTTPプロトコルに関連する幅広い機能を提供します。http.FileServerは、指定されたディレクトリから静的ファイルを配信するためのハンドラを提供します。

sendfileシステムコール

sendfileは、Unix系オペレーティングシステム(特にLinux)で利用可能なシステムコールです。その主な目的は、ファイルディスクリプタから別のファイルディスクリプタへデータを直接転送することです。Webサーバーの文脈では、ファイル(例: 静的コンテンツ)からネットワークソケットへデータを転送する際に使用されます。

  • 目的: ユーザー空間のバッファを介さずに、カーネル空間内で直接データを転送することで、データコピーの回数を減らし、I/O性能を向上させます。
  • 利点:
    • ゼロコピー: 通常のファイル読み込みとソケット書き込みでは、データがカーネルバッファからユーザーバッファへ、そして再びカーネルバッファへと複数回コピーされます。sendfileはこれらのコピーを省略し、CPUサイクルとメモリ帯域幅を節約します。
    • パフォーマンス向上: 特に大量の静的ファイルを配信するWebサーバーにおいて、スループットの向上とレイテンシの削減に寄与します。
  • 動作原理: sendfileが呼び出されると、カーネルはファイルの内容を直接ディスクから読み込み、それをネットワークスタックのバッファに直接コピーします。このプロセス中に、データはユーザー空間のアプリケーションメモリに一度もコピーされません。
  • OS依存性: sendfileはPOSIX標準の一部ではなく、OSによって実装が異なります。Linuxではsendfile()、FreeBSD/macOSではsendfile()、WindowsではTransmitFile()など、類似の機能が提供されています。このコミットのテストはLinuxに特化しています。

Goのテストフレームワークとヘルパープロセス

Go言語には、標準でtestingパッケージが提供されており、ユニットテスト、ベンチマークテスト、例のテストなどを記述できます。

  • testingパッケージ: go testコマンドによって実行されるテスト関数(TestXxxという名前の関数)を定義します。
  • ヘルパープロセス: 複雑なテストシナリオ(例: ネットワーク通信、プロセス間通信、環境変数のテスト)では、テスト対象のコードとは別のプロセスを起動してテストを行うことがあります。Goでは、os.Args[0](現在の実行可能ファイルのパス)を使って自身を再起動し、特定の環境変数(例: GO_WANT_HELPER_PROCESS)を設定することで、そのプロセスがヘルパープロセスとして動作するように制御するパターンがよく使われます。これにより、テストコードとヘルパープロセスのコードを同じバイナリ内に含めることができます。

straceコマンド

straceはLinuxで利用可能なコマンドラインツールで、プロセスが実行するシステムコールと、それらのシステムコールに渡されるシグナルをトレース(追跡)します。

  • 目的: プログラムの動作をデバッグしたり、パフォーマンスの問題を特定したり、セキュリティ上の問題を調査したりするために使用されます。どのシステムコールがどのような引数で呼び出され、どのような結果を返したかを詳細に表示します。
  • 使い方: strace -p <PID>で実行中のプロセスをトレースしたり、strace <command>で新しいコマンドを起動してトレースしたりできます。-fオプションは、トレース対象のプロセスがフォークした子プロセスも追跡するために使用されます。
  • このテストでの利用: sendfileシステムコールが実際に呼び出されていることを検証するために、HTTPサーバーとして動作するGoのヘルパープロセスをstraceで監視します。

技術的詳細

このコミットで追加されたテストは、src/pkg/net/http/fs_test.goファイル内のTestLinuxSendfile関数とTestLinuxSendfileChild関数によって構成されています。

  1. TestLinuxSendfile (親テスト):

    • OSチェック: まず、runtime.GOOS != "linux"で現在のOSがLinuxでない場合はテストをスキップします。これはsendfileのOS依存性によるものです。
    • straceの存在チェック: exec.LookPath("strace")でシステムにstraceコマンドが存在するかを確認します。存在しない場合もテストをスキップします。
    • リスナーの準備: net.Listen("tcp", "127.0.0.1:0")でTCPリスナーを作成し、動的にポートを割り当てます。
    • ファイルディスクリプタの継承: ln.(*net.TCPListener).File()を使ってリスナーのファイルディスクリプタ(*os.File)を取得します。このファイルディスクリプタは、子プロセスに渡すために使用されます。
    • ヘルパープロセスの起動:
      • exec.Command(os.Args[0], "-test.run=TestLinuxSendfileChild")で、現在のテストバイナリ自身を再実行し、TestLinuxSendfileChild関数のみを実行するように指示します。
      • child.ExtraFiles = append(child.ExtraFiles, lnf)で、親プロセスで作成したリスナーのファイルディスクリプタを子プロセスに継承させます。これにより、子プロセスは親プロセスがバインドしたソケットを再利用できます。
      • child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)で、GO_WANT_HELPER_PROCESS=1という環境変数を設定します。これは、子プロセスがヘルパープロセスとして動作していることを識別するためのフラグです。
      • child.Start()で子プロセスを起動します。
    • straceによるトレース:
      • strace := exec.Command("strace", "-f", "-p", strconv.Itoa(pid))で、起動したヘルパープロセス(pidで指定)をトレースするためにstraceコマンドを準備します。-fは子プロセスもトレース対象に含めることを意味し、-pは特定のPIDをトレースすることを意味します。
      • strace.Stdout = &bufおよびstrace.Stderr = &bufで、straceの出力をbytes.Bufferにリダイレクトし、後で解析できるようにします。
      • strace.Start()straceを起動します。
    • HTTPリクエストの送信: Get(fmt.Sprintf("http://%s/", ln.Addr()))で、ヘルパープロセスがリッスンしているアドレスに対してHTTP GETリクエストを送信します。これにより、ヘルパープロセスは静的ファイル(testdataディレクトリ内のファイル)を配信しようとし、その過程でsendfileが呼び出されることが期待されます。
    • ヘルパープロセスの終了: Get(fmt.Sprintf("http://%s/quit", ln.Addr()))で、ヘルパープロセスに終了を指示する特別なエンドポイントにリクエストを送信します。
    • プロセスの待機: child.Wait()strace.Wait()で、子プロセスとstraceプロセスの終了を待ちます。
    • strace出力の検証:
      • regexp.MustCompileを使って、straceの出力からsendfileシステムコールが呼び出されたことを示すパターン(sendfile(\d+,\s*\d+,\s*NULL,\s*\d+)=\s*\d+\s*\nまたは<... sendfile resumed> )=\s*\d+\s*\n)を検索します。
      • これらのパターンが見つからない場合、t.Errorfでテストを失敗させます。
  2. TestLinuxSendfileChild (ヘルパープロセス):

    • ヘルパープロセス識別: os.Getenv("GO_WANT_HELPER_PROCESS") != "1"で、この関数がヘルパープロセスとして起動されたかどうかを確認します。そうでなければすぐにリターンします。
    • ファイルディスクリプタの再構築: os.NewFile(3, "ephemeral-port-listener")で、親プロセスから継承されたファイルディスクリプタ(ファイルディスクリプタ番号3)を*os.Fileとして再構築します。
    • リスナーの再構築: net.FileListener(fd3)で、再構築したファイルディスクリプタからnet.Listenerを生成します。これにより、子プロセスは親プロセスがバインドしたソケット上でリッスンを継続できます。
    • HTTPハンドラの登録:
      • NewServeMux()で新しいHTTPマルチプレクサを作成します。
      • mux.Handle("/", FileServer(Dir("testdata")))で、ルートパス(/)に対してtestdataディレクトリの内容を配信するFileServerハンドラを登録します。
      • mux.HandleFunc("/quit", ...)で、/quitパスにアクセスがあった場合にos.Exit(0)を呼び出してプロセスを終了させるハンドラを登録します。これは親テストがヘルパープロセスをクリーンに終了させるために使用します。
    • サーバーの起動: s.Serve(ln)で、再構築したリスナー上でHTTPサーバーを起動します。

このテストの巧妙な点は、straceという外部ツールとGoのテストヘルパープロセス機能を組み合わせて、Goのnet/httpパッケージが内部的にsendfileシステムコールを利用していることを、実際のシステムコールレベルで検証している点です。

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

変更はsrc/pkg/net/http/fs_test.goファイルに集中しており、主に以下の2つの新しい関数が追加されています。

  1. TestLinuxSendfile関数:
    // verifies that sendfile is being used on Linux
    func TestLinuxSendfile(t *testing.T) {
        // ... (OS/straceチェック、リスナー作成、子プロセス起動、strace起動、HTTPリクエスト送信、strace出力検証のロジック)
    }
    
  2. TestLinuxSendfileChild関数:
    // TestLinuxSendfileChild isn't a real test. It's used as a helper process
    // for TestLinuxSendfile.
    func TestLinuxSendfileChild(*testing.T) {
        // ... (ヘルパープロセス識別、ファイルディスクリプタ再構築、リスナー再構築、HTTPハンドラ登録、サーバー起動のロジック)
    }
    

これらの関数は、既存のTestServeContent関数の後に追記されています。

コアとなるコードの解説

TestLinuxSendfileの解説

この関数は、net/httpパッケージがLinux上でsendfileシステムコールを適切に使用していることを検証する親テストです。

  1. 環境チェック:

    • if runtime.GOOS != "linux": Goの実行環境がLinuxでなければ、このテストはスキップされます。sendfileの動作はOSに依存するためです。
    • _, err := exec.LookPath("strace"): straceコマンドがシステムパスに存在するかを確認します。straceはシステムコールをトレースするために不可欠なツールであり、存在しない場合はテストをスキップします。
  2. リスナーの準備と子プロセスへの継承:

    • ln, err := net.Listen("tcp", "127.0.0.1:0"): ローカルホストの利用可能なポートでTCPリスナーを作成します。
    • lnf, err := ln.(*net.TCPListener).File(): 作成したTCPリスナーから、その基となるファイルディスクリプタ(*os.File型)を取得します。
    • child := exec.Command(os.Args[0], "-test.run=TestLinuxSendfileChild"): 現在実行中のテストバイナリ自身を、TestLinuxSendfileChild関数のみを実行するように指定して、新しいプロセスとして起動するコマンドを作成します。
    • child.ExtraFiles = append(child.ExtraFiles, lnf): 親プロセスで作成したリスナーのファイルディスクリプタを、子プロセスに継承させるように設定します。これにより、子プロセスは親プロセスがバインドしたソケットを再利用できます。
    • child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...): 子プロセスがヘルパープロセスであることを示す環境変数GO_WANT_HELPER_PROCESS=1を設定します。
  3. straceによるシステムコールトレース:

    • strace := exec.Command("strace", "-f", "-p", strconv.Itoa(pid)): 起動した子プロセス(pidで指定)のシステムコールをトレースするためにstraceコマンドを準備します。-fは子プロセスがさらにフォークした場合もトレースを継続し、-pは特定のプロセスIDをトレースします。
    • strace.Stdout = &buf / strace.Stderr = &buf: straceの標準出力と標準エラー出力をbytes.Bufferにリダイレクトし、後でその内容を解析できるようにします。
  4. HTTPリクエストと検証:

    • _, err = Get(fmt.Sprintf("http://%s/", ln.Addr())): ヘルパープロセスがリッスンしているアドレスに対してHTTP GETリクエストを送信します。このリクエストにより、ヘルパープロセスは静的ファイル(testdataディレクトリ内のファイル)を配信しようとします。
    • Get(fmt.Sprintf("http://%s/quit", ln.Addr())): ヘルパープロセスに終了を指示する/quitエンドポイントにリクエストを送信します。
    • child.Wait() / strace.Wait(): 子プロセスとstraceプロセスの終了を待ちます。
    • rx := regexp.MustCompile(...) / rxResume := regexp.MustCompile(...): straceの出力からsendfileシステムコールが呼び出されたことを示す正規表現パターンを定義します。
    • if !rx.MatchString(out) && !rxResume.MatchString(out): straceの出力にsendfileシステムコールのパターンが見つからない場合、テストは失敗し、エラーメッセージが表示されます。

TestLinuxSendfileChildの解説

この関数は、TestLinuxSendfileによって新しいプロセスとして起動されるヘルパープロセスです。

  1. ヘルパープロセスの識別:

    • if os.Getenv("GO_WANT_HELPER_PROCESS") != "1": 環境変数GO_WANT_HELPER_PROCESS"1"でない場合、この関数は通常のテスト実行の一部ではないと判断し、すぐにリターンします。これにより、この関数がgo testによって直接実行されることを防ぎます。
    • defer os.Exit(0): 関数が終了する際にプロセスを正常終了させます。
  2. 継承されたリスナーの再構築:

    • fd3 := os.NewFile(3, "ephemeral-port-listener"): 親プロセスから継承されたファイルディスクリプタ(ファイルディスクリプタ番号3)を*os.Fileとして再構築します。Goのos/execパッケージでExtraFilesを使用すると、ファイルディスクリプタは子プロセスで3から始まる番号で利用可能になります(0, 1, 2はstdin, stdout, stderr)。
    • ln, err := net.FileListener(fd3): 再構築した*os.Fileからnet.Listenerを生成します。これにより、子プロセスは親プロセスがバインドしたソケット上でHTTPリクエストをリッスンできます。
  3. HTTPサーバーのセットアップと起動:

    • mux := NewServeMux(): 新しいHTTPリクエストマルチプレクサ(ルーター)を作成します。
    • mux.Handle("/", FileServer(Dir("testdata"))): ルートパス(/)へのリクエストに対して、testdataディレクトリ内のファイルを配信するhttp.FileServerハンドラを登録します。このFileServerが内部的にsendfileを利用することが期待されます。
    • mux.HandleFunc("/quit", ...): /quitパスへのリクエストを受け取ると、os.Exit(0)を呼び出してプロセスを終了させるハンドラを登録します。これは親テストがヘルパープロセスを制御するために使用します。
    • s := &Server{Handler: mux}: 作成したマルチプレクサをハンドラとして持つHTTPサーバーインスタンスを作成します。
    • err = s.Serve(ln): 再構築したリスナー上でHTTPサーバーを起動し、リクエストの処理を開始します。

この二つの関数が連携することで、Goのnet/httpパッケージが静的ファイル配信時にsendfileシステムコールを実際に利用していることを、外部ツールstraceを用いて低レベルで検証する、堅牢なテストが実現されています。

関連リンク

参考にした情報源リンク