# 【NO.302】Linux 网络性能优化-C10K、C1000K、C10M 问题总结

## 1.C10K 问题以及优化方法简介

C10K 表示单机同时处理 1 万个请求 (并发连接 1 万) 的问题；

**【1.1】主要待解决的问题**

问题一，怎样在一个线程内处理多个请求，即要在一个线程内响应多个网络 I/O；

问题二，怎么更节省资源地处理客户请求，即要用更少的线程来服务这些请求；

**【1.2】事件 IO 通知的方式**

两种 I/O 事件通知的方式，水平触发和边缘触发；

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

**【1.3】优化方案**

【1.3.1】I/O 模型优化

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

根据水平触发的特性，select 和 poll 可以从文件描述符列表中，找出可以执行 I/O 操作的文件描述符，然后进行真正的网络 I/O 操作，由于 I/O 是非阻塞的，一个线程中就可以同时监控一批套接字的文件描述符，从而实现单线程处理多请求的目的；

**缺陷**

- 应用程序使用 select 和 poll 时，需要对这些文件描述符列表进行轮询，在请求数多的时候就会比较耗时；
- select 使用固定长度的位向量，表示文件描述符的集合，存在最大描述符数量的限制；并且，在 select 内部，检查套接字状态使用轮询的方法，处理耗时跟描述符数量是 O(N) 的关系；
- 应用程序每次调用 select 和 poll 时，需要把文件描述符的集合，从用户空间传入内核空间，由内核修改后，再传出到用户空间中，内核空间与用户空间切换增加了处理成本；

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

epoll 使用红黑树，在内核中管理文件描述符的集合，从而就不需要应用程序在每次操作时都传入、传出该集合；

epoll 使用事件驱动的机制，只关注有 I/O 事件发生的文件描述符，不需要轮询扫描整个集合；

**缺陷**

- 边缘触发只在文件描述符可读或可写事件发生时才通知，从而应用程序就需要尽可能多地执行 I/O 并要处理更多的异常事件；

使用异步 I/O (Asynchronous I/O，简称为 AIO)

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

**缺陷**

- 目前该方式不太完善，使用比较困难

**【1.3.2】工作模型优化**

\1. 主进程 + 多个 worker 子进程

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

![img](https://pic2.zhimg.com/80/v2-52c9d25906664f38ef89d67a17847149_720w.webp)

**惊群问题**

accept() 和 epoll_wait() 调用的惊群问题，即当网络 I/O 事件发生时，多个进程被同时唤醒，但实际上只有一个进程来响应这个事件，其他被唤醒的进程都会重新休眠；

解决

- accept() 的惊群问题，已经在 Linux 2.6 中解决；
- epoll 的问题，在 Linux 4.5 通过 EPOLLEXCLUSIVE 解决；
- Nginx 在每个 worker 进程中，都增加一个了全局锁(accept_mutex)，这些 worker 进程需要首先竞争到锁，只有竞争到锁的进程，才会加入到 epoll 中，这样就确保只有一个 worker 子进程被唤醒；

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

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

![img](https://pic1.zhimg.com/80/v2-cf57e4f185fe6f464a79e0eb9af04380_720w.webp)



## 2.C1000K 问题与优化

**物理资源**

100 万个请求需要大量的系统资源；

- 内存，假设每个请求需要 16KB 内存，则总共就需要大约 15 GB 内存；
- 带宽，假设只有 20% 活跃连接，即使每个连接只需要 1KB/s 的吞吐量，总共需要 1.6 Gb/s 的吞吐量；需要配置万兆网卡，或者基于多网卡 Bonding 承载更大的吞吐量；

**软件资源**

大量的连接会占用大量的软件资源；

- 比如文件描述符的数量、连接状态的跟踪(CONNTRACK)、网络协议栈的缓存大小(比如套接字读写缓存、TCP 读写缓存)等等；

**大量请求带来的中断处理**

- 需要多队列网卡、中断负载均衡、CPU 绑定、RPS/RFS (软中断负载均衡到多个 CPU 核上)，以及将网络包的处理卸载 (Offload) 到网络设备(如 TSO/GSO、LRO/GRO、VXLAN OFFLOAD) 等各种硬件和软件的优化；

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

## **3.C10M 问题与优化**

核心思想是跳过内核协议栈的冗长路径，把网络包直接送到要处理的应用程序中处理，这里有两种常见的机制，DPDK 和 XDP；

**DPDK**

- DPDK 是用户态网络的标准，跳过内核协议栈，直接由用户态进程通过轮询的方式处理网络接收；

![img](https://pic2.zhimg.com/80/v2-67721fb75c9e824a20f9d50ce1c3a551_720w.webp)

在 PPS 非常高的场景中，查询时间比实际工作时间少了很多，绝大部分时间都在处理网络包；而跳过内核协议栈后，就省去了繁杂的硬中断、软中断再到 Linux 网络协议栈逐层处理的过程，应用程序可以针对应用的实际场景，有针对性地优化网络包的处理逻辑，而不需要关注所有的细节；此外，DPDK 通过大页、CPU 绑定、内存对齐、流水线并发等多种机制，优化网络包的处理效率；

原文地址：https://zhuanlan.zhihu.com/p/537843366

作者：Linux