大家先思考一个问题,这也是在面试过程中经常遇到的问题。
如果你们公司现在的产品能够支持10W用户访问,你们老板突然和你说,融到钱了,会大量投放广告,预计在1个月后用户量会达到1000W,如果这个任务交给你,你应该怎么做?
如何支撑1000W用户其实是一个非常抽象的问题,对于技术开发来说,我们需要一个非常明确的对于执行关键业务上的性能指标数据,比如,高峰时段下对于事务的响应时间、并发用户数、QPS、成功率、以及基本指标要求等,这些都 必须要非常明确,只有这样才能够指导整个架构的改造和优化。所以,如果大家接到这样一个问题,首先需要去定位到问题的本质,也就是首先得知道一些可量化的数据指标。
在估算响应时间、并发用户数、TPS、成功率这些关键指标的同时,你仍需要关心具体的业务功能维度上的需求,每个业务功能都有各自的特点,比如有些场景可以不需要同步返回明确执行结果,有些业务场景可以接受返回“系统忙,请等待!”这样暴力的消息,以避免过大的处理流量所导致的大规模瘫痪,因此,学会平衡这些指标之间的关系是必要的,大多数情况下最好为这些指标做一个优先级排序,并且尽量只考察几个优先级高的指标要求。(SLA服务等级)
SLA:Service-Level Agreement的缩写,意思是服务等级协议。服务的SLA是服务提供者对服务消费者的正式承诺,是衡量服务能力等级的关键项。服务SLA中定义的项必须是可测量的,有明确的测量方法。
在分析上述问题之前,先给大家普及一下,系统相关的一些关键衡量指标。
TPS(Transaction Per Second)每秒处理的事务数。
站在宏观角度来说,一个事务是指客户端向服务端发起一个请求,并且等到请求返回之后的整个过程。从客户端发起请求开始计时,等到收到服务器端响应结果后结束计时,在计算这个时间段内总共完成的事务个数,我们称为TPS。
站在微观角度来说,一个数据库的事务操作,从开始事务到事务提交完成,表示一个完整事务,这个是数据库层面的TPS。
QPS(Queries Per Second)每秒查询数,表示服务器端每秒能够响应的查询次数。这里的查询是指用户发出请求到服务器做出响应成功的次数,可以简单认为每秒钟的Request数量。
针对单个接口而言,TPS和QPS是相等的。如果从宏观层面来说,用户打开一个页面到页面渲染结束代表一个TPS,那这个页面中会调用服务器很多次,比如加载静态资源、查询服务器端的渲染数据等,就会产生两个QPS,因此,一个TPS中可能会包含多个QPS。
QPS=并发数/平均响应时间
RT(Response Time),表示客户端发起请求到服务端返回的时间间隔,一般表示平均响应时间。
并发数是指系统同时能处理的请求数量。
需要注意,并发数和QPS不要搞混了,QPS表示每秒的请求数量,而并发数是系统同时处理的请求数量,并发数量会大于QPS,因为服务端的一个连接需要有一个处理时长,在这个请求处理结束之前,这个连接一直占用。
举个例子,如果QPS=1000,表示每秒钟客户端会发起1000个请求到服务端,而如果一个请求的处理耗时是3s,那么意味着总的并发=1000*3=3000,也就是服务端会同时有3000个并发。
上面说的这些指标,怎么计算呢?举个例子。
假设在10点到11点这一个小时内,有200W个用户访问我们的系统,假设平均每个用户请求的耗时是3秒,那么计算的结果如下:
从这个计算过程中发现,随着RT的值越大,那么并发数就越多,而并发数代表着服务器端同时处理的连接请求数量,也就意味服务端占用的连接数越多,这些链接会消耗内存资源以及CPU资源等。所以RT值越大系统资源占用越大,同时也意味着服务端的请求处理耗时较长。
但实际情况是,RT值越小越好,比如在游戏中,至少做到100ms左右的响应才能达到最好的体验,对于电商系统来说,3s左右的时间是能接受的,那么如何缩短RT的值呢?
继续回到最开始的问题,假设没有历史数据供我们参考,我们可以使用2/8法则来进行预估。
大概预估出了后端服务器需要支撑的最高并发的峰值之后,就需要从整个系统架构层面进行压力预估,然后配置合理的服务器数量和架构。既然是这样,那么首先需要知道一台服务器能够扛做多少的并发,那这个问题怎么去分析呢?我们的应用是部署在Tomcat上,所以需要从Tomcat本身的性能下手。
下面这个图表示Tomcat的工作原理,该图的说明如下。
从这个图中可以得出,限制Tomcat请求数量的因素四个方面。
我想可能大家遇到过类似“Socket/File:Can't open so many files”的异常,这个就是表示linux系统中的文件句柄限制。
在Linux中,每一个TCP连接会占用一个文件描述符(fd),一旦文件描述符超过Linux系统当前的限制,就会提示这个错误。
我们可以通过下面这条命令来查看一个进程可以打开的文件数量
ulimit -a 或者 ulimit -n
open files (-n) 1024 是linux操作系统对一个进程打开的文件句柄数量的限制(也包含打开的套接字数量)
这里只是对用户级别的限制,其实还有个是对系统的总限制,查看系统总线制:
cat /proc/sys/fs/file-max
file-max是设置系统所有进程一共可以打开的文件数量 。同时一些程序可以通过setrlimit调用,设置每个进程的限制。如果得到大量使用完文件句柄的错误信息,是应该增加这个值。
当出现上述异常时,我们可以通过下面的方式来进行修改(针对单个进程的打开数量限制)
vi /etc/security/limits.conf
root soft nofile 65535
root hard nofile 65535
* soft nofile 65535
* hard nofile 65535
另外还要注意,要确保针对进程级别的文件打开数量反问是小于或者等于系统的总限制,否则,我们需要修改系统的总限制。
vi /proc/sys/fs/file-max
TCP连接对于系统资源最大的开销就是内存。
因为tcp连接归根结底需要双方接收和发送数据,那么就需要一个读缓冲区和写缓冲区,这两个buffer在linux下最小为4096字节,可通过cat /proc/sys/net/ipv4/tcp_rmem和cat /proc/sys/net/ipv4/tcp_wmem来查看。
所以,一个tcp连接最小占用内存为4096+4096 = 8k,那么对于一个8G内存的机器,在不考虑其他限制下,最多支持的并发量为:810241024/8 约等于100万。此数字为纯理论上限数值,在实际中,由于linux kernel对一些资源的限制,加上程序的业务处理,所以,8G内存是很难达到100万连接的,当然,我们也可以通过增加内存的方式增加并发量。
我们知道Tomcat是JAVA程序,运行在JVM上,因此我们还需要对JVM做优化,才能更好的提升Tomcat的性能,简单带大家了解一下JVM,如下图所示。
在JVM中,内存划分为堆、程序计数器、本地方发栈、方法区(元空间)、虚拟机栈。
其中,堆内存是JVM内存中最大的一块区域,几乎所有的对象和数组都会被分配到堆内存中,它被所有线程共享。 堆空间被划分为新生代和老年代,新生代进一步划分为Eden和Surivor区,如下图所示。
新生代和老年代的比例是1:2,也就是新生代会占1/3的堆空间,老年代会占2/3的堆空间。 另外,在新生代中,空间占比为Eden:Surivor0:Surivor1=8:1:1 。 举个例子来说,如果eden区内存大小是40M,那么两个Survivor区分别是占5M,整个新生代就是50M,然后计算出老年代的内存大小是100M,也就是说堆空间的总内存大小是150M。
可以通过 java -XX:PrintFlagsFinal -version查看默认参数
uintx InitialSurvivorRatio = 8 uintx NewRatio = 2
InitialSurvivorRatio: 新生代Eden/Survivor空间的初始比例
NewRatio : Old区/Young区的内存比例
堆内存的具体工作原理是:
GC标记-清除算法 在执行过程中暂停其他线程??
程序计数器是用来记录各个线程执行的字节码地址等,当线程发生上下文切换时,需要依靠这个来记住当前执行的位置,当下次恢复执行后要沿着上一次执行的位置继续执行。
方法区是逻辑上的概念,在HotSpot虚拟机的1.8版本中,它的具体实现就是元空间。
方法区主要用来存放已经被虚拟机加载的类相关信息,包括类元信息、运行时常量池、字符串常量池,类信息又包括类的版本、字段、方法、接口和父类信息等。
方法区和堆空间类似,它是一个共享内存区域,所以方法区是属于线程共享的。
Java虚拟机栈是线程私有的内存空间,当创建一个线程时,会在虚拟机中申请一个线程栈,用来保存方法的局部变量、操作数栈、动态链接方法等信息。每一个方法的调用都伴随这栈帧的入栈操作,当一个方法返回之后,就是栈帧的出栈操作。
本地方法栈和虚拟机栈类似,本地方法栈是用来管理本地方法的调用,也就是native方法。
了解了上述基本信息之后,那么JVM中内存应该如何设置呢?有哪些参数来设置?
而在JVM中,要配置的几个核心参数无非是。
JVM内存的大小,取决于机器的配置,比如一个2核4G的服务器,能够分配给JVM进程也就2G左右,因为机器本身也需要内存,而且机器上还运行了其他的进程也需要占内存。而这2G还得分配给栈内存、堆内存、元空间,那堆内存能够得到的也就1G左右,然后堆内存还要分新生代、老年代。
The maximum number of request processing threads to be created by this Connector, which therefore determines the maximum number of simultaneous requests that can be handled. If not specified, this attribute is set to 200. If an executor is associated with this connector, this attribute is ignored as the connector will execute tasks using the executor rather than an internal thread pool. Note that if an executor is configured any value set for this attribute will be recorded correctly but it will be reported (e.g. via JMX) as -1 to make clear that it is not used.
server:
tomcat:
uri-encoding: UTF-8
#最大工作线程数,默认200, 4核8g内存,线程数经验值800
#操作系统做线程之间的切换调度是有系统开销的,所以不是越多越好。
max-threads: 1000
# 等待队列长度,默认100,
accept-count: 1000
max-connections: 20000
# 最小工作空闲线程数,默认10, 适当增大一些,以便应对突然增长的访问量
min-spare-threads: 100
前面我们分析过,NIOEndPoint接收到客户端请求连接后,会生成一个SocketProcessor任务给到线程池去处理,SocketProcessor中的run方法会调用HttpProcessor组件去解析应用层的协议,并生成Request对象。最后调用Adapter的Service方法,将请求传递到容器中。
容器主要负责内部的处理工作,也就是当前置的连接器通过Socket获取到信息之后,得到一个Servlet请求,而容器就是负责处理Servlet请求。
Tomcat使用MApper组件将用户请求的URL定位到一个具体的Serlvet,然后Spring中的DispatcherServlet拦截到该Servlet请求后,基于Spring本身的Mapper映射定位到我们具体的Controller中。
到了Controller之后,对于我们的业务来说,才是一个请求真正的开始,Controller调用Service、Service调用dao,完成数据库操作之后,讲请求原路返回给到客户端,完成一次整体的会话。也就是说,Controller中的业务逻辑处理耗时,对于整个容器的并发来说也会受到影响。
通过上述分析,我们假设一个tomcat节点的QPS=500,如果要支撑到高峰时期的QPS=18000,那么需要40台服务器,这四台服务器需要通过Nginx软件负载均衡,进行请求分发,Nginx的性能很好,官方给的说明是Nginx处理静态文件的并发能够达到5W/s。另外Nginx由于不能单点,我们可以采用LVS对Nginx做负载均衡,LVS(Linux VirtualServer),它是采用IP负载均衡技术实现负载均衡。
通过这样的一组架构,我们当前服务端是能够同时承接QPS=18000,但是还不够,再回到前面我们说的两个公式。
假设我们的RT是3s,那么意味着服务器端的并发数=18000*3=54000,也就是同时有54000个连接打到服务器端,所以服务端需要同时支持的连接数为54000,这个我们在前文说过如何进行配置。如果RT越大,那么意味着堆积的链接越多,而这些连接会占用内存资源/CPU资源等,容易造成系统崩溃的现象。同时,当链接数超过阈值时,后续的请求无法进来,用户会得到一个请求超时的结果,这显然不是我们所希望看到的,所以我们必须要缩短RT的值。
来源:
https://www.cnblogs.com/mic112/p/15416875.html