聊聊C10K问题及解决方案

#0 系列目录#node

#1 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,雅虎才有财力购买如此多的服务器。这就是C10K问题的本质数组

实际上当时也有异步模式,如:select/poll模型,这些技术都有必定的缺点,如selelct最大不能超过1024,poll没有限制,但每次收到数据须要遍历每个链接查看哪一个链接有数据请求。浏览器

#2 解决方案# 解决这一问题,主要思路有两个:一个是对于每一个链接处理分配一个独立的进程/线程;另外一个思路是用同一进程/线程来同时处理若干链接服务器

##2.1 每一个进程/线程处理一个链接## 这一思路最为直接。可是因为申请进程/线程会占用至关可观的系统资源,同时对于多进程/线程的管理会对系统形成压力,所以这种方案不具有良好的可扩展性。微信

所以,这一思路在服务器资源尚未富裕到足够程度的时候,是不可行的;即使资源足够富裕,效率也不够高。网络

问题:资源占用过多,可扩展性差。 ##2.2 每一个进程/线程同时处理多个链接(IO多路复用)##数据结构

  1. 传统思路

最简单的方法是循环挨个处理各个链接,每一个链接对应一个 socket,当全部 socket 都有数据的时候,这种方法是可行的。

可是当应用读取某个 socket 的文件数据不 ready 的时候,整个应用会阻塞在这里等待该文件句柄,即便别的文件句柄 ready,也没法往下处理。

思路:直接循环处理多个链接。

问题:任一文件句柄的不成功会阻塞住整个应用。

  1. select

要解决上面阻塞的问题,思路很简单,若是我在读取文件句柄以前,先查下它的状态,ready 了就进行处理,不 ready 就不进行处理,这不就解决了这个问题了嘛?

因而有了 select 方案。用一个 fd_set 结构体来告诉内核同时监控多个文件句柄,当其中有文件句柄的状态发生指定变化(例如某句柄由不可用变为可用)或超时,则调用返回。以后应用能够使用 FD_ISSET 来逐个查看是哪一个文件句柄的状态发生了变化

这样作,小规模的链接问题不大,但当链接数不少(文件句柄个数不少)的时候,逐个检查状态就很慢了。所以,select 每每存在管理的句柄上限(FD_SETSIZE)。同时,在使用上,由于只有一个字段记录关注和发生事件,每次调用以前要从新初始化 fd_set 结构体

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

思路:有链接请求抵达了再检查处理。

问题:句柄上限+重复初始化+逐个排查全部文件句柄状态效率不高。

  1. poll

poll 主要解决 select 的前两个问题:经过一个 pollfd 数组向内核传递须要关注的事件消除文件句柄上限,同时使用不一样字段分别标注关注事件和发生事件,来避免重复初始化

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

思路:设计新的数据结构提供使用效率。

问题:逐个排查全部文件句柄状态效率不高。

  1. epoll

既然逐个排查全部文件句柄状态效率不高,很天然的,若是调用返回的时候只给应用提供发生了状态变化(极可能是数据 ready)的文件句柄,进行排查的效率不就高多了么。

epoll 采用了这种设计,适用于大规模的应用场景。

实验代表,当文件句柄数目超过 10 以后,epoll 性能将优于 select 和 poll;当文件句柄数目达到 10K 的时候,epoll 已经超过 select 和 poll 两个数量级

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

思路:只返回状态变化的文件句柄。

问题:依赖特定平台(Linux)。

由于Linux是互联网企业中使用率最高的操做系统,Epoll就成为C10K killer、高并发、高性能、异步非阻塞这些技术的代名词了。FreeBSD推出了kqueue,Linux推出了epoll,Windows推出了IOCP,Solaris推出了/dev/poll。这些操做系统提供的功能就是为了解决C10K问题epoll技术的编程模型就是异步非阻塞回调,也能够叫作Reactor,事件驱动,事件轮循(EventLoop)。Nginx,libevent,node.js这些就是Epoll时代的产物。

select、poll、epoll具体原理详解,请参见:《聊聊IO多路复用之select、poll、epoll详解》

  1. libevent

因为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库的程序以下:

输入图片说明

