LabHub

ブログ

オペレーティングシステムの現代的理解 — io_uring、cgroups/namespaces、eBPF、NUMA、GPU UVM、EEVDF、Zero-Copy完全ガイド(2025)

한국어English日本語中文

なぜ今OSを学び直すのか

「OSは学部で習ったものでは?」と聞かれたら、答えはこうです:

2025年のエンジニアがOSを知らなければ、なぜ自分のアプリが遅いのか、なぜコンテナがOOMを食らうのか、なぜCPU 100%なのにスループットが上がらないのかを説明できません。

Part 1 — プロセス、スレッド、コルーチン — 現代の比較

プロセス

スレッド

コルーチン / Fiber / Green Thread

Java Virtual Threads — Project Loom (2023)

JDK 21 LTS。「既存のスレッドコードをほぼそのままにして、数百万の同時リクエストを処理する。」

従来のスレッド: スレッドごとにOSスタック1MB+ → 数万個が限界。 仮想スレッド: JVM内部でスケジューリングし、必要なときだけスタックを割り当てる → 数百万が可能。

ブロッキングI/Oを書きながらノンブロッキングの同時実行性を得られます。2024〜2025年のSpring Bootでの採用によって、Javaバックエンドの地形が変わりつつあります。

Go goroutine

いつ何を?

状況選択
CPUバウンドの並列スレッド + 共有メモリ
I/Oバウンドの大量同時実行コルーチン/async
強力なセキュリティ隔離が必要プロセス + IPC
JVMで数百万の同時実行Virtual Threads

Part 2 — io_uring — epollを越えて

I/O進化の歴史

  1. ブロッキングI/Oread()がデータが来るまでブロック。
  2. select/poll — FDセットを全部スキャン。O(n)。
  3. epoll (Linux, 2002) / kqueue (BSD) — イベントベース、O(1)。
  4. aio_read(POSIX AIO) — 限界が多い。ほとんど使われません。
  5. io_uring (2019, Jens Axboe) — 真の非同期。

io_uringの構造

Submission Queue (SQ) + Completion Queue (CQ)。どちらもmmapされた共有メモリです。syscallなしで作業を提出し、完了を確認します。

io_uring_prep_read(sqe, fd, buf, len, offset);
io_uring_submit(&ring);  // syscall 1回で複数のリクエストを提出
// ... 後で
io_uring_wait_cqe(&ring, &cqe);

利点:

採用:

セキュリティの問題: 2023年にGoogleがChromeのサンドボックスでio_uringを遮断しました。カーネルの攻撃面を広げるためです。そこで信頼できる環境限定で使うようにという勧告が出ています。

Part 3 — 仮想メモリの現代

4段階ページテーブル (x86_64)

CR3 → PML4 → PDPT → PD → PT → Physical Page

48ビット仮想アドレス = 9+9+9+9+12ビット。

5段階 — 57ビットアドレス空間 (Ice Lake 2021+)

大型サーバーに必要です。2024年にはLinuxの標準設定へと移行中。

TLB (Translation Lookaside Buffer)

仮想→物理変換のキャッシュ。サイズは数百エントリ。TLBミスは性能の隠れた殺し屋です

解決策:

Memory Overcommit

Linuxのデフォルト: 「要求されたメモリを全部渡すふりをする」→ 実際に使うときにページを割り当てる。後でメモリが足りなくなる → OOM Killerが発動します。

echo 2 > /proc/sys/vm/overcommit_memory  # strict accounting

Part 4 — cgroups + namespaces = コンテナ

cgroups v2

リソースグループの階層。CPU、メモリ、I/O、PID数を制限します。

/sys/fs/cgroup/
  my-app/
    cpu.max         # "50000 100000"50% CPU
    memory.max      # "1G"
    io.max          # "8:0 rbps=10485760"  → 10MB/s read

namespaces

プロセスの「自分だけのビュー」。

