服务端百万并发测试

  • 服务端并发的概念
    • 常见单机服务模型
  • 并发测试方法
    • 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 服务端百万并发测试相关推荐

  1. 【云风skynet】详解skynet的多核高并发编程丨actor模型丨游戏开发丨游戏服务端开发丨多线程丨Linux服务器开发丨后端开发

    skynet中多核高并发编程给我们的启发 1. 多核并发编程 2. actor模型详解 3. 手撕一个万人同时在线游戏 视频讲解如下,点击观看: [云风skynet]详解skynet的多核高并发编程丨 ...

  2. 适合新手:从零开发一个IM服务端(基于Netty,有完整源码)

    本文由"yuanrw"分享,博客:juejin.im/user/5cefab8451882510eb758606,收录时内容有改动和修订. 0.引言 站长提示:本文适合IM新手阅读 ...

  3. swagger 返回json字符串_[Swagger] Swagger Codegen 高效开发客户端对接服务端代码

    [Swagger] Swagger Codegen 高效开发客户端对接服务端代码 @TOC 手机用户请横屏获取最佳阅读体验,REFERENCES中是本文参考的链接,如需要链接和更多资源,可以关注其他博 ...

  4. web后端开发学习路线_学习后端Web开发的最佳方法

    web后端开发学习路线 My previous article described how you can get into frontend development. It also discuss ...

  5. python后端开发学习重点

    python后端开发学习 最近在学习python后端开发,简单的总结了python后端开发所需的技术栈,希望对自学python的同学有一点帮助. 1.python语言基础 python的语法特点 py ...

  6. python后端开发学习内容有哪些?

    python后端开发学习内容有哪些? [导语]Python是一个强大的面向对象的程序设计语言,在人工智能领域,在网络爬虫.服务器开发.3D游戏.网络编程.数据分析.Web开发.运维.测试等多个领域都有 ...

  7. 服务端高并发分布式架构演进之路(转载,图画的好)

    这个文章基本上从单机版到最终版,经历了加缓存,加机器,高可用,分布式,最后到云等过程,其实我一直想总结一套类似的东西,没想到有人已经先弄出来了,那就不重复造轮子了,而且我感觉这个文章也是花了功夫的. ...

  8. 服务端高并发分布式架构演进之路

    服务端高并发分布式架构演进之路 概述 基本概念 架构演进 单机架构 第一次演进:Tomcat与数据库分开部署 第二次演进:引入本地缓存和分布式缓存 第三次演进:引入反向代理实现负载均衡 第四次演进:数 ...

  9. 服务端高并发分布式架构演进之路(阿里巴巴90秒100亿)

    服务端高并发分布式架构演进之路 阿里巴巴为什么能抗住90秒100亿? 1. 概述 本文以淘宝作为例子,介绍从一百个到千万级并发情况下服务端的架构的演进过程,同时列举出每个演进阶段会遇到的相关技术,让大 ...

最新文章

  1. android tab 悬停效果代码,Android 仿腾讯应用宝 之 Toolbar +Scroolview +tab滑动悬停效果...
  2. 服务器mysql在哪里_mysql的服务器在哪里
  3. POJ 1118 求平面上最多x点共线
  4. 全局内存BSS,DATA,RODATA的区别以及其他内存区间相关
  5. 中科院罗平演讲全文:自动撰写金融文档如何实现,用 AI 解救“金融民工” | CCF-GAIR 2017
  6. 安装redis提示[test] error 2_技术干货分享:一次flask+redis的微服务实战
  7. Python弹窗提示警告框MessageBox
  8. ijkPlayer 集成
  9. 关于CCSpriteSheet报错问题
  10. 防火墙 规则与链的分类
  11. ps还原上一步快捷键_ps怎么返回上一步?PS返回上一步快捷键
  12. 程序员转项目管理,需要做什么?
  13. 搜索题集整理(DFSBFS)
  14. 手把手教学MFC吃豆子教程
  15. 零基础学Java语言---编程题
  16. Excel用自动填表快速实现一维表到二维表的转换
  17. python操作Excel【openpy】
  18. `云国` 数字公民 · 技术趋势
  19. -- Could NOT find GFlags (missing: GFLAGS_INCLUDE_DIR GFLAGS_LIBRARY)
  20. 【教程】dropbox+droppages搭建静态网页

热门文章

  1. js 打印(JavaScript 打印 CSS样式)
  2. 由解决方案想到的一些杂七杂八
  3. Python-Django毕业设计飞羽羽毛球馆管理系统(程序+Lw)
  4. JVM(JAVA虚拟机)、DVM(Dalvik虚拟机)和ART虚拟机
  5. 投资理财交易系统设计与开发
  6. 科普--新装电脑固态多大合适?
  7. 广东2021年高考成绩查询被屏蔽,广东省教育考试院:2021年广东高考查分入口、查分系统...
  8. 最新!2016-2019计算机历年校招真题、面经、复习资料总结(11G/2284份文件)
  9. 任务驱动教学法在JAVA教学中的问题和策略
  10. Mysql查看表结构的三种方法