「動く」から「スケールする」へ
sgcWebSockets は調整なしで数千の接続を最初から処理します。単一マシン上で 50,000 や 100,000 の同時ソケットを超えて進むことも可能ですが — 適切なサーバークラスを選び、ワークロードに合わせてスレッドプールを調整し、適切な圧縮戦略を選び、オペレーティングシステムを調整する必要があります。本稿では、引くべき順序通りに各レバーを解説し、Windows Server 2025 と Ubuntu 24.04 を実行する Hetzner AX102(Ryzen 9 7950X3D、128 GB RAM、1 Gbps NIC)からのベンチマーク数値を示します。
始める前に方法論について一言。以下に示すすべての数値は、合成エコーワークロード — 100 バイトのペイロード、クライアントあたり 10 メッセージ/秒、ビジネスロジックなし — から得られます。実アプリケーションは、OnMessage ハンドラーが実際に何をするかによって、これらの数値と桁違いに遅い数値の間のどこかに落ち着きます。これらを天井として扱い、保証ではないと考えてください。要点は、スケーリングカーブの形状と崖がどこにあるかを示すことで、任意のワークロードに保証を与えることではありません。
また: ユーザーに最も近いものから先にチューニングしてください。TLS ハンドシェイクに 80 ミリ秒かかるなら、ブロードキャストループから 200 マイクロ秒削ぐ意味はありません。プロファイル、ボトルネック発見、修正、繰り返し。以下のリストは工数あたりの影響で大まかに順序付けられていますが、ワークロードによって異なります。
1. 正しい I/O モデルを選ぶ
sgcWebSockets は 2 つのサーバーファミリーを出荷しています: Indy ベースの TsgcWebSocketServer(接続ごと 1 スレッド)と、IOCP/epoll 駆動の TsgcWebSocketHTTPServer(イベント駆動)です。約 5,000 同時接続を超えたら、後者を使用してください。問答無用。
| サーバー | I/O モデル | スイートスポット | ハード上限 |
| TsgcWebSocketServer(Indy) | 接続ごとスレッド | <5,000 | ~10,000(スレッドスタック枯渇) |
| TsgcWebSocketHTTPServer Win | IOCP | 10,000–100,000 | ~250,000(ファイルディスクリプタ) |
| TsgcWebSocketHTTPServer Lin | epoll | 10,000–100,000 | ~1,000,000(調整付き) |
| TsgcWebSocketHTTPServer Mac | kqueue | 10,000–50,000 | ~100,000 |
2. スレッドプールのサイズ調整
IOCP/epoll サーバーは固定サイズのワーカープールを使用します。デフォルトは CPUCount。純粋なエコー/ファンアウトワークロードでは小さく(コアあたり 2–4)保ってください。OnMessage 内でデータベースや外部 API を呼び出すワークロードでは引き上げてください — さもないと 1 つの遅いリクエストが N のピアをブロックします。
oServer := TsgcWebSocketHTTPServer.Create(Self); oServer.Port := 443; // Tune the worker pool oServer.ThreadPool.PoolSize := CPUCount * 2; // CPU-bound // oServer.ThreadPool.PoolSize := 128; // DB-bound (e.g. 16 cores) oServer.ThreadPool.QueueSize := 4096; oServer.ThreadPool.MaxConnections := 100000; oServer.Active := True;
目安: PoolSize = CPUCount + (average_blocking_ms / average_cpu_ms) * CPUCount。Application Insights / APM がキュー深度の増加を報告するなら、プールを倍にしてください。カーネルスケジューラーによる硬い上限があります — Windows で数万の OS スレッドは楽しくありません — が、妥当なワークロードでは通常そこに遠く及びません。256 ワーカーを超えるなら、代わりにブロッキング作業を専用のワーカープールにオフロードすることを検討してください。
最良のアーキテクチャ判断は、OnMessage をノンブロッキングに保ち、実際の作業単位をすべて別のスレッドプールでサービスされる専用キューにプッシュすることです。これは I/O スレッド(高速を保たねばならない)とビジネススレッド(結果なく遅くてもよい)を切り離します。観測可能性も得られます: キュー深度が「もうすぐ倒れる」の先行指標となります。
3. 圧縮: per-message-deflate
WebSocket 圧縮(RFC 7692)は JSON やテキストペイロードを 60–90% 削減します。CPU を多く使用します。ワークロードがテキスト中心で CPU に余裕があるときだけグローバル有効化してください。バイナリやすでに圧縮されたペイロード(JPEG、MP4、gzip 済みログ)には純粋なオーバーヘッドです。
oServer.Extensions.PerMessage_Deflate.Enabled := True; oServer.Extensions.PerMessage_Deflate.ServerMaxWindow := 15; // default oServer.Extensions.PerMessage_Deflate.MemLevel := 8; oServer.Extensions.PerMessage_Deflate.Threshold := 256; // skip tiny msgs
Threshold を設定して、ヘッダーオーバーヘッドより小さいメッセージは圧縮しないようにしてください。60 バイトのハートビートで deflate をスキップする方が、コストより CPU を節約します。
細かい罠: per-message-deflate は同じ接続上のメッセージ間で状態を保持するスライディングウィンドウを使用します。その状態は接続ごとのメモリです。ServerMaxWindow=15(デフォルト)では、各接続が約 32 KB の辞書を保持します。100,000 接続で乗じると、圧縮状態だけで 3 GB の RAM です。メモリ制約があるなら ServerMaxWindow を 10 か 11 に下げてください — 圧縮率を数パーセント失う代わりに、接続あたり約 8 倍少ないメモリです。
4. フラグメンテーション
大きなフレーム(>1 MB)は完全なメッセージが再構成されるまでワーカースレッドを保持します。レイテンシをスムーズに保ち、ワーカーを他のピアのために解放するため、送信メッセージをフラグメント化してください。
oClient.WriteOptions.FragmentEnabled := True; oClient.WriteOptions.FragmentSize := 65536; // 64 KB chunks
サーバー側では、ギガバイトバッファを割り当てようとする悪意あるピアから保護するため、ReadOptions.MaxFrameSize を妥当な上限(4 MB を使用)に設定してください。
5. ブロードキャスト最適化
接続済みクライアント全員に同じメッセージを送ることは、チャット/取引/pub-sub サーバーの最大のボトルネックです。素朴なループ for each client: client.Send(msg) は同じペイロードを N 回シリアライズして圧縮します。1 度シリアライズしてエンコード済みフレームを再利用する組み込みブロードキャストを使用してください。
// Slow: N encodes, N compresses for i := 0 to oServer.Connections.Count - 1 do oServer.Connections[i].WriteData(vJSON); // Fast: 1 encode, 1 compress, N writes oServer.Broadcast(vJSON); // Fastest for fan-out >10k: pre-encoded buffer vFrame := oServer.EncodeFrame(vJSON); oServer.BroadcastEncoded(vFrame);
16 コアの箱で、50,000 クライアントへのファンアウトに対する素朴ループと BroadcastEncoded の差は 12 秒対 380 ms です。同じ原則がチャネルにも適用されます — フレームを事前エンコードし、購読者リストを歩いてください。購読者が多くのチャネルに分散するなら、チャネルごとに 1 度エンコードして内部でブロードキャストしてください。このコードパスでの早すぎる悲観化は、それ以外は高速なサーバーを沈めます。
6. OS レベルのチューニング
カーネルはコンポーネントが課す前にハード制限を課します。ライブラリを責める前にこれらを調整してください。
| 設定 | Linux | Windows | 推奨 |
| ファイルディスクリプタ制限 | ulimit -n |
HKLM — MaxUserPort |
予想接続数 × 2 |
| TCP バックログ | net.core.somaxconn |
TcpMaxConnectResponseRetransmissions |
4096+ |
| TIME_WAIT 再利用 | tcp_tw_reuse=1 |
TcpTimedWaitDelay=30 |
ポート枯渇を削減 |
| SO_REUSEPORT | カーネル ≥3.9 | N/A | マルチプロセスアクセプター |
| エフェメラルポート範囲 | net.ipv4.ip_local_port_range |
MaxUserPort |
10000–65535 |
7. ハートビートとアイドル検出
モバイルクライアントは常にネットワークから消えます。ハートビートなしでは、TCP keepalive タイマーが発火する(典型的に 2 時間)までソケットを開いたままにします。短いハートビートと死亡ピア検出を構成してください。
oServer.HeartBeat.Enabled := True; oServer.HeartBeat.Interval := 30; // seconds oServer.HeartBeat.Timeout := 90; // close if no pong within this
これは半開接続を 2 時間ではなく 90 秒以内に捕捉し、忙しいサーバー上の数千の古いソケットを解放します。
8. ロードバランサーとのペアリング
単一マシンを超えてスケールする必要があるなら、sgcWebSockets と私たちの TsgcWebSocketHTTPServer_LoadBalancer または外部 L7 LB(HAProxy、nginx、AWS ALB)をペアにしてください。2 つのルール:
- スティッキーセッションを使用 — WebSocket フレームは冪等ではなく、会話の途中で再ルーティングできません。
- アプリケーションが実クライアント IP を見られるよう、元の
X-Forwarded-Forと TLS 終端ヘッダーを転送してください。
参照ベンチマーク
単一 AX102 箱(16 コア/32 スレッド、128 GB)でエコーサーバーを 100 バイトペイロード、クライアントあたり 10 メッセージ/秒で実行した数値。
| 同時クライアント | スループット(msg/s) | p50 レイテンシ | p99 レイテンシ | CPU 使用率 | RSS |
| 10,000 | 100,000 | 0.8 ms | 3.2 ms | 14% | 0.9 GB |
| 50,000 | 500,000 | 1.1 ms | 5.4 ms | 38% | 3.8 GB |
| 100,000 | 1,000,000 | 1.7 ms | 9.8 ms | 71% | 7.2 GB |
| 250,000 | 2,500,000 | 3.4 ms | 22 ms | 96% | 17.8 GB |
9. TLS 終端
TLS ハンドシェイクは CPU コストが高いです。毎秒数千の新規接続を提供するなら、Delphi プロセス内で TLS を終端することはフレーム提供の代わりに暗号化でコアを飽和させかねません。高チャーンワークロードでは、sgcWebSockets サーバーの前で nginx または HAProxy で TLS を終端し、バックエンドを平文 HTTP で動かしています。フロントエンドはハードウェア AES アクセラレーション、セッション再開、OCSP ステープリングを無料で得て、Delphi プロセスは CPU の 100% をアプリケーションロジックに費やせます。
永続的な長寿命接続のワークロード(典型的なチャット/取引シナリオ)では、ハンドシェイクが時間や日数にわたって償却されるため、プロセス内 TLS で問題ありません。接続-切断-再接続のバースト(不安定なネットワーク上のモバイルクライアント)では、前にリバースプロキシを置いてください。
10. NIC とネットワークチューニング
1 Gbps では NIC を飽和させる可能性は低いです。10 Gbps を超えると、割り込み合体、receive-side scaling(RSS)、sgcWebSockets ワーカースレッドの NUMA ローカルコアへのピン留めを考える必要があります。Linux では ethtool -L と set_irq_affinity.sh が味方です。Windows では NIC プロパティで RSS Profile を NUMAScaling に設定し、Get-NetAdapterRss で確認してください。モニタリングがカーネルが softirq や DPC に実時間を費やしていると示すときだけ調整する価値があります。
プロファイル駆動チューニングループ
チューニングは反復的です。デフォルトで始め、代表的な負荷を実行し、ワーカーごとの CPU、GC/割り当てレート、ファンアウト下の p99 レイテンシ、OS レベルの接続カウンターを見ます。1 つだけ変え、再実行、比較。最もよくある驚き:
- すでに圧縮されたペイロードで圧縮を有効化 → 帯域節約ゼロで CPU スパイク。
OnMessage内の同期 DB 呼び出し → CPU <1% でワーカープールが飽和。- ブロードキャストバッチングなし → 市場開始時のスパイクで先頭ブロッキング。
- 64 コア箱でデフォルトスレッドプール → 256 で 4 倍のスループットを解放できるところを 64 ワーカーに作業を直列化。
参考資料
まだ正しいサーバークラスを選んでいなければ、Which Edition から始めてください。次に、マルチボックススケーリングについては ロードバランサーコンポーネント に進んでください。ライブラリは初めてですか?はじめにハブ が 5 分でインストール手順を案内します。