C10K问题,单机1万个并发连接问题。高并发的网络编程需要知道的一个概念,我了解到这个概念也比较晚。
记录一下,原文连接是:http://www.52im.net/thread-566-1-1.html。 我按照自己的理解只截取部分的内容。
单台服务器并发TCP连接数到底可以有多少
在linux下编写网络服务器程序的朋友肯定都知道每一个tcp连接都要占一个文件描述符,一旦这个文件描述符使用完了,新的连接到来返回给我们的错误是“Socket/File:Can't open so many files”。
这时你需要明白操作系统对可以打开的最大文件数的限制。
ulimit -n 输出 1024,说明对于一个进程而言最多只能打开1024个文件。 修改/etc/security/limits.conf改变最大句柄数的限制。
* soft nofile 204800
* hard nofile 204800
如何标识一个TCP连接:
系统用一个4四元组来唯一标识一个TCP连接:{local ip, local port,remote ip,remote port}。好吧,我们拿出《UNIX网络编程:卷一》第四章中对accept的讲解来看看概念性的东西,第二个参数cliaddr代表了客户端的ip地址和端口号。而我们作为服务端实际只使用了bind时这一个端口,说明端口号65535并不是并发量的限制。
server最大tcp连接数:
server通常固定在某个本地端口上监听,等待client的连接请求。不考虑地址重用(unix的SO_REUSEADDR选项)的情况下,即使server端有多个ip,本地监听端口也是独占的,因此server端tcp连接4元组中只有remote ip(也就是client ip)和remote port(客户端port)是可变的,因此最大tcp连接为客户端ip数×客户端port数,对IPV4,不考虑ip地址分类等因素,最大tcp连接数约为2的32次方(ip数)×2的16次方(port数),也就是server端单机最大tcp连接数约为2的48次方。
上一个10年,著名的C10K并发连接问题
最初的服务器都是基于进程/线程模型的,新到来一个TCP连接,就需要分配1个进程(或者线程)。而进程又是操作系统最昂贵的资源,一台机器无法创建很多进程。如果是C10K就要创建1万个进程,那么单机而言操作系统是无法承受的(往往出现效率低下甚至完全瘫痪)。如果是采用分布式系统,维持1亿用户在线需要10万台服务器,成本巨大,也只有Facebook、Google、雅虎等巨头才有财力购买如此多的服务器。
C10K问题本质上是操作系统的问题。对于Web1.0/2.0时代的操作系统而言, 传统的同步阻塞I/O模型都是一样的,处理的方式都是requests per second,并发10K和100的区别关键在于CPU。
创建的进程线程多了,数据拷贝频繁(缓存I/O、内核将数据拷贝到用户进程空间、阻塞), 进程/线程上下文切换消耗大, 导致操作系统崩溃,这就是C10K问题的本质!
可见,解决C10K问题的关键就是尽可能减少这些CPU等核心计算资源消耗,从而榨干单台服务器的性能,突破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 结构体。
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库的程序如下:
很多人会想当然的认为,要实现C10M(即单机千万)并发连接和处理能力,是不可能的。不过事实并非如此,现在系统已经在用你可能不熟悉甚至激进的方式支持千万级别的并发连接。
通过改进操作系统内核以及用事件驱动服务器(典型技术实现如:Nginx和Node)代替线程服务器(典型代表:Apache),这也是前一个10年解决C10K问题的普遍方法。
实现10M(即1千万)的并发连接挑战意味着什么:
- 1千万的并发连接数;
- 100万个连接/秒:每个连接以这个速率持续约10秒;
- 10GB/秒的连接:快速连接到互联网;
- 1千万个数据包/秒:据估计目前的服务器每秒处理50K数据包,以后会更多;
- 10微秒的延迟:可扩展服务器也许可以处理这个规模(但延迟可能会飙升);
- 10微秒的抖动:限制最大延迟;
- 并发10核技术:软件应支持更多核的服务器(通常情况下,软件能轻松扩展到四核,服务器可以扩展到更多核,因此需要重写软件,以支持更多核的服务器)。
为什么说实现C10M的挑战不在硬件而在软件?
1理由概述
硬件不是10M问题的性能瓶颈所在处,真正的问题出在软件上,尤其是*nux操作系统。理由如下面这几点:
首先:最初的设计是让Unix成为一个电话网络的控制系统,而不是成为一个服务器操作系统。对于控制系统而言,针对的主要目标是用户和任务,而并没有针对作为协助功能的数据处理做特别设计,也就是既没有所谓的快速路径、慢速路径,也没有各种数据服务处理的优先级差别。
其次:传统的CPU,因为只有一个核,操作系统代码以多线程或多任务的形式来提升整体性能。而现在,4核、8核、32核、64核和100核,都已经是真实存在的CPU芯片,如何提高多核的性能可扩展性,是一个必须面对的问题。比如让同一任务分割在多个核心上执行,以避免CPU的空闲浪费,当然,这里面要解决的技术点有任务分割、任务同步和异步等。
再次:核心缓存大小与内存速度是一个关键问题。现在,内存已经变得非常的便宜,随便一台普通的笔记本电脑,内存至少也就是4G以上,高端服务器的内存上24G那是相当的平常。但是,内存的访问速度仍然很慢,CPU访问一次内存需要约60~100纳秒,相比很久以前的内存访问速度,这基本没有增长多少。对于在一个带有1GHZ主频CPU的电脑硬件里,如果要实现10M性能,那么平均每一个包只有100纳秒,如果存在两次CPU访问内存,那么10M性能就达不到了。核心缓存,也就是CPU L1/L2/LL Cache,虽然访问速度会快些,但大小仍然不够,我之前接触到的高端至强,LLC容量大小貌似也就是12M。
2解决思路
解决这些问题的关键在于如何将功能逻辑做好恰当的划分,比如专门负责控制逻辑的控制面和专门负责数据逻辑的数据面。数据面专门负责数据的处理,属于资源消耗的主要因素,压力巨大,而相比如此,控制面只负责一些偶尔才有非业务逻辑,比如与外部用户的交互、信息的统计等等。我之前接触过几种网络数据处理框架,比如Intel的DPDK、6wind、windriver,它们都针对Linux系统做了特别的补充设计,增加了数据面、快速路径等等特性,其性能的提升自然是相当巨大。
看一下这些高性能框架的共同特点:
- 数据包直接传递到业务逻辑:
而不是经过Linux内核协议栈。这是很明显的事情,因为我们知道,Linux协议栈是复杂和繁琐的,数据包经过它无非会导致性能的巨大下降,并且会占用大量的内存资源,之前有同事测试过,Linux内核要吃掉2.5KB内存/socket。我研究过很长一段时间的DPDK源码,其提供的82576和82599网卡驱动就直接运行在应用层,将接管网卡收到的数据包直接传递到应用层的业务逻辑里进行处理,而无需经过Linux内核协议栈。当然,发往本服务器的非业务逻辑数据包还是要经过Linux内核协议栈的,比如用户的SSH远程登录操作连接等。 - 多线程的核间绑定:
一个具有8核心的设备,一般会有1个控制面线程和7个或8个数据面线程,每一个线程绑定到一个处理核心(其中可能会存在一个控制面线程和一个数据面线程都绑定到同一个处理核心的情况)。这样做的好处是最大化核心CACHE利用、实现无锁设计、避免进程切换消耗等等。 - 内存是另外一个核心要素:
常见的内存池设计必须在这里得以切实应用。有几个考虑点,首先,可以在Linux系统启动时把业务所需内存直接预留出来,脱离Linux内核的管理。其次,Linux一般采用4K每页,而我们可以采用更大内存分页,比如2M,这样能在一定程度上减少地址转换等的性能消耗。
关于Intel的DPDK框架介绍:
随着网络技术的不断创新和市场的发展,越来越多的网络设备基础架构开始向基于通用处理器平台的架构方向融合,期望用更低的成本和更短的产品开发周期来提供多样的网络单元和丰富的功能,如应用处理、控制处理、包处理、信号处理等。为了适应这一新的产业趋势, Intel推出了基于Intel x86架构DPDK (Data Plane Development Kit,数据平面开发套件) 实现了高效灵活的包处理解决方案。经过近6年的发展,DPDK已经发展成支持多种高性能网卡和多通用处理器平台的开源软件工具包。
有兴趣的技术同行,也许可以参考下DPDK的源代码,目前DPDK已经完全开源并且可以网络下载了。