高并发网络编程

www.52im.net/thread-561-… 笔记。

1. 并发限制因素

1.1 文件句柄限制

一个tcp连接都要占一个文件描述符,一旦这个文件描述符使用完了,新的连接到来返回给我们的错误是“Socket/File:Can't open so many files”

  • 进程限制

    执行 ulimit -n 输出 1024,说明对于一个进程而言最多只能打开1024个文件,所以你要采用此默认配置最多也就可以并发上千个TCP连接。临时修改:ulimit -n 1000000,但是这种临时修改只对当前登录用户目前的使用环境有效,系统重启或用户退出后就会失效。

    重启后失效的修改(不过我在CentOS 6.5下测试,重启后未发现失效),编辑 /etc/security/limits.conf 文件, 修改后内容为:

    soft nofile 1000000
    hard nofile 1000000
    复制代码

    永久修改:编辑/etc/rc.local,在其后添加如下内容:

    ulimit -SHn 1000000
    复制代码
  • 全局限制

    执行 cat /proc/sys/fs/file-nr输出 9344 0 592026,分别为:

    • 已经分配的文件句柄数,
    • 已经分配但没有使用的文件句柄数,
    • 最大文件句柄数。

    但在kernel 2.6版本中第二项的值总为0,这并不是一个错误,它实际上意味着已经分配的文件描述符无一浪费的都已经被使用了 。

    我们可以把这个数值改大些,用 root 权限修改 /etc/sysctl.conf 文件:

    fs.file-max = 1000000
    net.ipv4.ip_conntrack_max = 1000000
    net.ipv4.netfilter.ip_conntrack_max = 1000000
    复制代码

2. C10k问题

最初的服务器都是基于进程/线程模型的,新到来一个TCP连接,就需要分配1个进程(或者线程)。而进程又是操作系统最昂贵的资源,一台机器无法创建很多进程。如果是C10K就要创建1万个进程,那么单机而言操作系统是无法承受的(往往出现效率低下甚至完全瘫痪)。

2.1 C10K问题的本质

创建的进程线程多了,数据拷贝频繁(缓存I/O、内核将数据拷贝到用户进程空间、阻塞), 进程/线程上下文切换消耗大, 导致操作系统崩溃,这就是C10K问题的本质!

可见,解决C10K问题的关键就是尽可能减少这些CPU等核心计算资源消耗,从而榨干单台服务器的性能,突破C10K问题所描述的瓶颈。

2.2 C10K问题的解决方案探讨

思路:每个进程/线程同时处理多个连接(IO多路复用)

该思路的实现存在以下历程:

  • 方式1: 一个线程挨个处理多个连接,等一个socket处理完成之后,再去处理下一个socket
    • 问题:socket是阻塞的,没有数据处理的时候,会阻塞整个线程。(非阻塞socket暂时不涉及)
  • 方式2: select方案。select要解决上面阻塞的问题,思路很简单,如果我在读取文件句柄之前,先查下它的状态,ready 了就进行处理,不 ready 就不进行处理,这不就解决了这个问题了嘛?于是有了 select 方案。
    • 问题:句柄上限+重复初始化+逐个排查所有文件句柄状态效率不高。
  • 方式3: poll方案。poll 主要解决 select 的前两个问题:通过一个 pollfd 数组向内核传递需要关注的事件消除文件句柄上限,同时使用不同字段分别标注关注事件和发生事件,来避免重复初始化。
    • 问题:逐个排查所有文件句柄状态效率不高。
  • 方式4: epoll方案。既然逐个排查所有文件句柄状态效率不高,很自然的,如果调用返回的时候只给应用提供发生了状态变化(很可能是数据 ready)的文件句柄,进行排查的效率不就高多了么。所以epoll模型成为了C10K问题的终极解决方案。
    • 依赖特定平台(linux)
  • 方式5: libevent/libuv方案。将各个平台的IO多路复用封装。

3. C10M问题

截至目前,40gpbs、32-cores、256G RAM的X86服务器在Newegg网站上的报价是几千美元。实际上以这样的硬件配置来看,它完全可以处理1000万个以上的并发连接,如果它们不能,那是因为你选择了错误的软件,而不是底层硬件的问题。

可以预见在接下来的10年里,因为IPv6协议下每个服务器的潜在连接数都是数以百万级的,单机服务器处理数百万的并发连接(甚至千万)并非不可能,但我们需要重新审视目前主流OS针对网络编程这一块的具体技术实现。

3.1 解决思路

Unix的设计初衷并不是一般的服务器操作系统,而是电话网络的控制系统。由于是实际传送数据的电话网络,所以在控制层和数据层之间有明确的界限。问题是我们现在根本不应该使用Unix服务器作为数据层的一部分。

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

综上所述,解决C10M问题的关键主要是从下面几个方面入手:

**网卡问题:**通过内核工作效率不高 **解决方案:**使用自己的驱动程序并管理它们,使适配器远离操作系统。

