
2026/03/05 23:11
高速サーバー
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
概要:
この記事では、CPU コアごとに 1 本の固定スレッドを割り当てる高性能ネットワーク構成を紹介しています。各スレッドは独自の epoll/kqueue イベントループを所有し、専用のファイルディスクリプタプールを管理することで、共有された決定ポイントと競合を排除します。
- スレッド作成 は
を使用してデタッチ状態かつpthread_create
で行い、その後各スレッドが CPU アフィニティ (PTHREAD_SCOPE_SYSTEM
または macOS のポリシー API) を設定します。pthread_setaffinity_np - ファイルディスクリプタ制限 は
(例:2048 個のディスクリプタ)で各スレッドごとに引き上げ、数多くの同時接続をサポートします。setrlimit(RLIMIT_NOFILE) - accept ループ は専用リスナースレッドで実行され、
またはaccept()
を呼び出した後、新しいソケットを選択されたワーカーの epoll/kqueue に即座に登録します(accept4()
/epoll_ctl
)。kevent - 各ワーカースレッドは
(Linux)またはepoll_wait
(BSD/macOS)でイベントを処理します。イベント時には EOF/close フラグが設定されていればディスクリプタを閉じ、そうでなければリクエストハンドラー (kevent
) を呼び出します。ソケットは通常非ブロッキングに設定され、handle(fd)
でタイムアウトを持つ場合があります。SO_RCVTIMEO - 状態遷移(accept → read → write → close)は別々のスレッドで処理されます。クライアントのファイルディスクリプタは対応するワーカーキュー間で渡され、各 FD が常に 1 本のスレッドだけに所有されるようにします。
- 設計では TCP_DEFER_ACCEPT を採用し、リッスンソケットのリーングを無効化(
)して遅延を低減しています。SO_LINGER{onoff=1, linger=0} - 新規接続は単純なラウンドロビン
関数でロードバランシングされ、ワーカーキューを順に巡回します。pick() - このアーキテクチャは、分離されたスレッド上のブロッキング I/O 呼び出しと状態遷移を所有スレッドへルーティングすることで、最新ハードウェアで約 100 k リクエスト/秒 を実現できます。
このモデルを採用すると、高接続量に対する CPU オーバーヘッドが低減され、クラウドサービスのスケーラビリティが向上し、開発者はイベントループロジックを簡素化できます。将来的な拡張としては、ワーカースレッドの動的スケーリング、ラウンドロビン以外の高度なロードバランシング、および
SO_RCVTIMEO を用いたより細粒度のタイムアウト処理が考えられます。本文
ネットワークサーバ設計パターン
ネットワークサーバを構築する際の標準的な手法は、単純なループに沿っています。
- メインスレッドが
やepoll
を通じてイベントを待ちます。kqueue - イベントが発生すると、ファイルディスクリプタとその現在状態に基づき処理を振り分けます。
昔は接続ごとに
fork() でプロセスを作成していましたが、今日では「ワーカースレッド」が多数生成され、同じタスクを実行しながらカーネルがファイルディスクリプタの割り当てを行う方式が主流です。epoll/kqueue を活用したより良い設計も可能ですが、多くの開発者は libevent のようなラッパーに頼り、古い遅延パターンを生かし続けています。
私の推奨設計
- コア数だけスレッドを作成 – CPU に固定(affinity)し、各スレッドが独自の
ディスクリプタを所有。epoll/kqueue - 状態遷移は専用スレッドで処理 – クライアントがある状態から別の状態へ移る際に、そのファイルディスクリプタを他のスレッドの
に渡す。epoll/kqueue
この設計では決定点がなく、単純なブロッキング/IO 呼び出しだけで動作します。結果として 1 ページ程度のサーバコードで、最新ハードウェア上で約 100 k リクエスト/秒を達成できます。
スレッドプールの構築
pthread_attr_t a; pthread_attr_init(&a); pthread_attr_setscope(&a, PTHREAD_SCOPE_SYSTEM); pthread_attr_setdetachstate(&a, PTHREAD_CREATE_DETACHED); long t = sysconf(_SC_NPROCESSORS_ONLN); /* コア数 */ for (int i = 0; i < t; ++i) pthread_create(&id, &a, (void *)run, (void *)i); /* 全スレッドが準備完了を通知するまで待機 */ while (busy(t)) { pthread_mutex_lock(&tm); pthread_cond_wait(&tc, &tm); pthread_mutex_unlock(&tm); }
各スレッドは以下のように動作します。
void *run(void *id) { int id = (int)(intptr_t)id; set_affinity(id); pthread_mutex_lock(&tm); #ifdef __linux__ worker[id].q = epoll_create1(0); #else worker[id].q = kqueue(); #endif /* …その他スレッド固有の初期化… */ pthread_mutex_unlock(&tm); pthread_cond_signal(&tc); }
プロセッサアフィニティの設定
cpu_set_t c; CPU_ZERO(&c); CPU_SET(id, &c); pthread_setaffinity_np(pthread_self(), sizeof(c), &c);
macOS では
pthread_setaffinity_np が無いので、以下を使用します。
extern int thread_policy_set(thread_t, thread_policy_flavor_t, thread_policy_t, mach_msg_type_number_t); thread_affinity_policy_data_t ap; thread_extended_policy_data_t ep; ap.affinity_tag = id + 1; ep.timeshare = FALSE; thread_policy_set(mach_thread_self(), THREAD_EXTENDED_POLICY, (thread_policy_t)&ep, THREAD_EXTENDED_POLICY_COUNT); thread_policy_set(mach_thread_self(), THREAD_EXTENDED_POLICY, (thread_policy_t)&ap, THREAD_EXTENDED_POLICY_COUNT);
リッスンソケットの作成
struct rlimit r; getrlimit(RLIMIT_NOFILE, &r); if (r.rlim_cur < n) { /* 必要なら制限を上げる */ r.rlim_cur = n; setrlimit(RLIMIT_NOFILE, &r); } int s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); struct linger lf = { .l_onoff = 1, .l_linger = 0 }; setsockopt(s, SOL_SOCKET, SO_LINGER, &lf, sizeof(lf)); listen(s, n * t); /* バックログ */
クライアントが HTTP などで接続を開始する場合、Linux では遅延受け入れ(deferred accept)を有効にします。
#ifdef __linux__ int o = 5; setsockopt(s, SOL_TCP, TCP_DEFER_ACCEPT, &o, sizeof(o)); #endif
Accept‑ループ
ここでは
epoll/kevent は不要で、接続受け入れのみを行います。
for (;;) { int f = accept(s, NULL, NULL); /* …オプションの初期化… */ int q = pick(); /* ワーカー選択 */ #ifdef __linux__ struct epoll_event ev = {0}; ev.data.fd = f; ev.events = EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLET; epoll_ctl(q, EPOLL_CTL_ADD, f, &ev); #else struct kevent ev; EV_SET(&ev, f, EVFILT_READ, EV_ADD | EV_CLEAR, 0, NM, NULL); kevent(q, &ev, 1, NULL, 0, NULL); #endif }
ハンドオフ前のソケット設定
struct timeval tv = { .tv_sec = 5 }; setsockopt(f, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); fcntl(f, F_SETFL, O_NONBLOCK);
Linux では
accept4() を使うと accept() と fcntl() を一度に実行できます。
ワーカー選択
int pick(void) { static int c = 0; return worker[(++c) % t].q; }
高度な負荷分散では、このロジックをチューニングしてスループットを最適化します。
リクエスト・ループ
各ワーカーは
epoll/kevent を待ち受け、入力があると処理します。
#ifdef __linux__ struct epoll_event e[1000]; for (int i = 0; i < epoll_wait(q, e, 1000, -1); ++i) { if (e[i].events & (EPOLLRDHUP | EPOLLHUP)) close(e[i].data.fd); else handle(e[i].data.fd); } #else struct kevent e[1000]; for (int i = 0; i < kevent(q, NULL, 0, e, 1000, NULL); ++i) { if (e[i].flags & EV_EOF) close(e[i].ident); else handle(e[i].ident); } #endif
ファイルディスクリプタは各リクエストの単一状態で使用されるため、FD ごとに入力バッファを配列化すると多くのアルゴリズムが簡素化できます。
handle(fd) は入力を読み取り、必要に応じて書き込みやファイル送信を行い、複数システムコールが必要な場合は適切なワーカーへタスクをスケジュールします。
まとめ
- コアごとに 1 スレッド(
を所有)epoll/kqueue - スレッドを CPU に固定し予測可能性を確保
- 状態遷移時にファイルディスクリプタを別スレッドへ渡す
- Accept‑ループは最小化、ノンブロッキングソケットとタイムアウト設定
- ワーカー選択は負荷特性に応じてローテーションやバイアス
この設計でクリーンかつ高性能なサーバを、わずかなコード量で実現できます。