你开发的系统到底可以支撑多少并发访问?
你开发的系统到底可以支撑多少并发访问?100万?10万?1万?1千?500?为什么能支撑,又为什么不能支撑?这是个直击心灵的问题,能否准确回答这个问题是程序员的一个分水岭,也是一个能否持续做技术的门槛,能够准确回答的人,可以不看这篇文章了,如果仔细思考过这个问题但是却没有思路去回答这个问题的人,建议读下去。
微服务不等于高并发,集群也不等于高并发,k8s更不等于高并发,一提性能优化就想到压测,连压测结果是否合理可能都不知道,如何去优化呢?就是用explain去看看sql么?
所谓的高并发是针对某些大用户量同时访问系统的场景抽象而出的一个模糊的概念,高并发只是所有那些场景的统称,所以不存在高并发的通用解决方案,只存在某些特定场景的解决方案。经过多年N多个高并发场景的不断积累,目前针对特定的高并发场景均有相对成熟的解决方案,但仅仅是解决方案,对于具体业务还需要具体分析。
讲个故事
针对一个简单场景分析,开发了一个系统,若干个功能,会存在问题么?如果只是几百个活跃用户,谈不上什么并发,那么很简单,只需一个应用集群和一个数据库主备即可,基本上80%的中小型公司开发的系统都是这样部署的,一个tomcat集群,一个主备mysql数据库,有钱的客户来个主备的oracle就行了。此时无所谓什么性能优化,如下图:
但是,此时,如果发现活跃用户数到了几千上万,突然有一天客户跟你说,系统崩了,无法访问或者特别慢,此时到服务器上一看,进程还在啊,CPU也算正常啊,数据库也还好啊,然后看日志,没什么报错啊,顶多是sql超时了,很可能会说,系统正常运行着,怎么访问不了了呢,重启下吧,重启之后一切正常,客户让你写故障报告,你憋了半天来了句可能是网络问题,或者实话实说系统没发现什么异常,不知道为何重启就好了,这个事儿可能就过去了。之后很可能每隔一段时间来一次重启,如果一直没解决,也许客户失去信心,也就不用这个系统了。这是一个比较常见的场景,我遇到过很多客户的软件开发商都是这样处理问题的,这也是市面上大多数开发人员想要高并发经验但是又没有高并发经验的原因。很多时候不是没场景,而是即使遇到问题也没思路解决。
故事的第一阶段完了,现在需要针对第一阶段来分析分析,如何解决这次的宕机问题。当系统的用户数从几百逐渐增加到几万的时候,如果预先没有针对高并发场景进行特殊设计,那么当高并发或频繁访问某个功能的时候就会出现此类问题,要么是数据库崩了一堆网络请求异常,要么是系统特别慢一个接口执行个几十秒,要么是系统内存溢出了。为何会出现这个问题?80%的原因都是数据库查询太慢了,这就是最常见的瓶颈点——数据库慢查询。
哪出的问题
数据库慢查询的原因实在太多,sql编写不规范,没有遵循ESR原则,业务复杂等等。抛去数据库问题先不谈,先说说瓶颈是如何出现的,为何某几个查询慢会导致服务器全面崩溃-雪崩呢,雪崩之后我们应该如何分析如何下手呢?看看下图,一个请求从服务端接收开始,一直到数据库,然后再返回给前端,大概经过这些步骤。这个时候我们就得逼着自己思考几个问题了,假定100并发进来的时候,哪会存在瓶颈呢?网络处理?程序执行?数据库查询?为什么存在或不存在瓶颈将是一个直击灵魂的问题让你久久不能寐。
网络原因?
首先看第一大类问题,网络处理,这涉及到一个网络编程的问题,即socket编程。我们开发的所有web服务或者说所有跟网络有关的服务基本上都涉及到这个问题-网络编程!
先看一个网络请求是如何到了我们的web容器的,一个网络请求从客户端经过若干网线传输和交换机路由器的中间转发,到了我们的网卡的时候,就是一堆的高低电频信号,网卡在接收到这些信号后,会有个MAC PHY芯片进行处理,首先将物理信号转变为数字信号,即01010101这样一串数字,然后进行转码,转变为帧,这就是程序处理网络请求的第一个概念,从一个个帧再逐渐转变成包,从IP包再转变成TCP包,最后转变成http包,这就是大学学网络里面讲过的四层协议或七层协议。这个过程,有网卡硬件的处理,有CPU和内存的交互,还有网卡驱动和操作系统的程序执行。如下图:
根据上图简单分析下网络请求处理过程:
- 网卡接收高低电频信号经过MAC处理后,将数据包先暂存于片内FIFO接收队列。
- 网卡控制器将FIFO队列放入内存的一个环形缓冲区。
- 网卡控制器将环形缓冲区的数据放入内存,上半部处理完成。
- 触发软中断,开始下半部处理。
- CPU软中断,调用网卡驱动程序继续处理。
- 网卡驱动程序通过NAPI开始处理内存中的网络包。
- 操作系统内核网络处理模块开始介入,执行大名鼎鼎的netif_receive_skb函数,执行完成后,网络处理第一大部分完成,开始进入协议处理。
- linux内核的socket模块开始介入,进行tcp或udp协议处理。
根据上图分析,我们普通的服务器在处理网络请求时,一般有三个瓶颈点,带宽、网卡速率和协议处理。带宽基本上是客户花钱买的,一般几十上百兆,1G也正常。网卡速率取决于硬件设备了,现在的千兆网卡和万兆网卡比较主流;协议处理算是一个比较重要的瓶颈,目前面试宝典中的epoll技术就是协议处理所使用的最重要的一种技术,协议处理技术的变革直接导致了网络并发请求和在线连接数从原来的几百到了现在的几十上百万。
在这些软硬件相互交互的过程中,大量前辈针对中断、数据包处理,协议处理等做过大量优化,有软硬中断、上半部下半部分离处理、NAPI、DMA、Epoll等等,这些技术很好的解决了网络请求处理过程中的各种瓶颈,发展到目前,出现了100Gb的网卡,阿里巴巴六年前就开始研究单机千万连接,即解决C10M的问题,目前在网络处理层面,基本上一台物理机可以满足我们正常的十万连接并发处理了。关于网络编程内容也不是一句两句能说透的,此处暂且分析到只要使用了正常的一个服务器,并且使用epoll技术,适当的估算下并发量和带宽,网络处理这块儿不太容易成为瓶颈。下面我们重点说说epoll和应用层的网络请求处理。
首先我们可以做个基准测试,先随意找一台虚拟机服务器,然后部署个nginx,直接用wrk来试试基准测试的结果,可以看到nginx在一台很普通的服务器上可以支撑8万并发。如果在这台服务器多跑几个nginx,基本上可以把网卡打满,并发也可以上到十几二十万,这就跟本身服务器性能有关了,如果是个物理机,性能会更高。nginx只是测试了一个http请求的处理并发,nginx基准测试完成后,可以进一步进行tomcat的基准测试和springboot的基准测试,最后使用公司内部的框架做一个网络处理的基准测试,得到一个相对合理的极限值。我们做的结果是nginx单机双实例占用4核可以到15万,tomcat可以到10万,springboot和内部封装后的框架可以到9万。由此看来web容器和spring框架多多少少会损耗一些性能。此时的瓶颈为CPU,因为一直在处理网络请求的收发,与预期相符,在网络处理层面,CPU、网卡和带宽绝对是瓶颈。下图是nginx的压测结果。
这篇文章把epoll的原理讲得比较透,nginx、tomcat、redis、kafka等等所有高性能的中间件的底层都是使用epoll。
此处的基准测试是个很重要的环节,我们网上能够看到的很多文章所谓的高并发高性能,都是一些理论值,或者说是推测出来的一个量级,实际值跟网络情况服务器情况的不同会有较大的差别,做好基准测试是我们做性能优化非常重要的一环,而基准测试的核心就是在一个简单的配置环境下发现核心性能点的瓶颈,例如如果一台服务器装了个nginx,发现压测结果是并发1万,那就是由于某些原因完全没测出来瓶颈,需要从CPU、网络等角度来排查哪一环出现了问题。很多时候我们可以使用压测工具得到一个数值,这不是关键点,关键点在于这个值对不对,到底瓶颈在哪,是CPU还是网络,是内存还是硬盘。
应用问题?
确认了网络编程在epoll配置下普通服务器都可以达到10万并发级别,现在来看看十万并发的请求到了应用代码执行层面会不会出现问题。如果抛去数据库的查询速度,单纯在代码执行和内存访问,基本都是在纳秒级别,下图可以看到各硬件访问的速度,单纯执行1000行代码还是处于微妙级别,访问1000次内存,也只是100微秒即0.1毫秒,所以如果没有高并发下的锁竞争,仅是代码执行也很少会出现高CPU的情况,除非是递归调用等特殊场景。
数据库问题?
分析了网络层瓶颈在单机十万,应用层基本不会存在瓶颈,那么下一层就是数据库查询了,也就是说,当十万并发请求进来的时候,基本上在网络处理和程序执行上不会存在多少损耗,会直接将流量打到数据库上,那么此时就需要了解数据库的瓶颈点和极限值了。数据库的知识点就比较多了,因为经过这么多年的发展,数据库做了N多优化,而且存储也是一个系统的重中之重,基本上所有的优化都是从数据库开始的。可以先从数据库的简单原理了解下数据库的性能到底怎么样。
上图以mysql为例列出了请求从网络到执行再到持久化四个大过程的基本实现原理。第一步是connection pool来处理连接,认证等工作;第二步是解析sql、优化sql和查询mysql库级别缓存;第三步进入存储引擎处理,mysql目前默认的存储引擎是innodb;第四步就是磁盘访问了。根据上述的网络处理分析,第一步基本不会存在瓶颈。第二步都是代码执行的内存访问,也不太会成为瓶颈。第三步是引擎执行,innodb支持三种行锁,但是查询时mvcc不加锁,并且当数据库记录数量不是特别大(百万或千万级)时,数据的定为都是基于内存中的page进行的,一般情况下也不会存在瓶颈,只有第四步的磁盘访问会成为瓶颈。
为了保证数据稳定持久化,一定是要将数据存储在磁盘上的,因为计算机一共就有两类存储,磁盘和内存,内存是基于电容的,原理就是通电后通过01信号来表示数据,而磁盘是通过磁极变化来记录表示01数据,通电改变磁场写入数据,断电后不会发生改变。所以要想让数据持久保留,必须用磁盘。当然磁盘有很多种,普通的机械硬盘,SSD硬盘,磁带等等。因为内存是靠电来表示数据,所以足够快,但是磁盘中,当磁头扫过盘面,通过感应电流就可以识别出不同状态,即读取数据;增强磁头的磁性,可以改变盘面记录单元的状态,实现写入数据。此时带着磁头的机械臂摆动就会成为访问瓶颈,由此可见,数据库的第一大瓶颈就是磁盘的写入和读取。
根据上面列出的硬件访问速度,磁盘访问在10ms左右,因为机械臂的移动速度在7ms左右。如果每次数据库查询都需要10ms,那么此时我们就可以算算,数据库CPU此时除了网络处理和数据查找(定位磁盘位置)以外,剩余的执行时间都是等待磁盘IO,可以适当增加线程数并行执行网络处理和数据查找,并行等待磁盘IO,即提高数据库连接数,传导到应用层面即为增加请求的连接数。此时通过增加应用连接数来提高并发能力,可以快速将数据库的瓶颈突破到磁盘瓶颈,按照正常物理机的固态硬盘的速度,每秒可以处理116k次16KB数据的随机读,即11万并发,当然此时数据库基本处于满负荷状态,崩溃边缘,正常支撑6万以下并发处于合理范围,如果将数据库部署到虚拟机上或者低配机上,那么并发量会直线下滑,我们内部的一个虚拟机作为数据库服务器的基准测试结果惨不忍睹,16k随机读的IOPS仅为600,即当有600查询请求进来时,磁盘IO为100%了。以上数据均属于内部基准测试得出的结果,实际生产环境或其他环境需要根据基准测试和全流程压测的实际值为准。
由此可见,即使我们使用高性能数据库服务器,安全运行的并发能力也只是6万的QPS,TPS可能还要低很多。如果一个高并发的业务接口需要访问5次数据库,那么最高能支撑1.2万的接口并发。如果前端业务功能每次点击需要走三四个接口,如果这三四个接口有查询有更新插入,也许连并发2000都撑不到。而这都是基于单次查询只有一次磁盘IO的情况,如果单次查询数据量大于1页,或者查询内容需要跨页访问,那就会产生多次IO,那性能就更低了,也许连四五百都撑不住,甚至于一个没有太高并发的业务,但是查询语句非常复杂,可能直接导致并发能力在几十甚至个位数。而数据库又因为ACID的原因,基本只能实现多读无法实现多写,分布式事务到现在也是难以突破的一个技术鸿沟,要实现需要付出巨大成本。
怎么优化
分析到此,就找到了几千用户访问时无论怎么加应用服务器,最后还会雪崩的原因了,根儿就不在应用层面,加再多的服务器都没有用。那么数据库的问题仅仅靠优化sql解决么?sql从慢到快,只是一个表象,系统宕机的根本原因在于数据库服务器达到了瓶颈,资源不足导致的,我们应该先分析什么问题导致了资源瓶颈,然后对症下药的分析。也许只需在某几个点加个缓存,也许见个合理的索引,也许是变动下只查id不查内容避免回表,都可以一针见血的解决掉性能问题。现在特别流行使用的nosql可以支持分布式存储,性能也可以随着节点数增多而线性递增,是不是换成分布式存储就可以支撑高并发了呢?也不是,仅仅支撑几万并发,使用数据库 缓存还是可以的,传统数据库的ACID、使用简单、技术成熟稳定是我们必须要考虑的,也是我们首次开发一个项目的首选存储。这也是为何很多大的金融机构还在使用IOE(IBM ORACLE EMC)的原因。
现在我们经过上述一系列分析,知道了系统真正的瓶颈点了,网络上只有流量会成为瓶颈,应用上只有锁、线程切换、递归调用和大数据量处理会成为运算和内存瓶颈,数据库因为ACID是单点,所以此处的性能是最难扩展的。面对大流量,核心目标就是尽量让高并发接口可以支持横向扩展。最开始的性能优化基本都是围绕着数据库的瓶颈而进行的,一般有如下几个阶段:
第一阶段:缓存热数据
有些热点数据如果需要多次查询,而且查多改少,那么一般是可以放到redis缓存中的,利用内存访问来替代磁盘访问,可以明显提升效率。同时,针对数据库的回表问题,也可以在mysql中只查id,根据id在redis中查询数据内容。这种缓存只适用于大多数人查询的内容都相同的情况,缓存只需要存一份,更新缓存相对容易。
第二阶段:扩散写
有些查询根据每个人会得到不同结果,那么每个人来访问系统都需要查询一次数据库,并发量上来后很可能会把数据库压到瓶颈,所以需要预先算出每个人的查询结果并缓存,这就是倒排索引,也就是扩散写的思路。此时为了降低数据库压力提高查询效率,需要为每个人冗余一份数据,更新会比较复杂,因为需要重新计算每个人的数据。但是查询会非常快,而且未来也可以针对查询做各种扩展。
第三阶段:异步处理
有些业务场景是需要高并发插入更新的,此时数据库也容易成为瓶颈。为了保证系统可以正常使用,只能延迟返回插入更新的结果,放入队列,慢慢消费,也算是削峰的一种。
第四阶段:读写分离
上面三个阶段都处于单机状态,但是热点数据的缓存有很多场景还是先查库后缓存,也容易把数据库压崩,所以此时需要横向扩展,通过读写分离,扩展读的mysql服务器,但此时就会存在读写服务器的数据同步延时问题需要考虑和解决。
第五阶段:分库分表
第四阶段的读写分离,但是一写多读,当写的单机成为瓶颈时,就只能横向或者众向分表了,我们一般说得分库分表都是众向分表,即选择一个合理的分表策略,一般是根据高并发的查询条件设置,因为要防止跨表查询,同时还得考虑分布式事务的问题。一个事务里涉及到的表尽量在一个库中。
第六阶段:NoSQL
有些复杂查询和聚合查询,真的不适合使用mysql这种关系型数据库来支撑,就需要使用es这类倒排索引的存储引擎或者一些列式存储的mapreduce的来解决,此时就需要考虑使用NoSQL来冗余数据存储,以解决这类特殊场景的业务查询。
总结
在性能优化过程中,加机器是最容易实现的。所以针对应用层的CPU算力问题是最容易解决的,网络层的带宽只要预先算好,客户也能欣然接受。而针对存储层的各种优化都是极为复杂的,单机的维护比多机简单的多,单写比多写简单的多,一个存储的维护也要比多个存储的维护简单的多,每一个阶段的优化都意味着更高的维护成本,所以优化是根据业务需求被动提出的而不是过度设计出来的。说白了我们都想舒舒服服地坐在这喝茶看着系统稳定运行,不要自己给自己加码提高维护成本。
提到并发大家就会想到秒杀,就会想到红包、春节活动、双十一、12306等等。他们确实是极为典型的高并发大流量,他们的接口qps甚至可以达到几十万上百万,他们的存储可能需要支撑几千万上亿的qps。他们在解决这个问题上使用了各种各样高大上的技术,限流熔断,分布式存储,队列,缓存,调用链跟踪,全链路监控等等等等。
对于软件开发人员来说,性能优化的根本就是在协调CPU、内存、磁盘和网络等硬件设备的性能瓶颈,把单机性能优化到极致,然后优化为可以支持多机扩展。不能随心所欲地使用一些流行技术堆积出一个系统,解铃还须系铃人,找到性能瓶颈的本质才是最关键的。