文章目录
推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接。
业务拆解
在上一篇 文章 中,我们介绍了利用 EPOLL 事件通知机制所搭建的事务驱动 reactor 服务器及其代码实现。我们将要在此基础上,用同一套代码实现事务驱动 reactor 服务器的百万并发。所谓的百万并发,就是一个服务器利用其所附着的现有设备的内存资源和计算资源实现一百万个客户端连接,并且能正常完成网络 I/O 事务。
这个测试是很有意义的,因为现如今生意火热的 APP 和 网页的用户量都是破百万的。
本次测试我们要超越代码本身,我们把目光放在我们所操作的这台主机上,从设备的角度上去审视我们所写的代码,通过调节设备参数,使之适应本代码的运行,让本服务器代码发挥其百万并发的潜力。
具体而言,我们需要再新开三个 Linux 虚拟机(这三个虚拟机 4 G 内存,两核的 CPU, 30 G 的硬盘),然后这三个 Linux 虚拟机各自运行一段服务器压力测试代码(其实就是不断地发送对本机服务器的网络请求,建立约 30 多万个连接,3台虚拟机合起来就是差不多 1 百多万了)。我们以下面的 UML 图表示。
实现服务器百万并发的核心参数
回顾我之前所写的有关服务器的文章,我们必须明确一点,当我们在做网络 I/O 时,其实是先预设了完成一件事:服务器的监听套接字监听到了来访客户端的 (IP, PORT),并且联同监听套接字所占用的端口与本机 IP 一起记录下来,形成五元组
(协议类型, 服务器IP, 服务器监听端口, 客户端IP, 客户端端口)
并借此分配一个套接字专门处理该连接的 I/O 操作,而套接字本身就意味着一个 I/O 文件。于是,我们可以得出一个结论了,网络连接的关键参数是操作系统能同时打开多少个文件(服务器和客户端有多少个文件描述符可同时存在)与可用的端口数。也就是说网络连接关键是客户端和服务器在各自的设备内存上形成一下这个配对。
| 文件描述符 | 连接信息 |
|---|---|
| 套接字 | (协议类型, 服务器IP, 服务器监听端口, 客户端IP, 客户端端口) |
我们可以在一段 accept 函数代码中窥探一二,代码中的 fd 就是服务器的监听套接字,它监听客户端的 IP 和 端口。
int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);
// accept 会因为无输入而阻塞(因为 sockfd 是默认的阻塞模式),本设计就是防止其阻塞(利用条件判断绕开阻塞)
if (clientfd < 0) {
printf("accept errno: %d --> %s\n", errno, strerror(errno));
return -1;
}
我们可以这么说,我们主要关注主机设备两个参数,并处理好他们之间的关系。
- 文件描述符上限
- 端口数
虽说本篇文章只关注上面两个参数的处理,但还是需要注意主机设备的内存和CPU 资源,毕竟打开文件与运行程序都是要消耗内存和CPU的,故而还需考虑
- 内存上限
- CPU 占用率
操作系统的文件描述符上限
文件描述符是操作系统为每个进程维护的非负整数索引,用于唯一标识打开的文件或I/O资源,本质是进程级资源访问句柄,指向内核管理的文件表项。

其中,
- 进程文件描述符表
- 每个进程独立维护的数组
- 索引=文件描述符值
- 存储指向系统级文件表项的指针
- 系统级打开文件表
- 全局共享的结构
- 包含文件状态标志(读/写/追加等)
- 存储当前文件偏移量
- 维护引用计数
- Inode表
- 文件系统级元数据结构
- 存储文件属性:权限、大小、时间戳
- 包含指向实际数据块的指针
那么,有多少个文件描述符就会有多少个在内存中被打开的文件。网络 I/O 套接字是文件描述符的一种的话,一百多万个连接就意味着一百多万个文件在内存中被打开,这意味着内存资源的耗费。而我们也必须要确保操作系统允许我们打开那么多的文件。
操作系统的端口总数
端口(Port)是网络通信中的逻辑端点,用于区分同一主机上的不同网络服务。按端口号范围分类(IANA标准),这是最权威的分类方式,由互联网号码分配机构(IANA)定义:
| 类型 | 范围 | 说明 | 示例 |
|---|---|---|---|
| 知名端口 | 0 - 1023 | 分配给系统级服务,需管理员权限绑定 | HTTP(80), HTTPS(443) |
| 注册端口 | 1024 - 49151 | 分配给用户级应用程序,需在IANA注册 | MySQL(3306), Redis(6379) |
| 动态/私有端口 | 49152 - 65535 客户端临时使用的端口 | (操作系统自动分配) | 浏览器访问网站时的临时端口 |
也就是说一个操作系统是有大约 6 万多个端口 PORT。
在计算机网络中,唯一标识一个网络连接需要包含以下关键要素,这些要素共同构成一个"连接五元组":
- 核心五元组(TCP/UDP)这是唯一标识网络连接的最小信息集:
(协议类型, 源IP, 源端口, 目标IP, 目标端口)
| 要素 | 说明 | 示例 |
|---|---|---|
| 协议类型 | 传输层协议(TCP/UDP等) | TCP |
| 源IP地址 | 发起连接的主机IP | 192.168.1.100 |
| 源端口 | 发起连接的主机端口 | 54321 |
| 目标IP地址 | 接受连接的主机IP | 10.0.0.50 |
| 目标端口 | 接受连接的服务端口 | 80 (HTTP) |
结合上面那句话。两台计算机的端对端的连接数,如果各自的 IP 唯一以及通信协议唯一,但所提供的端口数不唯一,假设分别是 A 和 B,那么这两台计算所能建立的连接数将会是 A × B 个。
综合起来就是,想要在 4 台计算机之间测试本服务器代码的百万并发连接潜力,得确定 4 台计算机所提供的端口数足够多(使之连接足够多),另外还要确保服务器主机能够提供一百多万个的文件描述符(可同时打开的文件数)以承受来自一百多万个连接的负担。
源代码的百万并发的潜力
我们现在分析一下,我们所写的事务驱动 reactor 服务器的代码(原文链接 在此)的哪一个设计是专门适配百万并发的。
服务器占用多个端口,设置服务器的监听套接字。此处,我们占用了 20 个端口,来建立服务器的监听套接字。
#define MAX_PORTS 20 // 该服务器占用本地 20 个端口,用以建立网络 I/O ,更好地实现百万并发
—————————— 主函数处 ——————————————————
unsigned short port = 2000; // 端口 port
int listen_fds[MAX_PORTS] = {0}; // 存储所有监听socket
int i = 0;
for (i = 0; i < MAX_PORTS; i++) {
int sockfd = init_server(port + i);
listen_fds[i] = sockfd;
conn_list[sockfd].fd = sockfd; // 使用fd作为索引
conn_list[sockfd].r_action.accept_callback = accept_cb;
set_event(sockfd, EPOLLIN, 1);
}
这样做是有原因的,根据前面的描述,本服务器代码所对应的进程能够 20 个 端口,而我们的每个客户端代码则会提供约 6 万 多个端口,我们使用三个 Linux 虚拟机作为客户端,1 个 Linux 虚拟机作为服务器,那么服务器所能提供的连接数将会是:
6
万
×
20
×
3
=
360
万
6万 × 20 × 3 = 360 万
6万×20×3=360万
整整 360 万个连接呀!够了够了,我们之后秩序要把文件描述符的上限设置成 1 百多万就 OK 了。
准备工作
reactor.c 代码上把主函数上的 recv_cb 函数之后的打印语句注释掉,因为打印数据是要占用 CPU 和 内存的。
if (conn_list[connfd].r_action.recv_callback(connfd) >= 0) { // 返回0表示成功
printf("[%ld] RECV: %s\n", conn_list[connfd].rlength, conn_list[connfd].rbuffer);
}
紧接着,把 send_cb 函数里的打印语句注释掉。
if (conn_list[fd].wlength != 0) {
count = send_all(fd, conn_list[fd].wbuffer, conn_list[fd].wlength, 0);
}
printf("SEND: %zd\n", count);
打印函数运行时会占用 CPU,因为任何程序的执行都需要 CPU 来执行指令。打印函数的 CPU 占用量取决于输出内容的复杂度、系统调用的开销以及 I/O 设备的速度。虽然打印函数的 CPU 占用通常不会很高,但在处理大量数据或频繁调用时,可能会对系统性能产生一定影响。
服务器主机上的准备工作(调试参数)
服务器主机的配置,我在测试百万并发时候所用的内存,大概会用到 6.3 G 的内存空间左右

