news 2026/10/8 18:59:56

你是否听说过C10K 问题?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
你是否听说过C10K 问题?

1. 来源:这个名词是怎么来的

C10K = Concurrent 10,000,意思是"单台服务器同时支撑一万个连接"。

这个说法来自Dan Kegel 在 1999 年整理的一份文档,标题就叫《The C10K problem》。它把当时各种操作系统和编程模型的做法汇总在一起,回答一个问题:

一台机器要同时照看一万个连接,为什么这么难?分别有哪些办法?

为什么是 1999 年?因为那几年互联网刚普及,服务器从"服务几百人"变成"服务几万人"。而当时的编程习惯是一个连接配一个线程——这个模型在几百连接时挺好用,到了一万就彻底崩了。

后来这个目标被攻克,又出现了C10M(一千万连接),但那是内核旁路、用户态协议栈那个层次的事了。C10K 的意义在于它把"高并发"这件事从概念变成了一个具体的工程目标。


2. 原理:为什么一万个连接会成为问题

关键在于:传统模型把"连接数"和"线程数"绑在了一起。

// 一连接一线程:连接数 = 线程数ServerSocketserver=newServerSocket(8080);while(true){Socketsocket=server.accept();// 阻塞等待新连接newThread(()->handle(socket)).start();// 每来一个连接就开一个线程}

这段代码很直观,也很容易写对。问题出在成本上——每多一个连接,就要多付一份固定的开销,而这些开销和"这个连接是否活跃"无关。

2.1 逐项拆开看

成本项说明
线程栈内存默认每个线程 8 MB(虚拟内存,Linux 上)
内核对象每个 socket 有接收缓冲区、发送缓冲区、协议控制块
上下文切换每个线程被调度一次,就要保存/恢复寄存器、刷新 TLB
调度器压力可运行线程越多,调度决策越慢
accept 惊群多个进程抢同一个 listen fd,新连接唤醒全部

光算栈内存这一项:一万个线程 × 8 MB =80 GB虚拟地址空间。

这就是为什么 32 位系统在这一关直接出局——用户态地址空间总共只有 3 GB 左右,几万个连接连地址空间都不够分,跟物理内存有多少毫无关系。

64 位系统绕过了地址空间限制,但物理内存和上下文切换的开销逃不掉:

  • 一万个活跃线程,每秒上下文切换上万次,每次几微秒,CPU 大量时间花在"换人"而不是"干活"上
  • 每个线程的实际内存占用(RSS)哪怕压到几十 KB,加起来也是几百 MB 到几 GB

2.2 更隐蔽的一层:等待 I/O 的白白占用

这是问题的真正核心。

// 这个线程 99% 的时间在这里阻塞等待intn=in.read(buf);

连接大部分时间是空闲的。一个聊天的长连接,可能几分钟才来一条消息。但在这几分钟里:

  • 线程一直占着栈内存
  • 线程一直挂在调度器里
  • 线程什么活都没干

一万个连接里有九千个空闲,你依然要养着一万个线程。资源被"连接数量"而非"实际工作量"决定——这才是 C10K 的病根。

新连接到达

新建一个线程来处理
连接数 = 线程数

每个线程占栈内存
还要参与 CPU 调度

连接空闲时资源也不释放
线程一直阻塞在等 I/O

一万连接 = 一万线程
内存和切换成本线性增长

瓶颈不在 CPU 和带宽
在线程数量本身

2.3 还有一层:怎么知道哪个连接有数据了

如果不想给每个连接配线程,就必然要面对一个新问题:一个线程怎么盯着上万个连接?

早期答案是select/poll:

// 每次调用:把整个 fd 集合从用户态拷进内核,// 内核再从头到尾遍历一遍,看哪个就绪select(maxfd+1,&readfds,NULL,NULL,NULL);

这是 O(n) 的。一万个连接,每次事件循环都要扫一万次,即使只有两个连接真的活跃。连接越多,无效的扫描工作越多——CPU 被烧在"检查有没有事"上。

这就是 epoll 出现的原因,后面细说。

2.4 别忘了这些不起眼的限制

限制默认值后果
单进程 fd 上限1024到一千就Too many open files
系统级 fd 上限视发行版同上
accept 队列长度128(somaxconn)高并发下新连接被丢
本地端口范围约 2.8 万个主动连接多时不够用

很多"连接数上不去"的故障,根因不在代码,而在这几个数字上。


3. 常见现象:撑不住时长什么样

C10K 撑不住的时候,症状很有辨识度:

