精华内容
下载资源
问答
  • 山特UPS电源C6K是一款继承上一代城堡系列UPS 的优秀品质,并在其基础上表现更加出色的双转换在线式UPS。在有效解决9 种电力问题(断电、市电电压过高或过低、电压瞬间跌落或减幅震荡、高压脉冲、电压波动、浪涌电压...
  • 导读:昱科环球储存公司(HGST)宣布推出容量、效能的10K RPM硬盘──Ultrastar C10K1800 。此款2.5吋企业级硬盘Ultrastar C10K1800 ,采用HGST经现场实证的高质量设计,不只提供大容量,更大幅提升随机写入和循序效能...
  • C10k问题简述

    千次阅读 多人点赞 2019-10-28 14:46:48
    所谓c10k问题,指的是:服务器如何支持10k个并发连接,也就是concurrent 10000 connection(这也是c10k这个名字的由来)。由于硬件成本的大幅度降低和硬件技术的进步,如果一台服务器能够同时服务更多的客户端,那么...

    所谓c10k问题,指的是:服务器如何支持10k个并发连接,也就是concurrent 10000 connection(这也是c10k这个名字的由来)。由于硬件成本的大幅度降低和硬件技术的进步,如果一台服务器能够同时服务更多的客户端,那么也就意味着服务每一个客户端的成本大幅度降低。从这个角度来看,c10k问题显得非常有意义。

    一、C10K问题由来

    互联网的基础是网络通信,早期的互联网可以说是一个小群体的集合。互联网还不够普及,用户也不多,一台服务器同时在线100个用户,在当时已经算是大型应用了,所以并不存在 C10K 的难题。互联网的爆发期是在www网站、浏览器出现后。最早的互联网称之为Web1.0,大部分的使用场景是下载一个HTML页面,用户在浏览器中查看网页上的信息,这个时期也不存在C10K问题。

    Web2.0时代到来后,就不同了。一方面是,互联网普及率大大提高了,用户群体几何倍增长。另一方面是,互联网不再是单纯地浏览www网页,逐渐开始进行交互,而且应用程序的逻辑也变得更复杂。从简单的表单提交,到即时通信和在线实时互动,C10K的问题才体现出来了。因为每一个用户都必须与服务器保持连接,才能进行实时数据交互。诸如Facebook这样的网站,同一时间的并发TCP连接很可能已经过亿。

    早期的腾讯QQ也同样面临C10K问题,只不过他们是用了UDP这种原始的包交换协议来实现的,绕开了这个难题,当然过程肯定是痛苦的。如果当时有epoll技术,他们肯定会用TCP。众所周之,后来的手机QQ、微信都采用TCP协议。

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

    这时候问题就来了,最初的服务器都是基于进程/线程模型的,新到来一个TCP连接,就需要分配1个进程(或者线程)。进程又是操作系统最昂贵的资源,一台机器无法创建很多进程。如果是C10K,就要创建1万个进程,那么就单机而言,操作系统是无法承受的(往往出现效率低下、甚至完全瘫痪)。如果是采用分布式系统,维持1亿用户在线需要10万台服务器,成本巨大,也只有Facebook、Google、Apple等巨头,才有财力购买如此多的服务器。

    基于上述考虑,如何突破单机性能局限,是高性能网络编程所必须要直面的问题。这些局限和问题,最早被Dan Kegel 进行了归纳和总结,并首次系统地分析和提出了解决方案。后来,这种普遍的网络现象和技术局限,都被大家称为 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多路复用)

    3.1 为每个连接分配一个独立的线程/进程

    这一思路最为直接。但是,由于申请进程/线程会占用相当可观的系统资源,同时对于多进程/线程的管理会对系统造成压力,因此,这种方案不具备良好的可扩展性。

    这一思路在服务器资源还没有富裕到足够程度的时候,是不可行的。即便资源足够富裕,效率也不够高。

    总之,此思路技术实现会使得资源占用过多,可扩展性差,在实际应用中已被抛弃。

    3.2 同一个线程/进程同时处理多个连接(IO多路复用)

    IO多路复用,从技术实现上,又分很多种。我们逐一来看看下述各种实现方式的优劣。

    实现方式1:循环逐个处理各个连接,每个连接对应一个 socket

    循环逐个处理各个连接,每个连接对应一个 socket。当所有 socket 都有数据的时候,这种方法是可行的。但是,当应用读取某个 socket 的文件数据不 ready 的时候,整个应用会阻塞在这里,等待该文件句柄ready,即使别的文件句柄 ready,也无法往下处理。

    实现小结:直接循环处理多个连接。

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

    实现方式2:使用select方法

    使用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方法

    poll 主要解决 select 的前两个问题:

    1.通过一个 pollfd 数组,向内核传递需要关注的事件,以消除文件句柄上限。

    2.使用不同字段分别标注“关注事件和发生事件”,来避免重复初始化。

    实现小结:设计新的数据结构,提高使用效率。
    问题归纳:逐个排查所有文件句柄状态,效率不高。

    实现方式4:使用epoll方法

    既然“poll逐个排查所有文件句柄状态”效率不高,很自然的,在调用返回的时候,如果只给应用提供发生了状态变化(很可能是数据 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:使用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库的程序如下:

    四、引申讨论C10M问题

    随着技术的演进,epoll已经可以较好地处理 C10K 问题。但是,如果要进一步的扩展,例如支持10M 规模的并发连接,原有的技术就无能为力了。那么,新的瓶颈在哪里呢?

    从前面的演化过程中,我们可以看到,根本的思路是:要高效地去除阻塞,让CPU更多地处理核心任务所以,就千万级并发而言,内核不是解决方案,而是问题所在!

    这意味着:

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

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

    所以我们面临的瓶颈有两个:一个是进程/线程作为处理单元,还是太厚重了;另一个是系统调度的代价太高了

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

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

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

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

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

    4.1 同步阻塞

    大家看完协程,是否感觉到:实际上,协程和同步阻塞是一样的。答案是正确的。所以,协程也叫做用户态进程/用户态线程。区别就在于:进程/线程是操作系统充当了EventLoop调度,而协程是应用程序自己用Epoll进行调度

    协程的优点是:它比系统线程开销小。其缺点是:如果其中一个协程中有密集计算,其他的协程就不运行了。

    操作系统进程的优点是:无论代码怎么写,所有进程都可以并发运行。其缺点是:开销大。

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

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

    4.2 异步回调和协程哪个性能好

    协程虽然是用户态调度,实际上还是需要调度的。既然存在调度,就存在上下文切换。所以,协程虽然比操作系统进程性能要好,但总还是有额外消耗的。而异步回调是没有切换开销的,它等同于顺序执行代码。所以,异步回调程序的性能是要优于协程模型的性能

    5、参考资料

    [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:协议 (在线阅读版)

    展开全文
  • c10k

    2019-05-27 01:39:28
    NULL 博文链接:https://jiangyongyuan.iteye.com/blog/565923
  • C10K

    千次阅读 2018-09-11 20:36:23
    1 C10K问题 大家都知道互联网的基础就是网络通信,早期的互联网可以说是一个小群体的集合。互联网还不够普及,用户也不多。一台服务器同时在线100个用户估计在当时已经算是大型应用了。所以并不存在什么C10K的难题...
    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多路复用)
    传统思路

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

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

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

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

    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);
    思路:有连接请求抵达了再检查处理。

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

    poll

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

    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    思路:设计新的数据结构提供使用效率。

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

    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详解》。

    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 异步回调和协程哪个性能好
    协程虽然是用户态调度,实际上还是需要调度的,既然调度就会存在上下文切换。所以协程虽然比操作系统进程性能要好,但总还是有额外消耗的。而异步回调是没有切换开销的,它等同于顺序执行代码。所以异步回调程序的性能是要优于协程模型的。

    转子: https://www.cnblogs.com/jjzd/p/6540205.html
    展开全文
  • 介绍了C10K问题,以及 五种解决方案: - 阻塞 I/O + 进程 - 阻塞 I/O + 线程 - 非阻塞 I/O + readiness notification + 单线程(单reactor线程) - 非阻塞 I/O + readiness notification + 多线程(主从reactor) - ...

    参考:

    https://time.geekbang.org/column/article/143388

    C10K问题

    什么是C10K 问题

    C10K 问题是这样的:如何在一台物理机上同时服务 10000 个用户?这里 C 表示并发,10K 等于 10000。得益于操作系统、编程语言的发展,在现在的条件下,普通用户使用框架或库就可以轻轻松松写出支持并发超过 10000 的服务器端程序,甚至于经过优化之后可以达到十万,乃至百万的并发,但在二十年前,突破 C10K 问题可费了不少的心思,是一个了不起的突破。

    C10K的本质与考虑方面

    C10K 问题本质上是一个操作系统问题,一台主机上同时支持 1 万个连接,需要考虑哪些方面?

    文件句柄

    每个客户连接都代表一个文件描述符,一旦文件描述符不够用了,新的连接就会被放弃并产生错误。在 Linux 下,单个进程打开的文件句柄数是有限制的,没有经过修改的值一般都是 1024。可以使用 root 权限修改 /etc/sysctl.conf 文件,使得系统可以支持 10000 个描述符上限。

    系统内存

    每个 TCP 连接占用的资源不简单的就是一个连接套接字,还需要占用一定的发送缓冲区和接收缓冲区。Linux 5.4.0 下发送缓冲区和接收缓冲区的值如下:

    leacock@leacock-virtual-machine:~$ cat /proc/sys/net/ipv4/tcp_wmem
    4096	16384	4194304
    leacock@leacock-virtual-machine:~$ cat /proc/sys/net/ipv4/tcp_rmem
    4096	131072	6291456
    leacock@leacock-virtual-machine:~$ 
    

    这三个值分别表示了最小分配值、默认分配值和最大分配值 。按照默认分配值计算,一万个连接需要的内存消耗为:

    发送缓冲区: 16384*10000 = 160M bytes
    接收缓冲区: 87380*10000 = 880M bytes
    

    可见支持 1 万个并发连接,当下内存并不是一个巨大的瓶颈。

    网络带宽

    假设 1 万个连接,每个连接每秒传输大约 1KB 的数据,那么带宽需要 10000 x 1KB/s x8 = 80Mbps。在当下千兆万兆网卡之下也是小菜一碟。

    在系统资源层面,C10K 问题是可以解决的。但是,能解决并不意味着可以很好地解决。在网络编程中,涉及到频繁的用户态 - 内核态数据拷贝,设计不够好的程序可能在低并发的情况下工作良好,一旦到了高并发情形,其性能可能呈现出指数级别的损失。

    两个层面考虑

    要想解决 C10K 问题,就需要从两个层面上来统筹考虑。

    • 第一个层面,应用程序如何和操作系统配合,感知 I/O 事件发生,并调度处理在上万个套接字上的 I/O 操作? 可参见IO模式与IO多路复用

    • 第二个层面,应用程序如何分配进程、线程资源来服务上万个连接?

    解决方案:

    两条思路方向

    主要思路有两个:

    • 一个是对于每个连接处理分配一个独立的进程/线程;

    • 另一个思路是用同一进程/线程来同时处理若干连接。

    几种解决方案

    • 阻塞 I/O + 进程

    • 阻塞 I/O + 线程

    • 非阻塞 I/O + readiness notification + 单线程

    • 非阻塞 I/O + readiness notification + 多线程

    • 异步 I/O+ 多线程

    阻塞 I/O + 进程

    最为简单直接最传统的方式,每个连接通过 fork 派生一个子进程进行处理,由于一个独立的子进程负责处理了该连接所有的 I/O,所以即便是阻塞 I/O,多个连接之间也不会互相影响。方法虽然简单,但是效率不高,扩展性差,资源占用率高。要处理好父子进程、僵尸进程等。

    父进程和子进程

    创建一个新的进程,使用函数 fork 就可以

    pid_t fork(void)
    返回:在子进程中为0,在父进程中为子进程ID,若出错则为-1
    

    在调用该函数的进程(即为父进程)中返回的是新派生的进程 ID 号,在子进程中返回的值为 0。通过返回值可以区分父子进程然后进行相应的处理。

    当一个子进程退出时,系统内核还保留了该进程的若干信息,比如退出状态。这样的进程如果不回收,就会变成僵尸进程。由父进程派生出来的子进程,也必须由父进程负责回收,否则子进程就会变成僵尸进程。

    有两种方式可以在子进程退出后回收资源,分别是调用 wait 和 waitpid 函数。

    pid_t wait(int *statloc);
    pid_t waitpid(pid_t pid, int *statloc, int options);
    

    wait 和 waitpid 函数参见 https://blog.csdn.net/csdn_kou/article/details/81091191

    函数 wait 和 waitpid 都可以返回两个值,一个是函数返回值,表示已终止子进程的进程 ID 号,另一个则是通过 statloc 指针返回子进程终止的实际状态。这个状态可能的值为正常终止、被信号杀死、作业控制停止等。

    处理子进程退出的方式一般是注册一个信号处理函数,捕捉信号 **SIGCHILD **信号,然后再在信号处理函数里调用 waitpid 函数来完成子进程资源的回收。SIGCHLD 是子进程退出或者中断时由内核向父进程发出的信号,默认这个信号是忽略的。

    阻塞 I/O + 进程 代码示例

    GitHub:BIOAndProgressDemo

    fork

    在这里插入图片描述

    服务端:

    
    #define MAX_LINE 4096
    #define SERV_PORT 5555
    
    char convert_char(char c) {
        if ( 'A' <= c && c <= 'Z')
            return c + 32; // 转换小写
        else if ( 'a' <= c && c <= 'z')
            return c - 32; // 转换大写
        else
            return c; // 其他不变
    }
    
    void child_run(int fd) {
    
        printf("child_run int fd = %d\n",fd);
    
        char outbuf[MAX_LINE + 1];
        size_t outbuf_used = 0;
        ssize_t result;
        char ch[128];
        while (1) {
            bzero(outbuf,MAX_LINE + 1);
            bzero(ch,128);
    
            result = recv(fd, &ch, 128, 0);
            if (result == 0) {
                // 这里表示对端的socket已正常关闭.
                break;
            } else if (result == -1) {
                perror("read");
                break;
            }
    
            u_long len = strlen(ch);
            outbuf_used = 0;
            for (int i = 0; i < len; ++i) {
                outbuf[outbuf_used++] = convert_char(ch[i]);
            }
            send(fd, outbuf, outbuf_used, 0);
    
        }
        printf("child_run out\n");
    }
    
    /**
     * 信号处理函数
     * @param sig
     */
    void sigchld_handler(int sig) {
        ///  pid =  -1 等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样
        /// WNOHANG 若由pid指定的子进程未发生状态改变(没有结束),则waitpid()不阻塞,立即返回0
        while (waitpid(-1, 0, WNOHANG) > 0);
        printf("sigchld_handler out\n");
    }
    
    /**
     * 创建服务端套 并 返回 监听套接字
     * @param port  监听端口
     * @return 监听套接字
     */
    int tcp_server_listen(int port) {
    
        int listenfd;
        /// 监听套接字
        listenfd = socket(AF_INET, SOCK_STREAM, 0);
    
        /// 填写 sockaddr_in
        struct sockaddr_in server_addr;
        bzero(&server_addr, sizeof(server_addr));
        server_addr.sin_family = AF_INET;
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        server_addr.sin_port = htons(port);
    
        int on = 1;
        /// 设置属性
        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    
        /// 绑定ip
        int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
        if (rt1 < 0) {
            error(1, errno, "bind failed ");
        }
    
        /// 监听 套接字
        int rt2 = listen(listenfd, 1024);
        if (rt2 < 0) {
            error(1, errno, "listen failed ");
        }
        /// 捕获SIGPIPE信号  参见 https://blog.csdn.net/xinguan1267/article/details/17357093
        signal(SIGPIPE, SIG_IGN);
    
        return listenfd;
    }
    int main(int c, char **v) {
    
        /// 创建服务端
        int listener_fd = tcp_server_listen(SERV_PORT);
    
        /// 捕获 SIGCHLD 信号, 设置信号处理函数  sigchld_handler
        signal(SIGCHLD, sigchld_handler);
        /// 循环 监听 有连接到来 fork 进程处理
        while (1) {
            struct sockaddr_storage ss;
            socklen_t slen = sizeof(ss);
            /// accept
            int fd = accept(listener_fd, (struct sockaddr *) &ss, &slen);
            if (fd < 0) { /// accept 失败
                error(1, errno, "accept failed");
                exit(1);
            }
    
            if (fork() == 0) { /// fork 子进程 并通过返回值 区分 子父进程
                /// 子进程
                close(listener_fd); /// 关闭从父进程复制来的 listener_fd
                child_run(fd); /// 运行子程序
                exit(0);
            } else {
                /// 父进程
                close(fd);
            }
        }
    
        return 0;
    }
    

    客户端:

    
    #define MAXLINE     4096
    #define SERV_PORT 5555
    
    int main() {
    
        int sockfd;
        struct sockaddr_in servaddr;
        // 创建了一个本地套接字
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0) {
            perror( "create socket failed");
        }
    
        // 初始化目标服务器端的地址, TCP 编程中,使用的是服务器的 IP 地址和端口作为目标
        bzero(&servaddr, sizeof(servaddr));
    
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(SERV_PORT);
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
    
        // 发起对目标套接字的 connect 调用
        if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) {
            perror("connect failed");
        }
    
        char send_line[MAXLINE];
        bzero(send_line, MAXLINE);
        char recv_line[MAXLINE];
        bzero(recv_line, MAXLINE);
        // 从标准输入中读取字符串,向服务器端发送
        while (1) {
            bzero(recv_line,MAXLINE); // 注意每次清空
    
            if (fgets(send_line, MAXLINE, stdin) == NULL)
                break;
    
            int nbytes = sizeof(send_line);
            if (send(sockfd, send_line, nbytes,0) != nbytes)
                perror("write error");
    
            bzero(recv_line, MAXLINE); // 注意每次清空
            if (recv(sockfd, recv_line, MAXLINE,0) == 0)
                perror("server terminated prematurely");
    
            fputs(recv_line, stdout);
    
        }
    
        exit(0);
    }
    
    

    测试:

    在这里插入图片描述

    阻塞 I/O + 线程

    使用进程模型来处理用户连接请求,进程切换上下文的代价是比较高的,有一种轻量级的模型可以处理多用户连接请求,就是线程模型。

    线程由操作系统内核管理。每个线程都有自己的上下文(context),包括一个可以唯一标识线程的 ID(thread ID,或者叫 tid)、栈、程序计数器、寄存器等。在同一个进程中,所有的线程共享该进程的整个虚拟地址空间,包括代码、数据、堆、共享库等。

    主要线程函数

    创建线程

    
    int pthread_create(pthread_t *tid, const pthread_attr_t *attr,
               void *(*func)(void *), void *arg);
    
    返回:若成功则为0,若出错则为正的Exxx值
    
    • 第一个参数为指向线程标识符的指针。创建线程成功,tid 就返回正确的线程 ID

    • 第二个参数用来设置线程属性。如优先级、是否为守护进程等,如无特殊设置,可以直接指定这个参数为 NUL。

    • 第三个参数是线程运行函数的起始地址

    • 最后一个参数是运行函数的参数。如果我们想给线程入口函数传多个值,那么需要把这些值包装成一个结构体

    在新线程的入口函数内,可以执行 pthread_self 函数返回线程 tid。

    pthread_t pthread_self(void)
    

    终止线程

    终止一个线程最直接的方法是在父线程内调用函数:

    void pthread_exit(void *status)
    

    调用这个函数之后,父线程会等待其他所有的子线程终止,之后父线程自己终止。

    但是绝大多数的子线程执行体都是一个无限循环。也可以通过调用 pthread_cancel 来主动终止一个子线程,和 pthread_exit 不同的是,它可以指定某个子线程终止。

    int pthread_cancel(pthread_t tid)
    

    回收已终止线程的资源

    pthread_join 回收已终止线程的资源

    
    int pthread_join(pthread_t tid, void ** thread_return)
    

    当调用 pthread_join 时,主线程会阻塞,直到对应 tid 的子线程自然终止。和 pthread_cancel 不同的是,它不会强迫子线程终止。

    分离线程

    一个线程的重要属性是可结合的,或者是分离的。一个可结合的线程是能够被其他线程杀死和回收资源的;而一个分离的线程不能被其他线程杀死或回收资源。一般来说,默认的属性是可结合的。

    pthread_detach 函数可以分离一个线程:

    int pthread_detach(pthread_t tid)
    

    阻塞 I/O + 线程 代码示例

    GitHub:BIOAndThreadDemo

    对上面服务端稍作修改,客户端不变

    pthread

    在这里插入图片描述

    服务端:

    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <strings.h>
    #include <error.h>
    #include <errno.h>
    #include <signal.h>
    #include  <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #include <pthread.h>
    
    #define MAX_LINE 4096
    #define SERV_PORT 5555
    
    char convert_char(char c) {
        if ( 'A' <= c && c <= 'Z')
            return c + 32; // 转换小写
        else if ( 'a' <= c && c <= 'z')
            return c - 32; // 转换大写
        else
            return c; // 其他不变
    }
    
    void thread_run(void *arg) {
    
        pthread_detach(pthread_self());
    
        int fd = (int)arg;
    
        printf("thread_run int fd = %d\n",fd);
    
    
        char outbuf[MAX_LINE + 1];
        size_t outbuf_used = 0;
        ssize_t result;
        char ch[128];
        while (1) {
            bzero(outbuf,MAX_LINE + 1);
            bzero(ch,128);
    
            result = recv(fd, &ch, 128, 0);
            if (result == 0) {
                // 这里表示对端的socket已正常关闭.
                break;
            } else if (result == -1) {
                perror("read");
                break;
            }
    
            u_long len = strlen(ch);
            outbuf_used = 0;
            for (int i = 0; i < len; ++i) {
                outbuf[outbuf_used++] = convert_char(ch[i]);
            }
            send(fd, outbuf, outbuf_used, 0);
    
        }
        printf("thread_run out\n");
    }
    
    
    /**
     * 创建服务端套 并 返回 监听套接字
     * @param port  监听端口
     * @return 监听套接字
     */
    int tcp_server_listen(int port) {
    
        int listenfd;
        /// 监听套接字
        listenfd = socket(AF_INET, SOCK_STREAM, 0);
    
        /// 填写 sockaddr_in
        struct sockaddr_in server_addr;
        bzero(&server_addr, sizeof(server_addr));
        server_addr.sin_family = AF_INET;
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        server_addr.sin_port = htons(port);
    
        int on = 1;
        /// 设置属性
        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    
        /// 绑定ip
        int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
        if (rt1 < 0) {
            error(1, errno, "bind failed ");
        }
    
        /// 监听 套接字
        int rt2 = listen(listenfd, 1024);
        if (rt2 < 0) {
            error(1, errno, "listen failed ");
        }
        /// 捕获SIGPIPE信号  参见 https://blog.csdn.net/xinguan1267/article/details/17357093
        signal(SIGPIPE, SIG_IGN);
    
        return listenfd;
    }
    int main(int c, char **v) {
        /// 创建服务端
        int listener_fd = tcp_server_listen(SERV_PORT);
    
        pthread_t tid;
        /// 循环 监听 有连接到来 fork 进程处理
        while (1) {
            struct sockaddr_storage ss;
            socklen_t slen = sizeof(ss);
            /// accept
            int fd = accept(listener_fd, (struct sockaddr *) &ss, &slen);
            if (fd < 0) { /// accept 失败
                error(1, errno, "accept failed");
    
            } else {
                pthread_create(&tid, NULL, &thread_run, (void *) fd);
            }
        }
    
        return 0;
    }
    

    测试:

    在这里插入图片描述

    非阻塞 I/O + readiness notification + 单线程(reactor)

    事件驱动模型

    事件驱动模型,也被叫做反应堆模型(reactor),或者是 Event loop 模型。这个模型的核心有两点。

    • 第一,它存在一个无限循环的事件分发线程,或者叫做 reactor 线程、Event loop 线程。这个事件分发线程的背后,就是 poll、epoll 等 I/O 分发技术的使用。
    • 第二,所有的 I/O 操作都可以抽象成事件,每个事件必须有回调函数来处理。通过事件分发,这些事件都可以一一被检测,并调用对应的回调函数加以处理。

    single reactor thread

    一个 reactor 线程上同时负责分发 acceptor 的事件、已连接套接字的 I/O 事件。

    在这里插入图片描述

    single reactor thread + worker threads

    进一步优化将耗时的操作分离出来,反应堆线程只负责处理 I/O 相关的工作,业务逻辑相关的工作都被裁剪成一个一个的小任务,放到线程池里由空闲的线程来执行。当结果完成后,再交给反应堆线程,由反应堆线程通过套接字将结果发送出去。

    在这里插入图片描述

    非阻塞 I/O + readiness notification + 单线程(reactor)代码示例

    参见:

    Reactor模式与单线程Reactor的C和C++实现

    非阻塞 I/O + readiness notification + 多线程(主 - 从 reactor)

    主 - 从 reactor 模式

    单 reactor 线程既分发连接建立,又分发已建立连接的 I/O;将 acceptor 上的连接建立事件和已建立连接的 I/O 事件分离,形成所谓的主 - 从 reactor 模式。

    主 - 从这个模式的核心思想是,主反应堆线程只负责分发 Acceptor 连接建立,已连接套接字上的 I/O 事件交给 sub-reactor 负责分发。其中 sub-reactor 的数量,可以根据 CPU 的核数来灵活设置。
    在这里插入图片描述

    主反应堆线程一直在感知连接建立的事件,如果有连接成功建立,主反应堆线程通过 accept 方法获取已连接套接字,接下来会按照一定的算法选取一个从反应堆线程,并把已连接套接字加入到选择好的从反应堆线程中。主反应堆线程唯一的工作,就是调用 accept 获取已连接套接字,以及将已连接套接字加入到从反应堆线程中。

    主 - 从 reactor+worker threads 模式

    主 - 从 reactor 模式解决了 I/O 分发的高效率问题,那么 work threads 就解决了业务逻辑和 I/O 分发之间的耦合问题。

    在这里插入图片描述

    主 - 从反应堆下加上 worker 线程池。

    主 - 从反应堆跟上面介绍的做法是一样的。和上面不一样的是,这里将 decode、compute、encode 等 CPU 密集型的工作从 I/O 线程中拿走,这些工作交给 worker 线程池来处理,而且这些工作拆分成了一个个子任务进行。encode 之后完成的结果再由 sub-reactor 的 I/O 线程发送出去。

    非阻塞 I/O + readiness notification + 多线程(主 - 从 reactor) 代码示例

    未实现

    异步 I/O+ 多线程

    待整理

    可以看下 AIO 的新归宿:io_uring

    展开全文
  • 著名的C10K并发连接问题

    万次阅读 多人点赞 2019-09-29 13:20:55
    C10K”概念最早由Dan Kegel发布于其个人站点,即出自其经典的《The C10K problem(英文PDF版、中文译文)》一文。 正如你所料,过去的10年里,高性能网络编程技术领域里经过众多开发者的努力,已很好地解决了C10K...

    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篇,总目录如下:

    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问题的解决方案探讨

    要解决这一问题,从纯网络编程技术角度看,主要思路有两个:

    • 一个是对于每个连接处理分配一个独立的进程/线程;
    • 另一个思路是用同一进程/线程来同时处理若干连接。

    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 结构体。

    1

    intselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *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:协议 (在线阅读版)

    10、更多资料

    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与MINA2、Netty4的跨平台UDP双向通信实战
    NIO框架入门(四):Android与MINA2、Netty4的跨平台UDP双向通信实战
    P2P技术详解(一):NAT详解——详细原理、P2P简介
    P2P技术详解(二):P2P中的NAT穿越(打洞)方案详解
    P2P技术详解(三):P2P技术之STUN、TURN、ICE详解
    高性能网络编程(一):单台服务器并发TCP连接数到底可以有多少
    高性能网络编程(二):上一个10年,著名的C10K并发连接问题
    高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了

    展开全文
  • c10k问题及其解决方案

    2021-02-26 14:35:09
    本文主要讲述高并发http应用中的c10k瓶颈问题:在很多服务器初始状态下,无法服务1w左右的...如果硬件配置没有问题,性能较高的服务器上,产生c10k问题,很多情况下与配置和软件栈相关:最大文件打开数、socket 端口...
  • 要解决C10K问题,传统的多线程模式和select模式都不再适用,应采用epoll,kqueue,dev_poll来捕获IO事件.zip
  • 在如此高负载量下,若服务端程序设计不够良好,网络服务在处理数以万计的客户端连接时、可能会出现效率低下甚至完全瘫痪的情况,即为C10K问题。那么,基于dubbo的分布式服务平台能否应对复杂的C10K场景?为此,我们...
  • C10K、C10M问题

    2020-10-23 09:30:22
    C10k(concurrently handling 10k connections)是一个在1999年被提出来的技术挑战,如何在一颗1GHz CPU,2G内存,1gbps网络环境下,让单台服务器同时为1万个客户端提供FTP服务。而到了2010年后,随着硬件技术的发展...
  • 在如此高负载量下,若服务端程序设计不够良好,网络服务在处理数以万计的客户端连接时、可能会出现效率低下甚至完全瘫痪的情况,即为 C10K 问题。那么,基于 Dubbo 的分布式服务平台能否应对复杂的 C10K 场景?为此...
  • C10K实验 对于我来说,这是一个学习项目,目的是使高性能服务器的工作水平比以前低。 特别是,我希望从该项目中受益的是: 套接字API的丰富知识 熟悉异步I / O /事件的成本和收益 Linux工具(包括strace和tcpdump)...
  • 接入量:服务器一段时间连接客户端的数量,一般情况下是1秒的连接数量QPS。也是服务器性能的重要指标 并发量:同时承载客户端的数量。承载:客户端一个请求能在200ms内返回正确的结果。 影响并发量的主要几个因素: ...
  • C10K 1999年由Dan Kegel ,那时的服务器运行着LINUX2.2系统,32位系统,内存很少(2G),千兆网卡。 怎么在这样的系统中支持并发 1 万的请求呢? 先来计算一下: 最大每个连接的内存:2GB / 10000 = 200kb 最大每个...
  • C10K 和 C1000K 回顾

    2021-08-03 16:04:29
    本文是通过学习倪朋飞老师的《Linux性能优化实战》 :C10K 和 C1000K 回顾 C10K 和 C1000K 回顾 注意,C10K 和 C1000K 的首字母 C 是 Client 的缩写。C10K 就是单机同时处理 1 万个请 求(并发连接 1 万)的问题,而 ...
  • C10K问题 这一篇,借着C10K 问题,系统地梳理一下高性能网络编程的方法论。 C10K 问题是这样的:如何在一台物理机上同时服务 10000 个用户?这里 C 表示并发,10K 等于 10000。得益于操作系统、编程语言的发展,在...
  • C10K 和 C1000K

    2021-03-10 09:48:14
    C10K 和 C1000K 的首字母 C 是 Client 的缩写。C10K 就是单机同时处理 1 万个请求(并发连接 1 万)的问题,而 C1000K 也就是单机支持处理 100 万个请求(并发连接 100 万)的问题。 C10K C10K 问题最早由 Dan Kegel...
  • BIO C10K问题

    2020-12-15 10:59:02
    BIO blocking io BIO 缺点: accept事件,会堵塞当前主线程 sockt 堵塞当前线程 C10K 1W个线程支持 CPU 上下文切换 堵塞IO 对每个socket 单独一个线程
  • C10K并发(英文)

    2014-11-11 11:30:24
    描述C10K并发问题的解决,英文版,供参考,理论性偏强
  • 【Linux 性能优化系列】Linux 性能优化 -- 网络优化(C10K、C1000K、C10M 问题总结) 【1】C10K 问题以及优化方法简介 C10K 表示单机同时处理 1 万个请求 (并发连接 1 万) 的问题; 主要待解决的问题 问题一,怎样...
  • 1、前言对于高性能即时通讯技术(或者说互联网编程)比较关注的开发者,对C10K问题(即单机1万个并发连接问题)应该都有所了解。“C10K”概念最早由Dan Kegel发布于其个人站点,即出自其经典的《The C10K problem ...
  • C10K问题

    2019-07-17 20:01:57
    C10K问题由来 随着互联网的普及,应用的用户群体几何倍增长,此时服务器性能问题就出现。最初的服务器是基于进程/线程模型。新到来一个TCP连接,就需要分配一个进程。假如有C10K,就需要创建1W个进程,可想而知单机...
  • 1 C10K问题 大家都知道互联网的基础就是网络通信,早期的互联网可以说是一个小群体的集合。互联网还不够普及,用户也不多。一台服务器同时在线100个用户估计在当时已经算是大型应用了。所以并不存在什么C10K的难题...
  • 何为C10k问题? 何为多路复用? select、poll与Epoll select与poll的主要思路: 但能不能减少遍历?能不能保存就绪的socket? Epoll就彻底解决了这些问题 Epoll 总结 前述: 何为C10k问题? 即:最初的...
  • 一,C10K问题的由来 大家都知道互联网的基础就是网络通信,早期的互联网可以说是一个小群体的集合。互联网还不够普及,用户也不多,一台服务器同时在线100个用户估计在当时已经算是大型应用了,所以并不存在什么 C10...
  • 今天,我们主要来回顾下经典的 C10K 和 C1000K 问题,以更好理解 Linux 网络的工作原理,并进一步分析,如何做到单机支持 C10M 注意,C10K 和 C1000K 的首字母 C 是 Client 的缩写。C10K 就是单机同时处理 1 万个...
  • C10K问题 在传统的同步阻塞处理模型中,当创建的进程或线程过多时,缓存I/O、内核将数据拷贝到用户进程空间、阻塞,进程/线程上下文切换消耗大,简而言之 ,C10K问题就是无法同时处理大量客户端(10,000)的网络套接字...
  • C10k简介

    2018-03-27 16:09:00
    虽然C10K问题已被妥善解决,但对于即时通讯应用(或其它网络编程方面)的开发者而言,研究C10K问题仍然价值巨大,因为技术的发展都是有规律和线索可循的,了解C10K问题及其解决思路,通过举一反三,或许可以为你以后...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 13,237
精华内容 5,294
关键字:

c10k