35-基础篇:C10K和C1000K回顾





C10K和C1000K的首字母C是Client的缩写
C10K就是单机同时处理1万个请求(并发连接1万)的问题
C1000K也就是单机支持处理100万个请求(并发连接100万)的问题




C10K

C10K问题最早由Dan Kegel在1999年提出
那时的服务器还只是32位系统,运行着Linux 2.2版本(后来又升级到了2.4和2.6,而2.6才支持x86_64)
只配置了很少的内存(2GB)和千兆网卡

怎么在这样的系统中支持并发1万的请求呢?

从资源上对2GB内存和千兆网卡的服务器来说
同时处理10000个请求,只要每个请求处理占用不到200KB(2GB/10000)的内存和100Kbit (1000Mbit/10000)的网络带宽就可以
所以,物理资源是足够的,接下来自然是软件的问题,特别是网络的I/O模型问题

I/O的模型,其实网络I/O模型也类似
在C10K以前,Linux中网络处理都用同步阻塞的方式,也就是每个请求都分配一个进程或者线程
请求数只有100个时,这种方式自然没问题
但增加到10000个请求时,10000个进程或线程的调度、上下文切换乃至它们占用的内存,都会成为瓶颈

既然每个请求分配一个线程的方式不合适
那么为了支持10000个并发请求,这里就有两个问题需要解决

  1. 怎样在一个线程内处理多个请求,也就是要在一个线程内响应多个网络I/O
    以前的同步阻塞方式下,一个线程只能处理一个请求,到这里不再适用
    是不是可以用非阻塞I/O或者异步I/O来处理多个网络请求呢?
  2. 怎么更节省资源地处理客户请求,也就是要用更少的线程来服务这些请求
    是不是可以继续用原来的100个或者更少的线程,来服务现在的10000个请求呢?


I/O模型优化

异步、非阻塞I/O的解决思路,其实就是在网络编程中经常用到的I/O多路复用(I/O Multiplexing)
I/O多路复用是什么意思呢?

详细了解前先来讲两种I/O事件通知的方式:水平触发和边缘触发,它们常用在套接字接口的文件描述符中

  1. 水平触发:只要文件描述符可以非阻塞地执行I/O ,就会触发通知
    也就是说,应用程序可以随时检查文件描述符的状态,然后再根据状态,进行I/O操作
  2. 边缘触发:只有在文件描述符的状态发生改变(也就是I/O请求达到)时,才发送一次通知
    这时候,应用程序需要尽可能多地执行I/O,直到无法继续读写,才可以停止
    如果I/O没执行完,或者因为某种原因没来得及处理,那么这次通知也就丢失了

接下来,再回过头来看I/O多路复用的方法,这里其实有很多实现方法

  1. 第一种,使用非阻塞I/O和水平触发通知,比如使用select或者poll

    根据刚才水平触发的原理,select和poll需要从文件描述符列表中,找出哪些可以执行I/O
    然后进行真正的网络I/O读写
    由于I/O是非阻塞的,一个线程中就可以同时监控一批套接字的文件描述符
    这样就达到了单线程处理多请求的目的
    所以,这种方式的最大优点,是对应用程序比较友好,它的API非常简单

    但是应用软件使用select和poll时,需要对这些文件描述符列表进行轮询
    这样请求数多的时候就会比较耗时
    并且select和poll还有一些其他的限制

    select使用固定长度的位相量,表示文件描述符的集合,因此会有最大描述符数量的限制
    比如在32位系统中,默认限制是1024,并且在select内部
    检查套接字状态是用轮询的方法,再加上应用软件使用时的轮询,就变成了一个O(n^2) 的关系

    poll改进了select的表示方法,换成了一个没有固定长度的数组
    这样就没有了最大描述符数量的限制(当然还会受到系统文件描述符限制)
    但应用程序在使用poll时,同样需要对文件描述符列表进行轮询
    这样处理耗时跟描述符数量就是 O(N) 的关系

    除此之外,应用程序每次调用select和poll时,还需要把文件描述符的集合
    从用户空间传入内核空间,由内核修改后,再传出到用户空间中
    这一来一回的内核空间与用户空间切换,也增加了处理成本

    有没有什么更好的方式来处理呢?答案自然是肯定的

  2. 第二种,使用非阻塞I/O和边缘触发通知,比如epoll

    既然select和poll有那么多的问题,就需要继续对其进行优化,而epoll就很好地解决 了这些问题

    1. epoll使用红黑树,在内核中管理文件描述符的集合
      这样就不需要应用程序在每次操作时都传入、传出这个集合
    2. epoll使用事件驱动的机制,只关注有I/O事件发生的文件描述符,不需要轮询扫描整个集合

    不过要注意,epoll是在 Linux 2.6 中才新增的功能(2.4 虽然也有,但功能不完善)
    由于边缘触发只在文件描述符可读或可写事件发生时才通知
    那么应用程序就需要尽可能多地执行I/O,并要处理更多的异常事件

  3. 第三种,使用异步I/O(Asynchronous I/O,简称为AIO)

    异步I/O允许应用程序同时发起很多I/O操作,而不用等待这些操作完成
    而在I/O完成后,系统会用事件通知(比如信号或者回调函数)的方式,告诉应用程序
    这时,应用程序才会去查询I/O操作的结果

    异步I/O也是到了Linux 2.6才支持的功能,并且在很长时间里都处于不完善的状态
    比如glibc提供的异步I/O库,就一直被社区诟病
    同时,由于异步I/O跟我们的直观逻辑不太一样,想要使用的话,一定要小心设计,其使用难度比较高



