sgcWebSockets パフォーマンスチューニング — 10 万接続へのスケーリング

· 機能

「動く」から「スケールする」へ

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 つのルール:

参照ベンチマーク

単一 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 -Lset_irq_affinity.sh が味方です。Windows では NIC プロパティで RSS ProfileNUMAScaling に設定し、Get-NetAdapterRss で確認してください。モニタリングがカーネルが softirq や DPC に実時間を費やしていると示すときだけ調整する価値があります。

プロファイル駆動チューニングループ

チューニングは反復的です。デフォルトで始め、代表的な負荷を実行し、ワーカーごとの CPU、GC/割り当てレート、ファンアウト下の p99 レイテンシ、OS レベルの接続カウンターを見ます。1 つだけ変え、再実行、比較。最もよくある驚き:

参考資料

まだ正しいサーバークラスを選んでいなければ、Which Edition から始めてください。次に、マルチボックススケーリングについては ロードバランサーコンポーネント に進んでください。ライブラリは初めてですか?はじめにハブ が 5 分でインストール手順を案内します。