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 リアルタイム性が重要

    まとめ

    このチャプターでは、高性能ネットワークサーバーの設計について学びました:

  • アーキテクチャパターン: Single-threaded, Multi-threaded, Reactor, Proactor
  • コネクション管理: プール、Keep-Alive、タイムアウト
  • パイプライニング: リクエストの並列処理
  • バックプレッシャー: 高負荷時の保護戦略
  • Graceful Shutdown: 安全なサーバー停止
  • TCP vs UDP: プロトコルの選択

次のExplanationでは、これらの概念を使った実践的なサーバー実装を詳しく見ていきます。