[C/C++后端开发学习] 9 服务端百万并发测试
服务端百万并发测试
- 服务端并发的概念
- 常见单机服务模型
- 并发测试方法
- socket数量的限制(描述符数量的限制)
- 客户端测试代码
- 测试结果
- 改进代码提升测试并发量
- 单线程多端口同时监听
- 修改代码:
- 测试环境说明
- 测试现象:
- 分析:
- 解决方法:
- 测试结果
- 实际的做法
- 其他可能的改进:增加多线程处理
- 处理方法:多线程accept
服务端并发的概念
并发量:同时承载的客户端的数量,实质上就是同时能够维护的 socket 的数量
比如要实现10W客户端同时在线,200ms内正常返回结果,承载量的影响因素:
- 数据库
- 网络带宽
- 内存操作
- 日志
测试服务器十万、百万并发能力的意义更多地在于测试服务器是否具备这种能力,它并不是与业务直接相关的。
常见单机服务模型
- 单线程同步:NTP
- 多线程同步:Natty
- 纯异步:Redis、HAProxy
- 半同步半异步:Natty
- 多进程同步:fastcgi
- 多线程异步:memcached
- 多进程异步:Nginx
- 一个请求一个进程/线程:Apache/CGI
- 微进程框架:erlang/go/lua
- 协程框架:libco/ntyco
并发测试方法
实际上,我们之前基于epoll实现的Reactor模型的简单echo服务器就应经可以实现几十万量级的并发了,我们可以测试一下。测试之前需要先修改一下描述符数量的限制,否则可用的描述符资源会耗尽。服务端测试代码为之前这篇博文中的Reactor模型的代码。
socket数量的限制(描述符数量的限制)
两种修改方法:
ulimit -n <value>
这种方法不能持久,重启后就得重新设置vim /etc/security/limits.conf
limits.conf文件的格式如下:
- domain就是指用户;
- 限制策略的type分soft软和hard硬,所谓hard就是硬性限制,数量达到value这个上限就不分配了;soft相对宽松,达到上限后开始回收;
- item:对于文件描述符的数量限制,其对应的item就是
nofile - max number of open files
增加两行:
* soft nofile 1048576
* hard nofile 1048576
客户端测试代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/time.h>
#include <errno.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <fcntl.h>#define MAX_CONNECTION 340000 // 单个客户端建立34W个连接,3个客户端就有100W
#define MAX_BUFSIZE 128
#define MAX_EPOLLSIZE (384*1024)
#define MAX_PORT 100 // 最大端口数量#define TIME_MS_USED(tv1, tv2) ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000) // 用于计算耗时/* 设置fd为非阻塞 */
static int fdSetNonBlock(int fd)
{int flags;flags = fcntl(fd, F_GETFL);if(flags < 0) return flags;flags |= O_NONBLOCK;if(fcntl(fd, F_SETFL, flags) < 0) return -1;return 0;
}/* 设置socket SO_REUSEADDR*/
static int sockSetReuseAddr(int sockfd)
{int reuse = 1;return setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
}struct epoll_event events[MAX_EPOLLSIZE];
int main(int argc, char *argv[])
{if(argc < 3){printf("Usage: %s ip port", argv[0]);return 0;}const char* ip = argv[1];int port = atoi(argv[2]);int connections = 0; // 建立连接的计数,用于统计char buffer[MAX_BUFSIZE] = {0};int i;int clientFinishTest = 0;int portOffset = 0;int epfd = epoll_create(1);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 loopBegin, loopEnd;gettimeofday(&loopBegin, NULL);while(1){struct epoll_event ev;int sockfd = 0;if(connections < MAX_CONNECTION) // 连接数量未达到最大值就一直新建连接并connect到服务端{sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){perror("socket");goto errExit;}addr.sin_port = htons(port + portOffset);portOffset = (portOffset + 1) % MAX_PORT; // 均匀地使用这100个端口if(connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0){perror("connect");goto errExit;}fdSetNonBlock(sockfd);sockSetReuseAddr(sockfd);ev.events = EPOLLIN | EPOLLOUT;ev.data.fd = sockfd;epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); // 当前连接添加到epoll中进行监视connections++; // 连接数增加}// 每增加1000个连接就执行一次if(connections % 1000 == 0 || connections == MAX_CONNECTION){int nready = epoll_wait(epfd, events, 256, 100); // 每次处理256个,避免处理太过于频繁for(i = 0; i < nready; i++){int clientfd = events[i].data.fd;if (events[i].events & EPOLLIN) {char rBuffer[MAX_BUFSIZE] = {0}; ssize_t length = recv(clientfd, rBuffer, MAX_BUFSIZE, 0);if (length > 0) {printf("# Recv from server:%s\n", rBuffer);} else if (length == 0) {printf("# Disconnected. clientfd:%d\n", clientfd);connections --;epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &events[i]);close(clientfd);} else {if (errno == EINTR) continue;printf(" Error clientfd:%d, errno:%d\n", clientfd, errno);epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &events[i]);close(clientfd);}} else if (events[i].events & EPOLLOUT){sprintf(buffer, "# data from %d\n", clientfd);send(clientfd, buffer, strlen(buffer), 0);}else {printf(" clientfd:%d, unknown events:%d\n", clientfd, events[i].events);}usleep(1000); // 适当停一下,避免服务端虚拟机网络IO压力过大}if(!clientFinishTest) // 当前客户端的测试尚未结束{/* 计算建立1000个连接需要的时间 */gettimeofday(&loopEnd, NULL);int msPer1K = TIME_MS_USED(loopEnd, loopBegin);gettimeofday(&loopBegin, NULL); // 进入下一轮计数printf("-- connections: %d, sockfd:%d, time_used:%d\n", connections, sockfd, msPer1K);if(connections == MAX_CONNECTION){printf("# Client finished the test.\n");clientFinishTest = 1;}}}usleep(1000); // 短暂休眠1ms再继续}return 0;errExit:printf("error : %s\n", strerror(errno));return 0;
}
此时,要记得先将上面代码中的宏定义MAX_PORT
先设为1,因此此时服务端只会开启一个端口监听。
同时,我们先限制了客户端建立的连接数量为34W,看看此时单个客户端对服务器是否能够达到这个量级。
(实际测试代码运行过程中epoll_wait()时我们并没有同时取出340000个事件,因为这导致处理的时间变得非常漫长而影响到了连接建立,且我的虚拟机的虚拟网卡也出现了问题,出现了看不懂的内核打印。所以每次epoll_wait()只取了256个事件处理)
测试结果
此时服务端只有一个端口监听时,而客户端的可用端口是有限的,因此当端口数量达到一定值后(此处为2.8W)就会报Cannot assign requested address
的错误。
改进代码提升测试并发量
单线程多端口同时监听
为了进一步增加可建立的连接数量,在服务端起100个监听端口并将所有监听 socket 描述符丢到epoll中,客户端循环连接到这100个端口。为什么这么做呢?可以看下文关于socket五元组的分析。
修改代码:
修改服务器端的代码如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>#define MAX_PORT 100#define MAX_BUFFER_SIZE 1024
struct sockitem
{int sockfd;int (*callback)(int fd, int events, void *arg);int epfd;char recvbuffer[MAX_BUFFER_SIZE]; // 接收缓冲char sendbuffer[MAX_BUFFER_SIZE]; // 发送缓冲int recvlength; // 接收缓冲区中的数据长度int sendlength; // 发送缓冲区中的数据长度
};static int client_cnt = 0; // 统计连接数#define MAX_EVENTS_NUM (1024*1024) // 100W个事件同时监听
struct reactor
{int epfd;struct epoll_event events[MAX_EVENTS_NUM];
};int recv_cb(int fd, int events, void *arg);/* 写IO回调函数 */
int send_cb(int fd, int events, void *arg)
{struct sockitem *si = arg;struct epoll_event ev;int clientfd = si->sockfd;/* 写回的数据此处先简单处理 */int ret = send(clientfd, si->sendbuffer, si->sendlength, 0);si->callback = recv_cb; // 切回读的回调ev.events = EPOLLIN;ev.data.ptr = si;epoll_ctl(si->epfd, EPOLL_CTL_MOD, si->sockfd, &ev);return ret;
}/* 读IO回调函数 */
int recv_cb(int fd, int events, void *arg)
{struct sockitem *si = arg;struct epoll_event ev;int clientfd = si->sockfd;int ret = recv(clientfd, si->recvbuffer, MAX_BUFFER_SIZE, 0);if(ret <= 0){if(ret < 0){if(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR){ // 被打断直接返回的情况return ret;}printf("# client err... [%d]\n", --client_cnt);}else{printf("# client disconn... [%d]\n", --client_cnt);}/* 将当前客户端socket从epoll中删除 */ev.events = EPOLLIN;ev.data.ptr = si;epoll_ctl(si->epfd, EPOLL_CTL_DEL, clientfd, &ev); close(clientfd);free(si);}else{//printf("# recv data from fd:%d : %s , len = %d\n", clientfd, si->recvbuffer, ret); // 为避免频繁打印影响运行速度,此句不放开si->recvlength = ret;memcpy(si->sendbuffer, si->recvbuffer, si->recvlength); // 将接收到的数据拷贝的发送缓冲区si->sendlength = si->recvlength;si->callback = send_cb; // 回调函数要切换成写回调struct epoll_event ev;ev.events = EPOLLOUT | EPOLLET; // 写的时候最好还是用ETev.data.ptr = si;epoll_ctl(si->epfd, EPOLL_CTL_MOD, si->sockfd, &ev);}return ret;
}/* accept也属于读IO操作的回调 */
int accept_cb(int fd, int events, void *arg)
{struct sockitem *si = arg;struct epoll_event ev;struct sockaddr_in client;memset(&client, 0, sizeof(struct sockaddr_in));socklen_t caddr_len = sizeof(struct sockaddr_in);int clientfd = accept(si->sockfd, (struct sockaddr*)&client, &caddr_len);if(clientfd < 0){printf("# accept error\n");return clientfd;}char str[INET_ADDRSTRLEN] = {0};printf("recv from %s:%d [%d]\n", inet_ntop(AF_INET, &client.sin_addr, str, sizeof(str)),ntohs(client.sin_port), ++client_cnt);struct sockitem *client_si = (struct sockitem*)malloc(sizeof(struct sockitem));client_si->sockfd = clientfd;client_si->callback = recv_cb; // accept完的下一步就是接收客户端数据client_si->epfd = si->epfd;memset(&ev, 0, sizeof(struct epoll_event));ev.events = EPOLLIN;ev.data.ptr = client_si;epoll_ctl(si->epfd, EPOLL_CTL_ADD, clientfd, &ev); // 把客户端socket增加到epoll中监听return clientfd;
}int init_port_and_listen(int port, int epfd)
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0)return -1;struct sockaddr_in addr;memset(&addr, 0, sizeof(struct sockaddr_in));addr.sin_family = AF_INET;addr.sin_addr.s_addr = INADDR_ANY;addr.sin_port = htons(port);if(bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0)return -2;if(listen(sockfd, 5) < 0)return -3;/**** 监听socket加入epoll ****/struct sockitem *si = (struct sockitem*)malloc(sizeof(struct sockitem)); // 自定义数据,用于传递给回调函数si->sockfd = sockfd;si->callback = accept_cb;si->epfd = epfd; // sockitem 中增加一个epfd成员以便回调函数中使用struct epoll_event ev;memset(&ev, 0, sizeof(struct epoll_event));ev.events = EPOLLIN; // 默认LTev.data.ptr = si;epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); // 添加事件到epollreturn sockfd;
}struct reactor ra; // 放到全局变量,避免大内存进入栈中int main(int argc, char* argv[])
{if(argc < 2){printf("Usage: %s <port>\n", argv[0]);return 0;}int port = atoi(argv[1]);int i;struct sockitem *si;int listenfds[MAX_PORT] = {0};/* go epoll */ra.epfd = epoll_create(1);for(i = 0; i < MAX_PORT; i++) // 建立100个端口进行监听,并且加入epolllistenfds[i] = init_port_and_listen(port + i, ra.epfd);while(1){int nready = epoll_wait(ra.epfd, ra.events, MAX_EVENTS_NUM, -1);if(nready < 0){printf("epoll_wait error.\n");break;}int i;for(i = 0; i < nready; i++){si = ra.events[i].data.ptr;if(ra.events[i].events & (EPOLLIN | EPOLLOUT)){if(si->callback != NULL)si->callback(si->sockfd, ra.events[i].events, si); // 调用回调函数}}}for(i = 0; i < MAX_PORT; i++){if(listenfds[i] > 0)close(listenfds[i]);}
}
该版程序除了增加100个端口同时监听,还将MAX_EVENTS_NUM
定位为1M大小,使之可容量100W个fd。同时将struct reactor ra
定义成全局变量,避免大块数据的内存进入栈中。
此时要记得将客户端程序的宏定义MAX_PORT
也设为100。
测试环境说明
为了减小客户端的压力并缩短测试时间,这里起三个客户端同时向服务端发起连接,每个客户端最终建立34W个连接,最终总体会有大于100W个连接数。
我开了4台Ubuntu16.04 Server版本的虚拟机,其中作为服务器的一台分配8G内存,其他3台作为客户端的各分配3G内存。
测试现象:
长时间运行后,虽然不再出现之前的错误,但连接数量继续增大后客户端会出现connect连接超时错误。
分析:
是客户端端口不够吗?注意,一个socket包含一个五元组,其中 src port 和 dst port 是组合出现的,所以对于客户端socket的数量限制不是在于端口号(65535)的限制,而是五元组数量的限制。现在服务端 dst port 有100个,那么它与客户端 src port 的组合也可以有更多(100×65535)。此时连接数还不算多,显然不是客户端端口不够引起的。
可以想象,这是客户端调用connect()进行三次握手时,服务端没有返回ACK。为什么没有返回呢?
这里涉及到了 iptables 的作用,实际上网络协议栈也会对连接的数量进行限制,超出限制就不接收连接了。
解决方法:
通过vim /etc/sysctrl.conf
可以修改这个限制,将这个字段net.nf_conntrack_max = 1048576
开放出来并设置一个最大值。此外,这个配置文件里边还可以配置一个字段为fs.file-max = 1048576
,这是指的 fd 的最大值(是值,非个数)。关于 sysctrl.conf 的更多内容可以参考:sysctl.conf文件详解
同时,由于开启的socket会非常多,我们可以适当地减小每个socket的发送和接收的缓冲区大小以节省内存。同样在vim /etc/sysctrl.conf
中增加这两个参数。最终服务端和客户端的/etc/sysctrl.conf
文件下都增加了这三行:
fs.file-max = 1048576
net.nf_conntrack_max = 1048576
net.ipv4.tcp_rmem =128 256 512
net.ipv4.tcp_wmem =128 256 512
最后运行sudo sysctl -p
使配置生效。如果出现sysctl: cannot stat /proc/sys/net/nf_conntrack_max: No such file or directory
这个报错则按照这篇文章的方法处理。
测试结果
经过了漫长地等待后,终于服务端成功建立了102W个连接:
......
recv from 192.168.2.203:33948 [1019996]
recv from 192.168.2.203:58886 [1019997]
recv from 192.168.2.203:43944 [1019998]
recv from 192.168.2.203:54874 [1019999]
recv from 192.168.2.203:56146 [1020000]
查看服务端虚拟机的内存情况,还剩2G内存可用:
$ free -htotal used free shared buff/cache available
Mem: 7.8G 2.1G 2.0G 8.9M 3.7G 2.6G
Swap: 974M 0B 974M
此时从客户端虚拟机的打印来看,三个客户端与服务端依然在正常交换数据。
......
# Recv from server:# data from 30976
# Recv from server:# data from 30977
# Recv from server:# data from 30978
# Recv from server:# data from 30979
......
可见,靠着Linux操作系统和 epoll 就能把并发量开到百万级别了。
实际的做法
实际中会采用直接fork出多进程进行监听,好处在于每个进程都有各自fd限制数量,互相不影响,因此可以监听地更多。
其他可能的改进:增加多线程处理
单线程处理的情况下,半连接、全连接队列满了之后,其他连接需要排队等待。当队列中的客户端堆积到最大值后,必须等线程从队列中accept出连接,才有更多的队列空间去装后面的客户端。如果用多线程去处理,接收连接就会更快,出现等待的概率会减小。
处理方法:多线程accept
- 将 accept 与 recv、send分开。
- 增加多个线程去accept同一个socket fd,实现多点接入。
[C/C++后端开发学习] 9 服务端百万并发测试相关推荐
- 【云风skynet】详解skynet的多核高并发编程丨actor模型丨游戏开发丨游戏服务端开发丨多线程丨Linux服务器开发丨后端开发
skynet中多核高并发编程给我们的启发 1. 多核并发编程 2. actor模型详解 3. 手撕一个万人同时在线游戏 视频讲解如下,点击观看: [云风skynet]详解skynet的多核高并发编程丨 ...
- 适合新手:从零开发一个IM服务端(基于Netty,有完整源码)
本文由"yuanrw"分享,博客:juejin.im/user/5cefab8451882510eb758606,收录时内容有改动和修订. 0.引言 站长提示:本文适合IM新手阅读 ...
- swagger 返回json字符串_[Swagger] Swagger Codegen 高效开发客户端对接服务端代码
[Swagger] Swagger Codegen 高效开发客户端对接服务端代码 @TOC 手机用户请横屏获取最佳阅读体验,REFERENCES中是本文参考的链接,如需要链接和更多资源,可以关注其他博 ...
- web后端开发学习路线_学习后端Web开发的最佳方法
web后端开发学习路线 My previous article described how you can get into frontend development. It also discuss ...
- python后端开发学习重点
python后端开发学习 最近在学习python后端开发,简单的总结了python后端开发所需的技术栈,希望对自学python的同学有一点帮助. 1.python语言基础 python的语法特点 py ...
- python后端开发学习内容有哪些?
python后端开发学习内容有哪些? [导语]Python是一个强大的面向对象的程序设计语言,在人工智能领域,在网络爬虫.服务器开发.3D游戏.网络编程.数据分析.Web开发.运维.测试等多个领域都有 ...
- 服务端高并发分布式架构演进之路(转载,图画的好)
这个文章基本上从单机版到最终版,经历了加缓存,加机器,高可用,分布式,最后到云等过程,其实我一直想总结一套类似的东西,没想到有人已经先弄出来了,那就不重复造轮子了,而且我感觉这个文章也是花了功夫的. ...
- 服务端高并发分布式架构演进之路
服务端高并发分布式架构演进之路 概述 基本概念 架构演进 单机架构 第一次演进:Tomcat与数据库分开部署 第二次演进:引入本地缓存和分布式缓存 第三次演进:引入反向代理实现负载均衡 第四次演进:数 ...
- 服务端高并发分布式架构演进之路(阿里巴巴90秒100亿)
服务端高并发分布式架构演进之路 阿里巴巴为什么能抗住90秒100亿? 1. 概述 本文以淘宝作为例子,介绍从一百个到千万级并发情况下服务端的架构的演进过程,同时列举出每个演进阶段会遇到的相关技术,让大 ...
最新文章
- android tab 悬停效果代码,Android 仿腾讯应用宝 之 Toolbar +Scroolview +tab滑动悬停效果...
- 服务器mysql在哪里_mysql的服务器在哪里
- POJ 1118 求平面上最多x点共线
- 全局内存BSS,DATA,RODATA的区别以及其他内存区间相关
- 中科院罗平演讲全文:自动撰写金融文档如何实现,用 AI 解救“金融民工” | CCF-GAIR 2017
- 安装redis提示[test] error 2_技术干货分享:一次flask+redis的微服务实战
- Python弹窗提示警告框MessageBox
- ijkPlayer 集成
- 关于CCSpriteSheet报错问题
- 防火墙 规则与链的分类
- ps还原上一步快捷键_ps怎么返回上一步?PS返回上一步快捷键
- 程序员转项目管理,需要做什么?
- 搜索题集整理(DFSBFS)
- 手把手教学MFC吃豆子教程
- 零基础学Java语言---编程题
- Excel用自动填表快速实现一维表到二维表的转换
- python操作Excel【openpy】
- `云国` 数字公民 · 技术趋势
- -- Could NOT find GFlags (missing: GFLAGS_INCLUDE_DIR GFLAGS_LIBRARY)
- 【教程】dropbox+droppages搭建静态网页
热门文章
- js 打印(JavaScript 打印 CSS样式)
- 由解决方案想到的一些杂七杂八
- Python-Django毕业设计飞羽羽毛球馆管理系统(程序+Lw)
- JVM(JAVA虚拟机)、DVM(Dalvik虚拟机)和ART虚拟机
- 投资理财交易系统设计与开发
- 科普--新装电脑固态多大合适?
- 广东2021年高考成绩查询被屏蔽,广东省教育考试院:2021年广东高考查分入口、查分系统...
- 最新!2016-2019计算机历年校招真题、面经、复习资料总结(11G/2284份文件)
- 任务驱动教学法在JAVA教学中的问题和策略
- Mysql查看表结构的三种方法