查看文件描述符个数的上限是多少。
qiming@qiming:~/share$ ulimit -a
real-time non-blocking time (microseconds, -R) unlimited
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 15051
max locked memory (kbytes, -l) 496096
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 15051
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
我们注意到
open files (-n) 1024
这也就是说,服务器主机所支持我们的打开的文件数(文件描述符个数)只有 1024 个。但还是可以处理的。在 Linux 命令行输入以下命令,
qiming@qiming:~/share$ ulimit -n 1048576
查看发现修改了文件描述符的上限
qiming@qiming:~/share$ ulimit -a
real-time non-blocking time (microseconds, -R) unlimited
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 15051
max locked memory (kbytes, -l) 496096
max memory size (kbytes, -m) unlimited
open files (-n) 1048576
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 15051
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
我们需要注意的是 ulimit 命令用于查看和设置用户进程的资源限制,但这些设置在系统重启后不会自动保存。ulimit 设置的资源限制仅对当前会话有效,当会话结束或系统重启时,这些设置会恢复到默认值。也就是说,重启计算机后,要是还想玩百万连接,就必须再重新使用 ulimit 命令设置参数。
当然,永久性的更改文件描述符上限也是可以的,我们配置文件 /etc/security/limits.conf
qiming@qiming:~/share/CTASK/TCP_test$ sudo vim /etc/security/limits.conf
[sudo] password for qiming:
修改配置,而后退出(这也是文件描述符的上限,不过这个改变是永恒的,而非暂时的)。
* soft nofile 1048576
* hard nofile 1048576
针对网络连接(他们是消耗计算机资源的),接下来配置文件 /etc/sysctl.conf
sudo vim /etc/sysctl.conf
修改配置
# /etc/sysctl.conf
# 基础网络优化
net.ipv4.tcp_fin_timeout = 30
net.ipv4.ip_local_port_range = 1024 65535
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 262144
# 内存管理
net.ipv4.tcp_mem = 524288 1048576 1572864
net.ipv4.tcp_wmem = 2048 2048 4096
net.ipv4.tcp_rmem = 2048 2048 4096
# 连接跟踪
net.nf_conntrack_max = 2000000
net.netfilter.nf_conntrack_tcp_timeout_established = 1200
# 文件系统
fs.file-max = 3000000
fs.nr_open = 3000000
# TIME-WAIT优化
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 0 # 在NAT环境中建议关闭
刷新 /etc/sysctl.conf 的配置
sudo sysctl -p
这些参数也是各有作用的
-
net.ipv4.tcp_fin_timeout = 30
作用:控制 TCP 连接处于 FIN-WAIT-2 状态的时间(秒)
优化效果:减少端口占用时间,加速资源释放,防止大量连接处于半关闭状态消耗资源
默认值:60秒 -
net.ipv4.ip_local_port_range = 1024 65535 ⭐
作用:定义本地出站连接可用的端口范围
百万并发关键价值:可用端口数 = 65535 - 1024 = 64511,防止客户端连接时出现 EADDRNOTAVAIL 错误
默认值:32768-60999(约28,232端口) -
net.core.somaxconn = 65535 ⭐
作用:设置每个监听 socket 的 全连接队列(accept queue) 最大长度
加速网络 I/O 建立:当新连接完成三次握手但尚未被 accept 时,存储在此队列,增大队列防止高并发时丢弃连接
默认值:128(严重不足!) -
net.ipv4.tcp_max_syn_backlog = 262144 ⭐
作用:设置每个监听 socket 的 半连接队列(SYN queue) 最大长度
加速网络 I/O 建立:存储已收到 SYN 但未完成三次握手的连接,防止 SYN Flood 攻击导致的连接丢弃
默认值:128(百万并发必须增大!) -
net.ipv4.tcp_mem = 524288 1048576 1572864
作用:TCP 栈全局内存控制(单位:页,通常4KB/页)
三个值含义:
524288:低水位 - 开始回收内存
1048576:压力水位 - 积极回收内存
1572864:高水位 - 拒绝新分配 -
net.ipv4.tcp_wmem = 2048 2048 4096
作用:单个 TCP 连接的 写缓冲区 大小(字节)
三个值含义:
2048:最小值
2048:默认值
4096:最大值 -
net.ipv4.tcp_rmem = 2048 2048 4096
作用:单个 TCP 连接的 读缓冲区 大小(字节)
百万并发优化:小缓冲区减少内存占用,适合高并发小数据包场景 -
net.nf_conntrack_max = 2000000
作用:设置连接跟踪表的最大条目数
百万并发核心参数:每个连接消耗约300字节内存,200万连接 ≈ 600MB 内存
低于此值会导致 nf_conntrack: table full 错误 -
net.netfilter.nf_conntrack_tcp_timeout_established = 1200
作用:已建立 TCP 连接的超时时间(秒)
优化效果:默认432000秒(5天)过长,缩短为20分钟加速连接回收 -
fs.file-max = 3000000
作用:系统全局最大文件描述符数
百万并发必须:每个 TCP 连接占用1个文件描述符,需配合 ulimit 用户级限制 -
fs.nr_open = 3000000
作用:单个进程可打开的最大文件描述符数
重要性:
必须大于等于 file-max
防止单进程达到上限 -
net.ipv4.tcp_tw_reuse = 1
作用:允许重用处于 TIME-WAIT 状态的端口
加速网络 I/O 建立:解决端口耗尽问题,减少 bind() 调用失败概率 -
net.ipv4.tcp_tw_recycle = 0
作用:快速回收 TIME-WAIT 连接(已废弃)
设为0的原因:在 NAT 环境中会导致连接失败
Linux 4.12+ 内核已移除此参数
加载 nf_conntrack 模块
sudo modprobe nf_conntrack
这是 Linux 系统中用于动态加载内核模块的命令,专门用于激活网络连接跟踪功能。对于百万并发服务器来说,这是实现高性能网络连接管理的关键一步。
-
实现网络连接状态跟踪
跟踪对象:TCP/UDP/ICMP 等协议的网络连接
记录信息:
源 / 目的 I P 和端口 源/目的 IP 和端口 源/目的IP和端口
协议类型 协议类型 协议类型
连接状态( N E W , E S T A B L I S H E D , R E L A T E D ) 连接状态(NEW, ESTABLISHED, RELATED) 连接状态(NEW,ESTABLISHED,RELATED)
超时计时器 超时计时器 超时计时器 -
支持关键网络功能
| 功能 | 依赖连接跟踪 | 百万并发重要性 |
|---|---|---|
| 状态防火墙 | ✅ 必需 | ⭐⭐⭐⭐⭐ |
| NAT(网络地址转换) | ✅ 必需 | ⭐⭐⭐⭐⭐ |
| 负载均衡 | ✅ 必需 | ⭐⭐⭐⭐ |
| QoS 流量控制 | ✅ 必需 | ⭐⭐⭐ |
客户端上的准备工作
客户端主机的配置

