并发量限制因素 (五元组)
五元组: (srcip, dstip, srcport, dstport, proto)
- 文件句柄, 文件描述符数量 open files
- 系统内存限制
- 端口数量限制
- 网络带宽的限制 (一般不做考虑)
- 数据库的并发量限制
准备
先将 open files 修改到 100W的上限
查看单个进程可以打开的文件句柄的数目, open files的大小
命令:ulimit
ulimit -a 显示当前所有的资源限制 ulimit -H 设置硬件资源限制 ulimit -S 设置软件资源限制 ulimit -n 设置进程最大打开文件描述符数
我的已经被我自己修改为了100W的量级了
修改方式:
命令修改 : ulimit -n <value> 缺陷:不是永久修改, 不涉及写磁盘, 重启shell之后修改消失
修改配置文件 limits.conf 文件限制着用户可以使用的最大文件数,最大线程,最大内存等资源使用量。 vim /etc/security/limits.conf 涉及写磁盘, 每一次登录shell都会加载配置文件, 永久修改 配置文件记忆技巧, 资源使用限制涉及到系统安全, 故而在security中
并发量:服务器可以承载的客户端的连接数量, 也就是可以维护的 sockfd的数量.
客户端并发量测试
初始版本测试结果如下:
思考局限
srcip 客户端的ip是固定的 srcport 客户端的可用端口数理论值是 65535
dstip + dstport 固定, 服务器ip 跟 端口固定
此时并发量可以达到2.8W, 然后报错不能分配地址了. 其实是客户端端口分配上限了.
如何打破
增加服务器端口数, 此时完全先从五元组确定唯一连接的方向切入,思考出可以增加服务端的监视窗口数量来提高并发量, 打破限制. (多端口, 多窗口监视,有效提升客户接入量)
- 将服务器的端口数开启到100个
- 核心改变代码:
创建监视端口 init_listen_sock
int init_sock(short port) { int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { err_exit("socket"); } sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_port = htons(port); addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; if (-1 == bind(sockfd, (SA*)&addr, sizeof(addr))) { err_exit("bind"); } if (-1 == listen(sockfd, 5)) { err_exit("listen"); } return sockfd; }
//循环向eventloop中添加多监视窗口, 多port int i = 0; for (i = 0; i < LISTEN_PORT; ++i) { sockfd = init_sock(i + port); //将其加入到event_loop中 struct epoll_event ev; ev.events = EPOLLIN; //level 触发 //注册监视事件 struct sockitem* si = (struct sockitem*)malloc(sizeof(struct sockitem)); si->sockfd = sockfd; si->callback = accept_cb;//设置事件处理器 ev.data.ptr = si; epoll_ctl(eventloop->epfd, EPOLL_CTL_ADD, sockfd, &ev); }
- 再测试
经过漫长等待之后它终于还是没有达到100W左右, 而是killed了, 因为内存限制被killed了
其实这个不是正常现象,我这里是因为内存限制而产生了killed, 实际上,内存限制没有打破的情况下也还是无法达到100W, 会出现 connection timeout 连接超时的错误
超时连接问题何在
100 * 2.8W 服务端端口数(100端口) * 客户端端口数 (随机2.8W端口, 之前测试) 可以达到100W,所以接入量的限制不是五元组. 而是其他因素
针对connect的超时连接错误, 我们透过TCP三次握手去看, 问题在于服务端没有向客户端返回一个ACK, 导致了connect 超时. --- server 的ACK为何没有到此时的限制其实在于协议栈了. iptables, 一种过滤装置 防火墙
connection timeout的解决办法
修改 /etc/sysctl.conf 配置文件, 打破限制.
net.nf_conntrack_max 就是防火墙的限制
fs.file-max = 1048576 net.nf_conntrack_max = 1048576 net.ipv4.tcp_rmem =128 256 512 net.ipv4.tcp_wmem =128 256 512
至此其实可以完成百万接入量了, 只是我的服务器内存是在太小, 无法达到要求, 内存足够是可以跑到100W的.
实际开发中的处理方案
采用多进程的方式, 而不是多端口的方式.
测试代码: MAX_PORT : 代表的是端口数, 与服务器端口数保持一致
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/epoll.h> #include <errno.h> #include <netinet/tcp.h> #include <arpa/inet.h> #include <netdb.h> #include <fcntl.h> #define MAX_BUFFER 128 #define MAX_EPOLLSIZE (384*1024) #define MAX_PORT 1 #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; } // s设置好地址可复用 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]; int epoll_fd = epoll_create(MAX_EPOLLSIZE); 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); while (1) { if (++index >= MAX_PORT) index = 0; struct epoll_event ev; int sockfd = 0; if (connections < 340000 && !isContinue) { sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket"); goto err; } //ntySetReUseAddr(sockfd); addr.sin_port = htons(port+index); if (connect(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0) { perror("connect"); goto err; } ntySetNonblock(sockfd); ntySetReUseAddr(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); connections ++; } //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); printf("connections: %d, sockfd:%d, time_used:%d\n", connections, sockfd, time_used); //每一次仅仅只是拿出来 int nfds = epoll_wait(epoll_fd, events, connections, 100); for (i = 0;i < nfds;i ++) { int clientfd = events[i].data.fd; 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); 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 --; close(clientfd); } else { if (errno == EINTR) continue; printf(" Error clientfd:%d, errno:%d\n", clientfd, errno); close(clientfd); } } else { printf(" clientfd:%d, errno:%d\n", clientfd, errno); close(clientfd); } } } usleep(1 * 1000); } return 0; err: printf("error : %s\n", strerror(errno)); return 0; }
文章小结
做并发测试时候出现了问题,我们思考的方式是: 内存限制, open files 文件句柄数限制, 五元组组合限制, 网络带宽, 数据库... 方向入手思考
对于各种系统资源的限制, 可以通过修改配置文件的方式做出永久修改 /etc/security/limits.conf + /etc/sysctl.conf
五元组组合限制 防火墙限制
ulimit -a 查看所有的资源限制, free -h 查看内存限制, htop 动态观察CPU + 内存占用情况, 便于分析异常