当前位置: 首页 > 后端技术 > Java

阿里P8面试官:如何设计能够处理千万级并发的架构?

时间:2023-04-01 20:54:39 Java

大家首先思考一个问题,也是面试过程中经常遇到的问题。如果你公司现在的产品能支持10万用户访问,你老板突然跟你说,一旦拿到钱,你就投放大量的广告。预计一个月内用户数将达到1000万。如果这个任务交给你,你应该怎么做?1000W用户的问题分解,如何支撑1000W用户,其实是一个很抽象的问题。对于技术开发,对于关键业务的执行,我们需要一个非常清晰的性能指标数据,比如高峰期交易的响应时间,并发用户数,QPS,成功率,基础指标要求必须非常清晰。只有这样才能引导整个架构的改造和优化。所以,如果接到这样的问题,首先需要定位到问题的本质,即首先要知道一些可以量化的数据指标。如果您之前有类似业务交易历史数据的经验,您需要尽量对收集到的原始数据(日志)进行处理,从而分析这段时间的高峰时段、交易行为、交易规模等,并得到whatyouwanttosee另一种明确需求明细的情况是没有相关数据索引作为参考。这时候就需要经验来分析了。例如,可以参考类似行业中一些成熟的业务交易模型(如银行业的日常交易活动或交通行业的售票和验票交易),或者干脆遵循“2/8”原则和“2/5/8”原则直接开始练习。当用户能在2秒内得到响应时,会觉得系统响应很快;当用户在2-5秒内得到响应时,会觉得系统的响应速度还不错;响应时,会感觉系统响应速度很慢,但可以接受;而当超过8秒后用户仍然得不到响应时,就会觉得系统很糟糕,或者认为系统已经失去响应,而选择离开这个网站,或者发起二次请求。在预估响应时间、并发用户数、TPS、成功率等关键指标的同时,还需要关注具体业务功能的需求。每个业务功能都有自己的特点。比如有些场景不需要同步返回。清除执行结果。在某些业务场景下,返回“系统繁忙,请稍候!”等暴力信息是可以接受的。避免处理流量过大造成大规模瘫痪。因此,需要学会平衡这些指标之间的关系,在大多数情况下,最好对这些指标进行排序,尽量只考察少数高优先级指标的需求。(SLA服务级别)SLA:Service-LevelAgreement的缩写,即服务级别协议。服务SLA是服务提供者对服务消费者的正式承诺,是衡量服务能力高低的关键项目。服务SLA中定义的项目必须是可测量的,并且有明确的测量方法。并发中相关概念的解释在分析上述问题之前,我先科普一下与系统相关的一些关键衡量指标。TPSTPS(TransactionPerSecond)是每秒处理的事务数。从宏观上看,事务是指客户端向服务器端发起请求,并等待请求返回的整个过程。计时从客户端发起请求开始,到服务端收到响应结果为止。在计算这个时间段内完成的交易总数时,我们称之为TPS。从微观上看,一个数据库事务操作,从事务开始到事务提交完成,代表一个完整的事务,也就是数据库层面的TPS。QPSQPS(QueriesPerSecond)是每秒的查询数,表示服务器每秒可以响应的查询数。这里的query指的是用户向服务器发送请求并成功响应的次数,可以简单的理解为每秒的Requests数。对于单个接口,TPS和QPS是相等的。从宏观上来说,如果用户打开一个页面,页面渲染结束代表一个TPS,那么这个页面会多次调用服务端,比如加载静态资源,查询服务端渲染数据等,两个QPS会生成。因此,一个TPS可能包含多个QPS。QPS=并发数/平均响应时间RTRT(ResponseTime),表示从客户端发起请求到服务端返回的时间间隔,一般表示平均响应时间。并发数并发数是指系统可以同时处理的请求数。需要注意的是,并发数和QPS不要混为一谈。QPS表示每秒的请求数,而并发数是系统同时处理的请求数。并发数会大于QPS,因为服务器上的一个连接需要有一个处理时间。在这个连接中保持,直到请求处理完成。例如QPS=1000,表示客户端每秒向服务端发起1000个请求,如果一个请求的处理时间为3s,则表示总并发数=1000*3=3000,即服务器同时有3000个并发。如何计算上面提到的这些指标呢?例如。假设在10:00到11:00的一个小时内有200万用户访问我们的系统,假设每个用户的平均请求时间为3秒,那么计算结果如下:QPS=2000000/60*60=556(表示每秒会向服务器发送556个请求)RT=3s(每个请求的平均响应时间为3秒)并发数=556*3=1668从这个计算过程中发现,随着RT值的增加,数字越大,并发数越多,并发数代表服务器同时处理的连接请求数,也就是服务器占用的连接数越多,这些连接数会消耗内存资源和CPU资源。因此,RT值越大,系统资源占用越大,也意味着服务器处理请求的时间越长。但实际情况是RT值越小越好。例如,在游戏中,响应时间至少为100ms才能获得最佳体验。对于电子商务系统来说,3s左右的时间是可以接受的,那么如何缩短呢?RT的价值呢?根据2/8法则估算1000w用户的访问量回到最初的问题,假设没有历史数据可供我们参考,我们可以使用2/8法则进行估算。1000W用户,每天有20%的用户访问本网站,即每天访问200W用户。假设每个用户平均来点击50次,那么总PV=1亿。一天是24小时。根据2/8法则,大部分用户的活跃时间集中在(240.2)约等于5小时,大部分用户参考(1亿点击的80%)约等于8000W(PV),也就是说5小时内,大约会有8000W的点击进来,也就是每秒大约有4500(8000W/5小时)次请求。4500只是一个平均数字。在这5小时内,请求不可能很平均,可能会有大量集中的用户访问(比如淘宝这样的网站,每天访问的高峰时间集中在下午14:00和21:00pm,其中21:00是当天的活跃高峰),一般情况下,访问高峰是平均访问请求的3~4倍左右(这是一个经验值),我们计算为4倍。那么这5小时内可能每秒有18000个请求。也就是说,问题已经从支持1000W用户变成了一个具体的问题,即服务器需要能够支持每秒18000个请求(QPS=18000)。服务器压力预估大概预估后端服务器需要支撑最高并发峰值后,需要从整个系统架构层面预估压力,然后配置合理的服务器数量和架构。既然如此,我们首先要知道一台服务器能处理多少并发量,那么如何分析这个问题呢?我们的应用是部署在Tomcat上的,所以需要从Tomcat本身的性能入手。下图展示了Tomcat的工作原理,下图说明如下。LimitLatch是连接控制器,负责控制Tomcat同时处理的最大连接数。NIO/NIO2模式下,默认为10000,如果是APR/native,则默认为8192。Acceptor是独立线程。在run方法中,在while循环中调用socket.accept方法接收客户端的连接请求。一旦有新的请求过来,accept会返回一个Channel对象,然后把Channel对象交给Poller处理。Poller的本质是一个Selector,同样实现了线程。Poller内部维护了一个Channel数组。它在无限循环中不断检测Channel的数据就绪状态。Channel一旦可读,就会生成一个SocketProcessor任务对象,丢给Executor处理,SocketProcessor实现了Runnable接口。当线程池在执行SocketProcessor的任务时,会通过Http11Processor处理当前的请求。Http11Processor读取Channel数据生成ServletRequest对象。Executor是线程池,负责运行SocketProcessor任务类。SocketProcessor的run方法会调用Http11Processor读取并解析请求数据。我们知道Http11Processor是对应用层协议的封装,它会调用容器获取响应,然后通过Channel写入响应。从这个图可以得出,有四个因素限制了Tomcat的请求数。对于目前的服务器系统资源,我想你可能遇到过类似“Socket/File:Can'topensomanyfiles”的异常,这意味着Linux系统中的文件句柄限制。在Linux中,每个TCP连接都会占用一个文件描述符(fd)。一旦文件描述符超过Linux系统的当前限制,就会提示这个错误。我们可以使用如下命令查看一个进程可以打开的文件数ulimit-a或者ulimit-nopenfiles(-n)1024是linux操作系统对一个进程打开的文件句柄数的限制(包括opensockets字数)这里只是对用户级别的限制。实际上,系统还有一个通用的限制。查看系统总线系统:cat/proc/sys/fs/file-maxfile-max是设置系统中所有进程可以打开的文件数量。同时,有些程序可以调用setrlimit来设置每个进程的限制。如果你收到很多关于文件句柄用完的错误消息,你应该增加这个值。当出现上述异常时,我们可以通过如下方式修改(针对单个进程开启次数的限制)vi/etc/security/limits.confrootsoftnofile65535roothardnofile65535*softnofile65535*hardnofile65535*代表所有用户,root代表root用户。noproc代表最大进程数,nofile代表最大打开文件数。soft/hard,前者达到阈值会发出警告,后者会报错。另外需要注意的是,需要保证进程级的打开文件数小于等于系统的总限制,否则需要修改系统的总限制。vi/proc/sys/fs/file-maxTCP连接对系统资源的最大开销是内存。因为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内核对部分资源的限制,以及程序的业务处理,8G内存很难做到100万个连接。当然我们也可以通过增加内存的方式来增加并发。Tomcat依赖的JVM的配置我们知道Tomcat是运行在JVM上的Java程序,所以我们需要对JVM进行优化,以更好的提高Tomcat的性能。下面简单介绍一下JVM,如下图所示。在JVM中,内存分为堆、程序计数器、本地栈、方法区(元空间)、虚拟机栈。堆空间说明其中,堆内存是JVM内存中最大的一块区域。几乎所有的对象和数组都会分配到堆内存中,堆内存是所有线程共享的。堆空间分为新生代和老年代,新生代又分为Eden区和Surrivor区,如下图所示。新生代和老年代的比例是1:2,即新生代会占据1/3的堆空间,老年代会占据2/3的堆空间。另外,在新生代中,空间比例为Eden:Surivor0:Surivor1=8:1:1。比如eden区的内存大小是40M,那么两个Survivor区各占5M,整个新生代是50M,那么计算出老年代的内存大小是100M,也就是说,堆空间的总内存大小为150M。可以通过java-XX:PrintFlagsFinal-version查看默认参数uintxInitialSurvivorRatio=8uintxNewRatio=2InitialSurvivorRatio:新生代Eden/Survivor空间的初始比例NewRatio:Old区/Young区的内存比例具体工作原理堆内存是:大多数对象创建后,会保存在Eden区。当Eden区满了,就会触发YGC(YoungGC),回收大部分对象。如果还有活着的物体,它们将被复制到Survivor0。这时候,伊甸园区域就清空了。如果后面再次触发YGC,将活体对象Eden+Survivor0中的对象复制到Survivor1区,则Eden和Survivor0都清空,再次触发YGC,将Eden+Survivor1中的对象复制到Survivor1区Survivor0区,如此循环下去。直到对象的年龄达到阈值,才被放入老年代。(这样设计的原因是Eden区的大部分对象都会被回收。)Survivor区装不下的对象会直接进入老年代。当老年代满了,就会触发FullGC。GCmark-clear算法在执行过程中挂起其他线程??程序计数器程序计数器用来记录每个线程执行的字节码地址等,当一个线程上下文切换时,需要依靠它来记住当前的执行位置。下次恢复执行时,会沿着上次执行的位置继续执行。方法区方法区是一个逻辑概念。在1.8版本的HotSpot虚拟机中,它的具体实现就是元空间。方法区主要用来存放已经被虚拟机加载的类相关信息,包括类元信息、运行时常量池和字符串常量池。类信息还包括类版本、字段、方法、接口和父类信息。方法区类似于堆空间。它是共享内存区,所以方法区是线程共享的。本地栈和虚拟机栈Java虚拟机栈是一个线程私有的内存空间。创建线程时,会在虚拟机中申请一个线程栈,用于保存方法的局部变量、操作数栈、动态链接方法。和其他信息。每个方法调用都伴随着栈帧的入栈操作。当一个方法返回时,就是栈帧的出栈操作。本机方法栈类似于虚拟机栈。本地方法栈用于管理本地方法的调用,即本地方法。JVM内存应该怎么设置?了解了上面的基本信息后,JVM中的内存应该怎么设置呢?有哪些参数需要设置?在JVM中,要配置的几个核心参数不外乎。-Xms,Java堆内存大小-Xmx,Java最大堆内存大小-Xmn,Java堆内存中新生代大小,扣除新生代后,剩下的就是老年代内存。新生代内存设置过小,会频繁触发MinorGC。触发GC会影响系统的稳定性-XX:MetaspaceSize,元空间大小,128M-XX:MaxMetaspaceSize,最大云空间大小(如果不指定这两个参数,元空间会在运行时根据需要动态调整。)256Ma新系统的元空间基本没有办法有计算方法。一般几百兆就够了,因为主要存放一些类信息。-Xss,线程栈内存大小,这个基本不用估计,从512KB到1M设置即可,因为这个值越小,可以分配的线程越多。JVM内存的大小取决于机器的配置。比如2核4G的服务器,只能分配2G左右给JVM进程,因为机器本身也需要内存,机器上运行的其他进程也需要占用内存。而这2G又要分配给栈内存、堆内存、元空间。堆内存只能得到1G左右,然后堆内存分为新生代和老年代。Tomcat自身的配置http://tomcat.apache.org/tomc...这个Connector要创建的最大请求处理线程数,因此决定了最大同时可以处理的请求数。如果未指定,则此属性设置为200。如果执行程序与此连接器相关联,则此属性将被忽略,因为连接器将使用执行程序而不是内部线程池执行任务。请注意,如果配置了执行程序,则为此属性设置的任何值都将被正确记录,但它将被报告(例如通过JMX)为-1以表明它未被使用。服务器:tomcat:uri-encoding:UTF-8#最大工作线程数,默认200,4核8g内存,线程数经验值800#操作系统有线程间切换调度的系统开销,越多越好。max-threads:1000#等待队列长度,默认100,accept-count:1000max-connections:20000#最小工作空闲线程数,默认10,适当增加以应对突然增加的流量min-spare-threads:100accept-count:最大等待数,当调用HTTP请求的次数达到tomcat的最大线程数时,又有新的HTTP请求到来,则tomcat会将请求放入等待队列,这个acceptCount是指可接受的最大等待数,默认100,如果等待队列也满了,此时有新的请求会被tomcat拒绝(connectionrefused)maxThreads:最大线程数,每次HTTP请求到达Webservice,tomcat会创建一个线程来处理请求,那么最大线程数决定了web服务容器可以同时处理多少个请求。maxThreads默认为200,绝对建议增加。但是,添加线程是有代价的。更多的线程不仅会带来更多的线程上下文切换成本,同时也意味着更多的内存消耗。默认情况下,JVM在创建新线程时会分配一个大小为1M的线程栈,所以线程越多意味着需要的内存也越大。线程数经验值为:1核2g内存为200,线程数经验值为200;4核8g内存,线程数经验值为800。maxConnections,最大连接数,这个参数是指tomcat同时可以接受的最大连接数。对于Java的blockingBIO,默认值为maxthreads的值;如果在BIO模式下使用自定义Executor,则默认值将是executor中maxthreads的值。对于Java新的NIO模式,maxConnections的默认值是10000。对于Windows上的APR/nativeIO模式,maxConnections的默认值是8192。如果设置为-1,maxconnections函数被禁用,这意味着tomcat容器的连接数没有限制。maxConnections和accept-count的关系是:当连接数达到maxConnections的最大值时,系统会继续接收连接,但不会超过acceptCount的值。1.3.4应用带来的压力我们之前分析过,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,如果要在高峰期支持18000的QPS,那么需要40台服务器。这四台服务器需要通过Nginx软件负载均衡来分发请求。Nginx性能非常好。官方的解释是Nginx处理静态文件的并发可以达到5W/s。另外,由于Nginx不能单点,我们可以使用LVS在Nginx上做负载均衡,LVS(LinuxVirtualServer),它使用IP负载均衡技术来实现负载均衡。通过这样一套架构,我们现在的服务器可以同时接受QPS=18000,但是还不够,我们再回到前面说的两个公式。QPS=Concurrency/AverageResponseTimeConcurrency=QPS*AverageResponseTime假设我们的RT为3s,表示服务器端的并发数=18000*3=54000,也就是此时服务器有54000个连接同时,所以服务器需要同时支持的连接数为54000,这个我们前面提到了如何配置。RT越大,意味着积累的链接越多,这些链接会占用内存资源/CPU资源等,容易造成系统崩溃。同时,当链接数超过阈值后,后续的请求就进不来了,用户会得到一个请求超时的结果,这显然不是我们希望看到的,所以我们必须缩短RT的值。

猜你喜欢