epoll_wait一次拿到了一批已经就绪的事件,但处理中间某个事件耗时太久,导致定时器认为后面的连接"超时",如果此时直接把这些连接释放掉,那么后面继续处理这批旧事件时,就可能访问已经释放的对象,造成崩溃。
1、2、3、4、5 理解成 epoll_wait() 一次返回的 5 个就绪事件。
假设:
epoll_wait 返回:
[ 1号事件, 2号事件, 3号事件, 4号事件, 5号事件 ]
EventLoop 会一个一个处理:
for (auto &event : events)
{
event.HandleEvent();
}
第一种情况:1~5 都是普通客户端连接
例如:
1:conn1
2:conn2
3:conn3
4:conn4
5:conn5
开始处理 conn1:
处理 conn1
↓
业务特别慢
↓
卡了 30 秒
假设服务器设置的非活跃超时只有:
10 秒
那么理论上 conn2~conn5 已经有 30 秒没有刷新活跃时间了。
但是此时还不一定出事。
因为接下来 EventLoop 会继续:
处理 conn2 → 刷新 conn2 活跃时间
处理 conn3 → 刷新 conn3 活跃时间
处理 conn4 → 刷新 conn4 活跃时间
处理 conn5 → 刷新 conn5 活跃时间
也就是说:
虽然时间已经超过 10 秒,但是还没有执行"超时检查任务",所以它们暂时没有被释放。
第二种情况:2号事件偏偏是"定时器事件"
这才是这段话真正想讲的问题。
现在 epoll_wait() 返回的是:
1:conn1
2:timerfd
3:conn3
4:conn4
5:conn5
处理过程:
① conn1 开始处理
conn1 业务执行 30 秒
↓
超过 10 秒超时时间
↓
conn3、conn4、conn5 都已经很久没刷新活跃度
然后处理第二个事件:
② timerfd
定时器开始检查:
if (conn3 超时)
释放 conn3;
if (conn4 超时)
释放 conn4;
if (conn5 超时)
释放 conn5;
于是:
conn3 → 已释放
conn4 → 已释放
conn5 → 已释放
但是有一个致命问题。
这次 epoll_wait() 返回的事件列表里,依然还有:
3号事件
4号事件
5号事件
EventLoop 接下来还会继续处理:
③ 处理 conn3
④ 处理 conn4
⑤ 处理 conn5
可是 conn3 刚刚已经在定时器事件里被释放了。
于是就可能变成:
conn3->HandleEvent();
实际上:
conn3
↓
对象已经销毁
↓
访问野指针 / 已释放内存
↓
程序崩溃
这就是图里说的:
"接下来在处理 345 事件的时候就会导致程序崩溃(内存访问错误)。"
所以为什么不能在定时器里立刻释放?
因为现在还处于:
本轮 epoll_wait 返回的事件处理过程中
应该改成:
timer发现 conn3 超时
不是:
立刻释放 conn3 ❌
而是:
添加一个"释放 conn3"的任务
↓
放入任务队列
先把这一轮事件全部处理完:
处理1
处理2(定时器)
处理3
处理4
处理5
----------- 本轮事件处理结束 -----------
然后再:
执行任务队列
释放 conn3
释放 conn4
释放 conn5
这样就不会出现:
对象已经释放
↓
后面还要处理它的事件
的问题。
一张图就能理解
epoll_wait
↓
一次返回
[ conn1 ][ timer ][ conn3 ][ conn4 ][ conn5 ]
↓
处理conn1
↓
卡了30秒
↓
处理timer
↓
发现conn3/4/5超时
↓
❌ 不能马上释放
↓
放入任务队列
↓
继续处理conn3/4/5
↓
本轮事件处理结束
↓
执行任务队列
↓
真正释放conn3/4/5
所以这段课程内容真正想强调的是:
连接的销毁不能随意发生在一次事件分发过程中,否则
epoll_wait已经返回的事件中可能还保存着这个连接对应的对象。要采用"延迟释放",等当前这一批事件处理结束后再销毁。
而你上一问说的"一个连接一个线程互不干扰",也正好和这里连起来:这里是一条 EventLoop 线程串行处理一批连接事件,所以 conn1 卡 30 秒,后面的定时器和连接事件全部都会被拖后 30 秒。
client4 5 6
测试大文件传输:
测并发量
性能压力测试
为了验证服务器在高并发场景下的处理能力,对服务器进行压力测试,主要关注两个指标:
① 并发量
并发量表示服务器在同一时间能够处理的客户端请求数量。
并发量越高,说明服务器同时处理大量客户端连接的能力越强。
② QPS
QPS(Queries Per Second)表示服务器每秒能够处理的请求数量。
QPS 越高,说明服务器单位时间内的请求处理能力越强。
一、压力测试工具------WebBench
本次测试使用 WebBench 对服务器进行压力测试。
WebBench 的基本原理是:
创建多个客户端进程 → 每个进程连接服务器 → 发送 HTTP 请求 → 接收服务器响应 → 关闭连接 → 重复上述过程
通过大量客户端持续向服务器发送请求,可以测试服务器在高并发场景下的处理能力。
二、测试环境
性能测试结果与硬件配置、网络带宽、客户端环境等因素密切相关,因此必须说明具体测试环境,否则单独讨论并发量和 QPS 没有太大意义。
测试环境:
服务器环境:
1 核 CPU
2 GB 内存
1 Mbps 带宽
云服务器
客户端环境:
VMware 虚拟机(Linux 环境)
使用 WebBench 作为压力测试工具
测试参数:
并发数:10000
测试时间:24 小时
请求方式:HTTP 请求
三、测试过程
使用 WebBench 创建大量客户端并发访问服务器,将并发数设置为 10000,并持续向服务器发送 HTTP 请求。
测试过程中主要观察:
• 是否出现连接失败
• 是否出现服务器崩溃
• 请求是否能够正常响应
• QPS 是否稳定
• CPU、内存等资源占用情况
四、测试结果
在上述测试环境下,对服务器进行了 24 小时持续压力测试。
最终测试结果:
并发量:xxx
QPS:xxx
连接失败数:xxx
服务器运行状态:xxx
注意:
这里最好不要直接写"服务器支持 10000 并发"。
如果 WebBench 参数设置为 10000,只能说明"测试时设置了 10000 并发",最终是否真正稳定支持 10000 并发,需要结合成功请求数、失败请求数、QPS、CPU/内存占用等结果进行判断。
所以写:
"使用 WebBench 将并发数设置为 10000,对服务器进行了 24 小时持续压力测试。"
会比:
"服务器支持 10000 并发。"
更加严谨。
服务器环境:
VMware 虚拟机(Linux)
CPU:2 核
内存:8 GB
网络:虚拟机网络环境
客户端环境:
VMware 虚拟机(Linux)
使用 WebBench 进行压力测试
测试环境:服务器与 WebBench 压测客户端均运行于同一 Linux 虚拟机中,通过本机回环地址 127.0.0.1 进行测试。因此测试结果主要用于服务器优化前后的性能对比,不代表真实跨网络环境下的绝对性能。