准备多台 Linux 虚拟机用作客户端,并且调试参数
与服务器的操作一样。
客户端上准备压力测试代码
这段代码是围绕服务器主机所占用的 20 个端口,与客户端主机的 6 万多个端口进行配对连接,发送请求,分配套接字。
#define MAX_BUFFER 128
#define MAX_EPOLLSIZE (384*1024) // 支持最多 384K 个epoll事件
#define MAX_PORT 20 // 服务器开了 20 个端口用以监听
#define TIME_SUB_MS(tv1, tv2) ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000) // 计时宏操作
int isContinue = 0; // 全局变量
// 这是用于获取并设置套接字的状态————非阻塞设置
static int ntySetNonblock(int fd) {
int flags;
flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) return flags;
flags |= O_NONBLOCK; // 按位或运算
if (fcntl(fd, F_SETFL, flags) < 0) return -1;
return 0;
}
// 设置端口复用,可以不用担心
static int ntySetReUseAddr(int fd) {
int reuse = 1;
return setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
}
int main(int argc, char **argv) {
if (argc <= 2) {
printf("Usage: %s ip port\n", argv[0]);
exit(0);
}
const char *ip = argv[1];
int port = atoi(argv[2]); // 转化端口文本为端口数字
int connections = 0; // 对连接进行计数
char buffer[128] = {0}; // 缓冲区
int i = 0, index = 0; // 指标
struct epoll_event events[MAX_EPOLLSIZE]; // 客户端要开 38 万个左右数量的连接
int epoll_fd = epoll_create(MAX_EPOLLSIZE); // 创建 EPOLL
strcpy(buffer, " Data From MulClient\n"); // 复制到缓冲区里
struct sockaddr_in addr; // 初始化地址
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip); // 点分十进制转化成十六进制
struct timeval tv_begin; // 初始化时间
gettimeofday(&tv_begin, NULL);
int sockfd = 0; // 初始化监听套接字
while (1) {
if (++index >= MAX_PORT) index = 0; // 这其实是一个模 20 变换,表示客户端尽可能占用本地端口依次与服务器 20 个端口建立连接
struct epoll_event ev; // ev 是可以循环利用的
if (connections < 340000 && !isContinue) { // 这里是不断产生远程连接,制造百万并发的根本
sockfd = socket(AF_INET, SOCK_STREAM, 0); // 建立 sockfd
if (sockfd == -1) {
perror("socket");
goto err;
}
//ntySetReUseAddr(sockfd);
addr.sin_port = htons(port+index); // 这其实也是模 20 变换,指代服务器的 20 个端口
if (connect(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) { // 建立远程连接
perror("connect");
goto err;
}
ntySetNonblock(sockfd); // 设置连接 sockfd 的非阻塞模式
ntySetReUseAddr(sockfd); // 设置连接 sockfd 的端口复用
sprintf(buffer, "Hello Server: client --> %d\n", connections);
send(sockfd, buffer, strlen(buffer), 0);
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLOUT; // 同时关注读事件与写事件
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sockfd, &ev); // 注册事件入 EPOLL
connections ++; // 连接计数加 1
}
//connections ++;
if (connections % 1000 == 999 || connections >= 340000) { // 避免打印过多,仅在周期结束之前打印一次,说明情况
struct timeval tv_cur;
memcpy(&tv_cur, &tv_begin, sizeof(struct timeval));
gettimeofday(&tv_begin, NULL); // 当前时间戳
int time_used = TIME_SUB_MS(tv_begin, tv_cur); // 作时间差 (tv_begin - tv_cur)
printf("connections: %d, sockfd:%d, time_used:%d\n", connections, sockfd, time_used); // 报告
int nfds = epoll_wait(epoll_fd, events, connections, 100); // 调用系统内核,执行 EPOLL 信号通知,等待 100 毫秒收集事情,针对当前的连接数量
for (i = 0;i < nfds;i ++) {
int clientfd = events[i].data.fd; // I/O clientfd
if (events[i].events & EPOLLOUT) { // 输出事件的时候
// sprintf(buffer, "data from %d\n", clientfd);
send(sockfd, buffer, strlen(buffer), 0);// 发送信号
} else if (events[i].events & EPOLLIN) { // 读事件的时候
char rBuffer[MAX_BUFFER] = {0}; // 初始化缓冲区
ssize_t length = recv(sockfd, rBuffer, MAX_BUFFER, 0); // 读去网络 I/O 的内容
if (length > 0) {
// printf(" RecvBuffer:%s\n", rBuffer);
if (!strcmp(rBuffer, "quit")) { // 匹配字符,断开连接的命令
isContinue = 0;
}
} else if (length == 0) { // 断开了连接
printf(" Disconnect clientfd:%d\n", clientfd);
connections --; // 连接计数减 1
close(clientfd); // 释放资源
} else {
if (errno == EINTR || errno == EAGAIN || errno == ENOTSOCK) continue;
// EAGAIN 是 “Try Again” 的缩写,表示“稍后再试”。含义:当一个非阻塞操作无法立即完成时,系统会返回 EAGAIN 错误码。这通常意味着当前没有足够的资源(如数据未准备好、缓冲区已满等),但操作可以在稍后重试。
// EINTR 是“Interrupted”的缩写,表示“中断”。含义:当一个系统调用正在执行时,如果进程接收到一个信号,并且该信号的处理方式是中断当前系统调用,那么系统调用会提前返回,并设置 errno 为 EINTR。
// ENOTSOCK 是“Not a socket”的缩写,表示“不是一个套接字”。含义:当尝试对一个文件描述符执行套接字相关的操作(如 send、recv、connect、bind 等),但该文件描述符并不是一个套接字时,系统会设置 errno 为 ENOTSOCK。
printf(" Error clientfd:%d, errno:%d\n", clientfd, errno); // 其他错误
close(clientfd);
}
} else {
printf(" clientfd:%d, errno:%d\n", clientfd, errno); // 发生除了读、写事件以外的事件
close(clientfd);
}
}
}
usleep(500); // 睡眠 500 微秒 == 0.5 毫秒
}
return 0;
err:
printf("error : %s\n", strerror(errno));
return 0;
}
压力测试与代码运行效果
运行
我们采用远程终端软件 Xshell 去控制 Linux 。

