高性能网络编程 (二):上一个 10 年,著名的 C10K 并发连接问题 - SegmentFault 思否
38
高性能网络编程 (二):上一个 10 年,著名的 C10K 并发连接问题

JackJiang发布于 2016-10-21
1、前言

对于高性能即时通讯技术(或者说互联网编程)比较关注的开发者,对 C10K 问题(即单机 1 万个并发连接问题)应该都有所了解。“C10K” 概念最早由 Dan Kegel 发布于其个人站点,即出自其经典的《The C10K problem(英文 PDF 版、中文译文)》一文。
正如你所料,过去的 10 年里,高性能网络编程技术领域里经过众多开发者的努力,已很好地解决了 C10K 问题,大家已开始关注并着手解决下一个十年要面对的 C10M 问题(即单机 1 千万个并发连接问题,C10M 相关技术讨论和学习将在本系列文章的下篇中开始展开,本文不作深入介绍)。
虽然 C10K 问题已被妥善解决,但对于即时通讯应用(或其它网络编程方面)的开发者而言,研究 C10K 问题仍然价值巨大,因为技术的发展都是有规律和线索可循的,了解 C10K 问题及其解决思路,通过举一反三,或许可以为你以后面对类似问题提供更多可借鉴的思想和解决问题的实践思路。而这,也正是撰写本文的目的所在。
2、学习交流
- 即时通讯开发交流群:215891622 [推荐]
- 移动端 IM 开发推荐文章:《新手入门一篇就够:从零开发移动端 IM》
3、C10K 问题系列文章
本文是 C10K 问题系列文章中的第 2 篇,总目录如下:
- 《高性能网络编程 (一):单台服务器并发 TCP 连接数到底可以有多少》
- 《高性能网络编程 (二):上一个 10 年,著名的 C10K 并发连接问题》(本文)
- 《高性能网络编程 (三):下一个 10 年,是时候考虑 C10M 并发问题了》
- 《高性能网络编程经典:《The C10K problem(英文)》[附件下载]》
4、C10K 问题的提出者

