假设我们编写了一个Web应用并将其置于共有云上以向用户提供服务。该应用的创意非常新颖并在短时间内就吸引了大量的用户。但是由于我们在编写该应用时并没有期望它来处理这么多用户的请求因此它的运行速度越来越慢甚至可能出现服务没有响应的情况。频繁发生这种事情的结果就是用户将无法忍受该应用经常性地宕机并将寻找其它类似应用来获得类似的服务。该应用所缺少的能够根据负载来对处理能力进行适当扩展的能力便是应用的扩展性而其衡量的标准则是处理能力扩展的简单程度。如果您的应用在添加了更多内存后就能运行得更好或者通过添加一个额外的服务实例就能解决服务实例过载的问题那么我们就可以说该应用的扩展性非常好。如果为了处理更多的负载而不得不重写整个应用那么应用的开发者就需要在多多注意应用的扩展性了。较好的扩展性不仅可以省却您重写应用的麻烦更重要的是它会帮助您在市场的争夺中获得先机。试想一下如果您的应用已经出现了处理能力不够的苗头却没有适当的解决方案来提高整个系统的处理能力那么您能做的事情只能是重新编写一个具有更高处理能力的具有同一个功能的应用。在该段时间内您的应用的处理能力显得越来越捉襟见肘。而体现在客户层面上的则是您的应用的响应速度越来越慢甚至有时都无法正常工作。在新应用上线之前您的应用将逐渐地流失客户。而这些流失的客户则很有可能变成类似软件的忠实客户从而使得您的产品失去了市场竞争的先机。反过来如果您的应用具有非常良好的扩展性而您的竞争对手并没有跟上用户的增长速度那么的应用就有了完全超越甚至压制竞争对手的可能。当然一个成功的应用不应该仅仅拥有高扩展性而是应该在一系列非功能性需求上都做得很好。例如您的应用不应该有太多的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的负载平衡。当然到底您的服务需要实现什么程度的XYZ轴扩展能力则需要根据服务的实际情况来决定。如果一个应用的最终规模并不大那么只拥有X轴扩展能力或者有部分Y轴扩展能力即可。如果一个应用的增长非常迅速并最终演变为对吞吐量有极高要求的应用那么我们就需要从一开始就考虑这个应用在XYZ轴的扩展能力。服务的扩展好了介绍了那么多理论知识相信您已经迫不及待地想要了解如何令一个应用具有良好的扩展性了吧。那好让我们首先从服务实例的扩展性说起。我们已经在前面介绍过对服务进行扩展主要有两种方法横向扩展以及纵向扩展。对于服务实例而言横向扩展非常简单无非是将服务分割为众多的子服务并在负载平衡等技术的帮助下在应用中添加新的服务实例上图展示了服务实例是如何按照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