并发量和承载的概念
并发量:一个服务器能同时承载客户端的数量
承载:客户端发送给服务器的请求(http或tcp等)在200ms内可以返回正确的结果
影响并发量的主要因素
- 数据库响应速度
- 网络带宽响应
- 内存操作
- 日志性能
服务器能同时建立的连接数量只是并发的基础,服务器处理客户端的请求包括请求、响应、处理和关闭。
网络五元组
- 源ip
- 目的ip
- 源端口号
- 目的端口号
- 协议
由于只有一台客户端不断建立连接,所以源ip是一样的;目的ip也只有一台服务器;源端口号是不确定的,由操作系统或自己指定,目的端口号则是指定的8080端口;协议使用tcp协议。此时只有源端口号是不唯一的。这五个元素只要有一个是不同的,就对应一个不同的fd。
第一次并发量测试
使用之前的epoll_reactor.c的服务器代码进行测试,使用网络调试助手进行连接测试,成功连接,发生两次数据,然后断开。
wang@ubuntu:~/Desktop/0vicevip/5.1$ gcc epoll_reactor.c -o epoll_reactor
wang@ubuntu:~/Desktop/0vicevip/5.1$ ./epoll_reactor 8080
recv from 192.168.211.1 at port 57216
Recv: http://www.cmsoft.cn, 20 Bytes
Recv: http://www.cmsoft.cn, 20 Bytes
disconnect 5
再将客户端连接代码mulport_client_epoll.c中的最大连接端口宏定义先修改为1
编译之后,先启动服务端再启动客户端
wang@ubuntu:~/Desktop/0vicevip/5.1$ ./mulport 192.168.211.128 8080
connections: 999, sockfd:1002, time_used:1915
socket: Too many open files
error : Too many open files
查看单进程可操作性文件描述符数
根据错误提示可知,是由于打开太多文件导致的,那么我们先查看一下当前系统允许操作的文件描述符数量。
wang@ubuntu:~/Desktop/0vicevip/5.1$ 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) 15391
max locked memory (kbytes, -l) 65536
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) 15391
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
由open files (-n) 1024
可得,最多打开的文件描述符个数为1024
修改单进程可操作性文件描述符数
有两种方法
- 使用
ulimit -n 数字
可以进行临时修改,重启当前终端后,修改效果消失 - 修改
/etc/security/limits.conf
配置文件中的内容
在文件尾部加入中间两行配置
#<domain> <type> <item> <value>
* hard nofile 1048576 ===》Hard limit限制,下面的设置不能超过该数
* soft nofile 1048576 ===》
#说明domain:域 记录那个用户组的程序有效 * 所有用户 root:root用户
type:soft(软限制) hard(硬限制) 软:能超过value,但是超过后操作系统回回收,硬不能超过设置的数量
value:对应数量
但是有个bug,切换后没有效果,重启也没有办法,切换到root用户,再切换回来就生效了,太奇怪了。每次这么搞好麻烦,还不如直接方法1修改,或者一直用root。
修改系统能打开的最大文件描述符数
同时还有个地方需要修改,就是在/etc/sysctl.conf
中,需要设置系统可以打开的文件描述符最大值。
# 系统最大文件打开数
fs.file-max = 20000000
# 单个用户进程最大文件打开数
fs.nr_open = 20000000
指向sysctl -p /etc/sysctl.conf
后生效,可通过cat /proc/sys/fs/file-max
查看。
第二次并发量测试
再次运行服务端和客户端
root@ubuntu:/home/wang/Desktop/0vicevip/5.1# ./mulport 192.168.211.128 8080
connections: 999, sockfd:1002, time_used:1857
connections: 1999, sockfd:2002, time_used:1898
connections: 2999, sockfd:3002, time_used:1908
connections: 3999, sockfd:4002, time_used:1881
connections: 4999, sockfd:5002, time_used:1886
connections: 5999, sockfd:6002, time_used:1924
connections: 6999, sockfd:7002, time_used:1950
connections: 7999, sockfd:8002, time_used:1940
connections: 8999, sockfd:9002, time_used:1931
connections: 9999, sockfd:10002, time_used:1956
connections: 10999, sockfd:11002, time_used:3026
connections: 11999, sockfd:12002, time_used:1987
connections: 12999, sockfd:13002, time_used:1992
connections: 13999, sockfd:14002, time_used:1940
connections: 14999, sockfd:15002, time_used:3805
connections: 15999, sockfd:16002, time_used:4060
connections: 16999, sockfd:17002, time_used:4240
connections: 17999, sockfd:18002, time_used:4179
connections: 18999, sockfd:19002, time_used:4220
connections: 19999, sockfd:20002, time_used:5522
connections: 20999, sockfd:21002, time_used:4605
connections: 21999, sockfd:22002, time_used:5540
connections: 22999, sockfd:23002, time_used:4621
connections: 23999, sockfd:24002, time_used:4711
connections: 24999, sockfd:25002, time_used:5049
connections: 25999, sockfd:26002, time_used:4931
connections: 26999, sockfd:27002, time_used:5046
connections: 27999, sockfd:28002, time_used:6185
connect: Cannot assign requested address
error : Cannot assign requested address
观察最后三行,并发量到达2.8W,连接出错。客户端提示无法分配地址,这是由于客户端端口限制导致的,系统分配给程序的端口已经用完了,linux系统默认0-1023为常用端口,1024-49151为注册服务的端口,49152-65535为自定义端口。
修改可用端口范围
此端口的范围可以通过range_from来修改。
#方法1 cat /proc/sys/net/ipv4/ip_local_port_range
root@ubuntu:/home/wang/Desktop/0vicevip/5.1# cat /proc/sys/net/ipv4/ip_local_port_range
32768 60999
#方法2 在/etc/sysctl.conf配置文件中直接修改
root@ubuntu:/home/wang/Desktop/0vicevip/5.1# sysctl -p /etc/sysctl.conf
net.ipv4.ip_local_port_range = 32768 65000
root@ubuntu:/home/wang/Desktop/0vicevip/5.1# cat /proc/sys/net/ipv4/ip_local_port_range
32768 65000
root@ubuntu:/home/wang/Desktop/0vicevip/5.1#
再次测试
connections: 31999, sockfd:32002, time_used:7004
connect: Cannot assign requested address
error : Cannot assign requested address
由于客户端可用端口号增加了4000,所以连接数也增加了4000,但是依然远远不够,我们可以考虑让服务端监听100个端口,我们通过客户端来访问这一百个端口。服务端端的代码同时也做出修改。(实际开发中不会采用这种方式提高并发量,一般使用多进程fork的方式来实现(只能使用进程不能使用线程,因为每个进程都有自己的文件系统空间,但是线程是共享的))
第三次并发量测试
connections: 102999, sockfd:103002, time_used:2009
connections: 103999, sockfd:104002, time_used:2133
RecvBuffer:Hello Server: client --> 103998
data from 4
data from 5
data from 6
data from 7
data from 8
data from 9
data from 10
。。。
data fro
Error clientfd:141002, errno:11
connect: Connection refused
error : Connection refused
连接数到达十万,由于网络协议栈中的netfilter组件配置限制的原因。
netfilter
客户端发送的data,由网卡通过sk_buffer组件传输到网络协议栈,到达网络协议栈时,会经过一个netfilter组件(操作系统为防止大量无效的数据攻击,在数据进入网络协议栈之前加一个层过滤器netfilter规则,对网络数据包进行过滤)
Linux系统中的iptables防火墙就是基于netfilter实现的。iptables一共两部分,一部分是内核实现的netfilter的接口,有一部分是提供出来应用程序可以用的东西,即通过iptables命令操作的一些内容(转发、拒绝某个接口数据等功能)。
在/etc/sysctl.conf
中添加如下配置,然后sysctl -p /etc/sysctl.conf
生效即可。
cat /proc/sys/net/netfilter/nf_conntrack_max
查看允许连接数。
//最大跟踪连接数,默认是65536个
net.nf_conntrack_max = 1048576
//稳定的连接状态最多保留多久,单位是秒,默认是432000秒,就是5天
net.netfilter.nf_conntrack_tcp_timeout_established = 1200
性能调优
修改了系统对fd的限制,单个进程对fd的限制,接下来就是内存的限制,tcp相关收发缓冲区,tcp内存设置,以及tcp相关参数设置。
# 全连接队列长度,默认128
net.core.somaxconn = 10240
# 半连接队列长度,当使用sysncookies无效,默认128
net.ipv4.tcp_max_syn_backlog = 16384
net.ipv4.tcp_syncookies = 0
# 网卡数据包队列长度
net.core.netdev_max_backlog = 41960
# time-wait 最大队列长度
net.ipv4.tcp_max_tw_buckets = 300000
# time-wait 是否重新用于新链接以及快速回收
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
# tcp报文探测时间间隔, 单位s
net.ipv4.tcp_keepalive_intvl = 30
# tcp连接多少秒后没有数据报文时启动探测报文
net.ipv4.tcp_keepalive_time = 900
# 探测次数
net.ipv4.tcp_keepalive_probes = 3
# 保持fin-wait-2 状态多少秒
net.ipv4.tcp_fin_timeout = 15
# 最大孤儿socket数量,一个孤儿socket占用64KB,当socket主动close掉,处于fin-wait1, last-ack
net.ipv4.tcp_max_orphans = 131072
# 每个套接字所允许得最大缓存区大小
net.core.optmem_max = 819200
# 默认tcp数据接受窗口大小
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
# tcp栈内存使用第一个值内存下限, 第二个值缓存区应用压力上限, 第三个值内存上限, 单位为page,通常为4kb
net.ipv4.tcp_mem = 786432 4194304 8388608
# 读, 第一个值为socket缓存区分配最小字节, 第二个,第三个分别被rmem_default, rmem_max覆盖
net.ipv4.tcp_rmem = 4096 4096 4206592
# 写, 第一个值为socket缓存区分配最小字节, 第二个,第三个分别被wmem_default, wmem_max覆盖
net.ipv4.tcp_wmem = 4096 4096 4206592
tcp_mem
- low:当TCP使用了低于该值的内存页面数时,TCP不会考虑释放内存。
- pressure:当TCP使用了超过该值的内存页面数量时,TCP试图稳定其内存使用,进入压力模式,当内存消耗低于low值时则退出压力状态。
- high:允许所有tcp sockets用于排队缓冲数据报的页面量,当内存占用超过此值,系统拒绝分配socket,后台日志输出“TCP: too many orphaned sockets”或“Out of socket memory”
每个tcp连接对应一个读缓冲区recvbuffer和写缓冲区sendbuffer,可以使用修改配置文件,也可以通过在程序中使用setsockopt函数。
tcp_rmem 读缓冲区
- low:默认是4096,缓冲区的最小大小
- pressure:默认是87380,接收缓冲区的初始大小
- high:允许接收缓冲区的最大大小
tcp_wmem 写缓冲区
- low:默认是4096,缓冲区的最小大小
- pressure:默认是16384,发送缓冲区的初始大小
- high:允许发送缓冲区的最大大小
高并发设计原则
自己的服务器程序,调试网络协议栈参数的原则:
- 传输文件数据量大时,将buffer调大。
- 数据量较小时,可将buff调小,当buffer为512时,ssh无法连接,且日志也无法打印。调小的目的,就是为了增加并发量,可以把fd的数量做到更多,占用的内存更少。
第四次并发量测试
因为只有一台设备,也懒得程序装额外的虚拟机,所以所有的测试都是在同一台上。最终的测试结果达到了70万连接。(当时虚拟机已经卡住了,我一看情况不对,赶紧先把图截下来,哈哈哈)伪c1000k并发完成!
抖动
每k连接建立,所用的时长差距较大。原因是只有一个线程在处理accept,处理不过来,半连接队列已满,只能等待。
解决方法
- accept和recv/send分开。将网络连接丢给一个线程去处理,分配fd后,交给线程池处理。参考redis
- 开多个线程accept,参考nginx
c1000k服务器客户端流程
C10M应该如何实现
C1000k,只需使用一个epoll的reactor模型就可以实现,那么如何实现千万级的并发呢?要解决这个问题,最重要就是跳过内核协议栈的冗长路径,把网络包直接送到要处理的应用程序那里去。有两种常见的机制,DPDK 和 XDP。
(1) DPDK:是用户态网络的标准。它跳过内核协议栈,直接由用户态进程通过轮询的方式,来处理网络接收。每时每刻都有新的网络包需要处理,轮询的优势就很明显。
(2) XDP:是 Linux 内核提供的一种高性能网络数据路径。它允许网络包,在进入内核协议栈之前,就进行处理,也可以带来更高的性能。