TCP Serverのアーキテクチャの歴史的な変化を学習するためのGoサンプルプログラム集です。
シンプルなEchoサーバーを題材に、各アーキテクチャの特徴と実装を比較できます。
| モード | ファイル | 説明 |
|---|---|---|
simple |
simple.go | シングルプロセス・シングルスレッド |
fork |
fork.go | 接続ごとにfork |
prefork |
prefork.go | 事前にWorkerプロセスをfork |
thread |
thread.go | POSIXスレッド (pthread) |
asyncio |
asyncio.go | 非同期I/O (epoll) |
microthread |
microthread.go | Goのgoroutine |
hybrid |
hybrid.go | Multi-Reactor (epoll + pthread) |
# ビルド
go build -o server .
# 実行 (モード名を引数に指定)
./server <mode> [port]
# 例
./server simple 8080
./server fork 8080
./server prefork 8080
./server thread 8080
./server asyncio 8080
./server microthread 8080
./server hybrid 8080# 別ターミナルでクライアント接続
nc localhost 8080
# 文字を入力するとエコーバックされる
hello
hello最も基本的な実装。1つのプロセスが順番に接続を処理します。
Client A ──┐
│ ┌─────────┐
├──│ Server │ 同時に1接続のみ処理可能
│ └─────────┘
Client B ──┘ (waiting...)
特徴:
- 実装が最もシンプル
- 同時接続数は1のみ
- 1つのクライアントが接続中は他のクライアントは待機
接続を受け付けるたびに子プロセスをforkして処理を委譲します。
┌─────────────┐
Client A ──────────│ Child Proc │
└─────────────┘
┌─────────┐
│ Parent │ ─── accept() ───┐
└─────────┘ │
┌─────────────┐
Client B ──────────│ Child Proc │
└─────────────┘
特徴:
- 並行処理が可能
- プロセス間のメモリ空間が分離(安全)
- forkのオーバーヘッドが大きい
- プロセス数が増えるとリソース消費が増大
起動時にWorkerプロセスを事前にforkしておき、各Workerがaccept()を呼び出して接続を奪い合います。
┌──────────────┐
│ Worker 0 │──┐
└──────────────┘ │
┌─────────┐ ┌──────────────┐ │
│ Parent │───│ Worker 1 │──┼── 共有listenFD
└─────────┘ └──────────────┘ │ accept()を競合
┌──────────────┐ │
│ Worker 2 │──┘
└──────────────┘
特徴:
- forkのオーバーヘッドを起動時に限定
- Apache HTTP Serverなどで採用
- Thundering Herd問題が発生しうる
接続ごとにpthreadを生成して処理します(cgoを使用)。
┌─────────────┐
Client A ──────────│ pthread │
└─────────────┘
┌─────────┐
│ Main │ ─── accept()
└─────────┘
┌─────────────┐
Client B ──────────│ pthread │
└─────────────┘
特徴:
- プロセスよりも軽量
- メモリ空間を共有(データ共有が容易)
- スレッド生成のオーバーヘッドはforkより小さい
- C10K問題の原因となる
シングルスレッドでepollを使い、複数の接続をイベント駆動で処理します。
┌─────────────────────────────────────┐
│ epoll │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │FD 1 │ │FD 2 │ │FD 3 │ │ ... │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
└─────────────────────────────────────┘
│
▼
Event Loop
(Single Thread)
特徴:
- 1スレッドで数万接続を処理可能
- コンテキストスイッチのオーバーヘッドがない
- コールバック地獄になりやすい
- Node.js, nginx, Redisなどで採用
Main ReactorがAcceptを担当し、複数のWorkerスレッド(Sub-Reactor)にラウンドロビンで振り分けます。各Workerは独自のepollを持ちます。
┌──────────────────────────────────────────────┐
│ Main Reactor │
│ (Go: accept) │
└──────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Worker 0 │ │ Worker 1 │ │ Worker 2 │
│ (pthread)│ │ (pthread)│ │ (pthread)│
│ epoll │ │ epoll │ │ epoll │
└──────────┘ └──────────┘ └──────────┘
特徴:
- 複数CPUコアを活用
- 各Workerが独立したイベントループを持つ
- Memcached, Nettyなどで採用
- 高スループットを実現
Goのgoroutineを使った実装。内部的にはランタイムがepoll等を使用します。
┌─────────────┐
Client A ──────────│ goroutine │
└─────────────┘
┌─────────┐ ┌───────────────────┐
│ Main │ │ Go Runtime │
└─────────┘ │ (epoll + M:N調整) │
└───────────────────┘
┌─────────────┐
Client B ──────────│ goroutine │
└─────────────┘
特徴:
- 軽量スレッド(数KB程度のスタック)
- ブロッキングI/Oのように書ける(シンプル)
- ランタイムが自動的にI/O多重化
- Go標準の手法
Simple → Fork → Prefork → Thread → AsyncIO → Microthread/Hybrid
│ │ │ │ │ │
1970s 1980s 1990s 2000s 2000s後半 2010s〜
- Simple: 初期のサーバー実装
- Fork: 並行処理の実現 (inetd, CGI)
- Prefork: forkコスト削減 (Apache 1.x)
- Thread: より軽量な並行処理 (Apache 2.x)
- AsyncIO: C10K問題への対応 (nginx, Node.js)
- Microthread/Hybrid: 開発効率と性能の両立 (Go, Erlang, Netty)
サーバーアーキテクチャの変遷とは別軸で、HTTPプロトコル自体も進化してきました。プロトコルの変化はサーバーアーキテクチャの設計に大きな影響を与えています。
HTTP/1.0 → HTTP/1.1 → HTTP/2 → HTTP/3 (QUIC)
│ │ │ │
1996 1997 2015 2022
Client Server
│ │
│──── TCP Connect ───────▶│
│──── GET /index.html ───▶│
│◀─── Response ───────────│
│◀─── TCP Close ──────────│
│ │
│──── TCP Connect ───────▶│ (新しいリクエストごとに再接続)
│──── GET /style.css ────▶│
│◀─── Response ───────────│
│◀─── TCP Close ──────────│
特徴:
- 1リクエスト = 1 TCP接続
- リクエスト完了後に接続を切断
アーキテクチャへの影響:
- Fork/Threadモデルでも問題なく動作
- 接続が短命なため、プロセス/スレッドはすぐ解放される
- TCPハンドシェイクのオーバーヘッドが大きい
Client Server
│ │
│──── TCP Connect ───────▶│
│──── GET /index.html ───▶│
│◀─── Response ───────────│
│──── GET /style.css ────▶│ (同じ接続を再利用)
│◀─── Response ───────────│
│──── GET /app.js ───────▶│
│◀─── Response ───────────│
│ ... │
│◀─── TCP Close ──────────│ (タイムアウトまで維持)
特徴:
- Keep-Alive: 接続の永続化(デフォルト有効)
- Pipelining: 複数リクエストを連続送信(実用上は普及せず)
- Host ヘッダ: バーチャルホストの実現
アーキテクチャへの影響:
- 接続が長時間維持されるため、Thread/Forkモデルではリソースが枯渇
- C10K問題を加速させた直接的な要因
- Keep-Alive接続を効率的に扱うため、イベント駆動モデルへの移行が加速
HTTP/1.0時代: HTTP/1.1時代:
┌────────┐ ┌────────┐
│Thread 1│→ 接続→処理→切断→解放 │Thread 1│→ 接続→処理→待機→処理→待機...
├────────┤ ├────────┤
│Thread 2│→ 接続→処理→切断→解放 │Thread 2│→ 接続→処理→待機... (idle)
├────────┤ ├────────┤
│Thread 3│→ 接続→処理→切断→解放 │Thread 3│→ 接続→処理→待機... (idle)
└────────┘ └────────┘
スレッドはすぐ解放 アイドル状態でもスレッドを占有
Client Server
│ │
│════ Single TCP Connection ═══│
│ │
│──▶ Stream 1: GET /index.html │
│──▶ Stream 2: GET /style.css │ (同時に複数リクエスト)
│──▶ Stream 3: GET /app.js │
│◀── Stream 2: Response ───────│ (順不同で返却)
│◀── Stream 1: Response ───────│
│◀── Stream 3: Response ───────│
│ │
特徴:
- Multiplexing: 単一TCP接続で複数ストリームを多重化
- Binary Protocol: テキストからバイナリへ(パース効率向上)
- Header Compression (HPACK): ヘッダの圧縮
- Server Push: サーバからのプッシュ配信
アーキテクチャへの影響:
- 1クライアント = 1 TCP接続で済むため、接続数が激減
- イベント駆動モデルとの親和性が極めて高い
- ストリーム管理のステートマシンが必要(複雑化)
- バイナリフレームのパース処理が必要
HTTP/1.1: HTTP/2:
┌─────────────────────┐ ┌─────────────────────┐
│ Connection Pool │ │ Single Connection │
│ ┌───┐┌───┐┌───┐┌───┐│ │ ┌─────────────────┐ │
│ │C1 ││C2 ││C3 ││C4 ││ │ │ Multiplexer │ │
│ └───┘└───┘└───┘└───┘│ │ │ S1 S2 S3 S4 │ │
└─────────────────────┘ │ └─────────────────┘ │
複数接続が必要 └─────────────────────┘
1接続で全て処理
Client Server
│ │
│════ QUIC (UDP) Connection ═══│
│ │
│──▶ Stream 1: GET /index.html │
│──▶ Stream 2: GET /style.css │
│◀── Stream 2: Response ───────│
│ (Stream 1 パケロス発生) │
│◀── Stream 3: Response ───────│ ← Stream 1の再送を待たずに配信可能
│◀── Stream 1: Response ───────│
│ │
特徴:
- UDPベース: TCPからUDPへ(カーネル依存からの脱却)
- 0-RTT接続確立: 再接続時のハンドシェイク省略
- 独立したストリーム: HOL (Head-of-Line) ブロッキングの解消
- Connection Migration: IPアドレスが変わっても接続維持
- 暗号化が必須: TLS 1.3を統合
アーキテクチャへの影響:
-
UDPソケットの管理
- TCPのような接続状態をカーネルが管理しない
- アプリケーション層で接続状態を管理する必要がある
accept()の概念がなく、データグラム単位で処理
-
暗号化処理の負荷
- 全通信が暗号化されるため、CPU負荷が増加
- ハードウェアアクセラレーション(AES-NI等)の活用が重要
-
イベント駆動モデルの必須化
- UDPは本質的にコネクションレス
- 大量のストリームを効率的に処理するにはイベント駆動が必須
TCP (HTTP/1.1, HTTP/2): QUIC (HTTP/3):
┌─────────────────────┐ ┌─────────────────────┐
│ Kernel │ │ Application │
│ ┌─────────────────┐ │ │ ┌─────────────────┐ │
│ │ TCP State Machine│ │ │ │QUIC State Machine│ │
│ │ (per conn) │ │ │ │ (per conn) │ │
│ └─────────────────┘ │ │ └─────────────────┘ │
└─────────────────────┘ └─────────────────────┘
カーネルが接続管理 アプリが接続管理
| プロトコル | 最適なアーキテクチャ | 理由 |
|---|---|---|
| HTTP/1.0 | Fork / Thread | 短命な接続、シンプルな処理 |
| HTTP/1.1 | AsyncIO / Hybrid | Keep-Aliveで接続が長寿命化 |
| HTTP/2 | AsyncIO / Hybrid | Multiplexingでイベント駆動と相性◎ |
| HTTP/3 | AsyncIO / Hybrid | UDPでイベント駆動が必須 |
| サーバー | アーキテクチャ | HTTP/2 | HTTP/3 |
|---|---|---|---|
| nginx | Event-driven (epoll) | ✓ | ✓ |
| Caddy | goroutine (Go) | ✓ | ✓ |
| H2O | Multi-thread + Event | ✓ | ✓ |
| Node.js | Event-loop | ✓ | ✓ |
┌─────────────────────────────────────────────────────────────────┐
│ アーキテクチャの進化 │
│ Simple → Fork → Prefork → Thread → AsyncIO → Microthread │
│ ▲ │
│ │ │
│ ┌─────────┴─────────┐ │
│ │ C10K問題への対応 │ │
│ └─────────┬─────────┘ │
│ │ │
│ ┌─────────────────────────────────────┴───────────────────┐ │
│ │ プロトコルの進化 │ │
│ │ HTTP/1.0 → HTTP/1.1 → HTTP/2 → HTTP/3 │ │
│ │ │ │ │ │ │ │
│ │ 短命接続 Keep-Alive 多重化 UDPベース │ │
│ │ (C10K加速) (相性◎) (必須化) │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
プロトコルとアーキテクチャは相互に影響し合いながら進化してきました。HTTP/1.1のKeep-Aliveがイベント駆動モデルへの移行を促し、HTTP/2以降はイベント駆動モデルを前提とした設計になっています。
サーバーアーキテクチャの変遷は、HTTPプロトコルの進化だけでなく、Linux Kernelの進化にも強く影響されています。カーネルが提供するシステムコール、スケジューラ、ネットワークスタックの改善が、新しいアーキテクチャパターンを実現可能にしてきました。
Linux Kernel システムコール/機能 サーバーアーキテクチャ
─────────────────────────────────────────────────────────────────────
1.x (1994) fork, select Simple, Fork
│ │
2.0 (1996) SMP対応 Fork (マルチCPU活用)
│ │
2.2 (1999) poll, sendfile Prefork
│ │
2.4 (2001) clone改善, sendfile強化 Thread (per-connection)
│ │
2.6 (2003) epoll, NPTL, O(1)スケジューラ AsyncIO, Hybrid
│ │
2.6.23 (2007) CFS ─┐
│ │ │ goroutine等の
3.9 (2013) SO_REUSEPORT ─┤ M:Nスレッドモデル
│ │ │ が効率的に動作
4.8 (2016) XDP (eBPF) ─┘
│ │
4.18 (2018) sockmap/sk_msg (eBPF) プログラマブルカーネル
│ │
5.1 (2019) io_uring 次世代AsyncIO
│ │
6.x (2022-) io_uring拡張, zero-copy強化 高効率ハイブリッド
TCPサーバーアーキテクチャに最も直接的な影響を与えたのは、I/O多重化のためのシステムコールの進化です。
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);┌─────────────────────────────────────────────┐
│ select() の動作 │
│ │
│ User Space Kernel Space │
│ ┌──────────┐ ┌──────────────┐ │
│ │ fd_set │──copy──▶│ 全FDを線形 │ │
│ │ [0..1023]│ │ スキャン │ │
│ │ │◀─copy──│ O(n) │ │
│ └──────────┘ └──────────────┘ │
│ │
│ 制約: FD_SETSIZE = 1024 (ハードコード) │
│ 毎回: fd_setをカーネルにコピー │
│ 毎回: 全FDを線形走査 │
└─────────────────────────────────────────────┘
制約:
- 監視可能なFD数が最大1024(
FD_SETSIZE) - 呼び出しのたびにfd_setをユーザー空間⇔カーネル空間でコピー
- カーネル内で全FDを線形走査(O(n))
- 数百接続を超えると性能が急激に劣化
アーキテクチャへの影響:
- 少数の接続を多重化するには十分だったが、大規模サーバーには不適
- 結果として、Fork/Preforkモデルが主流であり続けた
int poll(struct pollfd *fds, nfds_t nfds, int timeout);selectからの改善:
- FD数の上限が撤廃(動的配列)
- よりクリーンなAPI(ビットマスクではなく構造体配列)
残った問題:
- 毎回全FDをカーネルにコピーする必要がある
- カーネル内での線形走査(O(n))は変わらず
- 数千接続では依然としてオーバーヘッドが大きい
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);┌──────────────────────────────────────────────────────────┐
│ epoll の動作 │
│ │
│ User Space Kernel Space │
│ ┌──────────┐ ┌──────────────────────┐ │
│ │ │ │ epoll instance │ │
│ │ epoll_ctl│──1回登録──▶│ ┌────────────────┐ │ │
│ │ (ADD/MOD)│ │ │ Red-Black Tree │ │ │
│ │ │ │ │ (全監視FD) │ │ │
│ │ │ │ └────────────────┘ │ │
│ │ │ │ │ │ │
│ │ │ │ デバイスからの │ │
│ │ │ │ callback で通知 │ │
│ │ │ │ ▼ │ │
│ │ │ │ ┌────────────────┐ │ │
│ │epoll_wait│◀─ready分──│ │ Ready List │ │ │
│ │ │ だけ返却 │ │ (準備済FDのみ) │ │ │
│ └──────────┘ │ └────────────────┘ │ │
│ └──────────────────────┘ │
│ │
│ ・FDの登録は1回だけ (毎回コピーしない) │
│ ・イベント発生時にcallbackでReady Listに追加 │
│ ・epoll_waitは準備済FDだけを返す → O(ready) │
└──────────────────────────────────────────────────────────┘
革命的な改善:
- FDの登録は
epoll_ctl()で1回だけ(毎回コピー不要) - カーネル内部でRed-Black Treeにより管理
- イベント発生時にcallbackでReady Listに追加(線形走査不要)
epoll_wait()はReady状態のFDだけを返す → O(ready FD数)- Edge-Triggered (ET) / Level-Triggered (LT) モードの選択が可能
アーキテクチャへの影響:
- C10K問題を解決する技術的基盤を提供
- nginxやNode.jsの登場を可能にした
- AsyncIOアーキテクチャが現実的な選択肢に
- 本リポジトリの
asyncio.goとhybrid.goはepollに依存
接続数とシステムコールの性能比較 (概念図):
性能
▲
│ ████ select/poll: O(n)で劣化
│ ████████
│ ████████████
│ ████████████████
│ ░░░░░░░░░░░░░░░░░░ epoll: O(ready)でほぼ一定
│ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
└──────────────────────────────────────▶ 接続数
1K 5K 10K 50K 100K
// Submission Queue (SQ) と Completion Queue (CQ) を共有メモリで管理
struct io_uring_sqe; // Submission Queue Entry
struct io_uring_cqe; // Completion Queue Entry┌──────────────────────────────────────────────────────────────┐
│ io_uring の動作 │
│ │
│ User Space Kernel Space │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Shared Memory (mmap) │ │
│ │ ┌─────────────────┐ ┌─────────────────────┐ │ │
│ │ │ Submission Queue │ │ Completion Queue │ │ │
│ │ │ ┌───┬───┬───┐ │ │ ┌───┬───┬───┐ │ │ │
│ │ │ │SQE│SQE│SQE│ │ │ │CQE│CQE│CQE│ │ │ │
│ │ │ └───┴───┴───┘ │ │ └───┴───┴───┘ │ │ │
│ │ │ User が書込 │ │ Kernel が書込 │ │ │
│ │ └─────────────────┘ └─────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ・システムコール呼び出し自体を最小化 │
│ ・SQ/CQリングバッファでバッチ処理 │
│ ・カーネルポーリングモード (SQPOLL) でsyscall 0回も可能 │
└──────────────────────────────────────────────────────────────┘
epollからの改善:
- ユーザー空間とカーネル空間で共有メモリのリングバッファを使用
- 複数のI/O操作をバッチでサブミット(システムコール回数を削減)
IORING_SETUP_SQPOLLでカーネルスレッドがポーリング → システムコール0回での非同期I/O- read/write/accept/connect等あらゆるI/Oを統一的に非同期化
アーキテクチャへの影響:
- epollベースのイベントループをさらに高効率化
- システムコールのオーバーヘッド自体を排除可能
- 今後のサーバーフレームワークの基盤技術
fork() の進化:
初期のfork: CoW (Copy-on-Write):
┌──────────┐ fork ┌──────────┐ ┌──────────┐ fork ┌──────────┐
│ Parent │ ───────▶ │ Child │ │ Parent │ ────▶ │ Child │
│ [データ] │ │ [コピー] │ │ [データ] │ │ [共有] │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │
全メモリを即座にコピー └──────┬───────────┘
→ 非常に遅い │
ページテーブルのみコピー
書込時に初めて実コピー
→ fork自体は高速
アーキテクチャへの影響:
- CoWにより
fork()のコストが劇的に削減 - Fork-per-connectionモデルが実用的な選択肢になった
- Apache 1.xのPreforkモデルが高性能を発揮できた理由の一つ
プロセスとスレッドの統一 (Linux):
┌────────────────────────────────────────────────────────┐
│ clone() システムコール │
│ │
│ clone(CLONE_VM | CLONE_FS | CLONE_FILES | ...) │
│ ↓ ↓ ↓ │
│ メモリ共有 FS共有 FD共有 → スレッド │
│ │
│ clone(0) │
│ → 何も共有しない → fork相当 → プロセス │
│ │
│ Linuxではスレッドもプロセスも内部的にはtask_struct │
└────────────────────────────────────────────────────────┘
LinuxThreads (Linux 2.0〜2.4) の問題:
- スレッドごとに異なるPIDが割り振られる
- シグナル処理が壊れていた(POSIX非準拠)
- マネージャスレッドがボトルネック
- スレッド生成/破棄が遅い
NPTL (Native POSIX Thread Library, Linux 2.6):
- 同一プロセスのスレッドは同一TGIDを共有
- POSIX準拠のシグナル処理
futexシステムコールによる高効率な同期プリミティブ- スレッド生成が約10倍高速化
LinuxThreads (2.4以前): NPTL (2.6以降):
┌─────────────────────┐ ┌─────────────────────┐
│ Process (PID=100) │ │ Process (TGID=100) │
│ │ │ │
│ Thread1 (PID=101) │ │ Thread1 (TID=100) │
│ Thread2 (PID=102) │ │ Thread2 (TID=101) │
│ Thread3 (PID=103) │ │ Thread3 (TID=102) │
│ │ │ │
│ Manager (PID=100) │ │ (マネージャ不要) │
│ がスレッド管理 │ │ │
│ │ │ │
│ ⚠ PIDがバラバラ │ │ ✓ 同一TGID │
│ ⚠ シグナル処理が不正 │ │ ✓ POSIX準拠 │
│ ⚠ 生成が遅い │ │ ✓ futexで高速同期 │
└─────────────────────┘ └─────────────────────┘
アーキテクチャへの影響:
- NTPLにより Thread-per-connectionモデルが実用レベルに
- Apache 2.x (worker MPM) はNTPLの恩恵を直接受けた
- しかし、C10K規模ではスレッド数自体がボトルネックに → AsyncIOへ
従来のロック: futex:
┌──────────┐ ┌──────────┐
│User Space│ │User Space│
│ │ 毎回 │ │ 競合なし:
│ lock() │──syscall──▶ Kernel │ lock() │──atomic op──▶ 完了
│ │ │ │ (syscall不要)
│ │ │ │
│ │ │ │ 競合あり:
│ │ │ lock() │──syscall──▶ Kernel
│ │ │ │ (必要な時だけ)
└──────────┘ └──────────┘
アーキテクチャへの影響:
- マルチスレッドサーバーのロック性能が劇的に向上
- 競合がない場合はカーネルに入らずatomic操作で完了
- Hybridモデルのようなマルチスレッド×イベント駆動の実用性を向上
Linuxのプロセススケジューラの進化は、サーバーが多数のプロセス/スレッドを扱う際の性能に直接影響しました。
Runqueue (全CPUで1つ):
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ T │ T │ T │ T │ T │ T │ T │ T │ T │ T │ ← 全タスクをスキャン
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
↓ 毎回全走査して最高優先度を選択
O(n) — タスク数に比例して遅くなる
問題:
- 実行キューが1つ(全CPUで共有)→ ロック競合
- 次に実行するタスクを選ぶのにO(n)
- 数百スレッドで顕著に劣化 → Fork/Threadモデルの限界を助長
Per-CPU Runqueue:
CPU 0 CPU 1
┌─────────────────┐ ┌─────────────────┐
│ Active Array │ │ Active Array │
│ [pri 0] → T,T │ │ [pri 0] → T │
│ [pri 1] → T │ │ [pri 1] → T,T,T │
│ [pri 2] → │ │ [pri 2] → T │
│ ... │ │ ... │
│ [pri 139]→ │ │ [pri 139]→ │
├─────────────────┤ ├─────────────────┤
│ Expired Array │ │ Expired Array │
│ ... │ │ ... │
└─────────────────┘ └─────────────────┘
│ │
▼ ▼
bitmap で最高優先度を bitmap で最高優先度を
O(1) で発見 O(1) で発見
改善:
- CPU毎にRunqueueを分離(ロック競合の排除)
- 優先度配列+bitmapで次のタスクをO(1)で選択
- タスク数に関係なく一定時間でスケジューリング
アーキテクチャへの影響:
- 数千スレッドでもスケジューリングオーバーヘッドが一定
- Thread-per-connectionモデルの実用範囲を拡大(数千接続まで)
- しかし、スレッドのメモリ消費(スタック等)はスケジューラでは解決できない
Red-Black Tree (実行時間でソート):
┌───────┐
│ T(5ms)│ ← vruntime が最小のタスクを
└───┬───┘ 左端から O(log n) で取得
╱ ╲
┌───────┐ ┌───────┐
│ T(3ms)│ │ T(8ms)│
└───┬───┘ └───────┘
╱ ╲
┌───────┐ ┌───────┐
│ T(1ms)│ │ T(4ms)│
└───────┘ └───────┘
↑
最小vruntime = 次に実行
O(1)スケジューラからの改善:
- vruntimeベースの公平なCPU時間配分
- Red-Black Treeによる O(log n) のタスク選択
- ヒューリスティクスの削減で予測可能な動作
- ワークロードの種類を問わず安定した性能
アーキテクチャへの影響:
- goroutineのようなM:Nスレッドモデルのランタイムがカーネルスレッドを効率的に利用可能に
- 公平なスケジューリングにより、I/O boundとCPU boundのタスクが混在するサーバーでも安定動作
- Go runtimeはP (Processor) × M (Machine=OSスレッド) をCFSに委ね、G (Goroutine) の管理をユーザー空間で行う
Go Runtime と CFS の協調:
┌────────────────────────────────────────┐
│ User Space │
│ ┌──────────────────────────────────┐ │
│ │ Go Runtime │ │
│ │ G G G G G G G G (goroutines) │ │
│ │ ↓ ↓ ↓ ↓ │ │
│ │ P₀ P₁ P₂ P₃ (GOMAXPROCS) │
│ │ ↓ ↓ ↓ ↓ │ │
│ │ M₀ M₁ M₂ M₃ (OS threads) │
│ └──────────────────────────────────┘ │
├────────────────────────────────────────┤
│ Kernel Space │
│ ┌──────────────────────────────────┐ │
│ │ CFS Scheduler │ │
│ │ M₀, M₁, M₂, M₃ を公平にスケジュール │
│ └──────────────────────────────────┘ │
└────────────────────────────────────────┘
従来のファイル送信: sendfile():
┌──────────┐ ┌──────────┐
│User Space│ │User Space│
│ │ read() │ │
│ buf[] │◀──────── Kernel │sendfile()│──────▶ Kernel
│ │ │(1 syscall)│
│ │ write() │ │ ┌──────┐
│ buf[] │────────▶ Kernel │ │ │Disk │
└──────────┘ └──────────┘ │ ↓ │
│Socket│
4回のコンテキストスイッチ └──────┘
2回のデータコピー カーネル内で直接転送
(User↔Kernel) ユーザー空間のコピー不要
アーキテクチャへの影響:
- 静的ファイル配信の効率が劇的に向上
- nginxが高速な静的ファイル配信を実現できた技術的基盤
- zero-copy技術の先駆け
従来: 1つのプロセスがaccept() SO_REUSEPORT:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Process │ ← accept() │Worker 0 │ │Worker 1 │ │Worker 2 │
│ (1つ) │ ボトルネック │listen:80 │ │listen:80 │ │listen:80 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │ │ │
▼ カーネルが接続を分散(ハッシュベース)
全接続をさばく │ │ │
┌────┴────────────┴────────────┴────┐
│ Kernel (port 80) │
│ accept()を各Workerに分散 │
└──────────────────────────────────┘
従来の問題:
- Preforkモデルでは複数Workerが同じlistenソケットの
accept()を競合 - Thundering Herd問題: 1つの接続に全Workerが起こされる
- acceptのロック競合がボトルネック
SO_REUSEPORTの解決:
- 同一ポートに複数のlistenソケットをバインド可能
- カーネルが接続元のハッシュに基づきソケットを選択
- Thundering Herd問題を根本的に解消
- ロック競合なし
アーキテクチャへの影響:
- Preforkモデルの性能問題を解消
- nginx 1.9.1以降で採用、性能が大幅に向上
- マルチプロセス/マルチスレッドのイベント駆動サーバー(Hybrid型)に特に有効
// 従来: accept() + fcntl() で2回のsyscall
int fd = accept(listen_fd, &addr, &addrlen);
fcntl(fd, F_SETFL, O_NONBLOCK);
fcntl(fd, F_SETFD, FD_CLOEXEC);
// accept4(): 1回のsyscallでフラグ設定
int fd = accept4(listen_fd, &addr, &addrlen, SOCK_NONBLOCK | SOCK_CLOEXEC);アーキテクチャへの影響:
- AsyncIOサーバーでは新規接続のたびにnon-blocking設定が必要
- accept4()によりシステムコール回数を削減(高頻度accept時に効果大)
- レースコンディション(accept〜fcntl間にforkした場合のFD漏洩)を防止
通常のTCPハンドシェイク: TCP Fast Open:
Client Server Client Server
│ │ │ │
│──── SYN ─────▶│ │── SYN+Data ──▶│ ← 最初から
│◀─── SYN-ACK ──│ │◀─ SYN-ACK+Resp│ データ送信
│──── ACK ──────▶│ │── ACK ────────▶│
│──── Data ─────▶│ (3 RTT) │ │ (1 RTT)
│◀─── Response ──│ │ │
アーキテクチャへの影響:
- 短命な接続(HTTP/1.0的なワークロード)の効率を改善
- 接続確立のレイテンシを1 RTT削減
- CDNやAPIサーバーで効果を発揮
eBPF (extended Berkeley Packet Filter) は、カーネルのソースコードを変更したりカーネルモジュールをロードしたりすることなく、カーネル空間でサンドボックス化されたプログラムを実行できる技術です。TCPサーバーアーキテクチャにおいて、「カーネルの振る舞いをアプリケーション側からプログラムする」 という全く新しいアプローチを可能にしました。
BPF (classic) eBPF
パケットフィルタ専用 汎用カーネルプログラミング基盤
─────────────────────────────────────────────────────────
1992 BPF (BSD) tcpdump等のパケットフィルタリング
│
2014 Linux 3.18 eBPF基盤 (JITコンパイル, Maps)
│
2015 Linux 4.1 kprobes BPF (カーネル関数トレース)
│
2016 Linux 4.8 XDP (eXpress Data Path)
│
2018 Linux 4.15 BPF_PROG_TYPE_SOCK_OPS
Linux 4.18 sockmap / sk_msg
│
2020 Linux 5.6 BPF TCP輻輳制御
Linux 5.9 sk_lookup
│
2022- Linux 6.x struct_ops拡張, BPFメモリアロケータ
┌──────────────────────────────────────────────────────────┐
│ User Space │
│ │
│ ┌────────────────┐ ┌──────────────┐ │
│ │ eBPF Compiler │ │ User App │ │
│ │ (clang/LLVM) │ │ (bpftool, │ │
│ │ │ │ │ libbpf, │ │
│ │ ▼ │ │ cilium...) │ │
│ │ BPF Bytecode │ │ │ │ │
│ └───────┬────────┘ └──────┼───────┘ │
│ │ bpf() │ BPF Maps │
│ │ syscall │ (共有データ) │
├──────────┼────────────────────┼───────────────────────────┤
│ ▼ Kernel Space ▼ │
│ ┌────────────────┐ ┌──────────────┐ │
│ │ Verifier │ │ BPF Maps │ │
│ │ (安全性検証) │ │ ┌──────────┐ │ │
│ │ ・境界チェック │ │ │Hash Map │ │ │
│ │ ・ループ検出 │ │ │Array Map │ │ │
│ │ ・メモリ安全性 │ │ │Ring Buf │ │ │
│ └───────┬────────┘ │ └──────────┘ │ │
│ ▼ └──────────────┘ │
│ ┌────────────────┐ │
│ │ JIT Compiler │ → ネイティブ機械語に変換 │
│ └───────┬────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ Hook Points (実行箇所) │ │
│ │ ┌─────┐ ┌──────┐ ┌───────┐ ┌──────┐ │ │
│ │ │ XDP │ │TC │ │Socket │ │Sched │ │ │
│ │ │ │ │ │ │ Ops │ │ │ │ │
│ │ └─────┘ └──────┘ └───────┘ └──────┘ │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
eBPFプログラムはVerifierによって安全性が保証され(無限ループ禁止、境界外アクセス禁止等)、JITコンパイルによりネイティブコードとして実行されるため、カーネルモジュールに匹敵する性能を安全に発揮します。
XDPはNICドライバレベルでパケットを処理し、カーネルのネットワークスタック(TCP/IP処理)に到達する前にパケットの転送・破棄・書き換えを行います。
従来のパケット処理:
┌─────┐ ┌──────────────────────────────────────┐ ┌──────────┐
│ NIC │───▶│ Kernel Network Stack │───▶│ User App │
│ │ │ Driver→IRQ→sk_buff→IP→TCP→Socket │ │ │
└─────┘ └──────────────────────────────────────┘ └──────────┘
全パケットがフルスタックを通過
XDP:
┌─────┐ ┌────────┐ ┌────────────────────────┐ ┌──────────┐
│ NIC │───▶│ XDP │────▶│ Kernel Network Stack │───▶│ User App │
│ │ │ Program │ └────────────────────────┘ └──────────┘
└─────┘ └────────┘
│
├── XDP_DROP → パケット破棄 (DDoS防御)
├── XDP_TX → 同じNICから送り返す (LB)
├── XDP_REDIRECT→ 別NIC/CPUに転送
└── XDP_PASS → 通常のスタックへ
スタックに入る前に処理 → 桁違いの高速化
性能比較 (パケット処理速度の概念図):
パケット/秒
▲
│
│ ████████████████████████████████ XDP (~24Mpps)
│
│ ████████████████ DPDK (~15Mpps)
│
│ ██████ iptables (~ 5Mpps)
│
│ ███ User Space (~ 2Mpps)
│
└──────────────────────────────────────────────────────▶
TCPサーバーへの影響:
- DDoS防御: SYN Floodなどの攻撃パケットをスタック到達前に破棄。TCPサーバーの
accept()に不正な接続が到達しない - L4ロードバランシング: パケットヘッダを書き換えて別サーバーに転送。ユーザー空間LB(HAProxy等)の数十倍の性能
- 接続の前処理: TCPサーバーに到達する前にトラフィックを整形・分類
採用事例:
- Facebook Katran: XDPベースのL4ロードバランサー(毎秒数百万パケット処理)
- Cloudflare: DDoS Mitigation にXDPを大規模採用
sockmap/sk_msgは、ユーザー空間を経由せずにカーネル内でソケット間のデータ転送を行う技術です。プロキシサーバーのアーキテクチャに革命的な影響を与えました。
従来のプロキシサーバー:
┌────────┐ ┌──────────────────────────┐ ┌────────┐
│ Client │ │ User Space │ │Backend │
│ │ │ ┌──────────────┐ │ │ │
│ ─────┼──▶│───▶│ read(fd_c) │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ ▼ │ │ │ │
│ │ │ │ buf[4096] │ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ ▼ │ │ │ │
│ ◀────┼───│◀───│ write(fd_b) │ │ │ │
│ │ │ └──────────────┘ │ │ ◀────┤
└────────┘ └──────────────────────────┘ └────────┘
4回のコンテキストスイッチ + 4回のデータコピー
sockmap によるプロキシ:
┌────────┐ ┌──────────────────────────┐ ┌────────┐
│ Client │ │ Kernel Space │ │Backend │
│ │ │ ┌──────────────┐ │ │ │
│ ─────┼──▶│───▶│ sockmap │──────┼──▶│ │
│ │ │ │ (BPF prog) │ │ │ │
│ ◀────┼───│◀───│ sk_msg │◀─────┼───│ │
│ │ │ │ redirect │ │ │ │
│ │ │ └──────────────┘ │ │ │
└────────┘ └──────────────────────────┘ └────────┘
ユーザー空間を経由しない → コピー0回、コンテキストスイッチ0回
詳細なデータフローの比較:
従来:
Client Socket Backend Socket
│ ▲
▼ ①kernel→user copy │ ④user→kernel copy
┌──────────┐ ┌──────────┐
│ recv buf │ │ send buf │
│(user sp) │──③ memcpy ──────────────▶│(user sp) │
└──────────┘ └──────────┘
▲ ②kernel→user switch │ ⑤user→kernel switch
│ ▼
[kernel recv buf] [kernel send buf]
sockmap:
Client Socket Backend Socket
│ ▲
▼ │
[kernel recv buf]──BPF redirect──▶[kernel send buf]
カーネル内で直接転送 (zero-copy)
TCPサーバーへの影響:
- プロキシ/サービスメッシュの高速化: Envoy、Ciliumなどのサイドカープロキシがカーネル内でデータ転送
- レイテンシ削減: ユーザー空間往復のオーバーヘッド排除で、L7プロキシのレイテンシが最大50%削減
- CPU使用率削減: データコピーとコンテキストスイッチの排除により、プロキシのCPU消費が大幅に減少
採用事例:
- Cilium: Kubernetes環境でPod間通信をsockmapで最適化。iptablesベースのkube-proxyを置き換え
- Istio + eBPF: サービスメッシュのサイドカープロキシをカーネル内で処理
TCPの各イベント(接続確立、ACK受信、状態遷移など)にBPFプログラムをアタッチし、TCPの振る舞いをカスタマイズできます。
TCP接続のライフサイクルとBPFフックポイント:
Client Server
│ │
│──── SYN ─────────────────────▶│ ← BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB
│◀─── SYN-ACK ─────────────────│
│──── ACK ─────────────────────▶│ ← BPF_SOCK_OPS_TCP_CONNECT_CB
│ │
│◀──── Data ────────────────────│ ← BPF_SOCK_OPS_RTO_CB (再送タイムアウト)
│──── ACK ─────────────────────▶│ ← BPF_SOCK_OPS_RTT_CB (RTT計測)
│ │
│──── FIN ─────────────────────▶│ ← BPF_SOCK_OPS_STATE_CB (状態遷移)
│◀─── FIN-ACK ─────────────────│
│ │
各フックで可能な操作:
・TCPオプション (MSS, Window Scale等) の動的変更
・ソケットバッファサイズの接続ごとの最適化
・sockmap への登録 (プロキシ用)
・メトリクスの収集 (BPF Maps経由)
TCPサーバーへの影響:
- 接続確立時に動的にTCPパラメータをチューニング(例: データセンター内通信 vs 長距離通信で異なるバッファサイズ)
- アプリケーションを変更せずにTCPの挙動を最適化
- 接続ごとのメトリクス収集をカーネル内で完結
従来、受信パケットをどのソケット(どのサーバープロセス)に渡すかはカーネルの固定的なルックアップテーブルに基づいていました。sk_lookupにより、この振り分けロジックをBPFプログラムで自由にカスタマイズできます。
従来のソケットルックアップ:
受信パケット (dst=10.0.0.1:80)
│
▼
┌──────────────────────┐
│ Kernel Lookup Table │
│ (宛先IP:Port → FD) │ ← 固定ロジック
│ │
│ 10.0.0.1:80 → fd=5 │
└──────────┬───────────┘
▼
Socket (fd=5)
sk_lookup:
受信パケット (dst=10.0.0.1:80)
│
▼
┌──────────────────────────────────┐
│ BPF sk_lookup Program │
│ │
│ if (src_ip in datacenter_range) │ ← カスタムロジック
│ → socket_A (低レイテンシ設定) │
│ else if (src_port > 50000) │
│ → socket_B (高スループット設定) │
│ else │
│ → socket_C (デフォルト) │
└──────────────────────────────────┘
TCPサーバーへの影響:
- 同一ポートで複数のサーバーインスタンスを動かし、BPFで振り分け
- ブルーグリーンデプロイ: 新旧バージョンのサーバーにトラフィックを徐々に移行
- マルチテナント: 送信元に応じて異なるサーバーインスタンスに接続をルーティング
TCP輻輳制御アルゴリズム(Cubic, BBR, Reno等)をカーネルモジュールではなくBPFプログラムとして実装・ロードできます。
従来: BPF輻輳制御:
┌─────────────────────────┐ ┌─────────────────────────┐
│ Kernel (コンパイル時固定) │ │ Kernel (実行時ロード) │
│ │ │ │
│ ┌─────┐ ┌─────┐ ┌─────┐│ │ ┌─────┐ ┌─────┐ ┌─────┐│
│ │Cubic│ │ BBR │ │Reno ││ │ │Cubic│ │ BBR │ │ BPF ││
│ └─────┘ └─────┘ └─────┘│ │ └─────┘ └─────┘ │Prog ││
│ │ │ └─────┘│
│ アルゴリズム追加には │ │ │
│ カーネル再ビルドか │ │ 実行中のカーネルに │
│ モジュールロードが必要 │ │ 動的にアルゴリズムを追加 │
└─────────────────────────┘ └─────────────────────────┘
TCPサーバーへの影響:
- サーバー再起動なしで輻輳制御アルゴリズムをデプロイ・更新
- 接続の特性に応じて異なる輻輳制御を適用(例: 短いRPC通信とバルク転送で別アルゴリズム)
- 自社ネットワーク特性に最適化したカスタムアルゴリズムを安全にテスト
eBPFはサーバーアプリケーションに一切変更を加えることなく、カーネル内部から詳細なメトリクスを収集できます。
┌──────────────────────────────────────────────────────────────┐
│ TCP Server │
│ (変更不要) │
└──────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│accept()│ │ read() │ │write() │ │ close()│ ← syscall
└────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────────────────────────────────────┐
│ eBPF Programs │
│ │
│ ・接続確立レイテンシ (SYN→ESTABLISHED) │
│ ・RTT分布 │
│ ・再送率 │
│ ・接続ごとのスループット │
│ ・TCP状態遷移の追跡 │
│ ・ソケットバッファの使用状況 │
└──────────────────┬───────────────────────────┘
│ BPF Maps
▼
┌──────────────────────────────────────────────┐
│ User Space 可視化ツール │
│ (Prometheus, Grafana, bpftrace, ...) │
└──────────────────────────────────────────────┘
従来の可観測性手法との比較:
| 手法 | オーバーヘッド | カーネル変更 | アプリ変更 | 粒度 |
|---|---|---|---|---|
| アプリ内メトリクス | 中 | 不要 | 必要 | アプリ層のみ |
| strace | 大 | 不要 | 不要 | syscall |
| tcpdump | 大 | 不要 | 不要 | パケット |
| eBPF | 極小 | 不要 | 不要 | カーネル内部 |
eBPF以前は、サーバーアーキテクチャの選択は「カーネルが提供する固定的なAPI(epoll, accept, etc.)をいかに効率よく使うか」の問題でした。eBPF以降は、カーネルの振る舞い自体をアプリケーションの要件に合わせてカスタマイズできるようになりました。
eBPF以前のサーバーアーキテクチャ:
┌─────────────────────────────────────────────┐
│ Application │
│ (サーバーアーキテクチャの選択) │
│ Simple / Fork / Thread / AsyncIO / Hybrid │
└──────────────────┬──────────────────────────┘
│ 固定的なsyscall API
▼
┌─────────────────────────────────────────────┐
│ Kernel (固定) │
│ accept → TCP処理 → スケジューリング → 配信 │
└─────────────────────────────────────────────┘
カーネルの振る舞いは変えられない
→ アプリ側で工夫するしかない
eBPF以降のサーバーアーキテクチャ:
┌─────────────────────────────────────────────┐
│ Application │
└──────────────────┬──────────────────────────┘
│ syscall API
▼
┌─────────────────────────────────────────────┐
│ Kernel + eBPF Programs │
│ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
│ │XDP: │ │sockmap: │ │sk_lookup: │ │
│ │パケット │ │socket間 │ │接続の │ │
│ │前処理 │ │転送 │ │振り分け │ │
│ └──────────┘ └──────────┘ └─────────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
│ │SOCK_OPS: │ │CC: │ │tracing: │ │
│ │TCP │ │輻輳制御 │ │可観測性 │ │
│ │チューニング│ │カスタム │ │ゼロ変更 │ │
│ └──────────┘ └──────────┘ └─────────────┘ │
└─────────────────────────────────────────────┘
カーネルの振る舞いをプログラムで変更可能
→ アプリとカーネルの境界が柔軟に
具体的なパラダイムシフト:
| 従来のアプローチ | eBPFによるアプローチ |
|---|---|
| ユーザー空間でL4ロードバランシング (HAProxy) | XDPでカーネル内L4 LB (Katran) |
| ユーザー空間プロキシ (Envoy sidecar) | sockmap でカーネル内転送 (Cilium) |
| iptablesでパケットフィルタリング | XDPで高速フィルタリング |
| sysctl でTCPパラメータ一括設定 | SOCK_OPSで接続ごとに最適化 |
| カーネルモジュールで輻輳制御追加 | BPFで安全に動的ロード |
| strace/tcpdump で事後分析 | eBPFでリアルタイム可観測性 |
eBPFは「カーネルをリプログラムする」ことで、従来はアーキテクチャパターンの選択で対処していた問題を、カーネル自体の振る舞いを変えることで解決します。これは、Fork→Thread→AsyncIOという「アプリケーション側の工夫」の歴史に対して、カーネル側を適応させるという新しい次元の最適化です。
| Kernel機能 | 登場 | 影響を受けたアーキテクチャ | 影響の内容 |
|---|---|---|---|
| Copy-on-Write | 初期 | Fork, Prefork | fork()の高速化で実用的に |
| select | BSD由来 | Simple → 初期のAsyncIO | 最初のI/O多重化だが限界あり |
| poll | 2.2 | AsyncIO | FD上限を撤廃、しかしO(n)は未解決 |
| sendfile | 2.2 | 全般 | 静的配信のzero-copy化 |
| epoll | 2.6 | AsyncIO, Hybrid | C10K解決の核心技術 |
| NPTL | 2.6 | Thread, Hybrid | スレッドモデルを実用レベルに |
| O(1) Sched | 2.6 | Thread, Hybrid | 大量スレッドでのスケジューリング高速化 |
| futex | 2.6 | Thread, Hybrid | ユーザー空間ロックの高速化 |
| CFS | 2.6.23 | Microthread (goroutine) | M:Nモデルの基盤、公平なスケジューリング |
| accept4 | 2.6.28 | AsyncIO, Hybrid | accept時のsyscall削減 |
| TCP_FASTOPEN | 3.7 | 全般 | 接続確立の高速化 |
| SO_REUSEPORT | 3.9 | Prefork, Hybrid | Thundering Herd問題の解消 |
| XDP (eBPF) | 4.8 | L4 LB, DDoS防御 | カーネルスタック前のパケット処理 |
| sockmap (eBPF) | 4.18 | Proxy, Service Mesh | カーネル内zero-copyソケット転送 |
| io_uring | 5.1 | 次世代AsyncIO | syscall自体のオーバーヘッド排除 |
| BPF TCP CC | 5.6 | 全般 | 輻輳制御の動的カスタマイズ |
| sk_lookup (eBPF) | 5.9 | Hybrid, マルチテナント | プログラマブルな接続振り分け |
┌────────────────────────────────────────────────────────────────────┐
│ │
│ もしepollがなかったら: │
│ → nginx, Node.js, Redis のようなイベント駆動サーバーは │
│ Linux上で高性能を発揮できなかった │
│ → FreeBSD の kqueue が先行し、Linux は劣位に │
│ │
│ もしNTPLがなかったら: │
│ → Thread-per-connectionモデルは数百接続が限界 │
│ → Java/Apache のスレッドモデルがLinuxで不利に │
│ │
│ もしCFSがなかったら: │
│ → Go runtimeのようなM:Nスケジューラが │
│ カーネルスレッドを効率的に利用できなかった │
│ │
│ もしSO_REUSEPORTがなかったら: │
│ → マルチコア環境でのaccept()がボトルネックのまま │
│ → Prefork/Hybridモデルの性能向上が限定的に │
│ │
│ もしio_uringがなかったら: │
│ → システムコールのオーバーヘッドが性能の壁として残り続ける │
│ → 超高負荷環境でのさらなる最適化が困難に │
│ │
│ もしeBPFがなかったら: │
│ → サービスメッシュのサイドカープロキシが常にユーザー空間を経由 │
│ → L4ロードバランシングにDPDKや専用ハードウェアが必須 │
│ → カーネルの可観測性はstrace/tcpdumpの高オーバーヘッド手法のみ │
│ → TCPチューニングはsysctlでの一括設定に限定、接続単位の最適化は不可能 │
│ │
└────────────────────────────────────────────────────────────────────┘
Kernelの進化は単なる性能改善ではなく、新しいアーキテクチャパターンを生み出す原動力でした。epollなしにnginxは生まれず、NTPLなしにApache 2.xのworker MPMは機能せず、CFSなしにGoのgoroutineモデルは現在の効率を達成できませんでした。そしてeBPFは、アプリケーション側の工夫で対処していた問題を「カーネル自体をプログラムする」ことで解決するという、全く新しい次元の最適化を切り開きました。サーバーアーキテクチャの歴史は、Kernelの機能追加に対するアプリケーション層の適応の歴史であり、eBPF以降はその境界自体が溶解しつつあります。
- Go 1.16+
- Linux (epoll, forkのため)
- GCC (thread, hybridモードはcgoを使用)
- The C10K problem
- nginx architecture
- Go netpoller
- epoll(7) - Linux man page
- io_uring - Efficient IO with io_uring (kernel.dk)
- NPTL - Native POSIX Thread Library
- SO_REUSEPORT (lwn.net)
- CFS Scheduler Design
- eBPF - Introduction, Tutorials & Community
- BPF and XDP Reference Guide (Cilium)
- Facebook Katran - XDP L4 Load Balancer
- sockmap and sk_msg (lwn.net)