工作模型优化

了解I/O模型后,请求处理的优化就比较直观了
使用I/O多路复用后,就可以在一个进程或线程中处理多个请求
其中,又有下面两种不同的工作模型

  1. 第一种主进程+多个worker子进程,这也是最常用的一种模型

    1. 主进程执行bind()+listen()后,创建多个子进程
    2. 在每个子进程中,都通过accept()或epoll_wait() ,来处理相同的套接字

    比如最常用的反向代理服务器Nginx就是这么工作的
    它也是由主进程和多个worker进程组成
    主进程主要用来初始化套接字,并管理子进程的生命周期
    而worke 进程, 则负责实际的请求处理
    image-20211227144307451

    这里要注意,accept()和epoll_wait()调用,还存在一个惊群的问题
    换句话说,当网络I/O事件发生时,多个进程被同时唤醒
    但实际上只有一个进程来响应这个事件,其他被唤醒的进程都会重新休眠

    1. 其中,accept()的惊群问题,已经在Linux 2.6中解决了
    2. epoll的问题,到了Linux 4.5 ,才通过EPOLLEXCLUSIVE解决

    为了避免惊群问题, Nginx在每个worker进程中,都增加一个了全局锁 (accept_mutex)
    这些worker进程需要首先竞争到锁,只有竞争到锁的进程,才会加入到epoll中
    这样就确保只有一个worker子进程被唤醒

    根据前面CPU模块的学习,进程的管理、调度、上下文切换的成本非常高
    那为什么使用多进程模式的Nginx ,却具有非常好的性能呢?
    这里最主要的一个原因就是,这些worker进程,实际上并不需要经常创建和销毁
    而是在没任务时休眠,有任务时唤醒
    只有在worker由于某些异常退出时,主进程才需要创建新的进程来代替它

    当然也可以用线程代替进程
    主线程负责套接字初始化和子线程状态的管理
    子线程则负责实际的请求处理
    由于线程的调度和切换成本比较低,实际上可以进一步把epoll_wait()都放到主线程中
    保证每次事件都只唤醒主线程,而子线程只需要负责后续的请求处理

  2. 第二种,监听到相同端口的多进程模型
    在这种方式下,所有的进程都监听相同的接口, 并且开启SO_REUSEPORT选项
    由内核负责将请求负载均衡到这些监听进程中去
    image-20211227144722860

    由于内核确保了只有一个进程被唤醒,就不会出现惊群问题了
    比如Nginx在1.9.1中就已经支持了这种模式

    image-20211227144800595

    不过要注意,想要使用SO_REUSEPORT选项,需要用Linux 3.9以上的版本才可以




C1000K

基于I/O多路复用和请求处理的优化,C10K问题很容易就可以解决
不过,随着摩尔定律带来的服务器性能提升,以及互联网的普及
并不难想到新兴服务会对性能提出更高的要求

原来的C10K已经不能满足需求,所以又有了C100K和C1000K
也就是并发从原来的1万增加到10 、乃至100万
从1万到10万,其实还是基于C10K的这些理论,epoll配合线程池
再加上CPU、内存和网络接口的性能和容量提升
大部分情况 下,C100K很自然就可以达到

那么再进一步,C1000K是不是也可以很容易就实现呢?这其实没有那么简单了

首先从物理资源使用上来说,100万个请求需要大量的系统资源。比如

  1. 假设每个请求需要16KB内存的话,那么总共就需要大约15GB内存
  2. 从带宽上来说假设只有20%活跃连接,即使每个连接只需要1KB/s的吞吐量,总共也需要1.6Gb/s的吞吐量
    千兆网卡显然满足不了这么大的吞吐量
    所以还需要配置万兆网卡或者基于多网卡Bonding承载更大的吞吐量

