Linux服务器百万并发实现与问题排查

简介: Linux服务器百万并发实现与问题排查

前言

  实现一台服务器的百万并发,服务器支撑百万连接会出现哪些问题,如何排查与解决这些问题 是本文的重点

  • 服务器能够同时建立连接的数量 不是 并发量,它只是并发量一个基础。
  • 服务器的并发量:一个服务器能够同时承载客户端的数量;
  • 承载:服务器能够稳定的维持这些连接,能够响应请求,在200ms内返回响应就认为是ok的,其中这200ms包括数据库的操作,网络带宽,内存操作,日志等时间。

  本专栏知识点是通过零声教育的线上课学习,进行梳理总结写下文章,对c/c++linux课程感兴趣的读者,可以点击链接 C/C++后台高级服务器课程介绍 详细查看课程的服务。

测试介绍

 服务器 采用 1台 centos7 12G 1核虚拟机


 客户端 采用 2台 centos7 3G 1核虚拟机


 服务器代码:单reactor单线程,IO多路复用使用epoll


 客户端代码:IO多路复用使用epoll,每个客户端发51w个连接,每个连接发送一次数据,读取一次数据之后不再发送数据

服务器代码

  由于fd的数量未知,这里设计ntyreactor 里面包含 eventblock ,eventblock 包含1024个fd。每个fd通过 fd/1024定位到在第几个eventblock,通过fd%1024定位到在eventblock第几个位置。

struct ntyevent {
    int fd;
    int events;
    void *arg;
    NCALLBACK callback;
    int status;
    char buffer[BUFFER_LENGTH];
    int length;
};
struct eventblock {
    struct eventblock *next;
    struct ntyevent *events;
};
struct ntyreactor {
    int epfd;
    int blkcnt;
    struct eventblock *evblk;
};

客户端代码

#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>
#include <sys/time.h>
#include <unistd.h>
#define MAX_BUFFER    128
#define MAX_EPOLLSIZE (384*1024)
#define MAX_PORT    100
#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;
}
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;
}

error : Too many open files

确定问题

  程序执行到一半,创建了1023个连接后,报错Too many open files

//服务端
new connect [192.168.109.101:36994], pos[1019]
new connect [192.168.109.101:55832], pos[1020]
new connect [192.168.109.101:43460], pos[1021]
new connect [192.168.109.101:59938], pos[1022]
new connect [192.168.109.101:46098], pos[1023]
accept: Too many open files
accept: Too many open files
//客户端
connect: Connection refused
error : Connection refused

  怀疑是文件系统默认允许打开文件描述符数量个数(默认1024)的限制,使用ulimit -a查看open files的数量

  • open files:一个进程能够打开文件描述符的数量
[root@master temp]# ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 47748
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 47748
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

  那么我们把open files调大一点点,看是否会停在2047,如果是,则说明问题就是open files太小的问题,实验发现就是这个原因。

[root@master temp]# ulimit -n 2048
[root@master temp]# ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 47748
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 2048
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 47748
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited
new connect [192.168.109.101:53996], pos[2046]
new connect [192.168.109.101:60742], pos[2047]
accept: Too many open files

解决问题

  1. 临时修改,只在当前这个会话有效:ulimit -n 1048576
  2. 永久修改,对所有会话有效:添加下面两行代码

注意这里修改的是:一个进程能够打开文件描述符的数量

[root@master temp]# vim /etc/security/limits.conf
# 修改
[root@master temp]# reboot
# 重启生效
*               soft    nofile          1048576
*               hard    nofile          1048576
  • 软限制:超出软限制会发出警告
  • 硬限制:绝对限制,在任何情况下都不允许用户超过这个限制

 这里还需要注意一点:file-max : 系统一共可以打开的最大文件数(所有进程加起来)

[root@master temp]# cat /proc/sys/fs/file-max
1202172
# 编辑内核参数配置文件
vim /etc/sysctl.conf
# 修改fs.file-max参数
fs.file-max = 1048576
# 重新加载配置文件
sysctl -p

 另外这里建议ulimit -n 和limits.conf里nofile 设定最好不要超过/proc/sys/fs/file-max的值(虽然我测试了超过也没关系),这个小问题仁者见仁智者见智了,网上找到比较好的文章是这篇linux最大文件句柄数量之(file-max ulimit -n limit.conf)

error : Cannot assign requested address

确定问题

 现在的环境背景:服务器只开放一个端口,客户端不断的去请求去连接。然后客户端error : Cannot assign requested address


 Cannot assign requested address这代表着客户端端口耗尽,我们先来看看如何确定一个fd,反过来说一个fd代表着什么

  socket fd --- < 源IP地址 , 源端口 , 目的IP地址 , 目的端口 , 协议 > 一个fd就是一个五元组,在现在的环境中,五元组里面确定了四个,所以最多创建 1 * 源端口 * 1 * 1 * 1个fd

# 服务端
new connect [192.168.109.101:57921], pos[28234]
new connect [192.168.109.101:57923], pos[28235]
send[fd=21003] error Connection reset by peer
send[fd=22003] error Connection reset by peer
# 客户端
connections: 26999, sockfd:27002, time_used:2399
connections: 27999, sockfd:28002, time_used:2404
connect: Cannot assign requested address
error : Cannot assign requested address

 我们看到大概创建了2.8w的fd , 可是我们知道端口一个有6w多个,也就是说有6w个端口,为什么我们只使用了2.8w个?


 Linux中有限定端口的使用范围:60999 - 32768 = 2.8w ,与我们上面实验结果相符。


The /proc/sys/net/ipv4/ip_local_port_range defines the local port range that is used by TCP and UDP traffic to choose the local port. You will see in the parameters of this file two numbers: The first number is the first local port allowed for TCP and UDP traffic on the server, the second is the last local port number. For high-usage systems you may change its default parameters to 32768-61000 -first-last.


proc/sys/net/ipv4/ip_local_port_range范围定义TCP和UDP通信用于选择本地端口的本地端口范围。您将在该文件的参数中看到两个数字:第一个数字是服务器上允许TCP和UDP通信的第一个本地端口,第二个是最后一个本地端口号。对于高使用率的系统,您可以将其默认参数更改为32768-61000(first-last)。

[root@master temp]# sysctl net.ipv4.ip_local_port_range
net.ipv4.ip_local_port_range = 32768  60999

解决问题

  1. 修改net.ipv4.ip_local_port_range的范围,一般不这样做,我们这里研究的是服务器,怎么会去对客户端进行修改呢
  2. 之前已经说了这个问题的背景,就是只开放了一个端口,并且socket fd --- < 源IP地址 , 源端口, 目的IP地址 , 目的端口 , 运输层协议 >,在这个背景下才产生的这个问题,所以我们可以开放更多的端口,比如说100个,那么一个客户端就能连到280w了

error : Connection timed out

确定问题

  我们将服务器端口开100个,按理说客户端可以连280w,但是现在只连接到13w就error : Connection timed out,与我们的预期不符

//服务端
new connect [192.168.109.101:54585], pos[131165]
new connect [192.168.109.101:48265], pos[131166]
new connect [192.168.109.101:51997], pos[131167]
new connect [192.168.109.101:43239], pos[131168]
send[fd=20102] error Connection reset by peer
send[fd=21102] error Connection reset by peer
send[fd=22102] error Connection reset by peer
//客户端
connections: 127999, sockfd:128002, time_used:7576
connections: 128999, sockfd:129002, time_used:2683
connections: 129999, sockfd:130002, time_used:2669
connections: 130999, sockfd:131002, time_used:4610
connect: Connection timed out
error : Connection timed out

 网卡接收的数据,会发送到协议栈里面,通过sk_buff将数据传到协议栈,协议栈处理完再交给应用程序。由于操作系统在使用的时候,为防止被攻击,在数据发送给协议栈之前进行一个过滤,在协议栈前面加了一个小组件:过滤器,叫做netfilter。

 netfilter主要是对网络数据包进行一个过滤,在netfilter的基础上我们就可以实现防火墙,在linux里面有一个就叫做iptables,iptables是基于netfilter做的,iptables分为两部分,一部分是内核实现的netfilter接口,一部分是应用程序提供给用户使用的。iptables真正实现的是netfilter提供的接口。

  Connection timed out译为连接超时,也就是说,client发送的请求超时了,那么这个超时有两种情况,第一种:三次握手第一次的SYN没发出去,第二种:三次握手第二次ACK没收到。

  netfilter不管对发送的数据,还是对接收的数据,都是可以过滤的。当连接数量达到一定数量的时候,netfilter就会不允许再对外发连接了。所以现在推测是情况1造成的,发送的SYN被netfilter拦截了。

  事实是这样吗,我们来查看一下netfilter允许对外最大连接数量是多少。13w,与我们上面建立成功的数量一致,所以现在就可以确定是netfilter允许对外开放的最大连接数造成的了

