本番で出会う「OS由来の現象」
TypeScriptでAPIサーバーを書き、Dockerイメージにしてデプロイする。この流れの中で、OSを意識する場面はあまりありません。 ところが本番環境では、アプリのコードをいくら読んでも原因が分からない現象に出会います。
- Node.jsはJavaScriptを1スレッドで実行しているのに、数万の同時接続をさばけている。なぜ詰まらないのか。
- Kubernetesのダッシュボードに
OOMKilled、終了コード137と表示されてPodが再起動した。アプリにはエラーログが1行も残っていない。 - 負荷試験の途中で
EMFILE: too many open filesが出始めた。ファイルはほとんど開いていないはずなのに。 - 重いJSONを処理するエンドポイントを叩くと、関係のないヘルスチェックまで遅くなる。
どれもアプリケーションのバグというより、OSの仕組みがそのまま表に出てきた現象です。 137は「128 + シグナル番号9(SIGKILL)」で、カーネルがプロセスを強制終了した印です。 EMFILEはファイルではなく「ファイルディスクリプタ」の上限に達したことを示し、ソケット1本も1つのディスクリプタとして数えられます。 本シリーズでは、こうした現象を「なんとなく知っている」状態から、根拠を持って説明できる状態にすることを目指します。
| 実務で見る現象 | 裏にあるOSの仕組み | 扱う章 |
|---|---|---|
| Node.jsが1スレッドで大量接続をさばける | ノンブロッキングI/Oとepoll / kqueue、libuvのイベントループ | 第7章・第9章 |
| OOMKilled(終了コード137) | cgroupのメモリ上限、OOM Killer、シグナルSIGKILL | 第3章・第5章・第11章 |
| EMFILE: too many open files | ファイルディスクリプタとプロセスごとの上限(ulimit) | 第6章 |
| 重い処理で全リクエストが遅くなる | イベントループのブロッキング、スレッドプール | 第10章 |
| SIGTERMで即死して処理中のリクエストが消える | シグナルとハンドラ、PID 1問題 | 第3章・第11章 |
OSの3つの役割 — 抽象化・資源管理・保護
OS(オペレーティングシステム)の仕事は、大きく次の3つに分けられます。
抽象化 — ハードウェアの違いを隠す
ディスクにはSSDもHDDもあり、ネットワークカードもメーカーごとに制御方法が違います。 OSはこうした違いを隠して、プログラムに「ファイル」「ソケット」「プロセス」といった扱いやすい部品を提供します。 Node.jsで fs.readFile を呼ぶとき、NVMeのコマンドキューを意識する必要がないのはこのためです。 Unixはこの考え方を徹底し、ファイルもデバイスもパイプも同じ read / write で扱えるようにしました。 これが「Everything is a file」と呼ばれる設計で、第6章で詳しく扱います。
資源管理 — 限られたCPU・メモリを配る
1台のマシンでは、ブラウザ、エディタ、Dockerデーモン、Node.jsのプロセスが同時に動いています。 CPUコアの数より動きたいプログラムのほうが多いので、OSはどのプログラムをいつ、どのコアで動かすかを決めます(スケジューリング、第4章)。 メモリも同様で、各プロセスに「自分専用の広いメモリ」があるように見せつつ、実際の物理メモリをやりくりしています(仮想メモリ、第5章)。
保護 — プログラム同士を隔離する
あるプロセスのバグが、別のプロセスのメモリやカーネル自身を壊してはいけません。 OSはCPUの特権モードを使い、普通のプログラムがハードウェアや他人のメモリに直接触れないようにしています。 プログラムがファイルを開くなどの特権的な操作をしたいときは、システムコールという決められた窓口を通してOSに頼みます。 この境界の仕組みが第2章のテーマです。
あなたのTypeScriptからハードウェアまで
Node.jsで書いたコードが実際に動くまでに、どのような層を通るのかを図にしました。
graph TD A["あなたのTypeScriptコード<br/>(tscでJavaScriptに変換)"] --> B[V8 / Node.js<br/>JavaScriptの実行とNode API] B --> C[libuv / libc<br/>イベントループ・スレッドプール・C標準ライブラリ] C --> D[システムコール<br/>openat・read・write・epoll_wait など] D --> E[カーネル<br/>プロセス・メモリ・ファイル・ネットワーク管理] E --> F[ハードウェア<br/>CPU・メモリ・ディスク・NIC] style A fill:#3b82f6,stroke:#1d4ed8,color:#fff style B fill:#3b82f6,stroke:#1d4ed8,color:#fff style C fill:#8b5cf6,stroke:#6d28d9,color:#fff style D fill:#f97316,stroke:#ea580c,color:#fff style E fill:#8b5cf6,stroke:#6d28d9,color:#fff style F fill:#64748b,stroke:#475569,color:#fff
一番下のハードウェア、つまりCPUのパイプラインやキャッシュ、MESIプロトコルの話はCPUアーキテクチャ Deep Diveで扱いました。 本シリーズはその一つ上、カーネルとシステムコール、そしてNode.jsを支えるlibuvまでの層を対象にします。 CPUシリーズ第4章で見たスワップやページフォールトも、本シリーズの第5章で「OSから見るとどう見えるか」という角度で掘り直します。
60年の歴史を一気に辿る
今日のWebサーバーの仕組みは、ほとんどが過去のOS研究の積み重ねです。 タイムシェアリング、プロセス、ファイル、ノンブロッキングI/Oといった考え方がどの順に登場したのかを年表で見てみます。
CTSS(MIT)
1台の大型計算機を複数の利用者が端末から同時に使うタイムシェアリングを実現。CPU時間を細かく切って配る考え方の出発点
Multics
MIT・Bell研究所・GEの共同プロジェクト。階層型ファイルシステムや保護リングなど、後のOSに受け継がれる設計を多く持ち込んだ
Unix誕生(Bell研究所)
Multicsから撤退したKen ThompsonとDennis Ritchieが、小さく単純なOSとしてUnixを作り始める
UnixをC言語で書き直し
アセンブリからCへ移したことで、他の機種へ移植しやすくなった。1974年にはCACM論文「The UNIX Time-Sharing System」が発表される
Linux 0.01
ヘルシンキ大学の学生Linus Torvaldsが公開。翌1992年1月にはTanenbaumとの「LINUX is obsolete」論争が起きる
Windows NT 3.1
Dave Cutler率いるチームが開発。翌年のNT 3.5でIOCP(I/O完了ポート)が入り、Windowsの非同期I/Oの基盤になる
epoll(Linux 2.5.44)
1999年に提起されたC10K問題への回答。大量のソケットのうち準備ができたものだけを効率よく知る仕組み
Node.js 公開
Ryan Dahlが5月27日に初公開し、11月8日のJSConf EUで発表。2011年のv0.6.0でlibuvが導入され、Windows(IOCP)にも対応した
io_uring(Linux 5.1)
Jens Axboeが開発。カーネルとリングバッファを共有し、システムコールの回数を減らして大量のI/Oを投げられる
EEVDFスケジューラ(Linux 6.6)
2007年から使われてきたCFSに代わる、新しいプロセススケジューラが入る
Node.js 26 が Active LTS に
v26.13.0(V8 14.6、libuv 1.52.1)。27以降は年1回のメジャーリリースがすべてLTSになる
Node.jsは「OSの進化」の上に乗っている
年表の後半を見ると、Node.jsがどのような流れの中で生まれたかが分かります。 1999年、Dan Kegelは「1台のサーバーで1万接続(C10K)をどうさばくか」という問題を提起しました。 1接続ごとにプロセスやスレッドを作る方式では、メモリもコンテキストスイッチも追いつかなかったからです。 これに応えてFreeBSDのkqueue(2000年)やLinuxのepoll(2002年)が生まれ、2004年にはそれを使うnginxが登場します。
Node.jsはこの「1スレッドのイベントループで大量の接続を待つ」方式を、JavaScriptのコールバックと組み合わせたものです。 初期のNode.jsはUnix専用のライブラリlibevを使っていましたが、Windows対応のためにlibuvが作られ、epoll・kqueue・IOCPの違いを吸収するようになりました。 なぜこの方式が効くのか、それでもどこで詰まるのかは、第7章から第10章で順に解き明かします。
なぜLinuxを中心に学ぶのか
W3Techsの調査(2026年10月29日時点)では、OSが判明しているWebサイトのうち92.2%がUnix系で、Linuxと確認できるものだけでも63.3%を占めます。 AWSのEC2もKubernetesのノードも、Vercelなどのサーバーレス環境の裏側も、多くはLinuxです。 StatCounterの2026年8月の集計では、モバイルOSの約67.5%をAndroidが占めており、AndroidもLinuxカーネルの上で動いています。
Stack Overflow Developer Survey 2025では、Node.jsが48.7%でWeb技術の1位、Dockerの利用率は71.1%でした。 Node.jsのコードをDockerコンテナに入れてLinux上で動かす、というのが今のWebエンジニアのごく普通の構成です。 そのため本シリーズでは、Linuxの仕組みを基準に説明します。
全12章の地図
本シリーズは、下の層から順に積み上げていきます。 第2〜6章でOSの基本部品(カーネル境界・プロセス・スレッド・メモリ・ファイル)を押さえ、 第7〜10章でそれらがI/Oモデルと並行処理、Node.jsのイベントループにどうつながるかを見ます。 最後の第11〜12章で、コンテナ運用と実測に進みます。
| 章 | テーマ | この章で答える問い |
|---|---|---|
| 第1章 | なぜOSを学ぶのか | OSは何をしていて、Webエンジニアの仕事とどうつながるのか |
| 第2章 | カーネルとシステムコール | fs.readFileSyncはどうやってカーネルに頼み事をしているのか |
| 第3章 | プロセスの一生 | プロセスはどう生まれ、どう死に、なぜゾンビになるのか |
| 第4章 | スレッドとスケジューラ | プロセスとスレッドは何が違い、CPUはどう配られるのか |
| 第5章 | 仮想メモリ | RSSとheapUsedはなぜ違い、OOM Killerは誰を殺すのか |
| 第6章 | ファイルとFD | EMFILEは何の上限で、writeしたデータはいつディスクに届くのか |
| 第7章 | I/Oモデルの進化 | selectからepoll、io_uringへ何が改善されてきたのか |
| 第8章 | 並行処理モデル比較 | prefork・スレッド・イベントループ・goroutineはどう選ぶのか |
| 第9章 | イベントループとlibuv | Node.jsは本当にシングルスレッドなのか |
| 第10章 | イベントループを止めない設計 | ReDoSや巨大JSONはなぜ全リクエストを止めるのか |
| 第11章 | コンテナとプロセス運用 | OOMKilledやSIGTERMにどう備え、PID 1は何が特別なのか |
| 第12章 | 実測と2026年の動向 | straceで見るとNode.jsは実際に何をしているのか |
まとめ
OSは、ハードウェアの違いを隠す抽象化、CPUやメモリを配る資源管理、プログラム同士を隔離する保護の3つを担っています。 その歴史は1961年のCTSSから始まり、Unix、Linux、epollを経て、Node.jsやio_uringにつながっています。 Webサーバーの大半はLinuxで動いているので、Linuxの仕組みを知っておくと、OOMKilledやEMFILEのような障害を自分で読み解けるようになります。
次の第2章では、OSの「保護」を支えるユーザーモードとカーネルモードの境界と、その境界を越える唯一の窓口であるシステムコールを見ていきます。
理解度チェック
Kubernetesで終了コード137が表示されたとき、この数字が意味するものとして最も適切なものはどれですか?
キーボード: 1〜4 で選択、Enter で回答