登入一台服务器主机,3 台客户端主机,采用分组分页形式观察他们

服务器主机上代码编译
qiming@qiming:~/share/CTASK/TCP_test$ gcc -o reactor reactor.c
客户端主机上的代码编译
client_1@client1:~/share/C-Code$ gcc -o mul_port_client_epoll mul_port_client_epoll.c
服务器启动
qiming@qiming:~/share/CTASK/TCP_test$ ./reactor
客户端发送请求,进行压力测试。
client_1@client1:~/share/C-Code$ ./mul_port_client_epoll 自己的IP 2000(我代码中选择了端口 2000)
我们最终会得到以下结果,百万连接成功(这时候我想 Ctrl/Command + C 退出程序,但发现卡住了好一会儿,这也说明了,服务器是很占用计算机资源的)

我此次实验,发现
- 百万并发, OK
- qps, 请求处理时长
60 m i n 建立 1000000 个连接 = = 3.6 m s / 个连接建立 60 min 建立 1000000 个连接 == 3.6ms / 个连接建立 60min建立1000000个连接==3.6ms/个连接建立
效果其实不怎么好,我猜想有可能是 recv_cb (原文连接 在此)函数的动态内存扩展设计,导致 qps 速度下降。
htop 命令监控百万并发的过程
我们在服务器一侧 复制会话,并且在命令行中输入命令 htop,实时观察计算机的资源消耗情况(内存和 CPU)以及进程的运转情况。