现象说明
CPU 不高,吞吐也上不去大量线程在阻塞等待,不在计算
load average 很高,CPU 使用率很低典型特征:负载算的是"可运行 + 不可中断"的进程数
vmstat里cs列飙升上下文切换次数异常高
内存被吃光,OOM线程栈累积
unable to create new native thread线程数到达上限
新建连接超时accept 队列溢出
延迟随连接数急剧恶化尾延迟(P99)爆炸,均值看着还行
Too many open filesfd 耗尽
大量 TIME_WAIT短连接频繁开关
机器一重启就正常,跑一段时间又不行连接泄漏或 fd 泄漏

最迷惑人的一条是"CPU 不高但就是慢"。因为瓶颈不是算力,是调度和内存——CPU 在忙着切换线程,而不是在跑业务代码。


4. 如何改进:七个层次

按"收益 / 成本"从高到低排。

4.1 换 I/O 模型:从 select 到 epoll(核心)

这是 C10K 最重要的单项改进。

select / pollepoll
复杂度每次 O(n) 遍历全部连接每次 O(就绪连接数)
fd 集合每次调用都要拷进内核常驻内核,增删改时才动
上限select 有 fd 数量硬限制只受系统 fd 上限约束

epoll 的三个关键设计(Linux 2.6 起;BSD/macOS 是 kqueue,Windows 是 IOCP):

intepfd=epoll_create1(0);// 1. 建一张"关注列表"epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&event);// 2. 增删改:红黑树管理,O(log n)epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&event);// 连接注册一次就留在里面epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&event);n=epoll_wait(epfd,events,maxevents,-1);// 3. 只返回"就绪"的连接

它为什么快:

  1. fd 常驻内核——不用每次把上万个 fd 拷来拷去
  2. 红黑树管理——增删改单个连接是 O(log n),不是重建整个集合
  3. 就绪链表 + 回调——哪个连接的网卡收到数据了,内核把它挂到就绪链表上;epoll_wait直接返回链表里的,不遍历未就绪的连接

结果:一万个空闲连接和一百个空闲连接的等待成本几乎一样。

4.2 事件驱动:一个线程管上万个连接