Dockerの本質

container = chroot + namespaces + cgroups + capabilities + seccomp + AppArmor/SELinux

仮想マシンではありません。 1つのカーネルを共有し、「見える領域」だけが違います。

rootlessコンテナ (2020+)

user namespaceでルートなしにコンテナを実行します。Podman、Buildahが主導しました。

Part 5 — eBPF — カーネルにコードを注入する

eBPFとは

Extended BPF. カーネル空間で実行される小さなVMです。サンドボックスと検証器で安全性を保証します。

なぜ革命なのか:

使われる場所

XDP (eXpress Data Path)

ネットワークカードのNICドライバレベルでeBPFを実行します。パケットがカーネルスタックに入る前に処理する → DDoS防御などに使われます。毎秒数千万パケットの処理が可能です。

開発スタック

// bpftraceの例: open() syscallの追跡
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s -> %s\n", comm, str(args->filename)); }'

Part 6 — NUMA — 32コア以上での隠れたコスト

NUMAとは

Non-Uniform Memory Access. 大きなサーバーは複数のソケット(物理CPU)を持ち、各ソケットが自分のメモリバンクを持ちます。別のソケットのメモリアクセスは1.5〜3倍遅くなります。

確認

numactl --hardware
# node 0 cpus: 0-23
# node 0 size: 96GB
# node 1 cpus: 24-47
# node 1 size: 96GB
# node distances: 10 (local), 21 (remote)

NUMAバインディング

# プロセスをNUMAノード0でのみ実行
numactl --cpunodebind=0 --membind=0 ./my-app

DBサーバー、LLM推論、高性能プロキシでは必須です。デフォルトに任せると、スケジューラは最適ではない判断をします。

Kubernetes + NUMA

LLM推論のK8sクラスタでは、この設定が処理量に30%以上の差を生みます。

Part 7 — Linuxスケジューラの進化

CFS (Completely Fair Scheduler, 2007-2024)

EEVDF (Earliest Eligible Virtual Deadline First, 2024)

Linux 6.6から標準です。Peter Zijlstraが主導しました。

CPU隔離の技法

低遅延トレーディング・HFTインフラでは標準です。

Part 8 — GPUドライバとLLMの関係

GPUコンテナの複雑さ

DockerでGPUを使うには:

UVM (Unified Virtual Memory, CUDA 6+)

CPUとGPUが同じアドレス空間を共有します。ページフォルト時に必要なページだけを転送します。LLM訓練でGPUメモリより大きいモデルを扱うときに必須です。

CUDA Graph

繰り返されるカーネル呼び出しのパターンをグラフとしてキャプチャし、オーバーヘッドを除去します。LLM推論のデコードループで20%以上の速度改善という事例があります。

2024-2025のGPU OS課題

Part 9 — I/O性能の最終形

Zero-Copy

従来: read() → buffer → write()と各段階でコピー。 Zero-copy: sendfile(), splice(), io_uring, MSG_ZEROCOPY — カーネルが直接DMAします。

実際の事例: Kafkaがconsumerにデータを送るときにsendfile()を使用 → CPU使用率が半分に。

DMA (Direct Memory Access)

CPUの関与なしにデバイスがメモリへ直接アクセスします。ネットワークカード、SSD、GPUはいずれもDMAエンジンを内蔵しています。

RDMA (Remote DMA)

別マシンのメモリにCPUの関与なく直接アクセスします。Infiniband、RoCE(RDMA over Converged Ethernet)。

DPDK / AF_XDP

カーネルのネットワークスタックをバイパスして、ユーザー空間から直接NICを扱います。クラウドの仮想スイッチ、5G UPF、高性能プロキシ(Cloudflare、Fastly)。

Part 10 — WSL2、コンテナ、仮想化の交差点

WSL2 (Windows Subsystem for Linux 2)

Firecracker (AWS Lambdaの基盤)

gVisor (Google)

