在编写一个应用时,我们常常考虑的是该应用应该如何实现特定的业务逻辑。但是在逐渐发展出越来越多的用户后,这些应用常常会暴露出一系列问题,如不容易增大容量,容错性差等等。这常常会导致这些应用在市场的拓展过程中无法快速地响应用户的需求,并最终失去商业上的先机。
通常情况下,我们将应用所具有的用来避免这一系列问题的特征称为非功能性需求。相信您已经能够从字面意义上理解这个名词了:功能性需求用来提供对业务逻辑的支持,而非功能性需求则是一系列和业务逻辑无关,却可能影响到产品后续发展的一系列需求。这些需求常常包括:高可用性(High Avalibility),扩展性(Scalability),维护性(Maintainability),可测试性(Testability)等等。
而在这些非功能性需求中,扩展性可能是最有趣的一种了。因此在本文中,我们将对如何编写一个具有高可扩展性的应用进行讲解。
什么是扩展性
假设我们编写了一个Web应用,并将其置于共有云上以向用户提供服务。该应用的创意非常新颖,并在短时间内就吸引了大量的用户。但是由于我们在编写该应用时并没有期望它来处理这么多用户的请求,因此它的运行速度越来越慢,甚至可能出现服务没有响应的情况。频繁发生这种事情的结果就是,用户将无法忍受该应用经常性地宕机,并将寻找其它类似应用来获得类似的服务。
该应用所缺少的能够根据负载来对处理能力进行适当扩展的能力便是应用的扩展性,而其衡量的标准则是处理能力扩展的简单程度。如果您的应用在添加了更多内存后就能运行得更好,或者通过添加一个额外的服务实例就能解决服务实例过载的问题,那么我们就可以说该应用的扩展性非常好。如果为了处理更多的负载而不得不重写整个应用,那么应用的开发者就需要在多多注意应用的扩展性了。
较好的扩展性不仅可以省却您重写应用的麻烦,更重要的是,它会帮助您在市场的争夺中获得先机。试想一下,如果您的应用已经出现了处理能力不够的苗头,却没有适当的解决方案来提高整个系统的处理能力,那么您能做的事情只能是重新编写一个具有更高处理能力的具有同一个功能的应用。在该段时间内,您的应用的处理能力显得越来越捉襟见肘。而体现在客户层面上的,则是您的应用的响应速度越来越慢,甚至有时都无法正常工作。在新应用上线之前,您的应用将逐渐地流失客户。而这些流失的客户则很有可能变成类似软件的忠实客户,从而使得您的产品失去了市场竞争的先机。反过来,如果您的应用具有非常良好的扩展性,而您的竞争对手并没有跟上用户的增长速度,那么的应用就有了完全超越甚至压制竞争对手的可能。
当然,一个成功的应用不应该仅仅拥有高扩展性,而是应该在一系列非功能性需求上都做得很好。例如您的应用不应该有太多的Bug,也不应该有特别严重的Bug,以避免由于这些Bug导致您的用户无法正常使用应用。同时您的应用需要拥有较好的用户体验,这样才能让这些用户非常容易地熟悉您的应用,并产生用户粘性。
当然,这些非功能性需求并不仅仅局限在用户的角度。例如从开发团队的角度来讲,一个软件的可测试性常常决定了测试组的工作效率。如果一个应用需要在几十台机器上逐一安装部署,那么每次测试人员对新版本的验证都需要几个小时甚至成天的时间才能准备完毕。测试组也就很自然地成为了该软件开发组中效率最为低下的一部分。为此我们就需要招入大量的测试人员,大大地增加了应用的整体开销。
总的来说,一个应用所具有的非功能性需求非常多,如完整性(Completeness),正确性(Correctness),可用性(Availability),可靠性(Reliability),安全(Security),扩展性(Scalability),性能(Performance)等等。而这些需求都会对如何分析,设计以及编码提出一定的要求。不同的非功能性需求所提出的要求常常会发生冲突。而到底哪个非功能性需求更为重要则需要根据您所编写的应用类型来决定。例如在编写一个大规模Web应用的时候,扩展性,安全以及可用性较为重要,而对于一个实时应用来说,性能以及可靠性则占据上风。在这篇文章中,我们的讨论将主要集中在扩展性上。因此其所提出的一系列建议可能会对其它的非功能性需求产生较大的影响。而到底如何取舍则需要读者根据应用的实际情况自行决定。
应用的扩展方法
好的,让我们重新回到扩展性这个话题上来。导致一个软件需要扩展的最根本原因实际上还是其所需要面对的吞吐量。在用户的一个请求到达时,服务实例需要对它进行处理并将其转化为对数据的操作。在这个过程中,服务实例以及数据库都需要消耗一定的资源。如果用户的请求过多从而导致应用中的某个组成所无法应对,那么我们就需要想办法提高该组成的数据处理能力。
提高数据处理能力的方法主要分为两类,那就是纵向扩展及横向扩展。而这两种方法所对应的操作就是Scale Up以及Scale Out。
纵向扩展表示在需要处理更多负载时通过提高单个系统处理能力的方法来解决问题。最简单的情况就是为该系统提供更为强大的硬件。例如如果数据库所在的服务器实例只有2G内存,进而导致了数据库不能高效地运行,那么我们就可以通过将该服务器的内存扩展至8G来解决这个问题:
上图所展示的就是通过添加内存进行纵向扩展,以解决数据库所在服务实例IO过高的情况:当运行数据库服务的服务器所包含的内存不能加载数据库中所存储的最为常见的数据时,其会不断地从硬盘中读取持久化到磁盘中的内存页面,从而导致数据库的性能大幅下降。而在将服务器的内存扩展到8G的情况下,那些常用数据就能够长时间地驻留在内存中,从而使得数据库所在服务实例的磁盘IO迅速回复正常。
除了通过硬件方法来提高单个服务实例的性能之外,我们还可以通过优化软件的执行效率来完成应用的纵向扩展。最简单的示例就是,如果原有的服务实现只能使用单线程来处理数据,而不能同时利用服务器实例中所包含的多个CPU核心,那么我们可以通过将算法更改为多线程来充分利用CPU的多核计算能力,成倍地提高服务的执行效率。
但是纵向扩展并非总是最正确的选择。影响我们选择的最常见因素就是硬件的成本。我们知道,硬件的价格通常与该硬件所处的定位有关。如果一个硬件是当前市场上的主流配置,那么由于它已经大量出货,因此平摊的研发成本在每件硬件中已经变得非常小。反过来,如果一个硬件是刚刚投入市场的高端产品,那么每件硬件所包含的研发成本将会非常多。因此纵向扩展的投入性能比曲线常常如下所示:
也就是说,在单个实例优化到一定程度以后,再花费大量的时间和金钱来对单个实例的性能进行提高已经没有太多的意义了。在这个时候,我们就需要考虑横向扩展,也就是使用多个服务实例来一起提供服务。
就以一个在线的图像处理服务为例。由于图像处理是一个非常消耗资源的计算过程,因此单个服务器常常无法满足大量用户所发送的请求: