关于高性能高并发服务这个概念你们应该也都比较熟悉了,今天我主要是想讲一下对于如何作一个高性能高并发服务架构的一些本身的思考。
本次分享主要包括三个部分:
1. 服务的瓶颈有哪些
2. 如何提高总体服务的性能及并发
3. 如何提高单机服务的性能及并发
1、服务的瓶颈有哪些
一般来讲程序的定义是算法+数据结构+数据,算法简单的理解就是一种计算方式,数据结构顾名思义是一种存储组织数据的结构,这二者体现了程序须要用到的计算机资源涉及到CPU资源、内存资源,而数据部分除了内存资源,每每还可能涉及到硬盘资源,甚至是彼此之间传输数据时会消耗网络(网卡)资源。
当咱们搞清楚程序运行起来时涉及哪些资源后,就能够更好地分析咱们的服务中哪些多是临界资源。所谓临界资源就是多个进程(线程)并发访问某个资源时,该资源同只能服务某个或者某些进程(线程)。
服务的瓶颈主要就是在这些临界资源上,还有一些资源本来并非临界资源,好比内存在一开始是够的,可是由于链接数或者线程数不断的增多,最终致使其成为临界资源,其余的CPU、磁盘、网卡其实和内存同样,在访问量增大之后同样均可能会成为瓶颈。
因此怎么作到高性能高并发的服务,简单地说就是找到服务的瓶颈,在合理的范围内尽量的消除瓶颈或者下降瓶颈带来的影响,再通俗一点的说就是资源总量不够就加资源,确切的说是什么资源不够就加什么资源,同时尽可能下降单次访问的资源消耗,作到在资源总量必定的状况下有能力支撑更多的访问。
2、如何提高总体服务的性能及并发前端
1、数据拆分java

图1 单数据实例改为数据库集群
最典型的一个临界资源就是数据库,数据库在一个大访问量的系统中每每是最薄弱的一环,由于数据库自己的服务能力是有限的,以MySQL为例,可能MySQL能够支持的并发链接数可能也就几千个,假设是3000个,若是一个服务对其数据库的并发访问若是超过了3000,有部分访问可能在创建链接的时候就失败了。
在这种状况下,须要考虑的是如何将数据进行分片,引入多个MySQL实例,增长资源,如图1所示。
数据库这个临界资源经过数据拆分的方式,由原来的一个MySQL实例变成了多个MySQL实例,这种状况下数据库资源的总体并发服务能力天然提高了,同时因为服务压力被分散,整个数据库集群表现出来的性能也会比单个数据库实例高不少。
存储类的解决思路基本是相似的,都是将数据拆分,经过引入多个存储服务实例提高总体存储服务的能力,无论对于SQL类的仍是NoSQL类的或文件存储系统等均可以采用这个思路。
2
、服务拆分

图2 服务拆分
应用程序自身的服务须要根据业务状况进行合理的细化,让每一个服务只负责某一类功能,这个思想实际上是和微服务思想相似。
一句话就是尽可能合理地将服务拆分,同时有一个很是重要的原则是让拆分之后的同类服务尽可能是无状态或弱关联,这样就能够很容易进行水平扩展,若是拆分之后的同类服务的不一样实例之间自己是有一些状态引发彼此很是强的依赖,好比彼此要共享一些信息这些信息又会彼此影响,那这种拆分可能就未必很是的合理,须要结合业务从新进行审视。
固然生产环节上下游拆分之后不一样的服务彼此之间的关联又是另一种情形,由于同一个生产环节上每每是走完一个服务环节才能进入下一个服务环节,至关于有多个串行的服务,任何一个环节的服务都有可能瓶颈,因此须要拆分之后针对相应的服务进行单独优化,这是拆分之后服务与服务之间的关系。
假设各个同类服务自己是无状态或者弱依赖的状况下,针对应用服务进行分析,不一样的应用服务不太同样,可是一般都会涉及到内存资源以及计算资源,以受内存资源限制为例,一个应用服务能承受的链接数是有限的(链接数受限),另外若是涉及上传下载等大量数据传输的状况网络资源很快就会成为瓶颈(网卡打满),这种状况下最简单的方式就是一样的应用服务实例部署多份,达到水平扩展,如图2所示。
实际在真正拆分的时候须要考虑具体的业务特色,好比像京东主站这种类型的网站,在用户在访问的时候除了加载基本信息之外,还有商品图片信息、价格信息、库存信息、购物车信息以及订单信息发票信息等,以及下单完成之后对应的分拣配送等配套的物流服务,这些都是能够拆成单独的服务,拆分之后各个服务各司其职也能作更好的优化。
服务拆分这件事情,打个不是特别恰当的比方,就比如上学时都是学习,可是分了不少的科目,高考的时候要看总分,有些同窗会有偏科的现象,有些科成绩好有些科成绩差一点,由于分不少科目因此很容易知道本身哪科是比较强的、哪科是比较弱的,为了保证整体分数最优,通常在弱的科目上都须要多花点精力努力提升一下分数,否则整体分数不会过高。服务拆分也是一样的道理,拆分之后能够很容易知道哪一个服务是总体服务的瓶颈,针对瓶颈服务再进行重点优化比等就能够比较容易的提高总体服务的能力。
三、适当增加服务链路,尽可能缩短访问链路,下降单次访问的资源消耗
在大型的网站服务方案上,在各类合理拆分之后,数据拆分以及服务拆分支持扩展只是其中的一部分工做,以后还要根据需求看看是否须要引入缓存CDN之类的服务,我把这个叫作增加服务链路,原来直接打到数据库的请求,如今可能变成了先打到缓存再打到数据库,对整个服务链路长度来讲是变长的,增加服务链路的原则主要是将越脆弱或者说越容易成为瓶颈的资源(好比数据库)放置在链路的越末端。
在增加完服务链路以后,还要尽可能的缩短访问链路,好比能够在CDN层面就返回的就尽可能不要继续往下走了,若是能够在缓存层面返回的就不要去访问数据库了,尽量地让每次的访问链路变短,能够一步解决的事情就一步解决,能够两步解决的事情就不要走第三步,本质上是下降每次访问的资源消耗,尤为是越到链路的末端访问资源的消耗会越大。
好比获取一些产品的图片信息能够在访问链路的最前端使用CDN,将访问尽可能挡住,若是CDN上没有命中,就继续日后端访问利用nginx等反向代理将访问打到相应的图片服务器上,而图片服务器自己又能够针对性的作一些访问优化等。
好比像价格等信息比较敏感,若是有更改可能须要当即生效须要直接访问最新的数据,可是若是让访问直接打到数据库中,数据库每每直接就打挂了,因此能够考虑在数据库以前引入redis等缓存服务,将访问打到缓存上,价格服务系统自己保证数据库和缓存的强一致,下降对数据库的访问压力。
在极端状况下,数据量虽然不是特别大,几十台缓存机器就能够抗住,但访问量可能会很是大,能够将全部的数据都放在缓存中,若是缓存有异常甚至都不用去访问数据库直接返回访问失败便可。
由于在访问量很是大的状况下,若是缓存挂了,访问直接打到数据库上,可能瞬间就把数据库打趴下了,因此在特定场景下能够考虑将缓存和数据库切开,服务只访问缓存,缓存失效从新从数据库中加载数据到缓存中再对外服务也是能够的,因此在实践中是能够灵活变通的。
四、小结
如何提高总体服务的性能及并发,一句话归纳就是:
在合理范围内尽量的拆分,拆分之后同类服务能够经过水平扩展达到总体的高性能高并发,同时将越脆弱的资源放置在链路的越末端,访问的时候尽可能将访问连接缩短,下降每次访问的资源消耗。
3、如何提高单机服务的性能及并发
前面说的这些状况能够解决大访问量状况下的高并发问题,可是高性能最终仍是要依赖单台应用的性能,若是单台应用性能在低访问量状况下性能已经成渣了,那部署再多机器也解决不了问题,因此接下来聊一下单台服务自己若是支持高性能高并发。
一、多线程/线程池方式

图3 版本一
以TCP server为例来展开说明,最简单的一个TCP server代码,版本一示例如图3所示。这种方式纯粹是一个示例,由于这个server启动之后只能接受一条链接,也就是只能跟一个客户端互动,且该链接断开之后,后续就连不上了,也就是这个server只能服务一次。
这个固然是不行的,因而就有了版本二如图4所示,版本二能够一次接受一条链接,并进行一些交互处理,当这条链接所有处理完之后才能继续下一条链接。
这个server至关因而串行的,没有并发可言,因此在版本二的基础上又演化出了版本三如图5所示。

图4 版本二

图5 版本三
这实际上是咱们常常会接触到的一种模型,这种模型的特色是每链接每线程,MySQL 5.5之前用的就是这种模型,这种模型的特色是当有大量链接的时候会建立大量的线程,因此每每须要限制链接总数,若是不作限制可能会出现建立了大量的线程,很快就会将内存等资源耗干。
图6 版本四

图6 版本四
另外一个是当出现了大量的线程的时候,操做系统会有大量的cpu资源花费在线程间的上下文切换上,致使真正给业务提供服务的cpu资源比例反倒很小。同时,考虑到大多数时候即便有不少链接也并不表明全部的链接在同一个时刻都是活跃的,因此版本三又演化出了版本四,如图6所示,版本四的时候是不少的链接共享一个线程池,这些线程池里的线程数是固定的,这样就能够作到线程池里的一个线程同时服务多条链接了,MySQL 5.6以后采用的就是这种方式。
在绝大多数的开发中,线程池技术就已经足够了,可是线程池在充分榨干cpu计算资源或者说提供有效计算资源方面并非最完美的,以一核的计算资源为例,线程池里假设有x个线程,这x个线程会被操做系统依据具体调度策略进行调度,可是线程上下文切换自己是会消耗必定的cpu资源的,假设这部分消耗代价是w, 而实际有效服务的能力是c,那么理论上来讲w+c 就是总的cpu实际提供的计算资源,同时假设一核cpu理论上提供计算资源假设为t,这个是固定的。
因此就会出现一种状况,当线程池中线程数量较少的时候并发度较低,w虽然小了,可是c也是比较小的,也就是w+c < t甚至是远远小于t,若是线程数不少,又会出现上下文切换代价太大,即w变大了。虽然c也随之提高了一些,但由于t是固定的,因此c的上限值必定是小于t-w的,并且随着w越大,c的上限值反倒下降了,所以使用线程池的时候,线程数的设置须要根据实际状况进行调整。
二、基于事件驱动的模式
多线程(线程池)的方式能够较为方便地进行并发编程,可是多线程的方式对cpu的有效利用率其实并非最高的,真正可以充分利用cpu的编程方式是尽可能让cpu一直在工做,同时又尽可能避免线程的上下文切换等开销。
图7 epoll示例

图7 epoll示例
基于事件驱动的模式(也称I/O多路复用)在充分利用cpu有效计算能力这件事件上是很是出色的。比较典型的有select/poll/epoll/kevent(这些机制自己之间的优劣今天先不展开说明,后续以epoll为例说明),这种模式的特色是将要监听的socket fd注册在epoll上,等这个描述符可读事件或者可写事件就绪了,那么就会触发相应的读操做或者写操做,能够简单地理解为须要cpu干活的时候就会告知cpu须要作什么事情,实际使用时示例如图7所示。
这个事情拿一个经典的例子来讲明。就是在餐厅就餐,餐厅里有不少顾客(访问),每链接每线程的方式至关于每一个客户一个服务员(线程至关于一个服务员),服务的过程当中一个服务员一直为一个客户服务,那就会出现这个服务员除了真正提供服务之外有很大一段时间多是空闲的,且随着客户数越多服务员数量也会越多,可餐厅的容量是有限的,由于要同时容纳相同数量的服务员和顾客,因此餐厅服务顾客的数量将变成理论容量的50%。那这件事件对于老板(老板至关于开发人员,但愿能够充分利用cpu的计算能力,也就是在cpu计算能力<成本>必定的状况下但愿尽可能的多作一些事情)来讲代价就会很大。
线程池的方式是雇佣固定数量的服务员,服务的时候一个服务员服务好几个客户,能够理解为一个服务员在客户A面前站1分钟,看看A客户是否须要服务,若是不须要就到B客户那边站1分钟,看看B客户是否须要服务,以此类推。这种状况会比以前每一个客户一个服务员的状况节省一些成本,可是仍是会出现一些成本上的浪费。
还有一种模式也就是epoll的方式,至关于服务员就在总台等着,客户有须要的时候就会在桌上的呼叫器上按一下按钮表示本身须要服务,服务员每次看一下总台显示的信息,好比一共有100个客户,一次可能有10个客户呼叫,这个服务员就会过去为这10个客户服务(假设服务每一个客户的时候不会出现停顿且能够在较短的时间内处理完),等这个服务员为这10个客户服务员完之后再从新回到总台查看哪些客户须要服务,依此类推。在这种状况下,可能只须要一个服务员,而餐厅剩余的空间能够所有给客户使用。
nginx服务器性能很是好,也能支撑很是多的链接,其网络模型使用的就是epoll的方式,且在实现的时候采用了多个子进程的方式,至关于同时有多个epoll在工做,充分利用了cpu多核的特性,因此并发及性能都会比单个epoll的方式会有更大的提高。
另外Redis缓存服务器你们应该也很是熟悉,用的也是epoll的方式,性能也是很是好,经过这些现成的经典开源项目,你们就能够直观地理解基于事件驱动这一方式在实际生产环境中的性能是很是高的,性能提高之后并发效果通常都会随之提高。
可是这种方式在实现的时候是很是考验编程功底以及逻辑严谨性,换句话编程友好性是很是差的。由于一个完整的上下文逻辑会被切成不少片断,好比“客户端发送一个命令-服务器端接收命令进行操做-而后返回结果”这个过程,至少会包括一个可读事件、一个可写事件,可读事件简单地理解就是指这条命令已经发送到服务器端的tcp缓存区了,服务器去读取命令(假设一次读取完,若是一次读取的命令不完整,可能会触发屡次读事件),服务器再根据命令进行操做获取到结果,同时注册一个可写事件到epoll上,等待下一次可写事件触发之后再将结果发送出去,想象一下当有不少客户端同时来访问时,服务器就会出现一种状况——一下子在处理某个客户端的读事件,一下子在处理另外的客户端的写事件,总之都是在作一个完整访问的上下文中的一个片断,其中任何一个片断有等待或者卡顿都将引发整个程序的阻塞。
固然这个问题在多线程编程时也是一样是存在的,只不过有时候你们习惯将线程设置成多个,有些线程阻塞了,但可能其余线程并无在同一时刻阻塞,因此问题不是特别严重,更严谨的作法是在多线程编程时,将线程池的数量调整到最小进行测试,若是确实有卡顿,能够确保程序在最快的时间内出现卡顿,从而快速确认逻辑上是否有不足或者缺陷,确认这种卡顿自己是不是正常现象。
三、语言层提供协程支持
多线程编程的方式明显是支持了高并发,但由于整个程序线程间上下文调度可能形成cpu的利用率不是那么高,而基于事件驱动的编程方式效果很是好的,但对编程功底要求很是高,并且在实现的时候须要花费的时间也是最多的。因此一种比较折中的方式是考虑采用提供协程支持的语言好比golang这种的。
简单说就是语言层面抽象出了一种更轻量级的线程,通常称为协程,在golang里又叫goroutine,这些底层最终也是须要用操做系统的线程去跑,在golang的runtime实现时底层用到的操做系统的线程数量相对会少一点,而上层程序里能够跑不少的goroutine,这些goroutine会在语言层面进行调度,看该由哪一个线程来最终执行这个goroutine。
由于goroutine之间的切换代价是远小于操做系统线程之间的切换代价,而底层用到的操做系统数量又较少,线程间的上下文切换代价原本也会大大下降。
这类语言能比其余语言的多线程方式提供更好的并发,由于它将操做系统的线程间切换的代价在语言层面尽量挤压到最小,同时编程复杂度大大下降,在这类语言中上下文逻辑能够保持连贯。由于下降了线程间上下文切换的代价,而goroutine之间的切换成本相对来讲是远远小于线程间切换成本,因此cpu的有效计算能力相对来讲也不会过低,至关于能够比较容易的得到了一个高并发且性能还能够的服务。
四、小结
如何提高单机服务的性能及并发,若是对性能或者高并发的要求没有达到很是苛刻的要求,选型的时候基于事件驱动的方式能够优先级下降一点,选择普通的多线程编程便可(其实多数场景均可以知足了),若是想单机的并发程度更好一点,能够考虑选择有协程支持的语言,若是还嫌不够,那就将逻辑理顺,考虑采用基于事件驱动的模式,这个在C/C++里直接用select/epoll/kevent等就能够了,在java里能够考虑采用NIO的方式,而从这点上来讲像golang这种提供协程支持的语言通常是不支持在程序层面本身实现基于事件驱动的编程方式的。
4、总结
其实并无一刀切的万能法则,大致原则是根据实际状况具体问题具体分析,找到服务瓶颈,资源不够加资源,尽量下降每次访问的资源消耗,总体服务每一个环节尽可能作到能够水平扩展,同时尽可能提升单机的有效利用率,从而确保在扛住整个服务的同时尽可能下降资源消耗成本。
Q&A
Q1:在用NIO多线程下,涉及到线程间的数据,怎么交互比较好呢?
A1:在NIO的状况下,通常是避免使用多线程,其实NIO本质上和C/C++里使用epoll效果是相似的,因此像nginx/redis里并不存在多线程的状况(内部实现的时候一些特殊状况除外)。 可是若是确实是有NIO触发之后须要将链接丢给线程池去处理的状况,好比涉及到耗时操做,同时确实涉及到临界资源,那只能建议不要让NIO所在的线程去访问这个临界资源,不然整个NIO卡住整个服务就卡住了。尽可能避免NIO所在线程出现有锁等待等任何可能阻塞的状况。
Q2:请问老师MySQL也是采用epoll机制吗?
A2:MySQL链接池版参考mariadb的实现其实也有用到epoll这种机制,可是跟咱们一般理解基于事件驱动的方式不太同样,咱们通常会将其归类为每链接每线程/线程池的方式,至关于将链接最后仍是要分配丢给某个线程去处理,并且这个访问操做自己多是比较耗时的,会在较长一段时间内一直占用这个线程,并发主要是靠多个线程之间的调度达到并发效果。
Q3:Redis、MySQL数据强一致性方案能稍微讲讲吗?
A3:这个还得看具体业务场景,理论上没有特别完美能保证严格一致的,可是在实际状况下能够灵活处理。好比我以前提到的,像商品价格,若是访问量足够大,大到缓存失效打到数据库时直接能够将数据库打趴下,那也能够特殊状况特殊对待,直接让访问打到缓存为止。缓存挂了,访问直接失败,直到从新将数据加载进去。 还有一些状况是频繁的写操做,但写的内容未必那么重要的,能够接受丢失,可是写操做很是频繁,那么能够将写先写到缓存直接返回成功,后续再慢慢将数据同步到数据库。
做者:头条号 / DBAplus社群 连接:http://toutiao.com/a6329244529665310977/ 来源:头条号(今日头条旗下创做平台) 著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。