深蓝黑背景品牌封面:左侧竖向大标题「一台机器,万级连接」,副标题「C10K 问题与连接模型演进」;右侧原创装饰——一面由密集小圆点组成的点阵墙,点阵中几条青色亮线穿行连接,寓意海量并发连接;点阵右下角一枚徽章写着 C10K;左上标注「历史题目复盘 · 系统设计」,右上标注「Day 10 · 10-18」 图 1:本文封面,点阵墙上每一条亮线都是一条活着的连接。

先说明属性:这是一篇历史题目复盘,题目出自本地历史面试资料(原题问「什么是 C10K 问题,后来是怎么解决的」),与任何公司的当前招聘无关。C10K 本身是个老名词,但它背后的成本模型一点都不过时——大模型的流式长连接、消息推送、在线协同,今天仍站在同一条延长线上。

想象面试现场:「一台机器怎么扛住一万个并发连接?」急着答「用 epoll」的人,往往说不清 epoll 之前到底卡在哪。

核心结论一句话

C10K 的本质不是连接数,而是「等待的成本」由谁承担、按什么速度增长。这个提法源自工程师 Dan Kegel 在世纪之交整理的一份问题清单:当时的硬件已经不算瓶颈,他在清单里记载,1999 年最繁忙的 FTP 站点之一就已经同时服务过一万个客户端——问题出在软件的连接模型上。演进的主线就是一次次把等待成本降下来:多进程、多线程、IO 多路复用、协程与异步框架。

第一站:多进程模型。 每个连接 fork 一个进程,写起来直白,但进程占内存大、切换贵,千级连接内存和调度就先撑不住了。它解决的是「能同时服务」,留下的是「规模上不去」。

第二站:多线程模型。 一个线程伺候一个连接,比进程轻,但每个线程的栈空间和上下文切换依旧随连接数线性增长,万级仍然吃力。关键是大多数连接大部分时间在等待——等待对端发数据、等待磁盘返回——而为等待付出的是一整个线程。它解决了进程的重量,留下的是「等待照样很贵」。

第三站:IO 多路复用。 转折点在这里:一个线程监听全部连接,谁就绪处理谁。以 Linux 的 epoll 为例,官方手册对它的定位是监控大量文件描述符且「对大规模被监听器伸缩良好」:内核里维护一张兴趣列表登记所有被监控的连接,另备一张就绪列表,哪个连接有动静就进就绪列表,epoll_wait 每次只把就绪的那些还给应用。等待的成本不再随连接数线性增长,万级连接就此站稳。它解决了等待的重量,留下的是编程体验的债:业务逻辑被拆进回调和状态机里,代码难写难调。

第四站:协程与异步框架。 用同步的写法写异步的逻辑:代码看起来是一行一行顺序执行,底层调度器在每次等待时把执行权让给别人。内核的通知机制没变,变的是「让出执行权」从手写状态机变成了语言与运行时的能力。开发体验追平了,性能没有倒退。它解决的是人的成本。

演进时间线图:横向四站。第一站多进程模型——每个连接 fork 一个进程,内存与进程切换先撑不住,千级连接就到顶。第二站多线程模型——一线程一连接,比进程轻但线程栈与上下文切换依旧线性增长,万级仍吃力。第三站 IO 多路复用——一个线程监听全部连接,谁就绪处理谁,等待成本不再随连接数线性增长,万级连接站稳。第四站协程与异步框架——用同步写法写异步逻辑,业务代码不再被回调切碎,开发体验追平、性能不降。每站标注「解决了什么、留下了什么」。底部结论:C10K 的本质不是连接数,而是「等待的成本」由谁承担、按什么速度增长;长连接业务(推送、大模型流式输出)今天仍在同一条延长线上 图 2:连接模型演进四站,每站解决什么、留下什么。

把四站串成一句话。 多进程让并发成为可能,多线程让它变轻,多路复用让等待变便宜,协程让便宜的等待重新好写。回答这道题时按这个链条讲,比单独背一个 epoll 有说服力得多。

可能的追问与易错点。

「epoll 的边缘触发和水平触发有什么区别?」水平触发下只要还有数据没读完就会反复通知;边缘触发只在状态变化时通知一次,所以官方手册要求配非阻塞描述符、读到返回 EAGAIN 为止,否则数据还躺在缓冲区里却再也等不到通知。「万级之上呢?」同样的问题换了个量级继续存在:百万连接要对付内存占用、内核参数与多机分摊,思路没变。易错点有三:一是只背「epoll 比 select 快」却说不出快在哪——快在不必每次全量扫描连接清单;二是以为协程绕开了操作系统,内核的事件通知始终在,只是调度挪到了用户态;三是以为 C10K 已经解决就不用懂,大模型流式输出那种「大量慢生成连接挂在网关上」的形态,正是 C10K 的现代版本。

给你的行动建议。 找一个支持异步的运行时,写个同时挂着一千条长连接的小服务,观察内存和 CPU 曲线,再想象同样的连接数换成一线程一连接会怎样。亲手看一眼曲线,胜过背十遍演进史。

你现在的业务里有长连接吗?峰值在线连接数到什么量级,用的是哪种模型?欢迎分享。