一、前言<ignore_js_op> ![]() 对于高性能即时通信技术(或者说互联网编程)比较关注的开发者,对C10K问题(即单机1万个并发链接问题)应该都有所了解。“C10K”概念最先由Dan Kegel发布于其我的站点,即出自其经典的《The C10K problem (英文PDF版、中文译文)》一文。 正如你所料,过去的10年里,高性能网络编程技术领域里通过众多开发者的努力,已很好地解决了C10K问题,你们已开始关注并着手解决下一个十年要面对的C10M问题(即单机1千万个并发链接问题,C10M相关技术讨论和学习将在本系列文章的下篇中开始展开,本文不做深刻介绍)。 虽然C10K问题已被妥善解决,但对于即时通信应用(或其它网络编程方面)的开发者而言,研究C10K问题仍然价值巨大,由于技术的发展都是有规律和线索可循的,了解C10K问题及其解决思路,经过触类旁通,或许能够为你之后面对相似问题提供更多可借鉴的思想和解决问题的实践思路。而这,也正是撰写本文的目的所在。 二、C10K问题系列文章本文是C10K问题系列文章中的第2篇,总目录以下:
三、C10K问题的提出者<ignore_js_op> ![]() Dan Kegel:软件工程师 目前工做在美国的洛杉矶,当前受雇于Google公司。从1978年起开始接触计算机编程,是Winetricks的做者、也是Wine 1.0的管理员,同时也是Crosstool( 一个让 gcc/glibc 编译器更易用的工具套件)的做者。发表了著名的《The C10K problem》技术文章,是Java JSR-51规范的提交者并参与编写了Java平台的NIO和文件锁,同时参与了RFC 5128标准中有关NAT 穿越(P2P打洞)技术的描述和定义。 四、C10K问题的由来你们都知道互联网的基础就是网络通讯,早期的互联网能够说是一个小群体的集合。互联网还不够普及,用户也很少,一台服务器同时在线100个用户估计在当时已经算是大型应用了,因此并不存在什么 C10K 的难题。互联网的爆发期应该是在www网站,浏览器,雅虎出现后。最先的互联网称之为Web1.0,互联网大部分的使用场景是下载一个HTML页面,用户在浏览器中查看网页上的信息,这个时期也不存在C10K问题。 Web2.0时代到来后就不一样了,一方面是普及率大大提升了,用户群体几何倍增加。另外一方面是互联网再也不是单纯的浏览万维网网页,逐渐开始进行交互,并且应用程序的逻辑也变的更复杂,从简单的表单提交,到即时通讯和在线实时互动,C10K的问题才体现出来了。由于每个用户都必须与服务器保持TCP链接才能进行实时的数据交互,诸如Facebook这样的网站同一时间的并发TCP链接极可能已通过亿。 早期的腾讯QQ也一样面临C10K问题,只不过他们是用了UDP这种原始的包交换协议来实现的,绕开了这个难题,固然过程确定是痛苦的。若是当时有epoll技术,他们确定会用TCP。众所周之,后来的手机QQ、微信都采用TCP协议。 这时候问题就来了,最初的服务器都是基于进程/线程模型的,新到来一个TCP链接,就须要分配1个进程(或者线程)。而进程又是操做系统最昂贵的资源,一台机器没法建立不少进程。若是是C10K就要建立1万个进程,那么单机而言操做系统是没法承受的(每每出现效率低下甚至彻底瘫痪)。若是是采用分布式系统,维持1亿用户在线须要10万台服务器,成本巨大,也只有Facebook、Google、雅虎等巨头才有财力购买如此多的服务器。 基于上述考虑,如何突破单机性能局限,是高性能网络编程所必需要直面的问题。这些局限和问题最先被Dan Kegel 进行了概括和总结,并首次成系统地分析和提出解决方案,后来这种广泛的网络现象和技术局限都被你们称为 C10K 问题。 五、技术解读C10K问题C10K 问题的最大特色是:设计不够良好的程序,其性能和链接数及机器性能的关系每每是非线性的。 举个例子:若是没有考虑过 C10K 问题,一个经典的基于 select 的程序能在旧服务器上很好处理 1000 并发的吞吐量,它在 2 倍性能新服务器上每每处理不了并发 2000 的吞吐量。这是由于在策略不当时,大量操做的消耗和当前链接数 n 成线性相关。会致使单个任务的资源消耗和当前链接数的关系会是 O(n)。而服务程序须要同时对数以万计的socket 进行 I/O 处理,积累下来的资源消耗会至关可观,这显然会致使系统吞吐量不能和机器性能匹配。 以上这就是典型的C10K问题在技术层面的表现。这也是为什么一样的功能,大多数开发人员都能很容易地从功能上实现,但一旦放到大并发场景下,初级与高级开发者对同一个功能的技术实现所体现出的实际应用效果,则是大相径庭的。 因此说,一些没有太多大并发实践经验的技术同行,所实现的诸如即时通信应用在内的网络应用,所谓的理论负载动不动就宣称能支持单机上万、上十万甚至上百万的状况,是经不起检验和考验的。 六、C10K问题的本质C10K问题本质上是操做系统的问题。对于Web1.0/2.0时代的操做系统而言, 传统的同步阻塞I/O模型都是同样的,处理的方式都是requests per second,并发10K和100的区别关键在于CPU。 建立的进程线程多了,数据拷贝频繁(缓存I/O、内核将数据拷贝到用户进程空间、阻塞), 进程/线程上下文切换消耗大, 致使操做系统崩溃,这就是C10K问题的本质! 可见,解决C10K问题的关键就是尽量减小这些CPU等核心计算资源消耗,从而榨干单台服务器的性能,突破C10K问题所描述的瓶颈。 七、C10K问题的解决方案探讨要解决这一问题,从纯网络编程技术角度看,主要思路有两个:
1思路一:每一个进程/线程处理一个链接这一思路最为直接。可是因为申请进程/线程会占用至关可观的系统资源,同时对于多进程/线程的管理会对系统形成压力,所以这种方案不具有良好的可扩展性。 所以,这一思路在服务器资源尚未富裕到足够程度的时候,是不可行的。即使资源足够富裕,效率也不够高。总之,此思路技术实现会使得资源占用过多,可扩展性差。 2思路二:每一个进程/线程同时处理多个链接(IO多路复用)IO多路复用从技术实现上又分不少种,咱们逐一来看看下述各类实现方式的优劣。 ● 实现方式1:传统思路最简单的方法是循环挨个处理各个链接,每一个链接对应一个 socket,当全部 socket 都有数据的时候,这种方法是可行的。可是当应用读取某个 socket 的文件数据不 ready 的时候,整个应用会阻塞在这里等待该文件句柄,即便别的文件句柄 ready,也没法往下处理。 实现小结:直接循环处理多个链接。 问题概括:任一文件句柄的不成功会阻塞住整个应用。 ● 实现方式2:select要解决上面阻塞的问题,思路很简单,若是我在读取文件句柄以前,先查下它的状态,ready 了就进行处理,不 ready 就不进行处理,这不就解决了这个问题了嘛?因而有了 select 方案。用一个 fd_set 结构体来告诉内核同时监控多个文件句柄,当其中有文件句柄的状态发生指定变化(例如某句柄由不可用变为可用)或超时,则调用返回。以后应用可使用 FD_ISSET 来逐个查看是哪一个文件句柄的状态发生了变化。这样作,小规模的链接问题不大,但当链接数不少(文件句柄个数不少)的时候,逐个检查状态就很慢了。所以,select 每每存在管理的句柄上限(FD_SETSIZE)。同时,在使用上,由于只有一个字段记录关注和发生事件,每次调用以前要从新初始化 fd_set 结构体。
问题概括:句柄上限+重复初始化+逐个排查全部文件句柄状态效率不高。 ● 实现方式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库的程序以下: <ignore_js_op> ![]() 八、参考资料[1] 为何QQ用的是UDP协议而不是TCP协议? [2] 移动端IM/推送系统的协议选型:UDP仍是TCP? [3] 高性能网络编程经典:《The C10K problem(英文)》[附件下载] [4] 高性能网络编程(一):单台服务器并发TCP链接数到底能够有多少 [5] 《The C10K problem (英文在线阅读、英文PDF版下载、中文译文)》 [6] <ignore_js_op> ![]() [7] [通俗易懂]深刻理解TCP协议(上):理论基础 [8] [通俗易懂]深刻理解TCP协议(下):RTT、滑动窗口、拥塞处理 [9] 《TCP/IP详解 卷1:协议 (在线阅读版)》 九、更多资料《TCP/IP详解 - 第11章·UDP:用户数据报协议》 《TCP/IP详解 - 第17章·TCP:传输控制协议》 《TCP/IP详解 - 第18章·TCP链接的创建与终止》 《TCP/IP详解 - 第21章·TCP的超时与重传》 《技术往事:改变世界的TCP/IP协议(珍贵多图、手机慎点)》 《通俗易懂-深刻理解TCP协议(上):理论基础》 《通俗易懂-深刻理解TCP协议(下):RTT、滑动窗口、拥塞处理》 《理论经典:TCP协议的3次握手与4次挥手过程详解》 《理论联系实际:Wireshark抓包分析TCP 3次握手、4次挥手过程》 《计算机网络通信协议关系图(中文珍藏版)》 《UDP中一个包的大小最大能多大?》 《Java新一代网络编程模型AIO原理及Linux系统AIO介绍》 《NIO框架入门(一):服务端基于Netty4的UDP双向通讯Demo演示》 《NIO框架入门(二):服务端基于MINA2的UDP双向通讯Demo演示》 《NIO框架入门(三):iOS与MINA二、Netty4的跨平台UDP双向通讯实战》 《NIO框架入门(四):Android与MINA二、Netty4的跨平台UDP双向通讯实战》 《P2P技术详解(一):NAT详解——详细原理、P2P简介》 《P2P技术详解(二):P2P中的NAT穿越(打洞)方案详解》 《P2P技术详解(三):P2P技术之STUN、TURN、ICE详解》 《高性能网络编程(一):单台服务器并发TCP链接数到底能够有多少》 《高性能网络编程(二):上一个10年,著名的C10K并发链接问题》 《高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了》 《高性能网络编程(四):从C10K到C10M高性能网络应用的理论探索》 >> 更多同类文章 …… |