Chapter 04: Network Server Design
学習目標
このチャプターを完了すると、以下のことができるようになります:
- 高性能ネットワークサーバーのアーキテクチャパターンを理解する
- マルチスレッドサーバーの設計と実装ができる
- コネクションプール、リクエストパイプライン処理を実装できる
- バックプレッシャーとGraceful Shutdownを適切に処理できる
- ZigとTCP/UDPサーバーを実装できる
ネットワークサーバーの基本概念
サーバーアーキテクチャの進化
Evolution of Server Architecture:
1970s-1980s: プロセスベース
┌────────────────────────────┐
│ Parent Process │
│ (accept connections) │
└────────┬───────────────────┘
│
├─→ fork() → Child Process 1
├─→ fork() → Child Process 2
└─→ fork() → Child Process 3
問題点:
- プロセス生成コストが高い (数ms)
- メモリ使用量が大きい (各プロセスで数MB)
- C10K問題 (10,000同時接続不可能)
1990s-2000s: スレッドベース
┌────────────────────────────┐
│ Main Thread │
│ (accept connections) │
└────────┬───────────────────┘
│
├─→ Thread 1 (handle client)
├─→ Thread 2 (handle client)
└─→ Thread 3 (handle client)
改善点:
- プロセスより軽量 (数μs)
- メモリ共有が容易
問題点:
- スレッド数の制限 (数千程度)
- コンテキストスイッチのオーバーヘッド
- 同期処理の複雑さ
2000s-現在: イベント駆動 + 非同期I/O
┌────────────────────────────┐
│ Event Loop │
│ (epoll/io_uring) │
└────────┬───────────────────┘
│
├─→ Handler 1 (non-blocking)
├─→ Handler 2 (non-blocking)
└─→ Handler 3 (non-blocking)
改善点:
- 単一スレッドで数万接続可能
- CPUリソースの効率的利用
- 予測可能な性能
現在: ハイブリッドアプローチ
┌────────────────────────────────────┐
│ Thread Pool (Worker Threads) │
│ ┌──────────┬──────────┬──────────┐│
│ │ Thread 1 │ Thread 2 │ Thread N ││
│ │ Event │ Event │ Event ││
│ │ Loop │ Loop │ Loop ││
│ └──────────┴──────────┴──────────┘│
└────────────────────────────────────┘
特徴:
- マルチコアを最大限活用
- スレッド間でロードバランシング
- 高いスループットと低レイテンシ
サーバーアーキテクチャパターン
Pattern 1: Single-Threaded Event Loop
最もシンプルで予測可能なモデル。Node.js、Redis、Nginxのワーカープロセスなどで採用。
┌─────────────────────────────────────────┐
│ Main Event Loop │
│ ┌───────────────────────────────────┐ │
│ │ 1. Accept new connections │ │
│ │ 2. Read data from sockets │ │
│ │ 3. Process requests │ │
│ │ 4. Write responses │ │
│ └───────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Connection Pool │ │
│ │ ┌────┬────┬────┬────┬────┐ │ │
│ │ │Conn│Conn│Conn│Conn│Conn│... │ │
│ │ └────┴────┴────┴────┴────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
特徴:
✓ シンプルで理解しやすい
✓ デバッグが容易
✓ 競合状態が発生しない
✗ 単一コアのみ使用
✗ CPU集約的な処理に不向き
Zigでの実装例:
const std = @import("std");
const net = std.net;
const os = std.os;
pub fn singleThreadedServer(port: u16) !void {
const address = try net.Address.parseIp("0.0.0.0", port);
var server = try address.listen(.{ .reuse_address = true });
defer server.deinit();
std.log.info("Server listening on port {}", .{port});
// Accept loop
while (true) {
const conn = try server.accept();
defer conn.stream.close();
// Handle connection
handleConnection(conn.stream) catch |err| {
std.log.err("Error handling connection: {}", .{err});
};
}
}
fn handleConnection(stream: net.Stream) !void {
var buffer: [4096]u8 = undefined;
const n = try stream.read(&buffer);
_ = try stream.write(buffer[0..n]);
}
Pattern 2: Multi-Threaded with Thread Pool
複数のワーカースレッドで接続を処理。各スレッドは独立したイベントループを持つ。
┌──────────────────────────────────────────┐
│ Main Thread (Acceptor) │
│ │
│ while (true) { │
│ conn = accept() │
│ dispatch_to_worker(conn) │
│ } │
└────────┬─────────────────────────────────┘
│
dispatch_to_worker()
│
↓
┌────────────────────────────────────────────┐
│ Worker Thread Pool │
│ ┌──────────┬──────────┬──────────────┐ │
│ │Worker 1 │Worker 2 │Worker N │ │
│ │ │ │ │ │
│ │Event Loop│Event Loop│Event Loop │ │
│ │┌────────┐│┌────────┐│┌────────┐ │ │
│ ││Conn ││││Conn ││││Conn │ │ │
│ ││Pool ││││Pool ││││Pool │ │ │
│ │└────────┘│└────────┘│└────────┘ │ │
│ └──────────┴──────────┴──────────────┘ │
└────────────────────────────────────────────┘
特徴:
✓ マルチコアを活用
✓ 高いスループット
✓ ワーカー間で負荷分散
✗ 複雑な実装
✗ スレッド間同期が必要
Zigでの実装例:
const std = @import("std");
const Thread = std.Thread;
pub const ThreadPoolServer = struct {
allocator: std.mem.Allocator,
workers: []Worker,
next_worker: std.atomic.Value(usize),
const Worker = struct {
thread: Thread,
queue: ConnectionQueue,
running: std.atomic.Value(bool),
};
pub fn init(
allocator: std.mem.Allocator,
num_workers: usize,
) !ThreadPoolServer {
var workers = try allocator.alloc(Worker, num_workers);
for (workers, 0..) |*worker, i| {
worker.queue = try ConnectionQueue.init(allocator);
worker.running = std.atomic.Value(bool).init(true);
worker.thread = try Thread.spawn(.{}, workerLoop, .{worker});
}
return ThreadPoolServer{
.allocator = allocator,
.workers = workers,
.next_worker = std.atomic.Value(usize).init(0),
};
}
pub fn dispatch(self: *ThreadPoolServer, conn: Connection) !void {
const idx = self.next_worker.fetchAdd(1, .monotonic) % self.workers.len;
try self.workers[idx].queue.push(conn);
}
fn workerLoop(worker: *Worker) void {
while (worker.running.load(.monotonic)) {
if (worker.queue.pop()) |conn| {
handleConnection(conn) catch {};
}
}
}
};
Pattern 3: Reactor Pattern (libuv, Tokio)
イベント通知をCentralized Reactorで処理し、ハンドラーに委譲。
┌─────────────────────────────────────────┐
│ Reactor (Event Demultiplexer) │
│ │
│ ┌───────────────────────────┐ │
│ │ epoll_wait() / io_uring │ │
│ └───────┬───────────────────┘ │
│ │ │
│ events[] │
│ │ │
│ ┌───────┴───────┬──────────┐ │
│ ↓ ↓ ↓ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │Handler1│ │Handler2│ │Handler3│ │
│ │Accept │ │Read │ │Write │ │
│ └────────┘ └────────┘ └────────┘ │
└─────────────────────────────────────────┘
特徴:
✓ 関心事の分離
✓ 拡張性が高い
✓ 非同期処理に最適
Pattern 4: Proactor Pattern (Windows IOCP, io_uring)
非同期I/O操作をカーネルに委譲し、完了通知を受け取る。
Application Kernel
│ │
│ 1. Submit I/O Request │
├──────────────────────────→ │
│ │
│ Async I/O
│ Processing
│ │
│ 2. Completion Event │
│ ←──────────────────────────┤
│ │
↓ │
Handle Completion
│
↓
Continue Processing
特徴:
✓ 真の非同期I/O
✓ CPU効率が高い
✓ Zero-copy可能
✗ 実装が複雑
Connection Management
コネクションプール
効率的なコネクション管理のための戦略。
pub const ConnectionPool = struct {
connections: std.ArrayList(Connection),
free_list: std.ArrayList(usize),
mutex: std.Thread.Mutex,
pub const Connection = struct {
fd: i32,
state: State,
buffer: []u8,
read_offset: usize,
write_offset: usize,
last_activity: i64,
pub const State = enum {
idle,
reading,
processing,
writing,
closing,
};
};
pub fn init(allocator: std.mem.Allocator, capacity: usize) !ConnectionPool {
var pool = ConnectionPool{
.connections = try std.ArrayList(Connection).initCapacity(
allocator,
capacity,
),
.free_list = try std.ArrayList(usize).initCapacity(
allocator,
capacity,
),
.mutex = std.Thread.Mutex{},
};
// Pre-allocate connections
for (0..capacity) |i| {
try pool.free_list.append(i);
try pool.connections.append(.{
.fd = -1,
.state = .idle,
.buffer = undefined,
.read_offset = 0,
.write_offset = 0,
.last_activity = 0,
});
}
return pool;
}
pub fn acquire(self: *ConnectionPool) ?*Connection {
self.mutex.lock();
defer self.mutex.unlock();
if (self.free_list.popOrNull()) |idx| {
return &self.connections.items[idx];
}
return null;
}
pub fn release(self: *ConnectionPool, conn: *Connection) void {
self.mutex.lock();
defer self.mutex.unlock();
const idx = (@intFromPtr(conn) - @intFromPtr(self.connections.items.ptr)) /
@sizeOf(Connection);
conn.state = .idle;
self.free_list.append(idx) catch {};
}
pub fn timeoutCheck(self: *ConnectionPool, timeout_ms: i64) void {
const now = std.time.milliTimestamp();
for (self.connections.items) |*conn| {
if (conn.state != .idle and
now - conn.last_activity > timeout_ms)
{
std.log.warn("Connection timeout: fd={}", .{conn.fd});
self.release(conn);
}
}
}
};
Keep-Alive とタイムアウト
pub const KeepAliveConfig = struct {
enabled: bool = true,
timeout_secs: i32 = 60,
interval_secs: i32 = 10,
probes: i32 = 3,
pub fn apply(self: KeepAliveConfig, fd: i32) !void {
if (!self.enabled) return;
// Enable TCP keep-alive
try os.setsockopt(
fd,
os.SOL.SOCKET,
os.SO.KEEPALIVE,
&std.mem.toBytes(@as(c_int, 1)),
);
// Set keep-alive timeout
try os.setsockopt(
fd,
os.IPPROTO.TCP,
os.TCP.KEEPIDLE,
&std.mem.toBytes(self.timeout_secs),
);
// Set keep-alive interval
try os.setsockopt(
fd,
os.IPPROTO.TCP,
os.TCP.KEEPINTVL,
&std.mem.toBytes(self.interval_secs),
);
// Set keep-alive probes
try os.setsockopt(
fd,
os.IPPROTO.TCP,
os.TCP.KEEPCNT,
&std.mem.toBytes(self.probes),
);
}
};
Request Pipelining
HTTPパイプライニングやストリーミング処理で重要。
Without Pipelining:
Client Server
│ │
├─── Request 1 ────────────────→│
│ │ Process
│←──── Response 1 ──────────────┤
│ │
├─── Request 2 ────────────────→│
│ │ Process
│←──── Response 2 ──────────────┤
Total Time: 2 * (Network RTT + Processing)
With Pipelining:
Client Server
│ │
├─── Request 1 ────────────────→│
├─── Request 2 ────────────────→│ Queue
├─── Request 3 ────────────────→│ ↓
│ │ Process in order
│←──── Response 1 ──────────────┤
│←──── Response 2 ──────────────┤
│←──── Response 3 ──────────────┤
Total Time: Network RTT + (3 * Processing)
Speedup: ~50% for 3 requests
実装例:
pub const PipelineBuffer = struct {
buffer: []u8,
read_pos: usize,
write_pos: usize,
requests: std.ArrayList(Request),
pub const Request = struct {
start: usize,
end: usize,
processed: bool,
};
pub fn parse(self: *PipelineBuffer) !void {
var pos = self.read_pos;
while (pos < self.write_pos) {
// Find request boundary (e.g., "\r\n\r\n" for HTTP)
const end = std.mem.indexOf(
u8,
self.buffer[pos..self.write_pos],
"\r\n\r\n",
) orelse break;
try self.requests.append(.{
.start = pos,
.end = pos + end + 4,
.processed = false,
});
pos += end + 4;
}
self.read_pos = pos;
}
pub fn processNext(self: *PipelineBuffer) ?[]const u8 {
for (self.requests.items) |*req| {
if (!req.processed) {
req.processed = true;
return self.buffer[req.start..req.end];
}
}
return null;
}
};
Backpressure Handling
高負荷時にシステムを保護するための重要な概念。
Without Backpressure:
Requests → [Queue grows unbounded] → Server OOM
With Backpressure:
Requests → [Bounded Queue] → Server
│ Full?
↓ Yes
Reject/Slow down
実装戦略:
pub const BackpressureStrategy = enum {
reject, // 新規リクエストを拒否
drop, // 古いリクエストを破棄
slow_down, // クライアントを遅延
shed_load, // 優先度の低いリクエストを破棄
};
pub const BackpressureManager = struct {
max_connections: usize,
current_connections: std.atomic.Value(usize),
max_queue_size: usize,
current_queue_size: std.atomic.Value(usize),
strategy: BackpressureStrategy,
pub fn shouldAccept(self: *BackpressureManager) bool {
const current = self.current_connections.load(.monotonic);
return current < self.max_connections;
}
pub fn canEnqueue(self: *BackpressureManager) bool {
const current = self.current_queue_size.load(.monotonic);
if (current >= self.max_queue_size) {
return switch (self.strategy) {
.reject => false,
.drop => true, // Will drop oldest
.slow_down => false,
.shed_load => true, // Will drop lowest priority
};
}
return true;
}
pub fn onConnectionAccepted(self: *BackpressureManager) void {
_ = self.current_connections.fetchAdd(1, .monotonic);
}
pub fn onConnectionClosed(self: *BackpressureManager) void {
_ = self.current_connections.fetchSub(1, .monotonic);
}
};
Graceful Shutdown
サーバーを安全に停止するための手順。
Shutdown Sequence:
1. Stop Accepting New Connections
┌────────────────────┐
│ close(listen_fd) │
└────────────────────┘
2. Wait for Active Requests
┌────────────────────────────┐
│ while (active_conns > 0) { │
│ process_remaining() │
│ if (timeout) break │
│ } │
└────────────────────────────┘
3. Force Close Remaining
┌────────────────────────────┐
│ for conn in connections { │
│ close(conn.fd) │
│ } │
└────────────────────────────┘
4. Cleanup Resources
┌────────────────────────────┐
│ free_memory() │
│ close_files() │
│ flush_logs() │
└────────────────────────────┘
実装例:
pub const GracefulShutdown = struct {
shutdown_requested: std.atomic.Value(bool),
active_connections: std.atomic.Value(usize),
timeout_ms: i64,
pub fn init(timeout_ms: i64) GracefulShutdown {
return .{
.shutdown_requested = std.atomic.Value(bool).init(false),
.active_connections = std.atomic.Value(usize).init(0),
.timeout_ms = timeout_ms,
};
}
pub fn requestShutdown(self: *GracefulShutdown) void {
self.shutdown_requested.store(true, .monotonic);
std.log.info("Graceful shutdown requested", .{});
}
pub fn shouldAcceptConnections(self: *GracefulShutdown) bool {
return !self.shutdown_requested.load(.monotonic);
}
pub fn wait(self: *GracefulShutdown) !void {
const start = std.time.milliTimestamp();
while (true) {
const active = self.active_connections.load(.monotonic);
if (active == 0) {
std.log.info("All connections closed gracefully", .{});
return;
}
const elapsed = std.time.milliTimestamp() - start;
if (elapsed > self.timeout_ms) {
std.log.warn(
"Shutdown timeout: {} connections still active",
.{active},
);
return error.ShutdownTimeout;
}
std.time.sleep(100 * std.time.ns_per_ms);
}
}
};
TCP vs UDP Server
TCP Server の特徴
TCP Connection:
Client Server
│ │
├─── SYN ─────→ │
│ ←── SYN-ACK ──┤
├─── ACK ─────→ │
│ [Connected] │
├─── Data ────→ │
│ ←─── Data ────┤
├─── FIN ─────→ │
│ ←── FIN-ACK ──┤
特徴:
✓ 信頼性: パケット再送、順序保証
✓ フロー制御: ウィンドウサイズ調整
✓ 輻輳制御: ネットワーク状態に適応
✗ オーバーヘッド: 接続確立、状態管理
✗ Head-of-line blocking
UDP Server の特徴
UDP Communication:
Client Server
│ │
├─── Data ────→ │
├─── Data ────→ │
│ ←─── Data ────┤
特徴:
✓ 低レイテンシ: 接続確立不要
✓ 軽量: ヘッダーが小さい (8バイト)
✓ ブロードキャスト/マルチキャスト対応
✗ 信頼性なし: パケット損失の可能性
✗ 順序保証なし
使い分けの指針:
| Use Case | Protocol | Reason |
|---|---|---|
| Webサーバー | TCP | 信頼性が重要 |
| ゲームサーバー | UDP | 低レイテンシが重要 |
| 動画ストリーミング | UDP | リアルタイム性が重要 |
| ファイル転送 | TCP | データ完全性が重要 |
| DNS | UDP | 小さいクエリ、速度重視 |
| VoIP | UDP | リアルタイム性が重要 |
まとめ
このチャプターでは、高性能ネットワークサーバーの設計について学びました:
次のExplanationでは、これらの概念を使った実践的なサーバー実装を詳しく見ていきます。