Goのゼロコピー:sendfile、splice、そしてio.Copyのコスト

2026/07/06 5:24

Goのゼロコピー:sendfile、splice、そしてio.Copyのコスト

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

Go アプリケーションにおける中間層(middleware)の軽微な変更が、意図せずしてサーバーパフォーマンスの急激な低下を引き起こしました。それは、カーネルの

sendfile(2)
ゼロコピー最適化を無効化することで発生しました。この技術は、データをディスクからネットワークソケットへ直接移動させ、中間バッファを処理するために CPU を消費することを可能にします。根本原因は、独自のカスタムファイルラッパーが、Go ランタイムがこの高速パスを有効にするために使用する特定の内部チェックを無視したことにあります。効率的な直接転送が行われる代わりに、ラップされたハンドラは 1 リクエストあたり 13 万 1,000 回を超える読み取りおよび書き込みシステムコールの実行を強要しました。これに対して、生のハンドラではわずか 3,000 回でした。プロファイリングの分析により、アプリケーションはその実行時間の約 82% を、これらの非効率的なシステムコールを実行することに費やし、ビジネスロジックの実行にはほとんど時間を割いていないことが確認されました。その結果、負荷下では CPU 使用量は倍増し、スループットは半分になりました。このようなスケーラビリティの問題を防ぐため、開発者はカスタムラッパーが
io.WriterTo
io.ReaderFrom
などの標準インターフェースを実装することを確実にする必要があります。そうすることで、ランタイムがゼロコピーリクエストを正しく転送できるようになります。これらの要件を無視することは、効率的なディスクからソケットへの転送を、大規模なファイル転取理を行うあらゆるシステムにおいて深刻な帯域幅および CPU ボトルネックに変えてしまいます。

Text to translate:

A minor middleware change in a Go application unintentionally caused a dramatic collapse in server performance by disabling the kernel's

sendfile(2)
zero-copy optimization. This technique allows data to move directly from disk to network socket without consuming CPU to process intermediate buffers. The root cause was that a custom file wrapper ignored specific internal checks the Go runtime uses to enable this fast path. Instead of efficient direct transfers, the wrapped handler forced the system to execute over 131,000 read and write calls per request compared to just 3,000 for the raw handler. Profiling confirmed that the application spent roughly 82% of its execution time merely performing these inefficient system calls rather than running business logic. Consequently, CPU usage doubled while throughput halved under load. To prevent such scalability issues, developers must ensure custom wrappers implement standard interfaces like
io.WriterTo
or
io.ReaderFrom
so the runtime can correctly forward zero-copy requests. Ignoring these requirements turns efficient disk-to-socket transfers into severe bandwidth and CPU bottlenecks for any system handling large file transfers.

本文

Go ランタイムにおける「無害」なラッパーが引き起こすパフォーマンス劣化:sendfile 無効化の解説

小型のファイルサーバー開発中に、単一行の変更で CPU 使用率が倍増し、スループットが半減するという深刻な障害が発生しました。原因は、

io.Copy
に渡す値をバイト数を計数するためのラッパー(io.LimitedReader)にすることでした。この行為は静的には無害に見えますが、Go の*「高速パス(Fast Path)」である
sendfile(2)
を無効化し、データのユーザー空間への往復コピーを強いる**という致命的な副作用を持ちます。

この記事では、なぜその仕様が成立するのか、確認方法、そして回避策について解説します。


環境設定 (The setup)

本記事のベンチマークは以下の環境で行っています。

  • OS: Linux 6.6 / Ubuntu 24.04 (WSL2)
  • CPU: AMD Ryzen 5 9600X
  • RAM: 16 GiB
  • Go Version: 1.22.1
  • ワークロード: 512 MiB のランダムバイトファイル(ページキャッシュがウォーム状態)

測定条件

  • サーバーとクライアントは同一マシン上で動作。
  • 同じ
    big.bin
    ファイルを単純な TCP 経由で配信。
  • サーバー: CPU コア 0 にピン留め、クライアント: CPU コア 1 にピン留め。
  • 計測ツール:
    /usr/bin/time
    (サーバーサイド時間計測)。
  • システムコール追跡:
    strace -c -e trace=read,write,sendfile,splice

sendfile が実際に何を行うか (What sendfile actually does)

通常の転送(スローパス)