[root@node1 temp]# cat /proc/sys/net/netfilter/nf_conntrack_max
131072

解决问题

  我们可以通过设置netfilter允许对外最大连接数量,来解决这个问题

# 查看允许对外最大连接数量
[root@node1 temp]# cat /proc/sys/net/netfilter/nf_conntrack_max
131072
# 进行配置
vim /etc/sysctl.conf
# 在配置文件中把net.nf_conntrack_max参数修改为1048576(如果配置就自己添加一行)
net.nf_conntrack_max = 1048576
# 重新加载配置文件
sysctl -p
# 再次查看,发现生效了
[root@node1 temp]# cat /proc/sys/net/netfilter/nf_conntrack_max
1048576

killed(已杀死)

确定问题

  这里我们先给客户端虚拟机2G的内存,然后发现到24w的时候,客户端进程被杀死了

connections: 239999, sockfd:240002, time_used:9837
connections: 240999, sockfd:241002, time_used:10608
connections: 241999, sockfd:242002, time_used:13109
connections: 242999, sockfd:243002, time_used:15112
connections: 243999, sockfd:244002, time_used:12606
已杀死

  我们来看一下kill记录,发现是内存不足。

[root@node1 ~]# dmesg | egrep -i -B100 'killed process'
[ 2310.265218] Out of memory: Kill process 7266 (C1000Kclient) score 1 or sacrifice child
[ 2310.265962] Killed process 7266 (C1000Kclient) total-vm:8708kB, anon-rss:2960kB, file-rss:0kB, shmem-rss:0kB

  这里直接说原因吧,是因为程序每个fd都有一个tcp接收缓冲区和tcp发送缓冲区。而默认的太大了,导致Linux内存不足,进程被杀死,所有我们需要适当的缩小。进程空间,代码段,堆栈都是要占用内存的。

解决问题

  我们只需要对net.ipv4.tcp_mem,net.ipv4.tcp_wmem,net.ipv4.tcp_rmem进行适合的修改即可

# 编辑内核参数配置文件
vim /etc/sysctl.conf
# 添加以下内容
#           最小值   默认值   最大值
net.ipv4.tcp_mem = 252144 524288 786432 # tcp协议栈的大小,单位为内存页(4K),分别是 1G 2G 3G,如果大于2G,tcp协议栈会进行一定的优化
net.ipv4.tcp_wmem = 1024 1024 2048 # tcp接收缓存区(用于tcp接受滑动窗口)的最小值,默认值和最大值(单位byte)1k 1k 2k,每一个连接fd都有一个接收缓存区
net.ipv4.tcp_rmem = 1024 1024 2048 # tcp发送缓存区(用于tcp发送滑动窗口)的最小值,默认值和最大值(单位byte)1k 1k 2k,每一个连接fd都有一个发送缓存区
# 总缓存 = (每个fd发送缓存区 + 每个fd接收缓存区) * fd数量
# (1024byte + 1024byte ) * 100w 约等于 2G

  如果服务器是用来接收大文件,传输量很大的时候,就要把send buffer和read buffer调大。

  如果服务器只是接收小数据字符的时候。把buffer调小是为了把fd的数量做到更多,并发数量能做到更大。如果buffer调大的话,内存会不够。

百万并发测试结果

出现的问题总结

 想要实现服务器百万并发:

  1. 一个进程能够打开文件描述符的数量open files 和 file-max 改成100w以上
  2. 在不同的环境下要看开放的端口够不够socket fd --- < 源IP地址 , 源端口 , 目的IP地址 , 目的端口 , 协议 >
  3. 设置netfilter允许对外最大连接数量100w以上
  4. 根据内存和场景,适当调整net.ipv4.tcp_mem,net.ipv4.tcp_wmem,net.ipv4.tcp_rmem
目录
相关文章
|
15天前
|
关系型数据库 应用服务中间件 Linux
Linux云服务器如何搭建LNMP环境
LNMP环境是Linux系统中常用的Web服务架构,由Linux、Nginx、MySQL/MariaDB和PHP组成,适用于高效托管动态网站。本文以CentOS 7为例,详细介绍了LNMP环境的搭建步骤,包括Nginx、MariaDB和PHP的安装与配置,以及最终通过创建`index.php`文件验证环境是否成功部署。具体操作涵盖配置YUM仓库、安装服务、编辑配置文件、启动服务等关键步骤,确保用户能够顺利搭建并运行LNMP环境。
43 1
Linux云服务器如何搭建LNMP环境
|
2天前
|
存储 运维 监控
深度体验阿里云系统控制台:SysOM 让 Linux 服务器监控变得如此简单
作为一名经历过无数个凌晨三点被服务器报警电话惊醒的运维工程师,我对监控工具有着近乎苛刻的要求。记得去年那次大型活动,我们的主站流量暴增,服务器内存莫名其妙地飙升到90%以上,却找不到原因。如果当时有一款像阿里云 SysOM 这样直观的监控工具,也许我就不用熬通宵排查问题了。今天,我想分享一下我使用 SysOM 的亲身体验,特别是它那令人印象深刻的内存诊断功能。
|
12天前
|
应用服务中间件 Linux 开发者
用的到linux-tomcat端口占用排查-Day5
通过本文的介绍,详细讲解了在CentOS 8系统上排查和解决Tomcat端口占用问题的方法。从使用 `netstat`、`lsof`和 `ss`命令检查端口占用情况,到使用 `ps`和 `top`命令查找和停止占用端口的进程,再到修改Tomcat端口配置,最后介绍了自动化脚本的方法。希望本文能帮助系统管理员和开发者有效地解决端口占用问题,确保Tomcat服务器的正常运行。
31 11
|
7天前
|
Linux 虚拟化 Docker
Linux服务器部署docker windows
在当今软件开发中,Docker成为流行的虚拟化技术,支持在Linux服务器上运行Windows容器。流程包括:1) 安装Docker;2) 配置支持Windows容器;3) 获取Windows镜像;4) 运行Windows容器;5) 验证容器状态。通过这些步骤,你可以在Linux环境中顺利部署和管理Windows应用,提高开发和运维效率。
48 1
|
2月前
|
安全 大数据 Linux
云上体验最佳的服务器操作系统 - Alibaba Cloud Linux | 飞天技术沙龙-CentOS 迁移替换专场
本次方案的主题是云上体验最佳的服务器操作系统 - Alibaba Cloud Linux ,从 Alibaba Cloud Linux 的产生背景、产品优势以及云上用户使用它享受的技术红利等方面详细进行了介绍。同时,通过国内某社交平台、某快递企业、某手机客户大数据业务 3 大案例,成功助力客户实现弹性扩容能力提升、性能提升、降本增效。 1. 背景介绍 2. 产品介绍 3. 案例分享
|
3月前
|
运维 监控 Linux
推荐几个不错的 Linux 服务器管理工具
推荐几个不错的 Linux 服务器管理工具
258 6
|
3月前
|
存储 Oracle 安全
服务器数据恢复—LINUX系统删除/格式化的数据恢复流程
Linux操作系统是世界上流行的操作系统之一,被广泛用于服务器、个人电脑、移动设备和嵌入式系统。Linux系统下数据被误删除或者误格式化的问题非常普遍。下面北亚企安数据恢复工程师简单聊一下基于linux的文件系统(EXT2/EXT3/EXT4/Reiserfs/Xfs) 下删除或者格式化的数据恢复流程和可行性。
|
1月前
|
Linux
Linux系统之whereis命令的基本使用
Linux系统之whereis命令的基本使用
74 24
Linux系统之whereis命令的基本使用
|
7天前
|
Linux
Linux od命令
本文详细介绍了Linux中的 `od`命令,包括其基本语法、常用选项和示例。通过这些内容,你可以灵活地使用 `od`命令查看文件内容,提高分析和调试效率。确保理解每一个选项和示例的实现细节,应用到实际工作中时能有效地处理各种文件查看需求。
43 19
|
18天前
|
缓存 Ubuntu Linux
Linux中yum、rpm、apt-get、wget的区别,yum、rpm、apt-get常用命令,CentOS、Ubuntu中安装wget
通过本文,我们详细了解了 `yum`、`rpm`、`apt-get`和 `wget`的区别、常用命令以及在CentOS和Ubuntu中安装 `wget`的方法。`yum`和 `apt-get`是高层次的包管理器,分别用于RPM系和Debian系发行版,能够自动解决依赖问题;而 `rpm`是低层次的包管理工具,适合处理单个包;`wget`则是一个功能强大的下载工具,适用于各种下载任务。在实际使用中,根据系统类型和任务需求选择合适的工具,可以大大提高工作效率和系统管理的便利性。
106 25

热门文章

最新文章