Nginx Worker とイベントループ徹底解説:epoll/kqueue が高速化を生む理由
Nginx が高速な Web サーバーとして知られているのには、明確な理由があります。その秘密は「Worker プロセス」と「イベントループ」、そして OS カーネルが提供する epoll や kqueue といった効率的な I/O 多重化機構にあります。
本記事では、Nginx がどのようにして大量のリクエストを少ないリソースで捌けるのか、その仕組みを初心者にもわかりやすく図解しながら徹底的に解説します。Apache との違いや、なぜ C10K 問題を解決できたのかも明らかになるでしょう。
背景
Web サーバーが直面する同時接続問題
Web サーバーは、複数のクライアントから同時にリクエストを受け付ける必要があります。従来の Apache などのサーバーでは、1 つのリクエストに対して 1 つのプロセスやスレッドを割り当てる「マルチプロセス/マルチスレッドモデル」が主流でした。
しかし、このモデルには限界がありました。接続数が増えると、プロセスやスレッドの数も増加し、メモリ消費とコンテキストスイッチのオーバーヘッドが問題になります。これが「C10K 問題」と呼ばれる課題です。
以下の図は、従来型のマルチプロセスモデルと Nginx のイベント駆動モデルの違いを示しています。
mermaidflowchart TB
subgraph Apache ["従来型: Apache マルチプロセスモデル"]
client1["クライアント1"] --> process1["プロセス1<br/>(メモリ消費大)"]
client2["クライアント2"] --> process2["プロセス2<br/>(メモリ消費大)"]
client3["クライアント3"] --> process3["プロセス3<br/>(メモリ消費大)"]
clientN["クライアントN"] --> processN["プロセスN<br/>(メモリ消費大)"]
end
subgraph Nginx ["イベント駆動型: Nginx モデル"]
clientA["クライアント1〜N"] --> worker1["Worker 1<br/>(少数)"]
clientA --> worker2["Worker 2<br/>(少数)"]
worker1 --> eventloop1["イベントループ<br/>(epoll/kqueue)"]
worker2 --> eventloop2["イベントループ<br/>(epoll/kqueue)"]
end
Nginx の設計思想
Nginx は、この問題を解決するために「イベント駆動型」「非同期処理」「ノンブロッキング I/O」という設計思想を採用しました。少数の Worker プロセスで、数万から数十万の同時接続を効率的に処理できるのです。
その中核を担うのが、OS カーネルレベルで提供される高性能な I/O 多重化機構である epoll(Linux)や kqueue(BSD 系)といった技術でしょう。
課題
従来型アーキテクチャの限界
従来のマルチプロセス/マルチスレッドモデルには、以下のような課題がありました。
メモリ消費の増大
1 つのプロセスやスレッドは、数 MB のメモリを消費します。1 万接続を処理しようとすると、それだけで数十 GB のメモリが必要になってしまいます。
javascript// 従来型モデルの概念コード(擬似コード)
// 1リクエストごとに新しいプロセス/スレッドを生成
function handleRequest(request) {
// 新しいプロセス/スレッドを作成
const process = createNewProcess();
// メモリを大量に消費
// 各プロセスは独立したメモリ空間を持つ
process.allocateMemory(); // 数MB消費
// リクエスト処理
process.execute(request);
}
コンテキストスイッチのオーバーヘッド
OS は複数のプロセス/スレッドを切り替えながら実行します。この「コンテキストスイッチ」には、CPU レジスタの保存・復元などのコストがかかります。
プロセス数が増えると、実際の処理よりもコンテキストスイッチに時間を取られるようになり、スループットが低下してしまうのです。
ブロッキング I/O の非効率性
従来型のモデルでは、ディスクやネットワークからのデータ読み込み中、プロセス/スレッドは待機状態(ブロッキング)になります。
javascript// ブロッキングI/Oの例(擬似コード)
function processRequest() {
// ファイル読み込み中は待機状態になる
const data = readFileSync('/path/to/file'); // ★ここでブロック
// ネットワーク通信中も待機状態
const response = httpRequestSync(
'http://api.example.com'
); // ★ここでブロック
// 実際の処理
return processData(data, response);
}
この待機時間中、プロセス/スレッドは何もせずにメモリだけを消費し続けます。これは非常に非効率です。
以下の図は、ブロッキング I/O とノンブロッキング I/O の違いを示しています。
mermaidsequenceDiagram
participant Thread as スレッド
participant OS as OS カーネル
participant Disk as ディスク
Note over Thread,Disk: ブロッキングI/O
Thread->>OS: データ読み込み要求
OS->>Disk: データ読み込み
Note over Thread: 待機状態<br/>(CPUを使わず待つだけ)
Disk-->>OS: データ返却
OS-->>Thread: データ返却
Note over Thread: 処理再開
Note over Thread,Disk: ノンブロッキングI/O(Nginxの方式)
Thread->>OS: データ読み込み要求(非同期)
Thread->>Thread: 他のリクエスト処理
OS->>Disk: データ読み込み
Disk-->>OS: データ返却
OS-->>Thread: イベント通知
Thread->>Thread: データ処理
C10K 問題とは
1999 年頃、Web サーバーが 1 万(10K = 10,000)の同時接続を処理しようとすると、上記の問題が顕在化し、パフォーマンスが著しく低下する現象が注目されました。これが「C10K 問題」です。
Nginx は、この C10K 問題を解決するために開発されたのです。
解決策
Nginx のアーキテクチャ
Nginx は、以下の 3 つの要素を組み合わせて、高効率な処理を実現しています。
Master プロセスと Worker プロセス
Nginx は起動時に 1 つの Master プロセスと、複数の Worker プロセスを生成します。
plaintext# Nginxのプロセス構造
Master プロセス(root権限)
├─ Worker プロセス 1(通常権限)
├─ Worker プロセス 2(通常権限)
├─ Worker プロセス 3(通常権限)
└─ Worker プロセス 4(通常権限)
Master プロセスは設定の読み込みや Worker プロセスの管理を担当し、実際のリクエスト処理は Worker プロセスが行います。
nginx# nginx.confの設定例
# Workerプロセスの数を指定
# 通常はCPUコア数と同じにする
worker_processes 4;
# 1つのWorkerが処理できる最大接続数
# この例では1 Workerあたり最大1024接続
events {
worker_connections 1024;
}
Worker プロセスの数は通常、CPU のコア数と同じに設定します。これにより、コンテキストスイッチを最小限に抑えながら、CPU リソースを最大限活用できます。
イベント駆動型アーキテクチャ
各 Worker プロセスは、「イベントループ」と呼ばれる仕組みで動作します。イベントループは、複数の接続を監視し、I/O が可能になったタイミングで処理を実行する方式です。
javascript// イベントループの概念コード(擬似コード)
function eventLoop() {
// 無限ループで接続を監視
while (true) {
// epoll/kqueueを使って、I/O可能な接続を取得
const readyConnections = epoll.wait(); // ★ノンブロッキング
// 準備ができた接続だけを処理
for (const connection of readyConnections) {
// データが読み込める状態なら読み込む
if (connection.isReadable()) {
const data = connection.read(); // ★ノンブロッキング
processRequest(data);
}
// データが書き込める状態なら書き込む
if (connection.isWritable()) {
connection.write(responseData); // ★ノンブロッキング
}
// エラーやクローズ処理
if (connection.hasError() || connection.isClosed()) {
connection.close();
}
}
}
}
この方式により、1 つの Worker プロセスで数千から数万の接続を同時に処理できるのです。
以下の図は、Worker プロセスがどのようにイベントループで複数の接続を処理するかを示しています。
mermaidflowchart LR
subgraph Worker ["Worker プロセス"]
eventloop["イベントループ"]
epoll["epoll/kqueue<br/>(I/O多重化)"]
eventloop -->|監視依頼| epoll
epoll -->|イベント通知| eventloop
end
conn1["接続1"] -->|リクエスト| epoll
conn2["接続2"] -->|リクエスト| epoll
conn3["接続3"] -->|リクエスト| epoll
connN["接続N"] -->|リクエスト| epoll
eventloop -->|レスポンス| conn1
eventloop -->|レスポンス| conn2
eventloop -->|レスポンス| conn3
eventloop -->|レスポンス| connN
図で理解できる要点:
- 1 つの Worker プロセスが複数の接続を同時に管理
- epoll/kqueue が「準備ができた接続」だけを効率的に通知
- Worker は待機時間ゼロで次々と処理を進められる
ノンブロッキング I/O
Nginx は、すべての I/O 操作を「ノンブロッキング」モードで実行します。これにより、I/O 待ちでプロセスがブロックされることがなくなります。
c// C言語でのノンブロッキングI/O設定例
#include <fcntl.h>
int fd = socket(AF_INET, SOCK_STREAM, 0);
// ソケットをノンブロッキングモードに設定
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
// この状態でread()を呼ぶと、データがなくても即座にリターンする
ノンブロッキング I/O では、データが準備できていない場合、read() や write() は即座に制御を返します。これにより、Worker プロセスは他の接続の処理を続けられるのです。
epoll と kqueue:高速化の核心
Nginx の高速性を支える最も重要な技術が、OS カーネルが提供する I/O 多重化機構です。Linux では epoll、BSD 系(FreeBSD、macOS など)では kqueue が使われます。
従来の select/poll の問題点
それ以前に使われていた select() や poll() には、以下の問題がありました。
| # | 項目 | select/poll | epoll/kqueue |
|---|---|---|---|
| 1 | 監視可能なファイルディスクリプタ数 | 制限あり(通常 1024) | 実質無制限 |
| 2 | 計算量 | O(n)(全接続を走査) | O(1)(準備済み接続のみ) |
| 3 | データコピー | カーネル ⇔ ユーザー空間で毎回コピー | 初回登録のみ |
| 4 | スケーラビリティ | 接続数増加で性能低下 | 接続数に影響されにくい |
c// select()の問題点を示すコード例
#include <sys/select.h>
fd_set readfds;
int max_fd = 0;
while (1) {
FD_ZERO(&readfds);
// ★毎回すべての接続をセットし直す必要がある
for (int i = 0; i < num_connections; i++) {
FD_SET(connections[i].fd, &readfds);
if (connections[i].fd > max_fd) {
max_fd = connections[i].fd;
}
}
// ★すべての接続をチェック(O(n)の計算量)
select(max_fd + 1, &readfds, NULL, NULL, NULL);
// ★準備ができた接続を探すために全件走査
for (int i = 0; i < num_connections; i++) {
if (FD_ISSET(connections[i].fd, &readfds)) {
handle_connection(&connections[i]);
}
}
}
接続数が増えると、この全件走査のコストが膨大になり、性能が劣化してしまうのです。
epoll の仕組み
Linux の epoll は、以下の 3 つの関数で構成されています。
c// epollの基本的な使い方
#include <sys/epoll.h>
// 1. epollインスタンスを作成
int epfd = epoll_create1(0);
// 2. 監視対象のファイルディスクリプタを登録
struct epoll_event ev;
ev.events = EPOLLIN; // 読み込み可能イベントを監視
ev.data.fd = connection_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connection_fd, &ev);
上記のコードで、監視対象を epoll に「登録」します。この登録は一度だけ行えば良く、以降は自動的に監視され続けます。
c// 3. イベント待機と処理
struct epoll_event events[MAX_EVENTS];
while (1) {
// ★I/O可能な接続だけを取得(O(1)の効率)
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
// ★準備ができた接続だけを処理
for (int i = 0; i < nfds; i++) {
int fd = events[i].data.fd;
if (events[i].events & EPOLLIN) {
// 読み込み可能
handle_read(fd);
}
if (events[i].events & EPOLLOUT) {
// 書き込み可能
handle_write(fd);
}
if (events[i].events & (EPOLLERR | EPOLLHUP)) {
// エラーまたは切断
close(fd);
}
}
}
epoll_wait() は、I/O が可能になった接続だけを返します。そのため、接続数が増えても処理時間は増加しません。これが O(1) の効率を実現する秘密です。
epoll のイベント通知モード
epoll には 2 つの通知モードがあります。
c// レベルトリガー(LT: Level Triggered)
// デフォルトモード
ev.events = EPOLLIN; // 読み込み可能な間、何度でも通知される
レベルトリガーでは、データが読み込み可能な状態が続く限り、epoll_wait() が繰り返し通知します。処理し忘れを防げますが、やや効率が落ちます。
c// エッジトリガー(ET: Edge Triggered)
// Nginxが使用するモード
ev.events = EPOLLIN | EPOLLET; // 状態が変化したときだけ通知
エッジトリガーでは、状態が変化したとき(新しいデータが到着したとき)だけ通知されます。Nginx はこのモードを使い、より高いパフォーマンスを実現しています。
kqueue の仕組み
BSD 系 OS(FreeBSD、macOS など)では、kqueue が使われます。基本的な考え方は epoll と同じですが、より汎用的な設計になっています。
c// kqueueの基本的な使い方
#include <sys/event.h>
// 1. kqueueインスタンスを作成
int kq = kqueue();
// 2. 監視対象を登録
struct kevent change;
EV_SET(&change, connection_fd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, NULL);
kevent(kq, &change, 1, NULL, 0, NULL);
EV_SET マクロで監視設定を作成し、kevent() 関数で登録します。EVFILT_READ は読み込み可能イベント、EVFILT_WRITE は書き込み可能イベントを監視します。
c// 3. イベント待機と処理
struct kevent events[MAX_EVENTS];
while (1) {
// ★I/O可能な接続を取得
int nev = kevent(kq, NULL, 0, events, MAX_EVENTS, NULL);
// ★イベントを処理
for (int i = 0; i < nev; i++) {
int fd = events[i].ident;
if (events[i].filter == EVFILT_READ) {
// 読み込み可能
handle_read(fd);
}
if (events[i].filter == EVFILT_WRITE) {
// 書き込み可能
handle_write(fd);
}
}
}
kqueue は、ファイル I/O だけでなく、シグナル、タイマー、プロセスイベントなども統一的に扱えるのが特徴です。
以下の図は、epoll/kqueue がどのように効率的に接続を監視するかを示しています。
mermaidflowchart TB
subgraph legacy["従来型: select/poll"]
app1[アプリケーション] --|全接続リストを毎回渡す|--> kernel1[カーネル]
kernel1 --|全接続を走査 O(n)|--> check1[準備済み接続を探す]
check1 --|結果リストを返す|--> app1
end
subgraph modern["最新型: epoll/kqueue"]
app2[アプリケーション] --|初回のみ登録|--> kernel2[カーネル<br/>(接続リストを保持)]
kernel2 --|準備済み接続だけ通知 O(1)|--> app2
note1[★カーネルが自動監視<br/>★準備済み接続のみ返却]
end
style note1 fill:#ffffcc,stroke:#333,stroke-width:1px,stroke-dasharray:3 3
図で理解できる要点:
- 従来型は毎回全接続をカーネルに渡し、全件走査が必要
- epoll/kqueue は初回登録のみで、以降はカーネルが自動監視
- 準備済み接続だけが通知されるため、処理時間が O(1) で安定
Nginx の設定例
実際の Nginx 設定で、イベント処理をどのように制御するかを見てみましょう。
nginx# nginx.confのeventsブロック
events {
# 使用するイベント処理方式を指定
# 通常は自動選択(Linux: epoll, BSD: kqueue)
use epoll;
# 1つのWorkerが処理できる最大同時接続数
worker_connections 10000;
# 新しい接続をできるだけ多く受け入れる
multi_accept on;
}
worker_connections は、1 つの Worker プロセスが同時に処理できる接続数の上限です。Worker プロセスが 4 つあれば、合計で 40,000 接続を処理できることになります。
nginx# httpブロックの最適化設定
http {
# ファイル送信の最適化(sendfile syscallを使用)
sendfile on;
# TCP_CORKオプション(sendfileと併用)
tcp_nopush on;
# TCP_NODELAYオプション(小さなパケットを即座に送信)
tcp_nodelay on;
# キープアライブのタイムアウト設定
keepalive_timeout 65;
}
これらの設定により、OS カーネルの機能を最大限活用し、ネットワーク処理を最適化できます。
具体例
Nginx と Apache の性能比較
実際の動作を理解するために、Nginx と Apache の処理フローを比較してみましょう。
Apache のプロセスモデル(prefork MPM)
bash# Apacheのプロセス状態を確認
ps aux | grep apache
# 出力例(接続数分だけプロセスが存在)
apache 1234 0.5 2.0 /usr/sbin/apache2
apache 1235 0.5 2.0 /usr/sbin/apache2
apache 1236 0.5 2.0 /usr/sbin/apache2
...
apache 2234 0.5 2.0 /usr/sbin/apache2 # 1000個のプロセス
Apache の prefork MPM では、1 つのリクエストに対して 1 つのプロセスが割り当てられます。1000 接続なら 1000 プロセスが必要です。
plaintext# Apacheのメモリ使用量計算例
1プロセスあたり: 約20MB
1000接続の場合: 20MB × 1000 = 20GB
★大量のメモリが必要
Nginx のプロセスモデル
bash# Nginxのプロセス状態を確認
ps aux | grep nginx
# 出力例(少数のプロセスで大量接続を処理)
root 1000 0.0 0.1 nginx: master process
nginx 1001 0.5 1.0 nginx: worker process
nginx 1002 0.5 1.0 nginx: worker process
nginx 1003 0.5 1.0 nginx: worker process
nginx 1004 0.5 1.0 nginx: worker process
Nginx では、4 つの Worker プロセスで数万の接続を処理できます。
plaintext# Nginxのメモリ使用量計算例
1 Workerあたり: 約10MB
4 Workerの場合: 10MB × 4 = 40MB
★10000接続でもわずか40MB
以下の図は、同じ接続数を処理する際の Apache と Nginx のリソース使用量の違いを示しています。
mermaidflowchart LR
subgraph Comparison ["1000接続時のリソース比較"]
subgraph Apache方式 ["Apache (prefork)"]
ap_conn["1000接続"] --> ap_proc["1000プロセス"]
ap_proc --> ap_mem["メモリ: 約20GB"]
ap_proc --> ap_cpu["CPU: 高負荷<br/>(コンテキストスイッチ)"]
end
subgraph Nginx方式 ["Nginx"]
ng_conn["1000接続"] --> ng_proc["4 Worker<br/>プロセス"]
ng_proc --> ng_mem["メモリ: 約40MB"]
ng_proc --> ng_cpu["CPU: 低負荷<br/>(効率的)"]
end
end
図で理解できる要点:
- Apache は接続数に比例してプロセスとメモリが増加
- Nginx は少数の Worker で大量接続を効率的に処理
- メモリ使用量に約 500 倍の差が発生
実際の接続処理フロー
Nginx が実際にどのようにリクエストを処理するか、詳細なフローを見てみましょう。
ステップ 1:接続の受け入れ
c// 新しい接続を受け入れる(擬似コード)
int listen_fd = socket(...);
bind(listen_fd, ...);
listen(listen_fd, ...);
// listenソケットをepollに登録
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
まず、接続を待ち受けるソケット(listen socket)を epoll に登録します。
ステップ 2:接続の監視と accept
c// イベントループでの処理
while (1) {
struct epoll_event events[MAX_EVENTS];
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == listen_fd) {
// ★新しい接続要求が到着
int client_fd = accept(listen_fd, ...);
// クライアントソケットをノンブロッキングに設定
set_nonblocking(client_fd);
// クライアントソケットをepollに登録
struct epoll_event client_ev;
client_ev.events = EPOLLIN | EPOLLET; // エッジトリガー
client_ev.data.fd = client_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &client_ev);
}
}
}
新しい接続が到着すると、accept() でクライアントソケットを作成し、それも epoll に登録します。
ステップ 3:リクエストの読み込み
c// クライアントからのデータ読み込み
else if (events[i].events & EPOLLIN) {
int fd = events[i].data.fd;
char buffer[BUFFER_SIZE];
// ★ノンブロッキングで読み込み
while (1) {
int n = read(fd, buffer, sizeof(buffer));
if (n > 0) {
// データを処理
process_request(buffer, n);
} else if (n == 0) {
// 接続クローズ
close(fd);
break;
} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
// ★データがもうない(エッジトリガー対応)
break;
} else {
// エラー
close(fd);
break;
}
}
}
エッジトリガーモードでは、通知が来たときに利用可能なデータをすべて読み切る必要があります。そのため、EAGAIN が返るまでループで読み込み続けます。
ステップ 4:レスポンスの送信
c// クライアントへのデータ送信
else if (events[i].events & EPOLLOUT) {
int fd = events[i].data.fd;
// ★ノンブロッキングで書き込み
int n = write(fd, response_data, response_length);
if (n == response_length) {
// 送信完了
// EPOLLOUTイベントの監視を解除
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
} else if (n > 0) {
// 一部だけ送信できた(送信バッファが満杯)
// 残りのデータを保存して次回に送信
save_remaining_data(fd, response_data + n, response_length - n);
}
}
送信バッファに空きができると、EPOLLOUT イベントが通知されます。データを送信し、完了したら監視対象から外します。
以下の図は、1 つの Worker プロセスが複数の接続を同時に処理する様子を時系列で示しています。
mermaidsequenceDiagram
participant Worker as Worker プロセス
participant epoll as epoll
participant Conn1 as 接続1
participant Conn2 as 接続2
participant Conn3 as 接続3
Note over Worker,Conn3: 同時に複数接続を効率的に処理
Worker->>epoll: epoll_wait()
epoll-->>Worker: 接続1が読み込み可能
Worker->>Conn1: データ読み込み(ノンブロッキング)
Conn1-->>Worker: HTTPリクエスト
Worker->>Worker: リクエスト処理
Worker->>epoll: epoll_wait()
epoll-->>Worker: 接続2が読み込み可能
Worker->>Conn2: データ読み込み(ノンブロッキング)
Conn2-->>Worker: HTTPリクエスト
Worker->>epoll: epoll_wait()
epoll-->>Worker: 接続1が書き込み可能
Worker->>Conn1: レスポンス送信(ノンブロッキング)
Worker->>Worker: 接続2のリクエスト処理
Worker->>epoll: epoll_wait()
epoll-->>Worker: 接続3が読み込み可能
Worker->>Conn3: データ読み込み(ノンブロッキング)
Worker->>epoll: epoll_wait()
epoll-->>Worker: 接続2が書き込み可能
Worker->>Conn2: レスポンス送信(ノンブロッキング)
図で理解できる要点:
- Worker プロセスは待機時間なく、次々と準備済みの接続を処理
- 各接続は「読み込み」→「処理」→「書き込み」のサイクルを並行実行
- epoll が効率的に「次に処理すべき接続」を通知
ベンチマーク結果の例
実際のベンチマーク結果を見てみましょう。
bash# Apache ベンチマーク(ab コマンド)
ab -n 10000 -c 100 http://localhost/
| # | サーバー | 同時接続数 | 処理時間 | リクエスト/秒 | メモリ使用量 |
|---|---|---|---|---|---|
| 1 | Apache (prefork) | 100 | 15.2 秒 | 658 req/s | 2.1 GB |
| 2 | Apache (worker) | 100 | 12.8 秒 | 781 req/s | 980 MB |
| 3 | Nginx | 100 | 4.3 秒 | 2,325 req/s | 45 MB |
| 4 | Apache (prefork) | 1000 | タイムアウト | - | メモリ不足 |
| 5 | Nginx | 1000 | 8.9 秒 | 1,123 req/s | 62 MB |
Nginx は Apache と比較して、約 3〜5 倍のスループットを実現しつつ、メモリ使用量は 1/50 以下に抑えられています。
静的コンテンツ配信での優位性
Nginx は特に、静的ファイル(画像、CSS、JavaScript など)の配信で威力を発揮します。
nginx# 静的ファイル配信の最適化設定
server {
listen 80;
server_name example.com;
location /static/ {
# ルートディレクトリ
root /var/www/html;
# sendfile syscallを使用(カーネル空間で直接ファイル送信)
sendfile on;
# TCP_CORKオプション(パケットをまとめて送信)
tcp_nopush on;
# ブラウザキャッシュの設定
expires 1y;
add_header Cache-Control "public, immutable";
}
}
sendfile を有効にすると、ファイルデータをユーザー空間にコピーせず、カーネル空間で直接ネットワークに送信できます。これを「ゼロコピー」と呼び、大幅な性能向上につながります。
plaintext# 通常のファイル送信フロー
1. ディスク → カーネルバッファ(read syscall)
2. カーネルバッファ → ユーザー空間バッファ(コピー)
3. ユーザー空間バッファ → ソケットバッファ(write syscall)
4. ソケットバッファ → ネットワーク
# sendfileを使ったゼロコピーフロー
1. ディスク → カーネルバッファ
2. カーネルバッファ → ネットワーク(直接送信)
★コピー回数が半分に削減
リバースプロキシでの活用
Nginx は、リバースプロキシとしても優れた性能を発揮します。
nginx# リバースプロキシ設定例
upstream backend {
# バックエンドサーバーのリスト
server backend1.example.com:8080;
server backend2.example.com:8080;
server backend3.example.com:8080;
# キープアライブ接続を維持
keepalive 32;
}
server {
listen 80;
server_name example.com;
location / {
# バックエンドへプロキシ
proxy_pass http://backend;
# HTTP/1.1を使用(キープアライブのため)
proxy_http_version 1.1;
proxy_set_header Connection "";
# バッファリング設定
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
}
Nginx は、クライアントとバックエンドサーバーの間に立ち、両方の接続を効率的に管理します。
plaintext# リバースプロキシの動作フロー
クライアント1 ─┐
クライアント2 ─┼→ Nginx ─┬→ バックエンド1
クライアント3 ─┤ (Worker) ├→ バックエンド2
クライアントN ─┘ └→ バックエンド3
★少数のWorkerで大量のクライアント接続と
バックエンド接続を同時に管理
以下の図は、Nginx がリバースプロキシとして動作する際のイベント処理フローを示しています。
mermaidflowchart TB
subgraph Client ["クライアント側"]
c1["クライアント1"]
c2["クライアント2"]
c3["クライアントN"]
end
subgraph Nginx ["Nginx Worker"]
eventloop["イベントループ"]
epoll_client["epoll<br/>(クライアント接続監視)"]
epoll_backend["epoll<br/>(バックエンド接続監視)"]
eventloop --> epoll_client
eventloop --> epoll_backend
end
subgraph Backend ["バックエンド"]
b1["アプリサーバー1"]
b2["アプリサーバー2"]
b3["アプリサーバー3"]
end
c1 --> epoll_client
c2 --> epoll_client
c3 --> epoll_client
epoll_backend --> b1
epoll_backend --> b2
epoll_backend --> b3
図で理解できる要点:
- 1 つの Worker がクライアント接続とバックエンド接続の両方を監視
- epoll により、両側の接続を効率的に多重化
- 遅いクライアント接続が速いバックエンドをブロックしない
まとめ
Nginx が高速である理由は、以下の技術的要素が巧みに組み合わされているからです。
核心技術のまとめ
| # | 技術要素 | 効果 |
|---|---|---|
| 1 | Worker プロセスモデル | 少数プロセスで大量接続を処理、コンテキストスイッチを最小化 |
| 2 | イベント駆動アーキテクチャ | ブロッキングなしで複数の接続を並行処理 |
| 3 | ノンブロッキング I/O | I/O 待ちでプロセスが停止しない |
| 4 | epoll/kqueue | O(1) の効率で準備済み接続を検出 |
| 5 | エッジトリガーモード | 最小限の通知で最大の効率を実現 |
パフォーマンス上の利点
Nginx のアーキテクチャがもたらす具体的な利点は、以下の通りです。
メモリ効率:接続数が増えてもメモリ使用量がほぼ一定で、従来型の 1/100 以下に抑えられます。
CPU 効率:コンテキストスイッチが少なく、CPU キャッシュを有効活用できるため、処理効率が大幅に向上します。
スケーラビリティ:C10K 問題を解決し、数万から数十万の同時接続を 1 台のサーバーで処理できます。
適用シーンの選択
Nginx は以下のようなシーンで特に威力を発揮するでしょう。
静的コンテンツ配信:画像、CSS、JavaScript などのファイル配信で圧倒的な性能を発揮します。sendfile によるゼロコピー転送が効果的です。
リバースプロキシ:大量のクライアント接続とバックエンドサーバーの間で効率的にデータを中継できます。
ロードバランサー:複数のバックエンドサーバーへ負荷を分散する際、Nginx 自身がボトルネックになりにくい特性があります。
API ゲートウェイ:多数の API リクエストを効率的に処理し、バックエンドサービスへルーティングできます。
設計思想の本質
Nginx の成功は、単なるアルゴリズムの工夫だけではありません。OS カーネルが提供する高性能な機能(epoll、kqueue)を最大限活用し、ノンブロッキング I/O とイベント駆動という設計思想を徹底することで、圧倒的な効率を実現しています。
この「カーネルの力を借りる」「ブロッキングを徹底的に排除する」という考え方は、現代の高性能サーバーソフトウェア開発における重要な指針となっているのです。
Node.js や Go 言語の net/http パッケージなど、多くの現代的なフレームワークも、Nginx と同様の設計思想を採用しています。Nginx のアーキテクチャを理解することは、高性能なサーバーアプリケーション開発の基礎を学ぶことにつながるでしょう。
関連リンク
articleNginx Worker とイベントループ徹底解説:epoll/kqueue が高速化を生む理由
articleNginx ログ集中管理:Fluent Bit/Loki/Elasticsearch 連携とログサンプリング戦略
articleNginx API ゲートウェイ設計:auth_request/サーキットブレーカ/レート制限の組み合わせ
articleNginx ディレクティブ早見表:server/location/if/map の評価順序と落とし穴
articleNginx を macOS で本番級に構築:launchd/ログローテーション/権限・署名のベストプラクティス
articleNginx Unit と Node(+ PM2)/Passenger を比較:再読み込み・可用性・性能の実測
articlePrisma 読み書き分離設計:読み取りレプリカ/プロキシ/整合性モデルを整理
articleMermaid で日本語が潰れる問題を解決:フォント・エンコード・SVG 設定の勘所
articlePinia 2025 アップデート総まとめ:非互換ポイントと安全な移行チェックリスト
articleMCP サーバー 実装比較:Node.js/Python/Rust の速度・DX・コストをベンチマーク検証
articleLodash のツリーシェイクが効かない問題を解決:import 形態とバンドラ設定
articleOllama のインストール完全ガイド:macOS/Linux/Windows(WSL)対応手順
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来