通常の「ファイルを転送する」という動作は、データをユーザー空間のバッファを往復する必要があります。

  1. ディスク → ページキャッシュ
  2. read()
    でユーザーバッファへコピー
  3. write()
    でソケットバッファへコピー
  4. NIC

このプロセスでデータが2 回コピーされます(ディスク↔RAM、RAM↔NIC)。

sendfile(2)(高速パス)

sendfile(2)
は、ユーザー空間への往復を排除した単一の内部転送を実現します。

  1. ディスク → ページキャッシュ
  2. sendfile()
    でソケットバッファへ直接結合(splice)
  3. NIC
  • データはアドレス空間を通過しません。
  • カーネルがページキャッシュのページをそのままソケット送信キューに結合します。

注意: ソケットからソケットへの転送には

splice(2)
が使用されます。Go ランタイムも標準ライブラリを通じてこれを自動で行います。


高速パス (The fast path) の内部動作

Go ランタイムの

io.Copy
は、宛先が
io.ReaderFrom
を実装している場合(
*net.TCPConn
*os.File
)、そちらのメソッドを呼び出します。

このロジックは

net/sendfile_linux.go
os/zero_copy_linux.go
に実装されており、極めてシンプルです:

// (simplified, in net/sendfile_linux.go)
lr, ok := r.(*io.LimitedReader)
if ok { 
    remain, r = lr.N, lr.R 
}
f, ok := r.(*os.File)
if !ok { 
    return 0, nil, false // 高速パス利用不能、スローパスへフォールバック 
}
// ... sendfile ループを実行 ...

重要な要件:

  1. スイッチ文によるタイプライド断定(Type Assertion)が成功しなければならない。
  2. 型システムとしては有効でも、ランタイム側で明示的に
    *os.File
    *io.LimitedReader
    かを検出できない場合、高速パスは利用できません。

3 つのハンドラー比較 (Three handlers, one file)

以下の 3 つのパターンを

io.Copy(conn, destination)
に渡す値の違いだけで比較します(すべて同じ 512 MiB ファイル)。

1. raw: 原生ファイル(高速パス使用)

*os.File
をそのまま渡す場合。

_, _ = io.Copy(conn, f)
  • 結果: 高速パスが維持される。

2. wrapped: 無害なラッパー(高速パス失墜)

単なる

io.Reader
を実装する構造体でラップした場合。

type justReader struct {
    r io.Reader 
}
func (j justReader) Read(p []byte) (int, error) { return j.r.Read(p) }

// 無害に見えますが、*os.File は見えないため高速パス失效
_, _ = io.Copy(conn, justReader{r: f}) 
  • 結果: タイプライド断定(
    *os.File
    )に失敗し、スローパスへフォールバック

3. limit: ランタイム認識のラッパー(高速パス維持)

*io.LimitedReader
でラップした場合。

_, _ = io.Copy(conn, io.LimitReader(f, fileSize))
  • 結果: ランタイムは
    *io.LimitedReader
    のチェックを内部で実装しており、アンラップして処理するため、高速パスが維持される

結論: 「ただバイト数を数えたいだけ」というミドルウェアの実装(例:

justReader
)は、静的に無害に見えても、ランタイムの型検査を回避してしまいます。これが原因でパフォーマンスが低下します。


strace による監視 (Watching it with strace)

各パターンのシステムコール呼び出しを確認します。512 MiB の転送を 5 回繰り返し、サマリーを比較しました。

raw (
io.Copy(conn, f)
)

sendfile
が主体となり、read/write はsetupのみ。エラーはバッファ満杯時の
EAGAIN
(通常動作)。

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 99.79    0.231981          78      2958       860 sendfile
  0.15    0.000359          51         7         1 write
  0.05    0.000126          18         7           read
------ ----------- ----------- --------- --------- ----------------
100.00    0.232466          78      2972       861 total

wrapped (
io.Copy(conn, justReader{f})
)

sendfile はゼロ回。全てのデータがユーザー空間を往復します。

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 56.67    3.202339          48     65546         3 write
43.33    2.448353          37     65547           read
------ ----------- ----------- --------- --------- ----------------
100.00    5.650692          43    131093         3 total
  • read: ~6.5 万回、write: ~6.5 万回(合計 13 万回以上の Syscall)。
  • データサイズ:
    io.Copy
    のデフォルトバッファサイズ(32 KiB)のチャンクで往復。
  • CPU コスト: raw に比べて約 24 倍 のウォール時間が必要になります。
  • Flamegraph: ヒートスタック上部が
    io.Copy
    ->
    io.copyBuffer
    ->
    syscall.Read/Write
    と続くことを示しています。

