zoukankan      html  css  js  c++  java
  • 网络性能篇:C10K 和 C1000K 回顾

    问题

    1. C10K 就是单机同时处理 1 万个请求(并发连接 1 万)的问题

    2. C1000K 也就是单机支持处理 100 万个请求(并发连接 100 万)的问题

    C10K I/O 模型

    两种 I/O 事件通知的方式:

    水平触发边缘触发

    (1) 水平触发:只要文件描述符可以非阻塞地执行 I/O ,就会触发通知。

                          应用程序持续检查文件描述符的状态,根据状态进行 I/O 操作。

    (2) 边缘触发:只有在文件描述符的状态发生改变(也就是 I/O 请求达到)时,才发送一次通知。

                    应用程序需要尽可能多地执行 I/O,直到无法继续读写,才可以停止。如果 I/O 没执行完,那么这次通知也就丢失了。

    I/O 多路复用的方法1

     使用非阻塞 I/O 和水平触发通知,比如使用 select 或者 poll。

    (1) select 使用固定长度的位相量,会有最大描述符数量的限制,默认限制是 1024。

         检查套接字状态是用轮询,再加上应用软件使用时的轮询,复杂度是O(n平方)

    (2) poll 换成了一个没有固定长度的数组,没有最大描述符数量的限制。

         同样需要对文件描述符列表进行轮询,复杂度是O(n)

    (3) 每次调用 select 和 poll 时,还需要把文件描述符的集合,从用户空间传入内核空间,由内核修改后,再传出到用户空间。

           这一来一回的切换,也增加了处理成本。

    I/O 多路复用的方法2:   

    使用非阻塞 I/O 和边缘触发通知,比如epoll。

    (1) epoll 使用红黑树,在内核中管理文件描述符的集合,不需要应用程序在每次操作时传入、传出这个集合.

    (2) epoll 使用事件驱动的机制,只关注有 I/O 事件发生的文件描述符,不需要轮询扫描整个集合。复杂度是O(1)

    I/O 多路复用的方法3:   

    使用异步 I/O, 比如glibc 提供的异步 I/O 库

    (1) 异步 I/O 允许应用程序同时发起很多 I/O 操作

    (2) I/O 完成后,系统会用事件通知的方式,告诉应用程序。应用程序才会去查询 I/O 操作的结果。

     I/O 多路复用下的工作模型

    主进程 + 多个 worker 子进程

    (1) 主进程执行 bind() + listen() 后,创建多个子进程;

    (2) 每个子进程都 accept() 或epoll_wait() ,来处理相同的套接字。

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

    其中,accept() 的惊群问题,已经在 Linux 2.6 中解决了;

    而 epoll 的问题,到了 Linux 4.5 ,才通过 EPOLLEXCLUSIVE 解决。

    解决方案:

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

    监听到相同端口的多进程模型

    在这种方式下,所有的进程都监听相同的接口,并且开启 SO_REUSEPORT 选项,由内核负责将请求负载均衡到这些监听进程中去

     由于内核确保了只有一个进程被唤醒,就不会出现惊群问题了

    C1000K

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

    C10M

    原因:

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

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

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

    第一种机制,DPDK

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

     第二种机制,XDP(eXpress Data Path)

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

     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 万的请求。通过调整系统架构,把这些请求分发到多台服务器中来处理,通常是更简单和更容易扩展的方案

    select/poll是LT模式,epoll缺省使用的也是水平触发模式(LT)。
    目前业界对于ET的最佳实践大概就是Nginx了,单线程redis也是使用的LT
    说下我对水平触发(LT)和边缘触发(ET)我的理解。
    LT:文件描述符准备就绪时(FD关联的读缓冲区不为空,可读。写缓冲区还没满,可写),触发通知。
    也就是你文中表述的"只要文件描述符可以非阻塞地执行 I/O ,就会触发通知..."
    ET:当FD关联的缓冲区发生变化时(例如:读缓冲区由空变为非空,有新数据达到,可读。写缓冲区满变有空间了,有数据被发送走,可写),触发通知,仅此一次
    也就是你文中表述的"只有在文件描述符的状态发生改变(也就是 I/O 请求达到)时"

    第二种,监听到相同端口的多进程模型
  • 相关阅读:
    Linux共享对象之编译参数fPIC
    如果重新设计网络,有没有可能合并IP地址跟MAC地址?
    HTTP的长连接和短连接
    Icon资源详解[2]
    Icon资源详解[1]
    LZMA demo挑选使用备忘
    SSM 拦截器验证权限和登录与注销的实现
    Intellij idea workflow 工作流插件安装
    C# HttpWebRequest post 请求传参数
    PLSQL 11注册码
  • 原文地址:https://www.cnblogs.com/mysky007/p/12318956.html
Copyright © 2011-2022 走看看