在编写一个应用时,咱们经常考虑的是该应用应该如何实现特定的业务逻辑。可是在逐渐发展出愈来愈多的用户后,这些应用经常会暴露出一系列问题,如不容易增大容量,容错性差等等。这经常会致使这些应用在市场的拓展过程当中没法快速地响应用户的需求,并最终失去商业上的先机。html
一般状况下,咱们将应用所具备的用来避免这一系列问题的特征称为非功能性需求。相信您已经可以从字面意义上理解这个名词了:功能性需求用来提供对业务逻辑的支持,而非功能性需求则是一系列和业务逻辑无关,却可能影响到产品后续发展的一系列需求。这些需求经常包括:高可用性(High Avalibility),扩展性(Scalability),维护性(Maintainability),可测试性(Testability)等等。nginx
而在这些非功能性需求中,扩展性多是最有趣的一种了。所以在本文中,咱们将对如何编写一个具备高可扩展性的应用进行讲解。算法
什么是扩展性数据库
假设咱们编写了一个Web应用,并将其置于共有云上以向用户提供服务。该应用的创意很是新颖,并在短期内就吸引了大量的用户。可是因为咱们在编写该应用时并无指望它来处理这么多用户的请求,所以它的运行速度愈来愈慢,甚至可能出现服务没有响应的状况。频繁发生这种事情的结果就是,用户将没法忍受该应用常常性地宕机,并将寻找其它相似应用来得到相似的服务。apache
该应用所缺乏的可以根据负载来对处理能力进行适当扩展的能力即是应用的扩展性,而其衡量的标准则是处理能力扩展的简单程度。若是您的应用在添加了更多内存后就能运行得更好,或者经过添加一个额外的服务实例就能解决服务实例过载的问题,那么咱们就能够说该应用的扩展性很是好。若是为了处理更多的负载而不得不重写整个应用,那么应用的开发者就须要在多多注意应用的扩展性了。缓存
较好的扩展性不只能够省却您重写应用的麻烦,更重要的是,它会帮助您在市场的争夺中得到先机。试想一下,若是您的应用已经出现了处理能力不够的苗头,却没有适当的解决方案来提升整个系统的处理能力,那么您能作的事情只能是从新编写一个具备更高处理能力的具备同一个功能的应用。在该段时间内,您的应用的处理能力显得愈来愈捉襟见肘。而体如今客户层面上的,则是您的应用的响应速度愈来愈慢,甚至有时都没法正常工做。在新应用上线以前,您的应用将逐渐地流失客户。而这些流失的客户则颇有可能变成相似软件的忠实客户,从而使得您的产品失去了市场竞争的先机。反过来,若是您的应用具备很是良好的扩展性,而您的竞争对手并无跟上用户的增加速度,那么的应用就有了彻底超越甚至压制竞争对手的可能。安全
固然,一个成功的应用不该该仅仅拥有高扩展性,而是应该在一系列非功能性需求上都作得很好。例如您的应用不该该有太多的Bug,也不该该有特别严重的Bug,以免因为这些Bug致使您的用户没法正常使用应用。同时您的应用须要拥有较好的用户体验,这样才能让这些用户很是容易地熟悉您的应用,并产生用户粘性。服务器
固然,这些非功能性需求并不只仅局限在用户的角度。例如从开发团队的角度来说,一个软件的可测试性经常决定了测试组的工做效率。若是一个应用须要在几十台机器上逐一安装部署,那么每次测试人员对新版本的验证都须要几个小时甚至整天的时间才能准备完毕。测试组也就很天然地成为了该软件开发组中效率最为低下的一部分。为此咱们就须要招入大量的测试人员,大大地增长了应用的总体开销。网络
总的来讲,一个应用所具备的非功能性需求很是多,如完整性(Completeness),正确性(Correctness),可用性(Availability),可靠性(Reliability),安全(Security),扩展性(Scalability),性能(Performance)等等。而这些需求都会对如何分析,设计以及编码提出必定的要求。不一样的非功能性需求所提出的要求经常会发生冲突。而到底哪一个非功能性需求更为重要则须要根据您所编写的应用类型来决定。例如在编写一个大规模Web应用的时候,扩展性,安全以及可用性较为重要,而对于一个实时应用来讲,性能以及可靠性则占据上风。在这篇文章中,咱们的讨论将主要集中在扩展性上。所以其所提出的一系列建议可能会对其它的非功能性需求产生较大的影响。而到底如何取舍则须要读者根据应用的实际状况自行决定。数据结构
应用的扩展方法
好的,让咱们从新回到扩展性这个话题上来。致使一个软件须要扩展的最根本缘由实际上仍是其所须要面对的吞吐量。在用户的一个请求到达时,服务实例须要对它进行处理并将其转化为对数据的操做。在这个过程当中,服务实例以及数据库都须要消耗必定的资源。若是用户的请求过多从而致使应用中的某个组成所没法应对,那么咱们就须要想办法提升该组成的数据处理能力。
提升数据处理能力的方法主要分为两类,那就是纵向扩展及横向扩展。而这两种方法所对应的操做就是Scale Up以及Scale Out。
纵向扩展表示在须要处理更多负载时经过提升单个系统处理能力的方法来解决问题。最简单的状况就是为该系统提供更为强大的硬件。例如若是数据库所在的服务器实例只有2G内存,进而致使了数据库不能高效地运行,那么咱们就能够经过将该服务器的内存扩展至8G来解决这个问题:
上图所展现的就是经过添加内存进行纵向扩展,以解决数据库所在服务实例IO太高的状况:当运行数据库服务的服务器所包含的内存不能加载数据库中所存储的最为常见的数据时,其会不断地从硬盘中读取持久化到磁盘中的内存页面,从而致使数据库的性能大幅降低。而在将服务器的内存扩展到8G的状况下,那些经常使用数据就可以长时间地驻留在内存中,从而使得数据库所在服务实例的磁盘IO迅速回复正常。
除了经过硬件方法来提升单个服务实例的性能以外,咱们还能够经过优化软件的执行效率来完成应用的纵向扩展。最简单的示例就是,若是原有的服务实现只能使用单线程来处理数据,而不能同时利用服务器实例中所包含的多个CPU核心,那么咱们能够经过将算法更改成多线程来充分利用CPU的多核计算能力,成倍地提升服务的执行效率。
可是纵向扩展并不是老是最正确的选择。影响咱们选择的最多见因素就是硬件的成本。咱们知道,硬件的价格一般与该硬件所处的定位有关。若是一个硬件是当前市场上的主流配置,那么因为它已经大量出货,所以平摊的研发成本在每件硬件中已经变得很是小。反过来,若是一个硬件是刚刚投入市场的高端产品,那么每件硬件所包含的研发成本将会很是多。所以纵向扩展的投入性能比曲线经常以下所示:
也就是说,在单个实例优化到必定程度之后,再花费大量的时间和金钱来对单个实例的性能进行提升已经没有太多的意义了。在这个时候,咱们就须要考虑横向扩展,也就是使用多个服务实例来一块儿提供服务。
就以一个在线的图像处理服务为例。因为图像处理是一个很是消耗资源的计算过程,所以单个服务器经常没法知足大量用户所发送的请求:
就像上图中所展现的那样,虽然咱们的服务器已经安装了4个CPU,可是在单个服务器实例提供服务的状况下,CPU使用率仍是一直处于警惕线之上。若是咱们再在应用中添加一个相同的服务器来共同处理用户的请求,那么每台服务器的负载将会降到原有负载的一半左右,从而使得CPU使用率保持在警惕线之下。
在这种状况下,该服务所提供的一系列其它功能也随之获得了扩充。例如对处理结果进行保存的功能的性能也将变成原来的两倍。只是因为咱们暂时并不须要这种扩充,所以该部分性能的加强其实是毫无用处的,甚至形成了服务资源的浪费:
从上图中能够看到,在没有横向扩展以前,橙色组成的负载已经达到了90%,接近单个服务实例的极限。为了解决这个问题,咱们再引入一个服务器实例来分担工做。可是这样会致使其它几个原本资源利用率就已经不高的组成的利用率降得更低。而更为正确的扩展方式则是只扩展橙色组成:
从上面的讲解中能够看出,横向扩展实际上包含了不少种方式。相应地,《The Art of Scalability》一书则介绍了一个横向扩展所须要遵照的AKF扩展模型。根据AKF扩展模型,横向扩展实际上包含了三个维度,而横向扩展解决方案则是这三个维度上所作工做的结合:
上图中展现了AKF扩展模型的最通用的表示形式。在该图中,原点O表示的是应用实例并无能力执行任何横向扩展,而只能经过纵向扩展来提升它的服务能力。若是您的系统朝着某个坐标轴的方向前进,那么它就将获得必定程度的横向扩展能力。固然,这三个坐标轴并不互斥,所以您的应用可能同时拥有XYZ三个轴向的扩展能力:
如今就让咱们来看一下AKF扩展模型中各个坐标轴的意义。首先要讲解的就是X轴。在AKF扩展模型中,X轴表示的是应用能够经过部署更多的服务实例来解决扩展性的问题。在这种状况下,本来须要少许服务实例处理的大量负载就能够经过新添加的其它服务实例分担,从而扩大了系统容量,下降了单个服务实例的压力。
咱们刚刚提到过,一个服务的扩展性能够同时由多个轴向的扩展性共同组成,所以在该服务中,这种X轴方向的扩展性不只仅存在于服务这个层次上,更能够由子服务,甚至服务组成的扩展性来共同完成:
请注意上图中的橙色方块。在该服务中,橙色方块做为一个子服务来向整个服务提供特定功能。在须要扩展时,咱们能够经过添加一个新的橙色子服务实例来解决橙色服务负载过大的问题。所以就整个服务而言,其X轴的横向扩展能力并非经过从新部署整套服务来完成的,而是对独立的子服务进行扩容。
相信您会问:既然只经过添加新的服务或子服务实例就可以完成对服务容量的扩充,那么咱们还须要其它两个轴向的横向扩展能力么?
答案是确定的。首先,最为现实的问题就是服务运行场景的约束。例如在对服务进行X轴横向扩展的时候,咱们经常须要一个负载平衡服务。在《企业级负载平衡简介》一文中咱们已经说过,负载平衡服务器经常具备必定的性能限制。所以横向扩展并不是全无止境。除此以外,咱们也看到了横向扩展有时是使用在子服务上的,而将一个大的服务分割为多个子服务,自己也是沿着其它轴向的横向扩展。
Y轴横向扩展的意义则在于将全部的工做根据数据的类型或业务逻辑进行划分。而就一个Web服务而言,Y轴横向扩展所作的最主要工做就是将一个Monolith服务划分为一系列子服务,从而使不一样的子服务独立工做并拥有独立地进行横向扩展的能力。这一方面能够将本来一个服务所处理的全部请求分担给一系列子服务实例来运行,更可让您根据应用的实际运行状况来对某个成为系统瓶颈的子服务进行X轴横向扩展,避免因为对整个服务进行X轴横向扩展所形成的资源浪费:
这种组织各个子服务的方式被称为Microservice。使用Microservice组织子服务还能够帮助您实现一系列其它非功能性需求,如高可用性,可测试性等等。具体内容详见《Microservice架构模式简介》一文。
相较而言,执行Y轴扩展要比执行X轴扩展困难一些。可是其经常会使得其它一系列非功能性需求具备更高的质量。
而在Z轴上的横向扩展多是你们所最不熟悉的状况。其表示须要根据用户的某些特性对用户的请求进行划分。例如使用基于DNS的负载平衡。
固然,到底您的服务须要实现什么程度的X,Y,Z轴扩展能力则须要根据服务的实际状况来决定。若是一个应用的最终规模并不大,那么只拥有X轴扩展能力,或者有部分Y轴扩展能力便可。若是一个应用的增加很是迅速,并最终演变为对吞吐量有极高要求的应用,那么咱们就须要从一开始就考虑这个应用在X,Y,Z轴的扩展能力。
服务的扩展
好了,介绍了那么多理论知识,相信您已经火烧眉毛地想要了解如何令一个应用具备良好的扩展性了吧。那好,让咱们首先从服务实例的扩展性提及。
咱们已经在前面介绍过,对服务进行扩展主要有两种方法:横向扩展以及纵向扩展。对于服务实例而言,横向扩展很是简单:无非是将服务分割为众多的子服务并在负载平衡等技术的帮助下在应用中添加新的服务实例:
上图展现了服务实例是如何按照AKF扩展模型进行横向扩展的。在该图的最顶层,咱们使用了基于DNS的负载平衡。因为DNS拥有根据用户所在位置决定距离用户最近的服务这一功能,所以用户在DNS查找时所获得的IP将指向距离本身最近的服务。例如一个处于美国西部的用户在访问Google时所获得的IP可能就是64.233.167.99。这一功能即是AKF扩展模型中的Z轴:根据用户的某些特性对用户的请求进行划分。
接下来,负载平衡服务器就会根据用户所访问地址的URL来对用户的请求进行划分。例如用户在访问网页搜索服务时,服务集群须要使用左边的虚线方框中的服务实例来为用户服务。而在访问图片搜索服务时,服务集群则须要使用右边虚线方框中的服务实例。这则是AKF扩展模型中的Y轴:根据数据的类型或业务逻辑来划分请求。
最后,因为用户所最常使用的服务就是网页搜索,而单个服务实例的性能毕竟有限,所以服务集群中经常包含了多个用来提供网页搜索服务的服务实例。负载平衡服务器会根据各个服务实例的能力以及服务实例的状态来对用户的请求进行分发。而这则是沿着AKF扩展模型中的X轴进行扩展:经过部署具备相同功能的服务实例来分担整个负载。
能够看到,在负载平衡服务器的帮助下,对应用实例进行横向扩展是很是简单的事情。若是您对负载平衡功能比较感兴趣,请查看个人另外一篇博文《企业级负载平衡简介》。
相较于服务的横向扩展,服务的纵向扩展则是一个经常被软件开发人员所忽视的问题。横向扩展诚然能够提供近乎无限的系统容量,可是若是一个服务实例自己的效能就十分低下,那么这种无限的横向扩展经常是在浪费金钱:
就像上图中所展现的那样,一个应用固然能够经过部署4台具备一样功能的服务器来为用户提供服务。在这种状况下,搭建该服务的开销是5万美圆。可是因为应用实现自己的质量不高,所以这四台服务器的资源使用率并不高。若是一个肯于动脑的软件开发人员可以仔细地分析服务实例中的系统瓶颈并加以改正,那么公司将可能只须要购买一台服务器,而员工的我的能力及薪水都会获得提高,并可能获得一笔额外的嘉奖。若是该员工为应用所添加的纵向扩展性足够高,那么该应用将能够在具备更高性能的服务器上运行良好。也就是说,单个服务实例的纵向扩展性不只仅能够充分利用现有硬件所能提供的性能,以辅助下降搭建整个服务的花费,更能够兼容具备更强资源的服务器。这就使得咱们能够经过简单地调整服务器设置来完成对整个服务的加强,如添加更多的内存,或者使用更高速的网络等方法。
如今就让咱们来看看如何提升单个服务实例的扩展性。在一个应用中,服务实例经常处于核心位置:其接受用户的请求,并在处理用户请求的过程当中从数据库中读取数据。接下来,服务实例会经过计算将这些数据库中获得的数据糅合在一块儿,并做为对用户请求的响应将其返回。在整个处理过程当中,服务实例还可能经过服务端缓存取得以前计算过程当中已经获得的结果:
也就是说,服务实例在运行时经常经过向其它组成发送请求来获得运行时所须要的数据。因为这些请求经常是一个阻塞调用,服务实例的线程也会被阻塞,进而影响了单个线程在服务中执行的效率:
从上图中能够看到,若是咱们使用了阻塞调用,那么在调用另外一个组成以得到数据的时候,调用方所在的线程将被阻塞。在这种状况下,整个执行过程须要3份时间来完成。而若是咱们使用了非阻塞调用,那么调用方在等待其它组成的响应时能够执行其它任务,从而使得其在4份时间内能够处理两个任务,至关于提升了50%的吞吐量。
所以在编写一个高吞吐量的服务实现时,您首先须要考虑是否应该使用Java所提供的非阻塞IO功能。一般状况下,由非阻塞IO组织的服务会比由阻塞IO所编写的服务慢,可是其在高负载的状况下的吞吐量较非阻塞IO所编写的服务高不少。这其中最好的证实就是Tomcat对非阻塞IO的支持。
在较早的版本中,Tomcat会在一个请求到达时为该请求分配一个独立的线程,并由该线程来完成该请求的处理。一旦该请求的处理过程当中出现了阻塞调用,那么该线程将挂起直至阻塞调用返回。而在该请求处理完毕后,负责处理该请求的线程将被送回到线程池中等待对下一个请求进行处理。在这种状况下,Tomcat所能并行处理的最大吞吐量实际上与其线程池中的线程数量相关。反过来,若是将线程数量设置得过大,那么操做系统将忙于处理线程的管理及切换等一系列工做,反而下降了效率。而在一些较新版本中,Tomcat则容许用户使用非阻塞IO。在这种状况下,Tomcat将拥有一系列用来接收请求的线程。一旦请求到达,这些线程就会接收该请求,并将请求转给真正处理请求的工做线程。所以在新版Tomcat的运行过程当中将只包括几十个线程,却可以同时处理成千上万的请求。固然,因为非阻塞IO是异步的,而不是在调用返回时就当即执行后续处理,所以其处理单个请求的时间较使用阻塞IO所须要的时间长。
所以在服务少许的用户时,使用非阻塞IO的Tomcat对于单个请求的响应时间经常是Tomcat的2倍以上,可是在用户数量是成千上万个的时候,使用非阻塞IO的Tomcat的吞吐量则很是稳定:
所以若是想要提升您的单个服务性能,首先您须要保证您在Tomcat等Web容器中正确地使用了非阻塞模式:
<Connector connectionTimeout="20000" maxThreads="1000" port="8080"
protocol="org.apache.coyote.http11.Http11NioProtocol" redirectPort="8443"/>
固然,使用非阻塞IO并不只仅是经过配置Tomcat就完成了。试想在一个子服务实现中调用另外一个子服务的状况:若是在调用子服务时调用方被阻塞,那么调用方的一个线程就被阻塞在那里,而不能处理其它待处理的请求。所以在您的应用中包含了较长时间的的阻塞调用时,您须要考虑使用非阻塞方式组织服务的实现。
在使用非阻塞方式组织服务以前,您最好详细地阅读《Enterprise Integration Pattern》。Spring旗下的项目Spring Integration则是Enterprise Integration Pattern在Spring体系中的一种实现。由于它是在是一个很是大的话题,所以我会在其它博文中对它们进行简单地介绍。
在经过使用非阻塞模式提升了并发链接数以后,咱们就须要考虑是否其它硬件会成为单个服务实例的瓶颈了。首先,更大的并发会致使更大的内存占用。所以若是您所开发的应用对内存大小较为敏感,那么您首先要作的就是为系统添加内存。并且在您的内存敏感应用的实现中,内存管理也会变成您须要考虑的一项任务。虽说不少语言,如Java等,已经经过提供垃圾回收机制解决了野指针,内存泄露等一系列问题,可是在这些垃圾回收机制启动的时候,您的服务会暂时挂起。所以在服务实现的过程当中,您须要考虑经过一些技术来尽可能避免内存回收。
另一个和硬件有关的话题可能就是CPU了。一个服务器经常包含多个CPU,而这些CPU能够包含多个核,所以在该台服务实例上经常能够同时运行十几个,甚至几十个线程。可是在实现服务时,咱们经常忽略了这种信息,从而致使某些服务只能由少数几个线程并行执行。一般状况下,这都是由于服务过多地访问同一个资源,如过多地使用了锁,同步块,或者是数据库性能不够等一系列缘由。
还有一个须要考虑的事情就是服务的动静分离。若是一个应用须要提供一系列静态资源,那么那些经常使用的Servlet容器可能并非一个最优的选择。一些轻量级的Web服务器,如nginx在服务静态资源时的效率就将明显高于Apache等一系列动态内容服务器。
因为这篇文章的主旨并非为了讲解如何编写一个具备较高性能的服务,所以对于上面所述的各类加强单个服务性能的技巧将再也不进行深刻讲解。
除了从服务自身下功夫来加强一个服务实例的纵向扩展性以外,咱们还有一个重要的用来提升服务实例工做效率的武器,那就是服务端缓存。这些缓存经过将以前获得的计算结果记录在缓存系统中,从而尽量地避免对该结果再次进行计算。经过这种方式,服务端缓存能大大地减轻数据库的压力:
那它和服务的扩展性有什么关系呢?答案是,若是服务端缓存可以减轻系统中每一个服务的负载,那么它实际上至关于提升了单个服务实例的工做效率,减小了其它组成对扩容的需求,变相地增长了各个相关组成的扩展性。
如今市面上较为主流的服务端缓存主要分为两种:运行于服务实例之上并与服务实例处于同一个进程以内的缓存,以及在服务实例以外独立运行的缓存。然后一种则是如今较为流行的解决方案:
从上图中能够看出,因为进程内缓存与特定的应用实例绑定,所以每一个应用实例将只能访问特定的缓存。这种绑定一方面会致使单个服务实例所可以访问的缓存容量变得很小,另外一方面也可能致使不一样的缓存实例中存在着冗余的数据,下降了缓存系统的总体效率。相较而言,因为独立缓存实例是独立于各个应用服务器实例运行的,所以应用服务实例能够访问任意的缓存实例。这同时解决了服务实例可以使用的缓存容量太小以及冗余数据这两个问题。
若是您但愿了解更多的有关如何搭建服务端缓存的知识,请查看个人另外一篇博文《Memcached简介》。
除了服务端缓存以外,CDN也是一种预防服务过载的技术。固然,它的最主要功能仍是提升距离服务较远的用户访问服务的速度。一般状况下,这些CDN服务会根据请求分布及实际负载等众多因素在不一样的地理区域内搭建。在提供服务时,CDN会从服务端取得服务的静态数据,并缓存在CDN以内。而在一个距离该服务较远的用户尝试使用该服务时,其将会从这些CDN中取得这些静态资源,以提升加载这些静态数据的速度。这样服务器就没必要再处理从世界各地所发来的对静态资源的请求,进而下降了服务器的负载。
数据库的扩展性
相较于服务实例,数据库的扩展则是一个更为复杂的话题。咱们知道,不一样的服务对数据的使用方式经常具备很大的差别。例如不一样的服务经常具备很是不一样的读写比,而另外一些服务则更强调扩展性。所以如何对数据库进行扩展并无一个统一的方法,而经常决定于应用自身对数据的要求。所以在本节中,咱们将采起由下向上的方法讲解如何对数据库进行扩展。
一般状况下,对一个话题自上而下的讲解经常可以造成较好的知识系统。在使用该方式对问题进行讲解的时候,咱们将首先提出问题,而后再以该问题为中心讲解组成该问题的各个子问题。在讲解中咱们须要逐一地解决这些子问题,并将这些子问题的解决方案进行关联和比较。经过这种方式,读者经常可以更清晰地认识到各个解决方案的优势和缺点,进而可以根据问题的实际状况对解决方案进行取舍。这种方法较为适合问题较为简单而且清晰的状况。
而在问题较为复杂,包含状况较多的状况下,咱们就须要将这些问题拆分为子问题,并在讲清楚各个子问题以后再去分析整个问题如何经过这些子问题解决方案合做解决。
那么如何将数据库的扩展性分割为子问题呢?在决定一个数据库应该拥有哪些特性时,经常用来做为评判标准的就是CAP理论。该理论指出咱们很难保证数据库的一致性(Consistency),可用性(Availability)以及分区容错性(Partition tolerance):
所以一系列数据库都选择了其中的两个特性来做为其实现的重点。例如常见的关系型数据库主要保证的是数据的一致性及数据的可用性,而并不强调对扩展性很是重要的分区容错性。这也即是数据库的横向扩展成为业界难题的一个缘由。
固然,若是您的应用对一致性或可用性的要求并非那么高,那么您就能够选择将分区容错性做为重点的数据库。这些类型的数据库有不少。例如如今很是流行的NoSQL数据库大多都将分区容错性做为一个实现重点。
所以在本节中,咱们将会以关系型数据库做为重点进行讲解。又因为对关系型数据库进行横向扩展经常较纵向扩展更为困难,所以咱们将首先讲解如何对关系型数据库进行横向扩展。
首先,最为常见也最为简单的纵向扩展方法就是增长关系型数据库所在服务实例的性能。咱们知道,数据库在运行时会将其所包含的数据加载在内存之中,并且最常访问的数据是否存在于内存之中是数据库是否运行良好的关键。若是数据库所在的服务实例可以根据实际负载提供足够的内存,以承载全部最常被访问的数据,那么数据库的性能将获得充分地发挥。所以在执行纵向扩展的第一步就是要检查您的数据库所在的服务实例是否拥有足够的资源。
固然,仅仅从硬件入手是不够的。在前面的章节中已经介绍过,纵向扩展须要从两个方面入手:硬件的加强,以及软件的优化。就数据库自己而言,其最重要的保证运行性能的组成就是索引。在当代的各个数据库中,索引主要分为聚簇索引以及非聚簇索引两种。这两种索引可以加速对具备特定特征的数据的查找:
所以在数据库优化过程当中,索引能够说是最为重要的一环。从上图中能够看出,若是一个查找可以经过索引来完成,而不是经过逐个查找数据库中所拥有的记录来进行,那么整个查找只须要分析组成索引的几个节点,而不是遍历数据库所拥有的成千上万条记录。这将会大大地提升数据库的运行性能。
可是若是索引没有存在于内存中,那么数据库就须要从硬盘中将它们读取到内存中再进行操做。这明显是一个很是慢的操做。所以为了您的索引可以正常工做,您首先要保证数据库运行所在的服务实例拥有足够的内存。
除了保证拥有足够的内存以外,咱们还须要保证数据库的索引自身没有过多的浪费内存。一个最多见的索引浪费内存的状况就是Index Fragmentation。也就是说,在通过一系列添加,更新和删除以后,数据库中的数据在存储中的物理结构中将变得再也不规律。这主要分为两种:Internal Fragmentation,即物理结构中可能存在着大量空白;External Fragmentation,即这些数据在物理结构中并非有序排列的。Internal Fragmentation意味着索引所包含节点的增长。这一方面致使咱们须要更大的空间来存储索引,从而占用更多的内存,另外一方面也会让数据寻找所须要遍历的节点数量增长,从而致使系统性能的降低。而External Fragmentation则意味着从磁盘顺序读取这些数据时须要硬盘从新进行寻址等操做,也会显著下降系统的执行性能。还有一个须要考虑的有关External Fragmentation的问题则是是否咱们的服务与其它服务使用了共享磁盘。若是是,那么其它服务对于磁盘的使用会致使External Fragmentation的问题没法从根本上解决,巡道操做将经常发生。
另一个经常使用的对索引进行优化的方法就是在非聚簇索引中经过INCLUDE子句包含特定列,以加快某些请求语句的执行速度。咱们知道,聚簇索引和非聚簇索引的差异主要就存在因而否包含数据。若是从聚簇索引中执行数据的查找,那么在找到对应的节点以后,咱们就已经能够从该节点中获得须要查找的数据。而若是咱们的查找是在非聚簇索引中进行的,那么咱们获得的则是目标数据所在的位置。为了找到真正的数据,咱们还须要进行一次寻址操做。而在经过INCLUDE子句包含了所须要数据的状况下,咱们就能够避免此次寻址,进而提升了查找的性能。
可是须要注意的是,索引是数据库在其自己所拥有的数据以外额外建立的数据结构,所以其实际上也须要占用内存。在插入及删除数据的时候,数据库一样须要维护这些索引,以保证索引和实际数据的一致性,所以其会致使数据库插入及删除操做性能的降低。
还有一个须要考虑的则是经过正确地设置Fill Factor来尽可能避免Page Split。在常见的数据库中,数据是记录在具备固定大小的页中。当咱们须要插入一条数据的时候,目标页中的可用空间可能已经不足以再添加一条新的数据。此时数据库会添加一个新的页,并将数据从一个页分到这两个页中。在该过程当中,数据库不只仅要添加及修改数据页自己,更须要对IAM等页进行更改,所以是一个较为消耗资源的操做。FillFactor是一个用来控制在叶页建立时每页所填充的百分比的全局设置。在设置了FillFactor的基础之上,用户还能够设置PAD_INDEX选项,来控制非叶页也使用FillFactor来控制数据的填充。一个较高的FillFactor会使数据更加集中,由此拥有更高的读取性能。而一个较低的FillFactor则对写入较为友好,由于其防止了Page Split。
除了上面所述的各类方法以外,您还能够经过其它一系列数据库功能来提升性能。这其中最重要的固然是各个数据库所提供的执行计划(Execution Plan)。经过执行计划,您能够看到您正在执行的请求是如何被数据库执行的:
因为如何提升单个数据库的性能是一个庞大的话题,而咱们的文章主要集中在如何提升扩展性,所以咱们在这里再也不对如何提升数据库的执行性能进行详细的介绍。
反过来,因为单个服务器的性能毕竟有限,所以咱们并不能无限地对关系型数据库进行纵向扩展。所以在必要条件下,咱们须要考虑对关系型数据库进行横向扩展。而将AKF横向扩展模型施行在关系型数据库之上后,其各个轴的意义则以下所示:
如今就跟我来看看各个轴的含义。在AKF模型中,X轴表示的是应用能够经过部署更多的服务实例来解决扩展性的问题。而因为关系型数据库要管理数据的读写并保证数据的一致性,所以在X轴上的扩展将不能简单地经过部署额外的数据库实例来解决问题。在进行X轴扩展的时候,这些数据库实例经常拥有不一样的职责并组成特定的拓扑结构。这就是数据库的Replication。
而相较于X轴,数据库AKF模型中的Y轴和Z轴则较为容易理解。AKF模型中的Y轴表示的是将全部的工做根据数据的类型或业务逻辑进行划分,而Z轴则表示根据用户的某些特性对用户的请求进行划分。这两种划分实际上都是要将数据库中的数据划分到多个数据库实例中,所以它们对应的则是数据库的Partition。
让咱们先看看数据库的Replication。简单地说,数据库的Replication表示的就是将数据存储在多个数据库实例中。读请求能够在任意的数据库实例上执行,而一旦某个数据库实例上发生了数据的更新,那么这些更新将会自动复制到其它数据库实例上。在数据复制的过程当中,数据源被称为Master,而目标实例则被称为Slave。这两个角色并非互斥的:在一些较为复杂的拓扑结构中,一个数据库实例可能既是Master,又是Slave。
在关系型数据库的Replication中,最为常见的拓扑模型就是简单的Master-Slave模型。在该模型中,对数据的读取能够在任意的数据库实例上完成。而在须要对数据进行更新的时候,数据将只能写入特定的数据库实例。此时这些数据的更改将沿着单一的方向从Master向Slave进行传递:
在该模型中,数据读取的工做是由Master和Slave共同处理的。所以在上图中,每一个数据库的读负载将是原来的一半左右。可是在写入时,Master和Slave都须要执行一次写操做,所以各个数据库实例的写负载并无下降。若是读负载逐渐增大,咱们还能够加入更多的Slave节点以分担读负载:
相信您如今已经清楚了,关系型数据库的横向扩展主要是经过加入一系列数据库实例来分担读负载来完成的。可是有一点须要注意的是,这种写入传递关系是靠Master和Slave中的一个独立的线程来完成的。也就是说,一个Master拥有多少个Slave,它的内部就须要维持多少个线程来完成对属于它的Slave的更新。因为在一个大型应用中经常可能包含上百个Slave实例,所以将这些Slave都归于同一个Master将致使Master的性能急剧降低。
其中一个解决方法就是将其中的某些Slave转化为其它Slave的Master,并将它们组织成为一个树状结构:
可是Master-Slave模型拥有一个缺点,那就是有单点失效的危险。一旦做为Master的数据库实例失效了,那么整个数据库系统,至少是以该Master节点为根的子系统将会失效。
而解决该问题的一种方法就是使用多Master的Replication模型。在该模型中,每一个Master数据库实例除了能够将数据同步给各个Slave以外,还能够将数据同步给其它的Master:
在这种状况下,咱们避免了单点失效的问题。可是若是两个数据库实例对同一份数据更新,那么它们将产生数据冲突。固然,咱们能够经过将对数据的划分为绝不相干的多个子集并由每一个Master节点负责某个特定子集的更新的方式来防止数据冲突。
从上图中能够看到,用户对数据的写入会根据特定条件来分配到不一样的数据库实例上。接下来,这些写入会同步到其它实例上,从而保持数据的一致性。可是既然咱们能将这些数据独立地切割为各个子集,那么咱们为何不去尝试一下数据库的Partition呢?
简单地说,数据库的Partition就是将数据库中须要记录的数据划分为一系列子集,并由不一样的数据库实例来记录这些数据子集所包含的数据。经过这种方法,对数据的读取以及写入负载都会根据数据所在的数据库实例来进行划分。而这也就是数据库沿AKF扩展模型的Y轴进行横向扩展的方法。
在执行数据库的Partition时,数据库原有的数据将被切分到不一样的数据库实例中。每一个数据库实例将只包含原数据库中几个表的数据,从而将对整个数据库的访问切分到不一样的数据库实例中:
可是在某些状况下,对数据库中的数据按表切分并不能解决问题。切分完毕后的某个数据库实例仍然可能承担了过多的负载。那么此时咱们就须要将该数据库再次切分。只是此次咱们所切分的是数据库中的数据行:
在这种状况下,咱们在对数据进行操做以前首先须要执行一次计算来决定数据所在的数据库实例。
然而数据库的Partition并非没有缺点。最多见的问题就是咱们不能经过同一条SQL语句操做不一样数据库实例中记录的数据。所以在决定对数据库进行切分以前,您首先须要仔细地检查各个表之间的关系,并确认被分割到不一样数据库中的各个表没有过多的关联操做。
好了。至此为止,咱们已经讲解了如何建立具备可扩展性的服务实例,缓存以及数据库。相信您已经对如何建立一个具备高扩展性的应用有了一个较为清晰的认识。固然,在撰写本文的过程当中,我也发现了一系列能够继续讲解的话题,如Spring Integration,以及对数据库Replication以及Partition(Sharding)的讲解。在有些方面(如数据库),我并非专家。可是我会尽我所能把本文所写的知识点一一陈述清楚。
转载请注明原文地址并标明转载:http://www.cnblogs.com/loveis715/p/5097475.html
商业转载请事先与我联系:silverfox715@sina.com