其次,从软件资源上来说,大量的连接也会占用大量的软件资源
比如文件描述符的数量、连接状态的跟踪(CONNTRACK)
网络协议栈的缓存大小(比如套接字读写缓存、 TCP 读写缓存)等等

最后大量请求带来的中断处理,也会带来非常高的处理成本
这样,就需要多队列网卡、中断负载均衡、CPU绑定、RPS/RFS(软中断负载均衡到多个CPU核上)
以及将网络包的处理卸载(Offload)到网络设备(如TSO/GSO、LRO/GRO、VXLAN OFFLOAD)等
各种硬件和软件的优化

C1000K的解决方法,本质上还是构建在epoll的非阻塞I/O模型上
只不过,除了I/O模型之外,还需要从应用程序到Linux内核、再到CPU、内存和网络等各个层次的深度优化
特别是需要借助硬件,来卸载那些原来通过软件处理的大量功能




C10M

显然人们对于性能的要求是无止境的
再进一步,有没有可能在单机中,同时处理1000万的请求呢?这也就是C10M问题

实际上,在C1000K问题中,各种软件、硬件的优化很可能都已经做到头了
特别是当升级完硬件(比如足够多的内存、带宽足够大的网卡、更多的网络功能卸载等)后
可能会发现,无论怎么优化应用程序和内核中的各种网络参数,想实现1000万请求的并发,都是极其困难的

究其根本,还是Linux内核协议栈做了太多太繁重的工作
从网卡中断带来的硬中断处理程序开始,到软中断中的各层网络协议处理,最后再到应用程序
这个路径实在是太长 了,就会导致网络包的处理优化,到了一定程度后,就无法更进一步了

要解决这个问题,最重要就是跳过内核协议栈的冗长路径,把网络包直接送到要处理的应用程序那里去
这里有两种常见的机制DPDK和XDP

  1. DPDK是用户态网络的标准
    它跳过内核协议栈,直接由用户态进程通过轮询的方式,来处理网络接收
    image-20211227145455762

    说起轮询,会下意识认为它是低效的象征,它的低效主要体现在哪里呢?
    是查询时间明显多于实际工作时间的情况下吧!
    那么,换个角度来想,如果每时每刻都有新的网络包需要处理,轮询的优势就很明显了
    比如在PPS非常高的场景中,查询时间比实际工作时间少了很多,绝大部分时间都在处理网络包
    而跳过内核协议栈后,就省去了繁杂的硬中断、软中断再到Linux网络协议栈逐层处理的过程
    应用程序可以针对应用的实际场景,有针对性地优化网络包的处理逻辑,而不需要关注所有的细节

    此外DPDK还通过大页、CPU绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率

  2. XDP(eXpress Data Path),则是Linux内核提供的一种高性能网络数据路径
    它允许网络包,在进入内核协议栈之前,就进行处理,也可以带来更高的性能
    XDP底层跟之前用到的bcc-tools一样,都是基于Linux内核的eBPF机制实现的
    image-20211227145701121

    XDP对内核的要求比较高,需要的是Linux 4.8以上版本,并且它也不提供缓存队列
    基于XDP的应用程序通常是专用的网络应用,常见的有IDS(入侵检测系统)、DDoS防御、 cilium容器网络插件等




小结

C10K问题的根源,一方面在于系统有限的资源
另一方面,也是更重要的因素,是同步阻塞的I/O模型以及轮询的套接字接口,限制了网络事件的处理效率
Linux 2.6中引入的epoll ,完美解决了C10K的问题,现在的高性能网络方案都基于epoll

从C10K到C100K ,可能只需要增加系统的物理资源就可以满足
但从C100K到C1000K ,就不仅仅是增加物理资源就能解决的问题了
这时,就需要多方面的优化工作了
从硬件的中断处理和网络功能卸载、到网络协议栈的文件描述符数量、连接状态跟踪、
缓存队列等内核的优化、再到应用程序的工作模型优化,都是考虑的重点

再进一步要实现C10M ,就不只是增加物理资源,或者优化内核和应用程序可以解决的问题了
这时候,就需要用XDP的方式,在内核协议栈之前处理网络包
或者用DPDK直接跳过网络协议栈,在用户空间通过轮询的方式直接处理网络包

当然了,实际上,在大多数场景中并不需要单机并发1000万的请求
通过调整系统架构,把这些请求分发到多台服务器中来处理,通常是更简单和更容易扩展的方案


posted @ 2021-12-27 15:12  李成果  阅读(526)  评论(0编辑  收藏  举报