limit (
io.Copy(conn, io.LimitReader(f, n))
)

raw と同様に高速パス作動。

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 99.84    0.239191          82      2896       893 sendfile
  0.14    0.000330          47         7         1 write
  0.02    0.000047           6         7           read

この代償 (What this costs)

CPU 使用時間の比較

(サーバーユーザー+システム CPU / 10 x 512 MiB ファイル転送)

モードサーバー CPU 時間ガビ/GB (CPU/ms)
raw0.27 秒~54 ms
limit0.30 秒~60 ms
wrapped0.92 秒~184 ms
  • 結論:
    wrapped
    パターンでは、バイトあたりの CPU コストが約 3.4 倍 に増大します。
  • ネットワーク環境や負荷が高くなると、この差はさらに顕著になり、テールレイテンシ(tail latency)を大幅に悪化させます。

スplice の活用(プロキシの場合)

ソケットからソケットへの転送でも同様のルールが適用されます。

io.Copy
を介したプロキシで
splice
が作動します。しかし、片方のエンドポイントをラップすると(例:クライアント側を別のスコープの Reader として扱う)、自動的に read/write の往復にフォールバックします。

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 99.98    0.754999          70     10677       481 splice
  0.01    0.000109          13         8           read
  0.01    0.000065          65         1           write

ラップしていない場合は

splice
が 10,000 回以上、ラップした場合は数万回の read/write に置き換わります。


指針 (Rules of thumb)

高速パスを維持するための実践的なガイドラインです。

  • 🚫 ラップしない(原則):

    io.Reader
    io.Writer
    を実装してラップすることを避けてください。ファイルと接続の間に層が増えるほど、タイプ断定で高速パスが失われるリスクが高まります。

  • ✅ 必須の場合はインターフェースを実装せよ: ラップせざるを得ない場合でも、転送のディスパッチを

    io.Copy
    に委ねるためには、ソース側に
    io.WriterTo
    、宛先側に
    io.ReaderFrom
    を実装してください。これにより、ランタイムが独自の実装(splice 等)を利用し続けます。

  • 💡 io.LimitReader は例外なく無料: レンジレスポンスなど、バイト数の制御が必要な場合のみ、標準ライブラリの

    io.LimitReader
    を使用してください。ランタイムはこれを特定して高速パスを維持するため、他の自作ラッパーとは異なりコストゼロです。

  • 🛠️ strace で検証せよ: 直感に頼らず

    strace -c
    を実行してください。

    • 正常:
      sendfile
      または
      splice
      が主体で、read/write はほぼゼロ。
    • 異常: read/write のペアが数万分の回呼び出されている(32 KiB チャンク)場合、高速パスに乗っていない証拠です。
  • ⚠️ 常に高速パスがあると仮定せず: フレームワークや特定のライブラリによって

    io.Copy
    が裏で別の処理を行っている可能性があります。sendfile ルートを回避する理由を常に確認してください。

まとめ:

sendfile
splice
は Go ランタイムが無料で提供する重要な機能ですが、最も良意のある理由(メトリクス測定や型変換など)で追加された「無害なラッパー」によって容易に破壊されます。システムコールのプロファイリングを習慣化し、データコピーのコストを意識してコードを書きましょう。

同じ日のほかのニュース

一覧に戻る →

2026/07/06 6:03

Open プリンター