#3 协程(coroutine)# 随着技术的演进,epoll 已经能够较好的处理 C10K 问题,可是若是要进一步的扩展,例如支持 10M 规模的并发链接,原有的技术就无能为力了。

那么,新的瓶颈在哪里呢?

从前面的演化过程当中,咱们能够看到,根本的思路是要高效的去阻塞,让 CPU 能够干核心的任务因此,千万级并发实现的秘密:内核不是解决方案,而是问题所在!

这意味着:

不要让内核执行全部繁重的任务。将数据包处理,内存管理,处理器调度等任务从内核转移到应用程序高效地完成。让Linux只处理控制层,数据层彻底交给应用程序来处理。

当链接不少时,首先须要大量的进程/线程来作事。同时系统中的应用进程/线程们可能大量的都处于 ready 状态,须要系统去不断的进行快速切换,而咱们知道系统上下文的切换是有代价的。虽然如今 Linux 系统的调度算法已经设计的很高效了,但对于 10M 这样大规模的场景仍然力有不足。

因此咱们面临的瓶颈有两个,一个是进程/线程做为处理单元仍是太厚重了;另外一个是系统调度的代价过高了

很天然地,咱们会想到,若是有一种更轻量级的进程/线程做为处理单元,并且它们的调度能够作到很快(最好不须要锁),那就完美了。

这样的技术如今在某些语言中已经有了一些实现,它们就是 coroutine(协程),或协做式例程。具体的,Python、Lua 语言中的 coroutine(协程)模型,Go 语言中的 goroutine(Go 程)模型,都是相似的一个概念。实际上,多种语言(甚至 C 语言)均可以实现相似的模型。

它们在实现上都是试图用一组少许的线程来实现多个任务,一旦某个任务阻塞,则可能用同一线程继续运行其余任务,避免大量上下文的切换每一个协程所独占的系统资源每每只有栈部分。并且,各个协程之间的切换,每每是用户经过代码来显式指定的(跟各类 callback 相似),不须要内核参与,能够很方便的实现异步。

这个技术本质上也是异步非阻塞技术,它是将事件回调进行了包装,让程序员看不到里面的事件循环。程序员就像写阻塞代码同样简单。好比调用 client->recv() 等待接收数据时,就像阻塞代码同样写。其实是底层库在执行recv时悄悄保存了一个状态,好比代码行数,局部变量的值。而后就跳回到EventLoop中了。何时真的数据到来时,它再把刚才保存的代码行数,局部变量值取出来,又开始继续执行。

这就是协程的本质。协程是异步非阻塞的另一种展示形式。Golang,Erlang,Lua协程都是这个模型。

##3.1 同步阻塞## 不知道你们看完协程是否感受获得,实际上协程和同步阻塞是同样的。答案是的。因此协程也叫作用户态进/用户态线程。区别就在于进程/线程是操做系统充当了EventLoop调度,而协程是本身用Epoll进行调度

协程的优势是它比系统线程开销小,缺点是若是其中一个协程中有密集计算,其余的协程就不运行了。操做系统进程的缺点是开销大,优势是不管代码怎么写,全部进程均可以并发运行。

Erlang解决了协程密集计算的问题,它基于自行开发VM,并不执行机器码。即便存在密集计算的场景,VM发现某个协程执行时间过长,也能够进行停止切换。Golang因为是直接执行机器码的,因此没法解决此问题。因此Golang要求用户必须在密集计算的代码中,自行Yield

实际上同步阻塞程序的性能并不差,它的效率很高,不会浪费资源。当进程发生阻塞后,操做系统会将它挂起,不会分配CPU。直到数据到达才会分配CPU。多进程只是开多了以后反作用太大,由于进程多了互相切换有开销。因此若是一个服务器程序只有1000左右的并发链接,同步阻塞模式是最好的

##3.2 异步回调和协程哪一个性能好## 协程虽然是用户态调度,实际上仍是须要调度的,既然调度就会存在上下文切换。因此协程虽然比操做系统进程性能要好,但总仍是有额外消耗的。而异步回调是没有切换开销的,它等同于顺序执行代码。因此异步回调程序的性能是要优于协程模型的。

#赞扬个人文章给您带来收获# 输入图片说明

相关文章
相关标签/搜索