目录
一、前言
1.1分析目标:
1s的时间到底可以正确处理响应多少http请求。
1.2 前提条件:
-
讨论的是支持http的qps数,并非是事务级别的tps
-
讨论的基础在限定的机器大小上,典型的4C8G,虚拟机
-
整个讨论以实际为主,结合理论的理想情况
1.3 接近事实的假设
-
如果应用每次处理用时tms,并发可以处理n个,那么可以理论计算出,每秒的并发数=((1*1000)/t)*n。 第一理想假设:即若 t=100, n=10000,那么并发数 (1000 / 100) * 10000=10w,即恰好是别人常提到的单机10wqps。 第二优秀假设:即若 t=133, n=8192,那么并发数(1000/133) * 8192=10w,即恰好是别人常提到的单机6.2wqps。 为什么要做第二优秀假呢?是基于以下知识点,通过我们目前使用的是Springboot 2.x.x,改版本默认的Emebd容器事 Tomcat 9.x + Servlet 3.1+,而Tomcat的默认配置如下:
{
"name": "server.tomcat.max-connections",
"description": "“服务器在任何给定时间接受和处理的最大连接数。",
"defaultValue": 8192
},
{
"name": "server.tomcat.accept-count",
"description": "当所有可能的请求处理线程都在使用时,传入连接请求的最大队列长度。",
"defaultValue": 100
},
{
"name": "server.tomcat.max-http-form-post-size",
"description": "http使用Post请求时候的表达最大容量。",
"defaultValue": "2MB"
},
{
"name": "server.tomcat.max-keep-alive-requests",
"description": "在连接关闭之前可以管道化的最大HTTP请求数。",
"defaultValue": 100
},
{
"name": "server.tomcat.threads.max",
"description": "工作线程的最大数量。",
"defaultValue": 200
},
{
"name": "server.tomcat.threads.min-spare",
"description": "工作线程的最小数量。",
"defaultValue": 10
},
{
"name": "server.servlet.session.timeout",
"description": "会话超时。如果未指定持续时间后缀,将使用秒。",
"defaultValue": "30m" // 这里m表示分钟,s表示秒
},
其中,最大并发连接数就是8192.
此外,Servlet 3.1+开始已经默认切换到NIO,默认使用Http11NioProtocol。也就是说可以用认为在accept请求之后,会将加入到Pool进行性Select,等待事件触发后,会有work线程池来进行业务处理,整个过程是高效的,不会因为有频繁的线程创建和销毁带来更多的耗时。
此外,还有一些基础的假设,就目前Server侧基本都是JVM64位,一般使用JDK8或者JDK11,在JDK8如果不进行类似 -Xss128k 设置,那么默认会为每个线程分配预留1MB的栈空间,因为栈空间是独享的,线程多的时候这个对内存的消耗很明显。在JDK11,线程创建时不再主动分配给保留内存,情况要好一些。
1.4 基础的认知
-
经验数据显示,如果全链路耗时250毫秒左右,那么用户几乎感觉不到任何卡顿,即所谓用户侧的丝滑体验,对于国内一般的网路用去,抛开应用本身的耗时,上行链路100,下行链路50是较为正常的数值,那么此时留给应用本身也就100毫秒可用,也是我们上面的假设。
-
正常情况下,查询本地内存是可以10ms以内搞定的,查询redis走内网50ms也很正常,再加上应用的业务处理10+毫秒,理论上10+毫秒也是可以的
-
此外,从外部接入到ELB再到应用自身的GateWay也是需要25ms+级的耗时的
二、高并发的影响因素
-
操作系统级别:可以并发使用的端口数,fd数
-
硬件级别:CPU核心数,内存的大小,外部IO的速度,网络带宽,其中IO和网络带宽属于外部可控不做讨论。
-
应用级别:每次http请求要使用的内存、CPU大小,以及是否可以共享。
三、具体分析
经过上面两个环节的初步分析,接下来具体分析会容易很多:
3.1 TCP端口数限制
因为http是运行在TCP上的,可以启动TCP连接数直接影响到http的并发数,而影响TCP 连接数的核心就是端口数量。
TCP/IP协议栈从ip_local_port_range中随机选取源端口时,会排除ip_local_reserved_ports中定义的端口,因此不会出现端口被占用导致服务无法启动的问题。使用net.ipv4.ip_local_port_range参数,规划出一段端口段预留作为服务的端口,这种方法是可以解决当前问题。 一般Linux的默认随机端口范围是:32768 - 60999 (可以通过查看配置文件的方式来获取)
1.端口号0不使用 2.端口号1-1023,系统默认只给root使用 3.端口号1024-4999由客户端程序自由分配 4.端口号5000-65535由服务器程序自由分配
可以看到,可以使用的端口至少3w+
3.2 TCP打开最大文件数限制(fd)
ulimit:显示(或设置)用户可以使用的资源的限制(limit),这限制分为软限制(当前限制)和硬限制(上限),其中硬限制是软限制的上限值,应用程序在运行过程中使用的系统资源不超过相应的软限制,任何的超越都导致进程的终止。
ulimit -n 4096 将每个进程可以打开的文件数目加大到4096,缺省为1024,缺省表示当前用户的每个进程最多允许同时打开1024个文件,这1024个文件中还得除去每个进程必然打开的标準输入,标準输出,标準错误,监听socket,进程间通讯的unix域socket等文件,那麼剩下的可用於客户端socket连接的文件数就只有大概1024-10=1014个左右。也就是说缺省情况下,基於Linux的通讯程序最多允许同时1014个TCP并发连接。 对於想支持更高数量的TCP并发连接的通讯处理程序,就必须修改Linux对当前用户的进程同时打开的文件数量的软限制(soft limit)和硬限制(hardlimit)。其中软限制是指Linux在当前系统能够承受的范围内进一步限制用户同时打开的文件数;硬限制则是根据系统硬件资源状况(主要是系统内存)计算出来的系统最多可同时打开的文件数量。通常软限制小於或等於硬限制。 那么,硬件限制如何看呢?
$ cat /proc/sys/fs/file-max
12158
这表明这台Linux系统最多允许同时打开(即包含所有用户打开文件数总和)12158个文件,是Linux系统级硬限制,所有用户级的打开文件数限制都不应超过这个数值。通常这个系统级硬限制是Linux系统在啟动时根据系统硬件资源状况计算出来的最佳的最大同时打开文件数限制,如果没有特殊需要,不应该修改此限制,除非想為用户级打开文件数限制设置超过此限制的值。 到此,对于耽搁进程最大文件书的限制已经搞清楚了,直接执行cat /proc/sys/fs/file-max查看即可。对于4C8G的性能机器来说,一般都在1w+以上。实际上就是大于8192,相当于不构成限制。
3.3 TCP最大连接数限制
第二种无法建立TCP连接的原因可能是因為Linux网络内核的IP_TABLE防火墙对最大跟踪的TCP连接数有限制。此时程序会表现為在connect()调用中阻塞,如同死机,如果用tcpdump工具监视网络,也会发现根本没有TCP连接时客户端发SYN包的网络流量。由於IP_TABLE防火墙在内核中会对每个TCP连接的状态进行跟踪,跟踪信息将会放在位於内核内存中的conntrackdatabase中,这个数据库的大小有限,当系统中存在过多的TCP连接时,数据库容量不足,IP_TABLE无法為新的TCP连接建立跟踪信息,於是表现為在connect()调用中阻塞。此时就必须修改内核对最大跟踪的TCP连接数的限制,方法同修改内核对本地端口号范围的限制是类似的: 第一步,修改/etc/sysctl.conf文件,在文件中添加如下行:
net.ipv4.ip_conntrack_max = 10240
可以看到,这里也是可以修改成为1w+的。
另外,还有三个参数
1)加快 tcp 连接的回收(recycle):
采用 Nginx 反向代理服务后,操作系统会产生较多 TIME_WAIT 的 TCP(Transmission Control Protocol)连接,操作系统默认 TIME_WAIT 的 TCP 连接回收时间是 2 分钟,这样会使回收 TCP 过慢导致系统吞吐量下降,甚至出现 502 访问失败问题!如此大并发的场景下必须要加快 tcp 链接的回收!
执行命令:echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle #默认为0 表示不快速回收120s后回收
2)空的 tcp 是否允许回收利用(reuse)
先来了解一下 tcp_tw_reuse?
tcp_tw_reuse 设置允许在 TIME_WAIT 状态下重复使用一对套接字,由内核确保不会有类似于重复序列号的问题。由于两端都使用了时间戳,所以能够避免序列号重复。也可以基于某些关闭标识如 FIN(表示不会有新的流量),来重复使用套接字。echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse #默认为0 表示允许回收利用
3)洪水攻击(不做洪水攻击抵御)
什么是洪水攻击?
洪水攻击(FLOOD ATTACK) 是指利用计算机网络技术向目标主机发送大量无用的数据报文,使得目标主机忙于处理无用的数据报文而无法提供正常服务的网络行为!如果高并发,linux 内核会判断为是洪水攻击行为,会自动为我们抵御这种它认为是洪水攻击的行为,其实不是!
那么我们就要关闭这行机制!————————————————
原文作者:huxiaobai_001
转自链接:https://learnku.com/articles/42203
3.4 CPU限制
这个比较难估计,根据不同的业务逻辑差别比较大,但是对于一般读多写少的应用,响应的都会使用多级缓存了,缓存的应用不仅减少了对数据库的压力,也导致对业务逻辑的计算减少,对于一般业务,通常如果不是程序中有类似死循环这种BUG,可以认为业务逻辑不会对CPU造成什么压力的,尤其是对于一个4C的CPU.对于写多读少的业务,通常需要结合消息队列的削峰填谷+异步处理了。
3.4 内存的限制
内存的限制就是比较重点的了。 线程的内存占用 在Java中,每个线程需要占用的内存主要来自以下几个方面:
-
线程栈:每个线程在Java中都有其独立的栈空间,用于存放局部变量、方法调用和返回地址等。Java线程栈的默认大小通常是1MB,但可以通过JVM参数进行调整,例如使用 -Xss 来设置栈大小。
-
堆内存:虽然堆内存是共享的,但任何线程创建对象(比如实例化其他类的对象)时,会在堆上分配内存。
-
线程元数据:包括线程的状态信息、程序计数器和其他必要的管理信息。这部分内存的占用因JVM实现而异。
总体而言,线程的内存占用因许多因素(如堆大小、栈大小和运行的应用程序的具体状态)而异。
1)实际经验来看,我们一般的http线程在运行过程中,会用到多大的线程栈呢,就是局部变量、方法参数和返回值等占用的大小,激进的人建议设置为128K,我觉得没问题,但我觉得考虑分析的普适性就设置为-Xss 256K是Ok的,毕竟还有Native Stack,以及程序计数器、线程元数据的占用。这一块是线程独享的,也就是占用内存的大小直接与线程数成正线性关系。 2)堆内存,实际使用过程中差别较大,也是所有线程共享的,这是实际存放对象的地方,但考虑到通常本地的内存缓存,可以合理的推测为256K * 并发线程数,也就是与栈占用一样的。
接下来就可以计算一下了,可以并发运行的线程数了:(8 * 1024 * 1024)/512=16384,而这个数字也是远大于8192的并发连接数的,也即这并不构成大并发的瓶颈。
此外,在运行过程中我们也不可能让内存跑到100%,稳定在80%是比较理想的状态,那么进一步 16384 * 80%=13107,这个数值也是高于1w的。
3.4 结论分析
经过上述的分析,对于一台4C8G的虚拟机,其同时并发是可以轻松达到8192的,甚至说轻松1w。剩下影响每秒总的高并发的就是应用的响应时间了。这里我们算上应用本身+GateWay+ELB时间为200ms,属于正常较好的值。那么最后一个合理的每秒并发数就是:10000 * 1000/200=5w(qps).
最终,这个5w的qps是可以保证应用在充分压榨机器性能同时又能在一个合理水平稳定运行的实际QPS。
参考
3.为什么 1 万的并发和 5 万的并发, CPU 占用没有变化
4.Linux端口数
5.ulimit详解
8.秒杀
9.Tomcat配置