Skip to content

MS_C10kProblem

nishi_74322014 edited this page Aug 21, 2026 · 1 revision

C10k problem (C10K問題)

概要

「C10K 問題」(クライアント 1 万台問題)とは、ハードウェアの性能上は問題がなくても、
あまりにもクライアントの数が多くなると効率が悪化しサーバがパンクする問題のこと。

  • スレッドなどのリソースを大量に消費してしまう。
  • 最近は、「C10M 問題」(クライアント 1,000 万台問題)などが出て来ている。

補足(なぜスレッドを消費すると破綻するのか): 問題の本質を
数字で押さえておくと、以降の議論が理解しやすい。

【スレッド 1 本あたりのコスト】
   ・スタック領域        … 【既定 1MB】(Windows / .NET)★
   ・カーネル オブジェクト … 数 KB
   ・コンテキスト スイッチ … 数 μs(切り替えのたび)

【1 接続 = 1 スレッドだと】
   10,000 接続 × 1MB = 【10GB のスタック】
   → メモリが足りても、【スケジューラが回らない】
   → コンテキスト スイッチだけで CPU を食い潰す ★
【1 接続 = 1 スレッドが成立していた時代】
   ・同時接続が数百程度
   ・接続は【短命】(リクエスト → レスポンス → 切断)

【成立しなくなった理由】
   ・Keep-Alive、【WebSocket、SSE】= 接続が長時間居座る ★
   ・Ajax / SPA による細かい通信の増加
   ・IoT、モバイルの常時接続

「C10M」への言及も的確で、
現在はカーネルを迂回する手法(DPDK、io_uring、eBPF、XDP)まで
議論が進んでいる。ただし、業務システムで必要になることは稀である。