**CPU问题:**使用传统的内核方法来协调你的应用程序是行不通的。 **解决方案:**Linux管理前两个CPU,你的应用程序管理其余的CPU,中断只发生在你允许的CPU上。

**内存问题:**内存需要特别关注,以求高效。 **解决方案:**在系统启动时就分配大部分内存给你管理的大内存页。

以Linux为例,解决的思路就是将控制层交给Linux,应用程序管理数据。应用程序与内核之间没有交互、没有线程调度、没有系统调用、没有中断,什么都没有。

4. 从C10K到C10M高性能网络应用的理论探索

4.1 CPU亲和性 & 内存局域性

无论是多进程模型还是多线程模型,都要把所有的调度任务交给操作系统,让操作系统帮我们分配硬件资源。我们常用的服务器操作系统都属于分时操作系统,调度模型都尽可能的追求公平,并没有为某一类任务做特别的优化,如果当前系统仅仅运行某一特定任务的时候,默认的调度策略可能会导致一定程度上的性能损失。我运行一个A任务,第一个调度周期在0号核心上运行,第二个调度周期可能就跑到1号核心上去了,这样频繁的调度可能会造成大量的上下文切换,从而影响到一定的性能。

数据局域性是同样类似的问题。当前x86服务器以NUMA架构为主,这种平台架构下,每个CPU有属于自己的内存,如果当前CPU需要的数据需要到另外一颗CPU管理的内存获取,必然增加一些延时。所以我们尽可能的尝试让我们的任务和数据在始终在相同的CPU核心和相同的内存节点上,Linux提供了sched_set_affinity函数,我们可以在代码中,将我们的任务绑定在指定的CPU核心上。一些Linux发行版也在用户态中提供了numactltaskset工具,通过它们也很容易让我们的程序运行在指定的节点上。

4.2 RSS、RPS、RFS、XPS

这些技术都是近些年来为了优化Linux网络方面的性能而添加的特性,RPS、RFS、XPS都是Google贡献给社区,RSS需要硬件的支持,目前主流的网卡都已支持,即俗称的多队列网卡,充分利用多个CPU核心,让数据处理的压力分布到多个CPU核心上去。

RPS和RFS在linux2.6.35的版本被加入,一般是成对使用的,在不支持RSS特性的网卡上,用软件来模拟类似的功能,并且将相同的数据流绑定到指定的核心上,尽可能提升网络方面处理的性能。XPS特性在linux2.6.38的版本中被加入,主要针对多队列网卡在发送数据时的优化,当你发送数据包时,可以根据CPU MAP来选择对应的网卡队列,低于指定的kernel版本可能无法使用相关的特性,但是发行版已经backport这些特性。

4.3 IRQ 优化

关于IRQ的优化,这里主要有两点,第一点是关于中断合并。在比较早期的时候,网卡每收到一个数据包就会触发一个中断,如果小包的数据量特别大的时候,中断被触发的数量也变的十分可怕。大部分的计算资源都被用于处理中断,导致性能下降。后来引入了NAPI和Newernewer NAPI特性,在系统较为繁忙的时候,一次中断触发后,接下来用轮循的方式读取后续的数据包,以降低中断产生的数量,进而也提升了处理的效率。第二点是IRQ亲和性,和我们前面提到了CPU亲和性较为类似,是将不同的网卡队列中断处理绑定到指定的CPU核心上去,适用于拥有RSS特性的网卡。

这里再说说关于网络卸载的优化,目前主要有TSO、GSO、LRO、GRO这几个特性,先说说TSO,以太网MTU一般为1500,减掉TCP/IP的包头,TCP的MaxSegment Size为1460,通常情况下协议栈会对超过1460的TCP Payload进行分段,保证最后生成的IP包不超过MTU的大小,对于支持TSO/GSO的网卡来说,协议栈就不再需要这样了,可以将更大的TCPPayload发送给网卡驱动,然后由网卡进行封包操作。通过这个手段,将需要在CPU上的计算offload到网卡上,进一步提升整体的性能。GSO为TSO的升级版,不在局限于TCP协议。LRO和TSO的工作路径正好相反,在频繁收到小包时,每次一个小包都要向协议栈传递,对多个TCPPayload包进行合并,然后再传递给协议栈,以此来提升协议栈处理的效率。GRO为LRO的升级版本,解决了LRO存在的一些问题。这些特性都是在一定的场景下才可以发挥其性能效率,在不明确自己的需求的时候,开启这些特性反而可能造成性能下降。

4.4 Kernel 优化

关于Kernel的网络相关优化我们就不过多的介绍了,主要的内核网络参数的调整在以下两处:net.ipv4.*参数和net.core.*参数。

主要用于调节一些超时控制及缓存等,通过搜索引擎我们能很容易找到关于这些参数调优的文章,但是修改这些参数是否能带来性能的提升,或者会有什么弊端,建议详细的阅读kernel文档,并且多做一些测试来验证。

猜你喜欢

转载自juejin.im/post/5c9de22251882567cf047a51