Selectorselector=Selector.open();serverChannel.register(selector,SelectionKey.OP_ACCEPT);while(true){selector.select();// 阻塞,几乎不耗 CPUfor(SelectionKeykey:selector.selectedKeys()){// 只处理"就绪"的那几个连接// 读数据、处理、写回,然后立刻返回循环}}

一个线程 + 一张关注列表
里面登记上万个连接

调用 epoll_wait
阻塞等待,不消耗 CPU

内核只把就绪的连接报回来
不遍历全部连接

线程逐个处理就绪连接
读或写,处理完即走

回到 epoll_wait
循环往复

一万个空闲连接
几乎不占 CPU

这就是Reactor 模式,也是 Nginx、Redis、Netty 的底座。

⚠️ 这套模型最致命的坑:事件循环线程绝对不能阻塞。你在里面做一次数据库查询、一次文件读取,上万个连接全部一起卡住。异步编程"难写"的恶名,根源就在这里。

4.3 补上异步编程的易用性:协程

事件驱动解决了性能,但代码被拆成一堆回调,难写难调。

协程(用户态线程)解决的是这个体验问题:让异步代码写成同步的样子。

语言实现
Gogoroutine + channel
Java虚拟线程(JDK 21 正式版)
Kotlin协程
Pythonasyncio

注意一个常见误解:协程不是C10K 的解法。底层用的还是 epoll,只是把"回调地狱"换成了"看起来同步"的代码。性能来自 epoll,易用性来自协程。

4.4 减少单连接开销

如果暂时不能改模型,先抠成本:

  • 缩小线程栈:-Xss512k、ulimit -s
  • 用线程池:避免"来一个起一个",但注意池化后连接会排队
  • 调 fd 上限:ulimit -n 65535(还要看fs.file-max)

这些是"续命"手段,改变不了线性增长的本质。

4.5 内核参数调优

fs.file-max=1000000# 系统级 fd 上限net.core.somaxconn=32768# accept 队列长度net.ipv4.tcp_max_syn_backlog=8192# 半连接队列net.ipv4.ip_local_port_range=1000065000# 可用端口范围net.ipv4.tcp_tw_reuse=1# 复用 TIME_WAIT 端口net.core.rmem_max=16777216# 收发缓冲区上限net.core.wmem_max=16777216

SO_REUSEPORT值得单独提:它让每个进程/线程各自持有独立的 listen socket,由内核做负载均衡,从根上消除 accept 惊群。

4.6 从架构上减少连接数

有时候最有效的办法是别让连接那么多:

  • 长连接复用:HTTP keep-alive、连接池,避免频繁开关
  • HTTP/2 多路复用:一个连接跑多个请求
  • 合并请求:把 100 个小请求合成 1 个
  • WebSocket 替代轮询:轮询是"用连接数换实时性",代价很高

4.7 减少每次操作的代价

连接数降下来之后,还能继续抠"每个连接每次读写"的成本:

  • 零拷贝:sendfile、writev让数据少走几趟
  • 批量收发:一次系统调用处理多个包
  • 网卡多队列 + RSS:把不同连接散到不同 CPU 核心

5. 从 C10K 到 C10M

C10K 在 2000 年代中后期基本被解决了(epoll + 事件驱动 + 64 位)。之后有人提出C10M——单机一千万连接。

难度直接跳了几个量级,因为内核本身成了瓶颈:

手段说明
内核旁路DPDK:网卡数据直接进用户态,绕过内核协议栈
eBPF / XDP在驱动层做包过滤和转发
用户态协议栈自己实现 TCP/IP
CPU 亲和性绑定连接和核心绑定,避免缓存失效
NUMA 感知内存和网卡就近访问

但绝大多数业务系统一辈子也到不了 C10M。把 C10K 的三板斧用好——epoll、事件驱动、不阻塞事件循环——就足够支撑绝大多数互联网服务了。


6. 六个常见误解

误解一:C10K 说的是"一万个并发请求"。
是"一万个并发连接"。一万个空闲长连接远比一万个正在处理的请求容易——前者考验模型,后者考验算力和后端依赖。

误解二:上了 epoll 就能撑一万。
epoll 只优化了"等待 I/O"的成本。如果每个请求都要查三次数据库,瓶颈根本不在连接上。

误解三:换成协程就解决了。
性能提升来自 epoll,协程改善的是代码可读性。而且协程里阻塞了 IO,照样卡住底层线程。

误解四:加机器就行。
单机问题不解决,成本就随连接数线性增长。而且单个连接是打不到多台机器上的。

误解五:连接数上不去一定是代码写得烂。
先查ulimit -n、somaxconn、fd 上限。很多故障是一行配置的事。

误解六:C10K 是老话题,过时了。
它提出的两个思想——别把连接数和线程数绑在一起、不要用轮询去发现就绪——是今天所有高并发框架的基础。Netty、Nginx、Redis 全建立在这上面。


7. 结语

C10K 的解法浓缩成一句话:

让一个线程照看很多个连接,并且不要在等待 I/O 的时候占着线程。

前半句靠epoll(只报告就绪的连接),后半句靠事件驱动(让出线程而不是阻塞它)。

而这个问题的病根,也浓缩成一句话:

资源应该由"实际工作量"决定,而不是由"连接数量"决定。

传统模型里,一个空闲连接和一个繁忙连接开销完全一样——这就是它撑不住的根本原因。整个 C10K 的演进史,就是不断把"开销"从"连接数"上解绑的过程。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 18:57:15

Context Is All You Need:读千问办公CEO陈宇森2026云栖演讲

基于 2026 云栖大会技术主论坛MaaS & Agent 演讲《千问办公:Context Is All You Need》 演讲人:阿里巴巴集团副总裁、千问办公 CEO 陈宇森 视频源:Bilibili BV1K1hE6VEhq 核心论断:大模型参数狂飙的阶段过去后,企…

作者头像 李华
网站建设 2026/10/8 18:55:17

每天十分钟跟上 AI 进展:InBrief 的三块信息与用法

先说清楚这篇是写给谁的。如果你在做 AI 相关的研发、产品或投资,想用很少的时间知道"从昨天到今天发生了什么",这篇会有用;如果你要找的是其他行业的新闻,InBrief 不覆盖,这一点我放在最前面讲。 我为什么…

作者头像 李华
网站建设 2026/10/8 18:55:15

「AI 说」单机版三体人:孤独的终极形态

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/8 18:55:14

CS-144 checkpoint 0: networking warmup

Writing a network program using an OS stream socket 2026-10-07 完成 checkpoint 0 的第一部分:用 OS 提供的 stream socket 写一个最小化的 HTTP 客户端 webget。 实验目标 Checkpoint 0 是整个 CS144 的热身关,目标有两个层次: 表层&…

作者头像 李华
网站建设 2026/10/8 18:54:35

py -m venv .venv py : 无法将“py”项识别为 cmdlet、

py -m venv .venv py : 无法将“py”项识别为 cmdlet、PS C:\Users\Administrator> py -m venv .venv py : 无法将“py”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然 后再试一次…

作者头像 李华