- 顶部状态栏(关键系统指标)
| 区域 | 符号/颜色 | 含义 | 理想状态 |
|---|---|---|---|
| CPU 使用率 | 蓝色条形图 | 用户空间进程 CPU 使用 | < 70% |
| 红色条形图 | 内核空间 CPU 使用 | < 20% | |
| 绿色条形图 | 低优先级进程 CPU 使用 | 可忽略 | |
| 内存使用 | 绿色条形图 | 已用物理内存 | 留 10-20% 缓冲 |
| 交换空间 | 蓝色条形图 | 已用交换空间 | 应接近 0% |
| 任务数 | 数字显示 | 总进程数 | 根据系统负载变化 |
| 平均负载 | 3个数字 | 1/5/15分钟平均负载 | < CPU 核心数 |
- 进程列表(核心监控区域)
| 列标题 | 说明 | 重要值解读 |
|---|---|---|
| PID | 进程 ID | 系统分配的唯一标识符 |
| USER | 进程所有者 | root 进程需要特别关注 |
| PRI | 实时优先级 | 值越小优先级越高(-20 到 19) |
| NI | Nice 值 | 用户空间优先级调整(-20 到 19) |
| VIRT | 虚拟内存 | 进程申请的总内存空间 |
| RES ⭐ | 常驻内存 | 实际使用的物理内存(关键指标) |
| SHR | 共享内存 | 多个进程共享的内存大小 |
| S | 进程状态 | 代码:R=运行, S=睡眠, D=不可中断睡眠, Z=僵尸, T=停止 |
| %CPU ⭐ | CPU 使用率 | 单核 100%=完全占用一个核心 |
| %MEM ⭐ | 内存使用率 | 占物理内存的百分比 |
| TIME+ | CPU 时间 | 进程累计占用 CPU 时间 |
| COMMAND | 命令行 | 进程的完整启动命令 |
- 底部功能栏(交互控制)
| 功能键 | 作用 | 使用场景 |
|---|---|---|
| F1 | 帮助 | 查看所有快捷键 |
| F2 | 设置 | 配置显示选项 |
| F3 | 搜索 | 查找特定进程 |
| F4 | 过滤 | 按条件筛选进程 |
| F5 | 树状视图 | 显示进程父子关系 |
| F6 | 排序 | 按列排序进程 |
| F7 | 降低 Nice 值 | 提高进程优先级 |
| F8 | 增加 Nice 值 | 降低进程优先级 |
| F9 ⭐ | 发送信号 | 结束进程(kill) |
| F10 | 退出 | 关闭 htop |
运行中会出现的情况
服务器程序(EPOLL)在运行的时候 内核空间 CPU 使用会突然变得很高而后过一段时间又变得很低(变高的那段时间网络 I/O 效率变低了很多),全程共出现了 8 次。越是到后期,建立连接的效率就越低,建立了 70 多万个连接后,服务器的效能开始下降的很厉害。我只截了两幅图。


服务器在运行过程中出现内核空间 CPU 使用率周期性陡增的现象,这通常是由以下几个关键原因造成的。
-
TCP 连接建立风暴(SYN Flood 处理)
现象:当大量客户端同时发起连接请求时,内核需要处理海量的 SYN 包(SYN 包是 TCP 三次握手过程中的第一个数据包,用于请求建立连接。它携带了客户端的初始序列号,用于同步序列号和防止重复连接。)
内核工作:
为每个 SYN 创建 request_sock 结构
计算 SEQ 序列号
维护半连接队列 -
连接跟踪表 (nf_conntrack) 溢出
现象:当新建连接速率超过内核处理能力时,会出现表项创建/淘汰的竞争
CPU 高峰特征:
conntrack -S 显示 insert_failed 计数增加
dmesg 中出现 nf_conntrack: table full 警告 -
定时器中断风暴
现象:大量 socket 同时超时触发内核定时器回调
常见场景:
空闲连接保活检测
读写超时处理 -
内存分配竞争(SLAB 分配器压力)
现象:为每个新连接分配内核数据结构时出现锁竞争

384

被折叠的 条评论
为什么被折叠?