## Japanese Translation: Openprinter は、印刷技術における大きな転換点であり、コンパクトな耐久性と完全な修理可能性、そしてコスト削減および電子廃棄物削減を実現するオープンソースアーキテクチャを統合しています。Raspberry Pi Zero W および STM32 カートリッジ基板といった標準コンポーネントを基盤として構築されており、制限的な DRM を排除しつつ、Wi-Fi、Bluetooth、または USB を介して Windows、macOS、Linux、Android、iOS などの複数のオペレーティングシステムに対応します。このデバイスは高い汎用性を備えており、A4、A3、Letter の標準用紙、長さ 37.5m までのロール、バナーのようなカスタムフォーマットを処理でき、壁取付または机置き(統合されたカッターにより多様な構成が可能)に対応します。ユーザーは、特定の HP カートリッジ(地域によりモデルが異なる)に互換性のある再利用可能な Inkit ボトルを採用する低コストな消耗品戦略から恩恵を受け、モノクロ印刷においてすべての色が不要であっても、600〜1200 dpi の高品質出力を実現します。配布は Crowdsupply 上のクラウドソーシングモデルを通じて行われ、即席で使用できるユニットまたは組立キットが提供されます。さらに、該装置は電話アプリを介して HD ドキュメントキャプチャを含むスキャン機能を統合しています。結局のところ、このアプローチは個々に低価格な技術と統合されたスキャン機能を提供し、持続可能性を促進します。その成否は現在のキャンペーンにおける消費者の採用に依存し、印刷が引き続きアクセス可能な長期的な未来を約束しています。クリエイティブ・コモンズライセンスの下で配布され特許によって保護されている本プロジェクトは、倫理的利用を保証しつつ開示性を維持します。

2026/07/05 23:14

オーガニックマップ

## Japanese Translation: Organic Maps は、MapsWithMe および Maps.Me の背後にあるオープンソースコミュニティによって開発された、ハイキング、自転車走行、車での移動、地下鉄利用のための優れた無料オフラインファーストナビゲーションアプリです。完全におープンストリートマップのデータを動力源とし、アクティブなインターネット接続を必要とせず、かつデータを送信することなく、詳細なオフライン地図、曲がりくねるポイントごとの音声ガイダンス、等高線、高低差プロファイル、Wikipedia アーティクルを含むすべての機能を提供します。本アプリは、ゼロ広告、トラッカー、データ収集、強制登録、「ホームへ連絡」のない厳格なプライバシー設計で際立っており、これらは Exodus Privacy Project および iOS 用の TrackerControl による独立した監査によって検証されています。小さなフットプリントを持ち、1 つのバッテリー充電で 1 週間持つことができ、インターネットなしで機能の 100% をサポートするため、遠隔地域に最適です。Android、iOS、Linux(Flatpak/パッケージ)、Huawei AppGallery、Obtainium、Accrescent、F-Droid で利用可能で、Apache License 2.0 の下で動作し、寄付を通じて開発をサポートすることを可能にするコミュニティ主導のインディーズプロジェクトを育んでいます。本アプリは積極的にグローバルな参加者を招待しており、ベータテストプログラム(iOS 用の TestFlight、Android/Firebase/Flatpak/Linux デスクトップ用)を提供し、問題トラッカー、メール、Telegram、Matrix スペース、GitHub、Bluesky、FOSStodon などのソーシャルメディアプラットフォームなど複数のチャネルを通じてバグ報告、機能リクエスト、アップデートを提供しています。母語が多種類ある地元のコミュニティから支援され、専用の Matrix チャットおよび Telegram グループを通じてサポートされている Organic Maps は、ユーザーに世界を自由に探求できるようにすると同時に、監視慣行を拒否します。

2026/07/06 6:32

Show HN:自宅で作ってきたオープンソースゲームプラットフォーム「Homegames」

## Japanese Translation: Homegames は、アカウントを作成することもなくソフトウェアをインストールすることなく、ユーザーがゲームを楽しみ、作成し、共有できるブラウザベースのプラットフォームです。開発者は簡単なインブレーズアエディターを使用してコードを書き、ライブプレビューを確認しながら変更を即時テストし、マルチプレイヤーセッションを直接実行できます。資産管理は標準機能として搭載されており、ユーザーはファイルをアップロードするか、スタジオ内でコンテンツを描画または記録することもできます。ゲームはユーザーの好みに合わせて公開publishするか、プライベートに保つことが可能です。2018 年以来、GPLv3 ライセンスの下でオープンソースシステムとして構築され、単一サイドプロジェクトとして展開されてきた Homegames は、プラットフォーム全体とその API を個人ハードウェア上でのセルフホスティングを可能にし、元のサイトがオフラインになった場合でも長期的な保存を保証します。第三者サーバーへの依存を排除することで、Homegames はインディペンデント開発者に対する障壁を低下させ、プラットフォームの不安定さから創造的作業を守るための頑丈なインフラストラクチャを提供しています。

Goのゼロコピー:sendfile、splice、そしてio.Copyのコスト | そっか~ニュース