当然,使用非阻塞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中取得这些静态资源,以提高加载这些静态数据的速度。这样服务器就不必再处理从世界各地所发来的对静态资源的请求,进而降低了服务器的负载。
数据库的扩展性
相较于服务实例,数据库的扩展则是一个更为复杂的话题。我们知道,不同的服务对数据的使用方式常常具有很大的差异。例如不同的服务常常具有非常不同的读写比,而另一些服务则更强调扩展性。因此如何对数据库进行扩展并没有一个统一的方法,而常常决定于应用自身对数据的要求。因此在本节中,我们将采取由下向上的方法讲解如何对数据库进行扩展。
通常情况下,对一个话题自上而下的讲解常常能够形成较好的知识系统。在使用该方式对问题进行讲解的时候,我们将首先提出问题,然后再以该问题为中心讲解组成该问题的各个子问题。在讲解中我们需要逐一地解决这些子问题,并将这些子问题的解决方案进行关联和比较。通过这种方式,读者常常能够更清晰地认识到各个解决方案的优点和缺点,进而能够根据问题的实际情况对解决方案进行取舍。这种方法较为适合问题较为简单并且清晰的情况。
而在问题较为复杂,包含情况较多的情况下,我们就需要将这些问题拆分为子问题,并在讲清楚各个子问题之后再去分析整个问题如何通过这些子问题解决方案合作解决。