Dan Kegel:软件工程师
目前工作在美国的洛杉矶,当前受雇于 Google 公司。从 1978 年起开始接触计算机编程,是 Winetricks 的作者、也是 Wine 1.0 的管理员,同时也是 Crosstool( 一个让 gcc/glibc 编译器更易用的工具套件)的作者。发表了著名的《The C10K problem》技术文章,是 Java JSR-51 规范的提交者并参与编写了 Java 平台的 NIO 和文件锁,同时参与了 RFC 5128 标准中有关 NAT 穿越(P2P 打洞)技术的描述和定义。
5、C10K 问题的由来
大家都知道互联网的基础就是网络通信,早期的互联网可以说是一个小群体的集合。互联网还不够普及,用户也不多,一台服务器同时在线 100 个用户估计在当时已经算是大型应用了,所以并不存在什么 C10K 的难题。互联网的爆发期应该是在 www 网站,浏览器,雅虎出现后。最早的互联网称之为 Web1.0,互联网大部分的使用场景是下载一个 HTML 页面,用户在浏览器中查看网页上的信息,这个时期也不存在 C10K 问题。
Web2.0 时代到来后就不同了,一方面是普及率大大提高了,用户群体几何倍增长。另一方面是互联网不再是单纯的浏览万维网网页,逐渐开始进行交互,而且应用程序的逻辑也变的更复杂,从简单的表单提交,到即时通信和在线实时互动,C10K 的问题才体现出来了。因为每一个用户都必须与服务器保持 TCP 连接才能进行实时的数据交互,诸如 Facebook 这样的网站同一时间的并发 TCP 连接很可能已经过亿。
早期的腾讯 QQ 也同样面临 C10K 问题,只不过他们是用了 UDP 这种原始的包交换协议来实现的,绕开了这个难题,当然过程肯定是痛苦的。如果当时有 epoll 技术,他们肯定会用 TCP。众所周之,后来的手机 QQ、微信都采用 TCP 协议。 实际上当时也有异步模式,如:select/poll 模型,这些技术都有一定的缺点:如 selelct 最大不能超过 1024、poll 没有限制,但每次收到数据需要遍历每一个连接查看哪个连接有数据请求。
这时候问题就来了,最初的服务器都是基于进程 / 线程模型的,新到来一个 TCP 连接,就需要分配 1 个进程(或者线程)。而进程又是操作系统最昂贵的资源,一台机器无法创建很多进程。如果是 C10K 就要创建 1 万个进程,那么单机而言操作系统是无法承受的(往往出现效率低下甚至完全瘫痪)。如果是采用分布式系统,维持 1 亿用户在线需要 10 万台服务器,成本巨大,也只有 Facebook、Google、雅虎等巨头才有财力购买如此多的服务器。
基于上述考虑,如何突破单机性能局限,是高性能网络编程所必须要直面的问题。这些局限和问题最早被 Dan Kegel 进行了归纳和总结,并首次成系统地分析和提出解决方案,后来这种普遍的网络现象和技术局限都被大家称为 C10K 问题。
6、技术解读 C10K 问题
C10K 问题的最大特点是:设计不够良好的程序,其性能和连接数及机器性能的关系往往是非线性的。
举个例子:如果没有考虑过 C10K 问题,一个经典的基于 select 的程序能在旧服务器上很好处理 1000 并发的吞吐量,它在 2 倍性能新服务器上往往处理不了并发 2000 的吞吐量。这是因为在策略不当时,大量操作的消耗和当前连接数 n 成线性相关。会导致单个任务的资源消耗和当前连接数的关系会是 O(n)。而服务程序需要同时对数以万计的 socket 进行 I/O 处理,积累下来的资源消耗会相当可观,这显然会导致系统吞吐量不能和机器性能匹配。
以上这就是典型的 C10K 问题在技术层面的表现。这也是为何同样的功能,大多数开发人员都能很容易地从功能上实现,但一旦放到大并发场景下,初级与高级开发者对同一个功能的技术实现所体现出的实际应用效果,则是截然不同的。
所以说,一些没有太多大并发实践经验的技术同行,所实现的诸如即时通讯应用在内的网络应用,所谓的理论负载动不动就宣称能支持单机上万、上十万甚至上百万的情况,是经不起检验和考验的。
7、C10K 问题的本质
C10K 问题本质上是操作系统的问题。对于 Web1.0/2.0 时代的操作系统而言, 传统的同步阻塞 I/O 模型都是一样的,处理的方式都是 requests per second,并发 10K 和 100 的区别关键在于 CPU。
创建的进程线程多了,数据拷贝频繁(缓存 I/O、内核将数据拷贝到用户进程空间、阻塞), 进程 / 线程上下文切换消耗大, 导致操作系统崩溃,这就是 C10K 问题的本质!
可见,解决 C10K 问题的关键就是尽可能减少这些 CPU 等核心计算资源消耗,从而榨干单台服务器的性能,突破 C10K 问题所描述的瓶颈。
8、C10K 问题的解决方案探讨
要解决这一问题,从纯网络编程技术角度看,主要思路有两个:
- [1] 一个是对于每个连接处理分配一个独立的进程 / 线程;
- [2] 另一个思路是用同一进程 / 线程来同时处理若干连接。
8.1 思路一:每个进程 / 线程处理一个连接
这一思路最为直接。但是由于申请进程 / 线程会占用相当可观的系统资源,同时对于多进程 / 线程的管理会对系统造成压力,因此这种方案不具备良好的可扩展性。
因此,这一思路在服务器资源还没有富裕到足够程度的时候,是不可行的。即便资源足够富裕,效率也不够高。总之,此思路技术实现会使得资源占用过多,可扩展性差。
8.2 思路二:每个进程 / 线程同时处理多个连接(IO 多路复用)
IO 多路复用从技术实现上又分很多种,我们逐一来看看下述各种实现方式的优劣。
● 实现方式 1:传统思路最简单的方法是循环挨个处理各个连接,每个连接对应一个 socket,当所有 socket 都有数据的时候,这种方法是可行的。但是当应用读取某个 socket 的文件数据不 ready 的时候,整个应用会阻塞在这里等待该文件句柄,即使别的文件句柄 ready,也无法往下处理。
实现小结:直接循环处理多个连接。
问题归纳:任一文件句柄的不成功会阻塞住整个应用。
● 实现方式 2:select 要解决上面阻塞的问题,思路很简单,如果我在读取文件句柄之前,先查下它的状态,ready 了就进行处理,不 ready 就不进行处理,这不就解决了这个问题了嘛?于是有了 select 方案。用一个 fd_set 结构体来告诉内核同时监控多个文件句柄,当其中有文件句柄的状态发生指定变化(例如某句柄由不可用变为可用)或超时,则调用返回。之后应用可以使用 FD_ISSET 来逐个查看是哪个文件句柄的状态发生了变化。这样做,小规模的连接问题不大,但当连接数很多(文件句柄个数很多)的时候,逐个检查状态就很慢了。因此,select 往往存在管理的句柄上限(FD_SETSIZE)。同时,在使用上,因为只有一个字段记录关注和发生事件,每次调用之前要重新初始化 fd_set 结构体。
intselect(intnfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,structtimeval *timeout);
实现小结:有连接请求抵达了再检查处理。
问题归纳:句柄上限 + 重复初始化 + 逐个排查所有文件句柄状态效率不高。
● 实现方式 3:poll 主要解决 select 的前两个问题:通过一个 pollfd 数组向内核传递需要关注的事件消除文件句柄上限,同时使用不同字段分别标注关注事件和发生事件,来避免重复初始化。
实现小结:设计新的数据结构提供使用效率。
问题归纳:逐个排查所有文件句柄状态效率不高。
● 实现方式 4:epoll 既然逐个排查所有文件句柄状态效率不高,很自然的,如果调用返回的时候只给应用提供发生了状态变化(很可能是数据 ready)的文件句柄,进行排查的效率不就高多了么。epoll 采用了这种设计,适用于大规模的应用场景。实验表明,当文件句柄数目超过 10 之后,epoll 性能将优于 select 和 poll;当文件句柄数目达到 10K 的时候,epoll 已经超过 select 和 poll 两个数量级。
实现小结:只返回状态变化的文件句柄。
问题归纳:依赖特定平台(Linux)。
因为 Linux 是互联网企业中使用率最高的操作系统,Epoll 就成为 C10K killer、高并发、高性能、异步非阻塞这些技术的代名词了。FreeBSD 推出了 kqueue,Linux 推出了 epoll,Windows 推出了 IOCP,Solaris 推出了 / dev/poll。这些操作系统提供的功能就是为了解决 C10K 问题。epoll 技术的编程模型就是异步非阻塞回调,也可以叫做 Reactor,事件驱动,事件轮循(EventLoop)。Nginx,libevent,node.js 这些就是 Epoll 时代的产物。
● 实现方式 5:由于 epoll, kqueue, IOCP 每个接口都有自己的特点,程序移植非常困难,于是需要对这些接口进行封装,以让它们易于使用和移植,其中 libevent 库就是其中之一。跨平台,封装底层平台的调用,提供统一的 API,但底层在不同平台上自动选择合适的调用。按照 libevent 的官方网站,libevent 库提供了以下功能:当一个文件描述符的特定事件(如可读,可写或出错)发生了,或一个定时事件发生了,libevent 就会自动执行用户指定的回调函数,来处理事件。目前,libevent 已支持以下接口 / dev/poll, kqueue, event ports, select, poll 和 epoll。Libevent 的内部事件机制完全是基于所使用的接口的。因此 libevent 非常容易移植,也使它的扩展性非常容易。目前,libevent 已在以下操作系统中编译通过:Linux,BSD,Mac OS X,Solaris 和 Windows。使用 libevent 库进行开发非常简单,也很容易在各种 unix 平台上移植。一个简单的使用 libevent 库的程序如下:
9、参考资料
[1] 为什么 QQ 用的是 UDP 协议而不是 TCP 协议?
[2] 移动端 IM / 推送系统的协议选型:UDP 还是 TCP?
[3] 高性能网络编程经典:《The C10K problem(英文)》[附件下载]
[4] 高性能网络编程 (一):单台服务器并发 TCP 连接数到底可以有多少
[5] 《The C10K problem (英文在线阅读、英文 PDF 版下载、中文译文)》
[6] 搜狗实验室技术交流文档《C10K 问题探讨》(52im.net).pdf (350.83 KB)
[7] [通俗易懂]深入理解 TCP 协议(上):理论基础
[8] [通俗易懂]深入理解 TCP 协议(下):RTT、滑动窗口、拥塞处理
[9] 《TCP/IP 详解 卷 1:协议 (在线阅读版)》
(本文同步发布于:http://www.52im.net/thread-56…)
阅读 27.1K 更新于 2016-10-21
赞 38 收藏 72
本作品系原创,采用《署名 - 非商业性使用 - 禁止演绎 4.0 国际》许可协议

[
即时通讯开发
](/blog/im)
实时推送、IM 等即时通讯相关技术的研究、分享的专栏。让即时通讯技术能更好地学习、交流与传播,不再零…
关注专栏

JackJiang
专注即时通讯(IM / 推送)技术学习和研究。
909 声望
57 粉丝
关注作者
3 条评论

提交评论

慎之:
大佬 系列文章貌似没有更新在 sf 上呀?能否也同步更新到 sf?
回复2019-04-23
慎之:
了解,已经去即时通讯网了解大作了。
回复2019-04-24
sf 的 markdown 编辑方式,效率太低了,很费劲
回复2019-04-24
推荐阅读
[
高性能网络通信框架 Netty - 基础概念篇
摘要: Netty 是一种可以轻松快速的开发协议服务器和客户端网络应用程序的 NIO 框架,它大大简化了 TCP 或者 UDP 服务器的网络编程, 但是你仍然可以访问和使用底层的 API,Netty 只是对其进行了高层的抽象。
阿里云云栖号阅读 2.9K9 赞
](/a/1190000015161859?utm_source=sf-related)[
可能会搞砸你的面试:你知道一个 TCP 连接上能发起多少个 HTTP 请求吗?
大多数回答都是说请求响应之后 DOM 怎么被构建,被绘制出来。但是你有没有想过,收到的 HTML 如果包含几十个图片标签,这些图片是以什么方式、什么顺序、建立了多少连接、使用什么协议被下载下来的呢?
JackJiang 阅读 2.0K8 赞
](/a/1190000019950653?utm_source=sf-related)[
一泡尿的时间,快速读懂 QUIC 协议
但 TCP 协议在创建连接之前需要进行三次握手(如下图 1,更详细原理请见《理论经典:TCP 协议的 3 次握手与 4 次挥手过程详解》),如果需要提高数据交互的安全性,既增加传输层安全协议(TLS),还会增加更多的更多握…
JackJiang 阅读 2.2K6 赞
](/a/1190000020881220?utm_source=sf-related)[
swoole 入门 1 - 认识 swoole
官网原话:使 PHP 开发人员可以编写高性能的异步并发 TCP、UDP、Unix Socket、HTTP,WebSocket 服务。Swoole 可以广泛应用于互联网、移动通信、企业软件、云计算、网络游戏、物联网(IOT)、车联网、智能家居等领…
云天河 9527 阅读 1.5K5 赞
](/a/1190000017186408?utm_source=sf-related)[
知乎技术分享:知乎千万级并发的高性能长连接网关技术实践
实时的响应总是让人兴奋的,就如你在微信里看到对方正在输入,如你在王者峡谷里一呼百应,如你们在直播弹幕里不约而同的 666,它们的背后都离不开长连接技术的加持。
JackJiang 阅读 2.1K5 赞
](/a/1190000020296140?utm_source=sf-related)[
实时联网游戏后台服务技术选型与挑战(网络接入篇)
概述:本系列文章将从开发者角度梳理开发实时联网游戏后台服务过程中可能面临的挑战,并针对性地提供相应解决思路,期望帮助开发者依据自身游戏特点做出合理的技术选型。
Matchvs 游戏云阅读 1.5K4 赞
](/a/1190000015323399?utm_source=sf-related)[
Java 的 BIO 和 NIO 很难懂?用代码实践给你看,再不懂我转行!
这段时间自己在看一些 Java 中 BIO 和 NIO 之类的东西,也看了很多博客,发现各种关于 NIO 的理论概念说的天花乱坠头头是道,可以说是非常的完整,但是整个看下来之后,发现自己对 NIO 还是一知半解、一脸蒙逼的状态(请原…
JackJiang 阅读 1.4K3 赞
](/a/1190000021091737?utm_source=sf-related)[
一文读懂即时通讯应用中的网络心跳包机制:作用、原理、实现思路等
一般来说,没有真正动手做过网络通信应用的开发者,很难想象即时通讯应用中的心跳机制的作用。但不可否认,作为即时通讯应用,心跳机制是其网络通信技术底层中非常重要的一环,有没有心跳机制、心跳机制的算法实…
JackJiang 阅读 1.5K2 赞
](/a/1190000020008277?utm_source=sf-related)

JackJiang
专注即时通讯(IM / 推送)技术学习和研究。
909 声望
57 粉丝
关注作者
宣传栏
▲
产品
课程
资源
合作
关注
条款
Copyright © 2011-2021 SegmentFault. 当前呈现版本 19.02.27
浙 ICP 备 15005796 号 - 2浙公网安备 33010602002000 号杭州堆栈科技有限公司版权所有
https://segmentfault.com/a/1190000007240744