Kata Containers

Part 11 — 観測の道具たち

ツール用途
perfハードウェア + ソフトウェアのイベント
ftraceカーネル関数の追跡
bpftraceeBPFの一行スクリプト
bcceBPFツール集(execsnoop、opensnoopなど)
stracesyscallの追跡(遅い)
ltraceライブラリ呼び出しの追跡
pmapプロセスのメモリマップ
iotop / biolatencyI/O分析
perf topCPUサンプリング
flame graphスタックの可視化(Brendan Gregg)

Continuous Profiling

Part 12 — OSチェックリスト(12項目)

  1. ulimitの確認 — プロダクションサーバーのFD制限、nproc制限。
  2. Swapポリシー — swappiness=1(DB)または0(レイテンシに敏感)。
  3. Transparent Huge Pages — DBはほとんどの場合オフにしたほうが良いです。
  4. NUMAバインディング — ソケットが2つ以上のシステムで。
  5. io_uringのサポート確認 — 最新カーネルでfdの上限を調整。
  6. cgroups v2の使用 — v1は機能が限られます。
  7. seccompプロファイル — コンテナのsyscallを制限。
  8. 基本TCPパラメータのチューニング — somaxconn、tcp_max_syn_backlog。
  9. カーネルバージョンの確認 — 最新LTS(6.6+)はEEVDF、io_uringが成熟。
  10. eBPF観測インフラ — プロファイリングツールを1つは常時稼働。
  11. OOM Killerログの監視 — dmesgの手がかり。
  12. CPU governorperformanceモードを設定(サーバー)。

Part 13 — 10大アンチパターン

  1. コンテナをVMとして扱う — 「カーネル共有」を忘れると、セキュリティ・性能の誤解が生じます。
  2. 1プロセスに数万スレッド — コンテキストスイッチ地獄。コルーチンが答えです。
  3. ブロッキングI/Oで大量の同時実行 — イベントループ / コルーチン / Virtual Thread。
  4. swappiness=60(デフォルト)を維持 — DBは下げるべきです。
  5. 大容量RAMでTHPを無視 — 遅延スパイクの原因になり得ます。
  6. NUMAを無視 — 大きなマシンだからとプロセス1つを回すだけでは性能が半分です。
  7. syscallの多いアプリでstraceを常時稼働 — 100倍遅くなります。eBPFを使ってください。
  8. コンテナをルートで実行 — rootlessまたはdrop capabilities。
  9. Firecrackerを「一般的なK8sコンテナ」と同じように扱う — ストレージ・ネットワークが違います。
  10. GPUをdocker runにそのまま接続 — Toolkit + ドライバのバージョンマトリクスを確認。

おわりに — OSは依然として「性能の境界」

2025年のアプリの性能上限は、しばしばOSが決めます。io_uringを知っているかどうかがネットワークサーバーの処理量に2倍の差を生み、cgroups v2の設定がコンテナのOOMを決め、NUMAバインディングがLLM推論のコストを左右します。

OSは「自分のアプリより下」ではなく「自分のアプリの一部」です。良いエンジニアは必要な瞬間にこの境界を越えられます。/proc/を探検し、perf topを回し、bpftrace一行で答えを得ます。

学部のOS授業で学んだこと(ページテーブル、スケジューラ、セマフォ)は今も生きています。ただ形が進化しました。2025年のOSは、単一カーネル + 数千のコンテナ + GPU + RDMAが共存する世界です。この世界を理解することがエンジニアの新しい基礎です。

次回予告 — 「コンパイラと現代の言語ランタイム」 — LLVM、JIT、GC、Inline Caching、Escape Analysis、WASMランタイムまで

OSの下はハードウェア、OSの上はランタイム。次は言語がどのように実行されるかです。

「自分のコードが実行されるまでに何が起きているのか?」次回の記事で。

コメント

まだコメントはありません。

ログインするとコメントできます