nginxとNode.js

  • nginx(エンジンエックス)(DNET_nginx.md
  • Node.js(ノードジェイエス)

nginx(DNET_nginx.md

Node.js

Node.jsの概要

サーバサイド JavaScript の Node.js はノンブロッキング I/O というモデルにより、
イベントループを止めてしまうようなブロッキングを回避し、C10K 問題に対応する。

WindowsでNode.jsを使う。

  • Unix 系 OS 向けの非同期 I/O 環境(epoll/kqueue/event port)を使用する
    「libev」ライブラリを使用していた。
  • Windows 向けの非同期 I/O 環境(IOCP : Input/output completion port)を使用する
    「libuv」を開発した。
  • 現在、libeio / libev に代わって libuv が、Node.js のコアとして置き換わりつつある。

補足(この記述は正確。現在は置き換えが完了している): 「置き換わり
つつある」と書かれているが、
libuv への統一は 2012 年頃に完了している(Node.js 0.9 以降)。

【libuv が抽象化しているもの】★

   アプリのコード(Node.js)
        ↓ libuv が OS 差を吸収する
   Linux   → epoll
   macOS   → kqueue
   Solaris → event ports
   Windows → 【IOCP】

ここで重要なのは、Windows だけ方式が違う点である。

【epoll / kqueue(Unix 系)】── 「準備完了通知」型
   「読める状態になった」とだけ教えてくれる
     → 通知を受けてから、【自分で read する】
     → Readiness-based

【IOCP(Windows)】── 「完了通知」型 ★
   「読み終わった。データはここ」と教えてくれる
     → OS が【カーネル側で読み込みまで済ませる】
     → Completion-based

この違いは .NET でも重要である。

・.NET の非同期 I/O は、Windows では【IOCP を使う】
   → ThreadPool の「I/O 完了ポート スレッド」がこれ
・Linux では epoll ベース(.NET Core 以降)
・【どちらでも async/await のコードは同じ】★
   → libuv と同じく、ランタイムが差を吸収している

なお、Linux でも近年 io_uring(完了通知型)が登場しており、
IOCP に近い方式へ収束しつつある

2つの方式

イベントループ

UI サブシステムのメッセージループと同じ意味で利用される用語だが、
Web サーバーに関するコンテキストでは、C10k に対応するアーキテクチャ用語として使用される。

  • 方式
    シングルスレッドでループ処理を回し、

    • キューに溜まったイベントを処理していく方式。
    • リクエストを1つのスレッドで受け取ることができる。
  • 特徴

    • スレッドなどのリソース消費が少なくて済む。
  • 問題

    • イベントループは、昔懐かしい、ノンプリエンプティブ・マルチタスク OS のように、
      どこかでブロッキングが発生すると、イベントループ全体がストップしてしまう。
    • この「イベントループ」の問題を解決するのが「ノンブロッキング I/O」らしい。

補足(ノンプリエンプティブ・マルチタスク との対比が秀逸): この
たとえは本質を突いているので、明示的に整理しておく。

【協調的(ノンプリエンプティブ)マルチタスク】
   ・各タスクが【自発的に制御を返す】
   ・1 つが返さないと【全体が止まる】
   → Windows 3.1 の時代の問題

【イベントループ】
   ・各コールバックが【自発的に制御を返す】
   ・1 つが返さないと【全体が止まる】★ 同じ構造
   → Node.js で重い同期処理を書くと、全リクエストが待たされる
// Node.js で絶対にやってはいけない例
const data = fs.readFileSync('huge.csv');   // ← 同期 I/O
// → この間、【他の全リクエストが止まる】★

for (let i = 0; i < 1e10; i++) { /* CPU を回す */ }
// → これも同様。CPU バウンドな処理はイベントループを塞ぐ
【対策(Node.js)】
   ・同期 API を使わない(readFileSync 等)
   ・CPU バウンドな処理は【Worker Threads / 別プロセス】へ逃がす

.NET の場合はここが違う

【.NET(ASP.NET Core)】
   ・イベントループ 1 本ではなく【スレッド プール】で処理する
   ・1 つの要求が CPU を回しても、他のスレッドが動く
     → 【全体が止まることはない】★
   ・ただし、スレッド プールが枯渇すると遅くなる(後述)

 → 「.NET は Node.js より壊れにくいが、
   枯渇のしかたが分かりにくい」とも言える

ノンブロッキングI/O

  • 簡単に、ブロッキングを防止することでイベントループの停止を防止する。

  • 仕組みとしては、ざっくり、

    • ワーカースレッドプールと I/O 処理がうまく協調して動き、
    • スレッドを節約して動作しているクライアントに処理完了後のコールバックを返す。

    という、非常に優れた方法であるらしい。

  • .NET の「async/await」は、≒この、イベントループ、
    ノンブロッキング I/O で動作する。

補足(async/await との関係 ── ここが本ページの核心): 「≒この、
イベントループ、ノンブロッキング I/O で動作する」という指摘は
方向として正しいが、重要な違いがあるので明確にしておく。

【共通する本質】★
   「I/O を待っている間、スレッドを占有しない」
     → 待ち時間中にスレッドを他の仕事に回せる
     → 【少ないスレッドで多数の接続を捌ける】= C10K への回答

【違い】
   Node.js  … 【シングル スレッド】のイベントループ
   .NET     … 【スレッド プール】+ I/O 完了ポート
               → 複数スレッドで並行に処理する
// async/await が「待たない」仕組み
public async Task<IActionResult> Get(int id)
{
    var order = await _db.Orders.FindAsync(id);   // ← ここでスレッドを【返す】★
    //  DB が応答するまでの間、このスレッドは別の要求を処理できる
    return Ok(order);                              // ← 完了後に再開(別スレッドかも)
}

async/await を使っても速くならない点は誤解が多い。

【async/await が改善するもの】
   ・【スループット】(同時に捌ける要求数)★
   ・スレッド数・メモリ消費

【改善しないもの】
   ・【個々の要求のレイテンシ】(むしろ僅かに増える)
   ・CPU バウンドな処理の速度

同期と非同期を混ぜると最悪になる——これが実務で最も重要である。

// ✗ 絶対にやってはいけない(Sync over Async)★
var order = _service.GetOrderAsync(id).Result;      // デッドロック / スレッド枯渇
_service.DoSomethingAsync().Wait();

// ✗ async void(例外を捕捉できない。イベント ハンドラ以外で使わない)
public async void DoWork() { }

// ○ 最後まで非同期で通す(async all the way)
var order = await _service.GetOrderAsync(id);
【スレッド プール枯渇(Thread Pool Starvation)】★
   ・.Result / .Wait() でスレッドを塞ぐ
   ・→ プールのスレッドが足りなくなる
   ・→ .NET は【1 秒に 1~2 本しか増やさない】
   ・→ 応答時間が【突然、階段状に悪化する】
   ・→ 負荷が下がると回復する(原因が分かりにくい)

【検知】
   ・dotnet-counters で ThreadPool Queue Length を見る
   ・ThreadPool.GetAvailableThreads

参考

C10k

イベントループ

ノンブロッキングI/O

補足(用語の整理 ── 参考リンクの主題): 「ノンブロッキング I/O」と
「非同期 I/O」は厳密には別なので、整理しておく。

用語 意味
ブロッキング I/O 完了するまで呼び出しが返らない
ノンブロッキング I/O すぐ返るが、「まだ準備できていない」と返る → 自分でポーリングする
I/O 多重化 複数の fd をまとめて監視(select / poll / epoll / kqueue)
非同期 I/O(AIO) 完了したら通知される(IOCP、io_uring)★
【Node.js が「ノンブロッキング I/O」と呼ばれる理由】
   正確には【I/O 多重化 + イベント通知】である
     → Unix 系では epoll(準備完了通知)
     → Windows では IOCP(完了通知= 真の非同期 I/O)

【.NET の async/await】
   Windows では【IOCP= 真の非同期 I/O】★
   Linux では epoll ベース

 → 利用者から見た効果(スレッドを塞がない)は同じだが、
   仕組みの層が違う

nginx(DNET_nginx.md)とNode.js

nginx(DNET_nginx.md

Node.js

Windows, .NET

補足(現在の .NET における C10K): 結論として、
現在の ASP.NET Core では C10K は問題にならない

【ASP.NET Core(Kestrel)】
   ・最初から【非同期 I/O 前提】で設計されている
   ・スレッド プール + IOCP / epoll
   ・1 台で【数万~数十万接続】を扱える
   → TechEmpower のベンチマークでも上位に位置する

【[ASP.NET Web Forms](MS_ASPNETWebForms) / 旧 ASP.NET との違い】
   ・旧来は同期処理が既定
   ・[部分描画とJavaScript](MS_PartialRenderingAndJavaScript) の時代は
     接続も短命だった

現在、同時接続数で問題になるのはむしろ以下である。

① 【スレッド プール枯渇】(前述)
     → 同期呼び出しが 1 箇所あるだけで発生する ★

② 【DB のコネクション プール枯渇】
     → アプリは非同期でも、DB 接続数には上限がある
     → SQL Server の既定は 100
     → 「アプリは捌けるが DB で詰まる」★ 現在の典型的なボトルネック

③ 【外部 API 呼び出しの HttpClient の使い方】
     → using で毎回 new すると【ソケット枯渇】(TIME_WAIT)
     → IHttpClientFactory を使う

④ WebSocket / SignalR の接続あたりのメモリ
     → 接続ごとに状態を持つ設計は要注意
       ([WebAssembly](MS_WebAssembly) の Blazor Server の項も参照)
【つまり】
   C10K 問題そのものは【ランタイムが解決した】
   現在は「その先の資源(DB 接続、ソケット、メモリ)」が
   ボトルネックになる ★

Tags: 移行, インフラストラクチャ, Windows, プログラミング, .NET開発

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally