c10k问题是什么意思,错误C100

  c10k问题是什么意思,错误C100

  为什么80%的码农做不了架构师?

  #0系列目录#

  谈远程通信Java远程通信技术及原理分析谈Socket、TCP/IP、HTTP、FTP及网络编程RMI原理及实现RPC原理及实现轻量级分布式RPC框架使用RMI ZooKeeper实现远程调用框架。讲解SOA思想微服务,SOA和API对比分析说说同步,异步,说说Linux有阻塞和无阻塞的五种IO模型。谈IO复用的选择、轮询和epoll。谈谈C10K问题及解决方案#1 C10K问题#大家都知道互联网的基础是网络通信。早期的互联网可以说是小群体的集合。互联网不够普及,用户也不多。一个100个用户同时在线的服务器,估计是当时的大型应用。所以不存在C10K的问题。互联网的爆发应该是在www网站、浏览器、雅虎出现之后。最早的互联网叫Web1.0,互联网的大部分使用场景是下载一个Html页面,用户在浏览器中查看网页上的信息。这个时期不存在C10K问题。

  Web2.0时代到来后,就不一样了。一方面,普及率大大提高,用户群体呈几何级数增长。另一方面,互联网不再是简单的浏览万维网网页,而是逐渐开始交互,应用程序的逻辑变得更加复杂,从简单的表单提交到即时通讯和在线实时交互。C10K的问题就体现出来了。每个用户都必须与服务器保持TCP连接,以便进行实时数据交互。脸书等网站同时并发TCP连接的数量可能超过1亿。

  腾讯也有C10K的问题,但是他们用UDP这种原始的分组交换协议来实现,从而绕过了这个问题。当然,过程肯定是痛苦的。如果当时有epoll技术,他们肯定会用TCP。后来手机QQ和微信都采用了TCP协议。

  这时,问题就出现了。最初的服务器都是基于进程/线程模型的。当一个新的TCP连接到达时,需要分配一个进程(或线程)。而且进程是操作系统最昂贵的资源,一台机器不可能创建很多进程。如果是C10K,要创建10000个进程,那么操作系统承受不起。如果采用分布式系统,要保持1亿用户在线需要10万台服务器,成本很大。只有脸书、谷歌和雅虎买得起这么多服务器。这就是C10K问题的本质。

  其实当时也有异步模式,比如选择/轮询模式。这些技术都有一定的缺点,比如selelct最大不能超过1024。Poll没有限制,但是每次接收到数据时,都需要遍历每一个连接,看哪一个有数据请求。

  #2解决方案#解决这个问题,主要有两个思路:一个是为每个连接进程分配一个独立的进程/线程;另一个想法是使用同一个进程/线程同时处理几个连接。

  ##2.1每个进程/线程处理一个连接的思想##是最直接的。但是应用进程/线程会占用相当多的系统资源,多个进程/线程的管理会给系统带来压力,所以这种方案不具备良好的可扩展性。

  所以这种想法在服务器资源不够丰富的情况下是不可行的;即使资源足够丰富,效率也不够高。

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

  传统思路最简单的方法是逐个循环处理每个连接,每个连接对应一个套接字。当所有套接字都有数据时,这种方法是可行的。

  但是,当应用程序没有准备好读取一个套接字的文件数据时,整个应用程序将阻塞在这里等待文件句柄,即使其他文件句柄准备好了,也无法进一步处理。

  思路:直接回收多个连接。

  问题:任何文件句柄的失败都会阻塞整个应用程序。

  select解决上面的堵塞问题,思路很简单。如果我在读取文件句柄之前检查它的状态,我会在它准备好的时候处理它。如果没准备好,我就不处理了。这样不就解决问题了吗?

  所以有了选择方案。fd_set结构用于告诉内核同时监控多个文件句柄。当其中一个文件句柄的状态发生变化(例如,一个句柄从不可用变为可用)或超时时,调用返回。之后,应用程序可以使用FD_ISSET逐个检查哪个文件句柄的状态发生了变化。

  这样小范围的连接问题不大,但是当连接比较多的时候(大量的文件句柄),逐个检查状态就比较慢了。因此,select总是有一个句柄上限(FD_SETSIZE)。同时,在使用中,由于记录关注点和事件的字段只有一个,所以在每次调用前都要重新初始化fd_set结构。

  int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct time val * time out);想法:当连接请求到达时,检查并处理它。

  问题:句柄上限的重复初始化对于逐个检查所有文件句柄的状态是低效的。

  pollpoll主要解决select的前两个问题:需要关注的事件通过一个pollfd数组传递给内核,消除文件句柄上限,用不同的字段分别标记关注事件和发生事件,避免重复初始化。

  int poll(struct pollfd *fds,nfds_t nfds,int time out);想法:设计一个新的数据结构来提供效率。

  问题:逐个检查所有文件句柄的状态是低效的。

  epoll由于逐个检查所有文件句柄的状态效率很低,自然,如果应用程序只提供了调用返回时状态发生变化(可能是数据就绪)的文件句柄,检查的效率会高得多。

  Epoll采用这种设计,适合大规模应用场景。

  实验表明,当文件句柄数量超过10时,epoll的性能将优于select和poll。当文件句柄的数量达到10K时,epoll已经超过了select和poll两个数量级。

  int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int time out);想法:只返回状态改变的文件句柄。

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

  由于Linux是互联网企业使用率最高的操作系统,Epoll成为了C10K杀手、高并发、高性能、异步、无阻塞技术的代名词。BSD推出了kqueue,Linux推出了epoll,Windows推出了IOCP,Solaris推出了/dev/poll。这些操作系统提供的功能就是为了解决C10K问题。epoll技术的编程模型是异步非阻塞回调,也可称为Reactor、event-driven和EventLoop。Nginx,libevent,node.js这些都是Epoll时代的产物。

  关于select、poll和epoll的具体原理的详细说明,请参考:《聊聊IO多路复用之select、poll、epoll详解》。

  libevent因为epoll、kqueue、IOCP的每个接口都有自己的特点,程序移植起来非常困难。因此,有必要对这些接口进行封装,使其易于使用和移植,libevent库就是其中之一。跨平台,封装底层平台的调用并提供统一的API,但底层平台自动选择不同平台上合适的调用。

  根据libevent官网介绍,libevent库提供了以下功能:当一个文件描述符的特定事件(如可读、可写或错误)发生,或者定时事件发生时,libevent会自动执行用户指定的回调函数来处理事件。目前,libevent已经支持以下接口/dev/poll、kqueue、eventports、select、poll和epoll。Libevent的内部事件机制完全基于所使用的接口。所以libevent非常容易移植,这也使得它的可扩展性非常容易。目前,libevent已经编译在以下操作系统中:Linux、BSD、Mac OS X、Solaris和Windows。

  使用libevent库进行开发非常简单,在各种unix平台上移植也很容易。下面是一个使用libevent库的简单程序:

  #3协程)#随着技术的演进,epoll已经可以很好的处理C10K的问题,但是如果需要进一步扩展,比如支持10M规模的并发连接,原有的技术就无能为力了。

  那么,新的瓶颈在哪里?

  从之前的演进过程中,我们可以看到,根本的思路是高效地解除对CPU的阻塞,使其能够完成核心任务。所以,千万级并发实现的秘密:内核不是解决方案,而是问题所在!

  这意味着:

  不要让内核执行所有繁重的任务。将数据包处理、内存管理、处理器调度等任务从内核转移到应用程序,高效完成。让Linux只处理控制层,把数据层留给应用。

  当有很多连接的时候,首先需要很多进程/线程来做事情。同时,系统中大量的应用进程/线程可能处于就绪状态,这就需要系统不断的快速切换,我们知道系统上下文的切换是有代价的。虽然Linux系统的调度算法已经被设计得非常高效,但是对于10M这样的大规模场景来说,还是显得力不从心。

  所以我们面临两个瓶颈,一个是进程/线程作为处理单元还是太重;另一个是系统调度的成本太高。

  很自然,我们会想到,如果有一种更轻量级的进程/线程作为处理单元,而且它们的调度可以做到很快(最好不需要锁),那就完美了。

  这些技术已经在一些语言中实现了,它们是协同例程,或协作例程。具体来说,Python,Lua语言中的协程模型,Go语言中的Go例程模型是类似的概念。其实很多语言(甚至C语言)都可以实现类似的模型。

  在实践中,它们都试图用少量的线程完成多个任务。一旦一个任务被阻塞,同一个线程可以继续运行其他任务,以避免大量的上下文切换。每个进程独占的系统资源往往只是堆栈部分。而且各种协程之间的切换往往是用户通过代码明确指定的(类似于各种回调),所以不需要内核参与就可以方便地实现异步。

  这种技术本质上也是一种异步无阻塞技术。它包装了事件回调,这样程序员就看不到里面的事件循环了。程序员就像写阻塞代码一样简单。比如调用client-recv()等待接收数据的时候,像写阻塞代码一样写。实际上,底层库在执行recv时会悄悄地保存一个状态,比如代码行数和局部变量的值。然后跳回事件循环。当真正的数据到达时,它取出刚刚保存的代码行数和局部变量值,再次开始执行。

  这是协同学的精髓。协同处理是异步非阻塞的另一种表现形式。Golang,Erlang,Lua协同学都是模型。

  ##3.1同步块##不知道大家看了协同过程能不能感受到。其实协同过程和同步块是一样的。答案是肯定的。所以协程也叫用户态进度/用户态线程。不同的是,进程/线程被操作系统调度为EventLoop,而进程/线程是由Epoll自己调度的。

  协程的优点是比系统线程花费少,但缺点是如果一个协程有密集的计算,其他协程就不会运行。操作系统进程的缺点是开销大,优点是不管代码怎么写,所有进程都可以并发运行。

  Erlang解决了协作密集型计算的问题。基于自研VM,不执行机器码。即使存在密集计算的场景,VM也可以中止和切换,即使它发现协程执行时间太长。Golang无法解决这个问题,因为它直接执行机器码。所以Golang要求用户在密集计算的代码中屈服。

  其实同步阻塞程序的性能还不错,非常高效,不会浪费资源。当一个进程被阻塞时,操作系统会挂起它,不会分配CPU。在数据到达之前,不会分配CPU。进程太多副作用太多,因为进程太多,互相切换有开销。因此,如果一个服务器程序只有大约1000个并发连接,那么同步阻塞模式是最好的。

  ##3.2异步回调和协同例程哪个性能更好?#虽然协程是用户态调度,但实际上是需要调度的。既然调度,就会有上下文切换。因此,尽管协同进程的性能优于操作系统进程,但总会有额外的消耗。但是异步回调没有切换开销,相当于顺序代码执行。因此,异步回调程序的性能优于协同模型。

  #欣赏我的文章让你获益#

  转载于:https://my.oschina.net/xianggao/blog/664275

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: