前言
最近学习在 linux 环境下跑高并发服务器去测试服务器 并发毗连数量 偶遇浩繁题目,拼尽尽力无法降服。因此决定好好研究一番,下面通过一些我遇到的题目分享一下我是如何办理这些错误让服务器并发毗连数量从一两万达到几十万的。只想修改设置提高并发量的可以直接下方或右侧目次跳到题目1,题目2。
对了,我的语雀文档更新了嵌入式系列相关的系统学习条记,详细且图文并茂,盼望能帮到你
alive的语雀文档 嵌入式方向系列条记
服务器工作流程分析及环境准备
项目运行环境
项目技能栈
- C++、epoll、线程池 、内存池 tcmalloc
工程运行环境
- VirtualBox + ubuntu 22.04
模块关系图
服务器工作流程
服务器核心工作流程图
我这种 基于 epoll + 线程池 的并发服务器模式,通常也叫做 多线程 Reactor模子
不整这些高大上的名词,简单来说就是
- epoll 负责 事件监听。
- 主线程 负责检测 I/O 事件 并分发给 线程池 处理。
- 线程池 负责数据读取、业务逻辑处理,并返回结果给客户端。
长处:
- 将大型数据分离出主线程,克制 I/O 阻塞影响主线程,可以提升并发本事。
- 充实使用多核 CPU,线程池可并行处理多个请求。
缺点:
- 线程切换本钱高,这里涉及到 CPU 调理的题目,通俗点来说就是在一个空间狭隘质料有限的厨房,每个厨师抢着同一瓶酱油往本身的锅里倒还没倒完又被其他厨师抢走了,得重新抢回来,耗费时间。
- 线程池里的多个线程 可能会争抢同一个资源,通俗点来说就是饭堂多个学生去同一个窗口打菜,假如窗口不够多,各人都得排队,效率就低了。
注意:
- 我这里只对服务器举行并发毗连数量测试,根据上面的优缺点分析可知,单线程 Reactor 处理并发毗连也是完全可以的。
- 我使用这个处理方法仅仅是为了预留万一以后必要拓展处理其他业务,直接拿当前代码拿来用也是很不错的。
下面也拓展一下 epoll 常见的 6 种设计模式,每种模式各有优缺点,选择适合本身的即可
模子线程/进程特点实用场景单线程 Reactor1 个线程实用于小型应用低并发服务器多线程 Reactor线程池epoll监听,线程池处理高并发服务器多进程 Reactor多进程epoll监听,子进程处理Web 服务器(Nginx)主从 Reactor主线程 + Worker(线程/进程)主线程监听epoll_wait(),分发给 Worker 处理 I/O高吞吐服务器协程 Reactor线程 + 协程低切换开销,高并发游戏服务器、微服务Proactor 模式线程池 + 异步 I/O事件驱动,内核完成 I/OCDN、分布式存储 Proactor 模式 和 Reactor 的区别可以看下面文章
https://zhuanlan.zhihu.com/p/430986475
性能瓶颈辨认思绪先容
在举行高并发服务器开发的时候,每每很多时候遇到的性能题目都不是本身的代码原因,因为现在网上都有很多公共的服务器代码可以参考学习,这个根本题目不大,固然前提是你要分的清楚本身的代码报错是由于业务逻辑的原因还是性能的原因。
分析性能瓶颈一般从下面这五个方向入手,使用排除法一个一个排除:
内存、操纵系统、CPU、网卡、磁盘
1️⃣ 内存
遇到和内存有关的东西我们通常要思量两个题目
- 当前内存使用是否靠近上限?
- 是否有 swapping 或内存分配延迟?
在高并发的环境下内存导致的性能下降通常体现为
- 高并发下,每个毗连占用内存累积,服务器性能下降,相应时间变长
查抄方法
- free -m
- #查看可用内存 (available)和 swap 使用情况
复制代码 若 avaiabel 过低或者 swap 的 used 增加 证实内存不敷
使用命令 top/htop 列出占用内存最高的进程
高并发环境下优先思量 TCP 创建时占用过多内存,实验降低 TCP 的发送缓冲区和罗致缓冲区
2️⃣CPU
CPU 我们通常必要确认
● CPU 是否过载?
● 是否由创建过多线程或 CPU 上下文切换开销导致?
性能瓶颈体现为
● CPU使用率靠近 100%,任务处理速率变慢,乃至导致系统卡顿
查抄方法
- top #top或 htop 查看 CPU 使用率和每个核心负载。
复制代码 若%CPU 持续过高,阐明 CPU 占用过大。
可能原因: 线程池题目、代码有低效算法。
3️⃣操纵系统(OS)
操纵系统的题目就比力多了,通常可以思量以下两点
- 是不是 fd 数量不够了?
- 是不是操纵系统本身限制了你的毗连数量?
- 是不是端口用完了?
查抄方法
- ulimit -a #查看文件描述符打开数是多少
- sysctl net.ipv4.ip_local_port_range #查看端口范围
复制代码 端口或文件描述符设置的过小的话可以得当调大一点再去测试是不是这个题目
4️⃣网卡
网卡可以思量以下两点
- 是否带宽达到上限?
- TCP 等毗连的设置优化有没有做好
性能瓶颈可体现为
查抄方法
- sudo iftop #监控实时带宽
- sysctl -w net.ipv4.tcp_tw_reuse=1 #优化TCP参数
复制代码 可能原因: 高并发环境下数据吞吐量凌驾网卡负载
5️⃣磁盘
关于磁盘我们通常要确认
- 是否有大量的 数据写入操纵?
- 磁盘是否老旧,性能不行?
性能瓶颈体现为
查抄方法
- iostat -x 1 #查看磁盘 I/O,关注 await(等待时间)和 %util(利用率)
复制代码 可能原因: 高并发下环境下,频繁读写数据导致磁盘压力过大。
办理方法: 使用内存缓存或更换硬盘
我遇到的题目
题目 1:Too many open files
描述:
客户端毗连到肯定数量时,终端出现 Too many open files。
根本原因:
在 Linux 系统中,每创建一个 socket(无论是服务端的监听 socket 还是客户端的毗连 socket),操纵系统都会为之分配一个 FD。随着毗连数增加,FD 数量快速累积,终极凌驾进程或系统的限制,导致无法创建新的 FD,从而触发该错误。
验证:
- ulimit -n #查看进程级别的文件描述符限制 默认为1024表示最多只能打开1021个,
- #前面三个为标准输入输出出错
- sysctl fs.file-max #查看系统级别文件描述符限制
复制代码
办理方案:
1️⃣设置进程级别文件描述符巨细
- #临时设置,只在当前终端生效,关闭后恢复原状
- ulimit -n 1048576 #设置为最大值
- -------------------------------------------
- #永久设置
- sudo nano /etc/security/limits.conf
- #添加
- li soft nofile 1048576 #第一个为我的用户名,这里修改成你自己的
- li hard nofile 1048576
- #添加
- echo "session required pam_limits.so" | sudo tee -a /etc/pam.d/common-session
- echo "session required pam_limits.so" | sudo tee -a /etc/pam.d/common-session-noninteractive
- #重启生效
- sudo reboot
复制代码 2️⃣修改系统级别限制
- #进入以下文件
- sudo vi /etc/sysctl.conf
- #文件末尾添加
- fs.file-max = 9223372036854775807
- #生效
- sudo sysctl -p
复制代码 此时这个Too many open files题目办理。
题目 2:提升并发数量
描述:
系统并发到肯定数量毗连时时候忽然无法继续毗连,有时候可能会崩掉。
根本原因:
Linux 默认的当地端口范围(net.ipv4.ip_local_port_range)设置为 32768 到 60999,共约 28,000 个端口,因此默认只能并发 2.8 万左右。
办理办法:
1️⃣修改 linux 默认分配的端口范围
- sudo vim /etc/sysctl.conf #修改配置文件
- #文件末尾添加端口范围为1024~65535,1024以下是系统默认使用的
- net.ipv4.ip_local_port_range = 1024 65535
- #生效
- sudo sysctl -p
- sudo modprobe ip_conntrack
复制代码
- 修改完理论上可用端口数为 65535 - 1024 + 1 = 64,512 个,现实毗连数达到 64,500 左右,根本符合预期。
那么题目来了!!有没有办法继续提高这个限制呢
有的,兄弟有的
继续提升并发毗连客户端
首先我们要知道 TCP 毗连的五元组
- 在 TCP 中,每个毗连由五元组唯一标识:
- 源 IP 地址(Source IP):发送方的 IP 地址。
- 源端口(Source Port):发送方使用的端口号。
- 目标 IP 地址(Destination IP)x罗致方的 IP 地址。
- 目标端口(Destination Port):罗致方监听的端口号。
- 协议(Protocol):通常是 TCP。
例如,当客户端(IP 为 192.168.11.15)毗连到服务器(IP 为 192.168.11.10,端口 9999)时,假设客户端使用随机端口 50001,五元组为:
(192.168.11.15, 50001, 192.168.11.10, 9999, TCP)
五元组的每项必须唯一,以区分不同的 TCP 毗连。在客户端发起毗连的场景中,服务器的 IP 和端口(192.168.11.10:9999)通常固定,而客户端的源 IP 和源端口则可能变化。
以是,当客户端使用单一源 IP(例如 192.168.11.10)毗连到固定的服务器 IP 和端口(192.168.11.10:9999),五元组中只有源端口可以变化。因此,毗连数的上限取决于可用临时端口的数量:
- 默认范围下:约 28,000 个毗连。
- 最大范围下:约 64,000 个毗连。
办理方案:
要突破这一限制,我们必要使五元组中的某项变化,从而创建更多的唯一毗连。
1️⃣使用多个 IP 突破单 IP 只能并发 6 万的限制
原理:
五元组包含源 IP,通过使用多个源 IP,每个 IP 可以独立使用其临时端口范围。例如,若每个 IP 支持 60,000 个毗连,10 个 IP 可支持 600,000 个毗连。
办理方法:
- #临时添加多个虚拟进程
- sudo ip addr add 127.0.0.2/8 dev lo
- sudo ip addr add 127.0.0.3/8 dev lo
- sudo ip addr add 127.0.0.4/8 dev lo
- sudo ip addr add 127.0.0.5/8 dev lo
- ......
- -----------------------------------------
- #或者使用下面这句代码,一行搞定 添加了 127.0.0.2 到 127.0.0.20,共 19 个额外 IP。
- for i in {2..20}; do sudo ip addr add 127.0.0.$i/8 dev lo; done
- #不是回环地址也可以这样子设置 这种形式创建的相当于192.168.11.15的子IP
- for i in {2..10}; do sudo ip addr add 192.168.11.15$i/32 dev enp0s3; done
- #添加192.168.16 到 192.168.11.30的IP地址
- for i in {16..30}; do sudo ip addr add 192.168.11.$i/24 dev enp0s3; done
复制代码 这里要说一下,我是在局域网举行测试,有些人可能会选择放在云平台去跑,但是测试并发毗连不必要,现实测起来可能测的想吐,因为网络时延的题目。选择当地局域网即可,我这里直接使用的当地回环地址。
客户端循环遍历去毗连这几个 IP 提升并发数量
- #include <iostream>
- #include <sys/socket.h>
- #include <arpa/inet.h>
- #include <unistd.h>
- #include <cstring>
- #include <chrono>
- #include <vector>
- using namespace std;
- class Client {
- public:
- Client(const string &serverIP, int serverPort, const vector<string> &clientIPs)
- : serverIP_(serverIP), serverPort_(serverPort), clientIPs_(clientIPs), ipIndex_(0) {}
- ~Client() {
- for (int sock : sockets_) {
- close(sock);
- }
- }
- void run() {
- int count = 0;
- auto lastReportTime = chrono::steady_clock::now();
- while (true) {
- string localIP = clientIPs_[ipIndex_ % clientIPs_.size()];
- ipIndex_++;
- int sock = socket(AF_INET, SOCK_STREAM, 0);
- if (sock < 0) {
- perror("创建 sock 失败");
- continue;
- }
- struct sockaddr_in localAddr;
- memset(&localAddr, 0, sizeof(localAddr));
- localAddr.sin_family = AF_INET;
- localAddr.sin_port = 0;
- inet_pton(AF_INET, localIP.c_str(), &localAddr.sin_addr);
- if (bind(sock, (struct sockaddr*)&localAddr, sizeof(localAddr)) < 0) {
- perror("bind 错误");
- close(sock);
- continue;
- }
- struct sockaddr_in serverAddr;
- memset(&serverAddr, 0, sizeof(serverAddr));
- serverAddr.sin_family = AF_INET;
- serverAddr.sin_port = htons(serverPort_);
- inet_pton(AF_INET, serverIP_.c_str(), &serverAddr.sin_addr);
- if (connect(sock, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
- perror("connect 错误");
- close(sock);
- continue;
- }
- sockets_.push_back(sock);
- count++;
- if (count % 999 == 0) {
- auto now = chrono::steady_clock::now();
- auto duration = chrono::duration_cast<chrono::milliseconds>(now - lastReportTime).count();
- lastReportTime = now;
- string message = "当前连接数量: " + to_string(count) +
- ", clientIP: " + localIP +
- ", clientFD: " + to_string(sock) +
- ", timeuse: " + to_string(duration) + "ms";
- ssize_t n = write(sock, message.c_str(), message.size());
- if (n < 0) {
- perror("发送失败");
- } else {
- char buffer[1024] = {0};
- n = read(sock, buffer, sizeof(buffer) - 1);
- if (n > 0) {
- buffer[n] = '\0';
- cout << "服务器返回: " << buffer << endl;
- }
- }
- }
- }
- }
- private:
- string serverIP_;
- int serverPort_;
- vector<string> clientIPs_;
- size_t ipIndex_;
- vector<int> sockets_;
- };
- int main() {
- // 配置 10 个虚拟 IP
- vector<string> clientIPs = {
- "127.0.0.1", "127.0.0.2", "127.0.0.3", "127.0.0.4" //可以继续填写剩余IP
- };
- Client client("127.0.0.1", 9999, clientIPs);
- client.run();
- return 0;
- }
复制代码 验证
2️⃣毗连多个服务器端口
原理:
五元组其他四项固定,服务器 IP 是变化的,那么客户端可以毗连到不同的目标端口,每个端口支持独立的 60,000 个毗连。
办理方法:
服务器监听多个端口,将多个端口添加到同一个epoll即可,无需多个epoll,详细步骤就不列出来了因为我没写哈哈。
题目 3:毗连耗时题目
描述:
当毗连数量变多的时候从最开始每 1000 个毗连用时 几十毫秒大大增加到上千乃至上万毫秒
根本原因:
- 首先排除文件描述符靠近上限的原因,在前面的设置中已经设置了非常大的文件描述符,这才几十万毗连不会达到上限
- 每个毗连必要内存来存储套接字缓冲区、TCP 控制块等,随着毗连数增加,内存使用量增大,系统可能开始使用交换空间(swap)或内存分配变慢。
因此我们从优化内存的思绪去办理题目,于我的项目而言我必要设置 TCP 缓冲区巨细
办理思绪
1️⃣优化套接字缓冲区
- int bufsize = 4096;//TCP建立连接的时候设置更少的缓冲区
- setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
- setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
复制代码 2️⃣修改 linux 当地设置
- sudo vim /etc/sysctl.conf
- #下面几个是系统默认的,可以修改为更大的值
- net.ipv4.tcp_mem = 262144 524288 786432 #524288 1048576 1572864 修改为更大的值
- net.ipv4.tcp_wmem = 2048 2048 4096
- net.ipv4.tcp_rmem = 2048 2048 4096
- net.nf_conntrack_max = 1048576
复制代码 net.ipv4.tcp_mem = 262144 524288 786432
- 控制全局 TCP 内存使用,单位是页面巨细(通常 4KB),有三个值:
- 262144:低阈值,低于此值时不限制内存使用(约 1GB)。
- 524288:压力阈值,达到此值时开始限制新毗连内存(约 2GB)。
- 786432:最大阈值,上限内存量,超出可能丢包(约 3GB)。
net.ipv4.tcp_wmem = 2048 2048 4096TCP 发送缓冲区巨细(单位:字节)
net.ipv4.tcp_rmem = 2048 2048 4096TCP 罗致缓冲区巨细(单位:字节)
因此,只必要将tcp_mem设置为更大值即可降低毗连耗时,如下,30 多万后每一千毗连耗时比之前降低很多
题目 4: Bad file descriptor
描述:
客户端主动断开毗连时,终端出现 epoll_ctl 移除fd失败: Bad file descriptor。
场景:
多个线程同时处理同一个 clientFD,一个线程关闭 fd 后,另一个线程实验操纵已关闭的 fd。
根本原因:
我的代码逻辑是 while 循环检测分配事件,假如是客户端有数据的话就交给工作线程行止理,也就是调用线程池去接受处理客户端发送的数据。这就导致水平触发模式(EPOLLET)下,epoll_wait 可能多次返回同一 fd,导致多个线程使用同一个 clientfd 处理并发冲突。
办理方案:
- 使用 EPOLLONESHOT,确保 fd 在触发事件后禁用:
- 在 Epoll::addClientFD 中设置 EPOLLIN | EPOLLONESHOT。
- 添加 rearm 方法重新启用 fd。
- 在 readFromClient 处理完成后调用 rearm(假如客户端毗连未关闭)。
epoll.cpp
server.cpp
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |