所谓系统的伸缩性是指不需要改变系统的软硬件设计,仅仅通过改变部署的服务器数量就可以扩大或者缩小系统的服务处理能力。
基础设计
在应用系统渐进式的演化过程中,最重要的技术手段就是使用服务器集群,通过不断地向集群中添加服务器来增强整个集群的处理能力。
一般说来,系统的伸缩性设计可分成两类,一类是根据功能进行物理分离实现伸缩,一类是单一功能通过集群实现伸缩。前者是不同的服务器部署不同的服务,提供不同的功能;后者是集群内的多台服务器部署相同的服务,提供相同的功能。
在应用系统发展的早期,通过增加服务器提高处理能力时,新增服务器总是从现有服务器中分离出部分功能和服务。具体又可以分成纵向分离和横向分离两种情况,纵向分离也即业务分层,横向分离则是业务拆分。
将不同功能分离部署可以实现一定程度的伸缩性,但是随着访问量的逐步增加,即使分离到最小粒度,单一的服务器也不能满足业务规模的要求。因此必须使用服务器集群,即将相同服务部署在多台服务器上构成一个集群整体对外提供服务。
应用层设计
应用服务器应该设计成无状态的,如果将部署有相同应用的服务器组成一个集群,每次用户请求都可以发送到集群中任意一台服务器上去处理。这样只要能将用户请求按照某种规则分发到集群的不同服务器上,就可以构成一个应用服务器集群。
这个请求分发装置就是所谓的负载均衡器。负载均衡是网站必不可少的基础技术手段,不但可以改善网站的可用性,同时还可以实现网站的伸缩性。具体的技术实现也多种多样,基础技术不外以下几种。
HTTP重定向
HTTP重定向服务器是一台普通的应用服务器,其唯一的功能就是根据用户的HTTP请求计算一台真实的Web服务器地址,并将该Web服务器地址写入HTTP重定向响应中(响应状态码302)返回给用户浏览器。
这种负载均衡方案的优点是比较简单。缺点是浏览器需要两次请求服务器才能完成一次访问,性能较差;重定向服务器自身的处理能力有可能成为瓶颈,整个集群的伸缩性规模有限;使用302响应码重定向,有可能使搜索引擎判断为SEO作弊,降低搜索排名。因此这种方案很少见。
DNS解析
我们可以在DNS服务器中为应用网址注册多条A记录,这样的话,每次域名解析请求都会根据负载均衡算法计算一个不同的IP地址返回,A记录中配置的多个服务器就构成一个集群,实现了负载均衡。
DNS域名解析负载均衡的优点是将负载均衡的工作转交给DNS,省掉了网站管理维护负载均衡服务器的麻烦,同时许多DNS还支持基于地理位置的域名解析,即会将域名解析成距离用户地理最近的一个服务器地址,这样可加快用户访问速度。
但是DNS域名解析负载均衡也有缺点,DNS是多级解析,每一级DNS都可能缓存A记录,当下线某台服务器后,即使修改了DNS的A记录,要使其生效也需要较长时间,这段时间,DNS依然会将域名解析到已经下线的服务器,导致用户访问失败;而且DNS负载均衡的控制权在域名服务商那里,网站无法对其做更多改善和更强大的管理。
大型网站总是部分使用DNS域名解析,利用域名解析作为第一级负载均衡手段。域名解析得到的一组服务器并不是实际提供Web服务的物理服务器,而是同样提供负载均衡服务的内部服务器,这组内部负载均衡服务器再进行负载均衡,将请求分发到真实的Web服务器上。
反向代理
之前我们提到可以利用反向代理缓存资源,以改善网站性能。同时大多数反向代理服务器还提供负载均衡的功能,管理一组Web服务器,将请求根据负载均衡算法转发到不同Web服务器上,Web服务器处理完成的响应也需要通过反向代理服务器返回给用户。
由于反向代理服务器转发请求在HTTP协议层面,因此也叫应用层负载均衡。其优点是和反向代理服务器功能集成在一起,部署简单。缺点是反向代理服务器是所有请求和响应的中转站,其性能可能会成为瓶颈。
IP负载均衡
在网络层,可以通过修改请求目标地址进行负载均衡。用户请求数据包到达负载均衡服务器后,负载均衡服务器根据负载均衡算法计算得到一台真实Web服务器地址,然后将数据包的目的IP地址修改为该地址。真实Web应用服务器处理完成后,响应数据包回到负载均衡服务器,负载均衡服务器再将数据包源地址修改为自身的IP地址发送给用户浏览器。
这里的关键在于真实物理Web服务器响应数据包如何返回给负载均衡服务器。一种方案是负载均衡服务器在修改目的IP地址的同时修改源地址,将数据包源地址设为自身IP,即源地址转换(SNAT),这样Web服务器的响应会再回到负载均衡服务器;另一种方案是将负载均衡服务器同时作为真实物理服务器集群的网关服务器,这样所有响应数据都会到达负载均衡服务器。
IP负载均衡在内核进程完成数据分发,较反向代理负载均衡(在应用程序中分发数据)有更好的处理性能。但是由于所有请求响应都需要经过负载均衡服务器,集群的最大响应数据吞吐量不得不受制于负载均衡服务器网卡带宽。
那么,能不能让负载均衡服务器只分发请求,而使响应数据从真实物理服务器直接返回给用户呢?
数据链路层负载均衡
顾名思义,数据链路层负载均衡是指在通信协议的数据链路层修改mac地址进行负载均衡。负载均衡数据分发过程中不修改IP地址,只修改目的mac地址,通过配置真实物理服务器集群所有机器虚拟IP和负载均衡服务器IP地址一致,从而达到不修改数据包的源IP地址和目的IP地址就可以进行数据分发的目的。
由于实际处理请求的真实物理服务器IP和数据请求目的IP一致,不需要通过负载均衡服务器进行地址转换,可将响应数据包直接返回给用户浏览器,避免负载均衡服务器网卡带宽成为瓶颈。这种负载均衡方式又称作直接路由方式(DR)。
如图所示,用户请求到达负载均衡服务器114.100.80.10后,负载均衡服务器将请求数据的目的mac地址修改为00:0c:29:d2,由于Web服务器集群所有服务器的虚拟IP地址都和负载均服务器的IP地址相同,因此数据可以正常传输到达mac地址00:0c:29:d2对应的服务器,该服务器处理完成后发送响应数据到网站的网关服务器,网关服务器直接将该数据包发送到用户浏览器,响应数据不需要通过负载均衡服务器。
数据链路层负载均衡是目前使用较广的一种负载均衡手段。在Linux平台上最好的链路层负载均衡开源产品是LVS(Linux Virtual Server)。
负载均衡算法
前面描述了如何将请求数据发送到Web服务器,而具体的负载均衡算法通常有以下几种:
- 轮询(Round Robin,RR):所有请求被依次分发到每台应用服务器上,适合于所有服务器硬件都相同的场景。
- 加权轮询(Weighted Round Robin,WRR):根据应用服务器硬件性能的情况,在轮询的基础上,按照配置的权重将请求分发到每个服务器。
- 随机(Random):请求被随机分配到各个应用服务器,在许多场合下,这种方案都很简单实用。即使应用服务器硬件配置不同,也可以使用加权随机算法。
- 最少连接(Least Connections):记录每个应用服务器正在处理的连接数(请求数),将新到的请求分发到最少连接的服务器上。同样,也可以实现加权最少连接。
- 源地址散列(Source Hashing):根据请求来源的IP地址进行Hash计算,得到应用服务器,这样来自同一个IP地址的请求总在同一个服务器上处理。
缓存设计
和所有服务器都部署相同应用的应用服务器集群不同,分布式缓存服务器集群中不同服务器中缓存的数据各不相同,缓存访问请求不可以在缓存服务器集群中的任意一台处理,必须先找到缓存有需要数据的服务器,然后才能访问。这个特点会严重制约分布式缓存集群的伸缩性设计,因为新上线的缓存服务器没有缓存任何数据,而已下线的缓存服务器还缓存着网站的许多热点数据。
必须让新上线的缓存服务器对整个分布式缓存集群影响最小,也就是说新加入缓存服务器后应使整个缓存服务器集群中已经缓存的数据尽可能还被访问到,这是分布式缓存集群伸缩性设计的最主要目标。
路由算法
在分布式缓存服务器集群中,对于服务器集群的管理,路由算法至关重要,和负载均衡算法一样,决定着究竟该访问集群中的哪台服务器。
余数Hash算法
余数Hash是最简单的一种路由算法:用服务器数目除以缓存数据KEY的Hash值,余数为服务器列表下标编号。由于HashCode具有随机性,因此使用余数Hash路由算法可保证缓存数据在整个服务器集群中比较均衡地分布。
对余数Hash路由算法稍加改进,就可以实现和负载均衡算法中加权负载均衡一样的加权路由。事实上,如果不需要考虑缓存服务器集群伸缩性,余数Hash几乎可以满足绝大多数的缓存路由需求。
但是,当分布式缓存集群需要扩容的时候,会出现严重的问题。很容易就可以计算出,如果由3台服务器扩容至4台服务器,大约有75%(3/4)被缓存了的数据不能正确命中,随着服务器集群规模的增大,这个比例线性上升。当100台服务器的集群中加入一台新服务器,不能命中的概率是99%(N/(N+1))。
一种解决办法是在网站访问量最少的时候扩容缓存服务器集群,这时候对数据库的负载冲击最小。然后通过模拟请求的方法逐渐预热缓存,使缓存服务器中的数据重新分布。但是这种方案对业务场景有要求,还需要选择特定的时段,要求较为严苛。
一致性Hash算法
一致性Hash算法通过一个叫作一致性Hash环的数据结构实现KEY到缓存服务器的Hash映射,如图所示。
具体算法过程为:先构造一个长度为0~2³²的整数环(这个环被称作一致性Hash环),根据节点名称的Hash值(其分布范围同样为0~2³²)将缓存服务器节点放置在这个Hash环上。然后根据需要缓存的数据的KEY值计算得到其Hash值(其分布范围也同样为0~2³²),然后在Hash环上顺时针查找距离这个KEY的Hash值最近的缓存服务器节点,完成KEY到服务器的Hash映射查找。
当缓存服务器集群需要扩容的时候,只需要将新加入的节点名称(NODE3)的Hash值放入一致性Hash环中,由于KEY是顺时针查找距离其最近的节点,因此新加入的节点只影响整个环中的一小段,如图6中深色一段。
加入新节点NODE3后,原来的KEY大部分还能继续计算到原来的节点,只有KEY3、KEY0从原来的NODE1重新计算到NODE3。这样就能保证大部分被缓存的数据还可以继续命中。
具体应用中,这个长度为2³²的一致性Hash环通常使用二叉查找树实现,Hash查找过程实际上是在二叉查找树中查找不小于查找数的最小数值。当然这个二叉树的最右边叶子节点和最左边的叶子节点相连接,构成环。
但是,上述算法还存在一个小小的问题。假设原本3台服务器的负载大致相等,新加入的节点NODE3只分担了节点NODE1的部分负载,这就意味着NODE0和NODE2缓存数据量和负载压力比NODE1与NODE3的大,从概率上来说大约是2倍。这种结果显然不是我们想要的。
解决办法也很简单,计算机的任何问题都可以通过增加一个虚拟层来解决。解决上述一致性Hash算法带来的负载不均衡问题,也可以通过使用虚拟层的手段:将每台物理缓存服务器虚拟为一组虚拟缓存服务器,将虚拟服务器的Hash值放置在Hash环上,KEY在环上先找到虚拟服务器节点,再得到物理服务器的信息。
这样新加入物理服务器节点时,是将一组虚拟节点加入环中,如果虚拟节点的数目足够多,这组虚拟节点将会影响同样多数目的已经在环上存在的虚拟节点,这些已经存在的虚拟节点又对应不同的物理节点。最终的结果是:新加入一台缓存服务器,将会较为均匀地影响原来集群中已经存在的所有服务器。如图所示。
显然每个物理节点对应的虚拟节点越多,各个物理节点之间的负载越均衡,新加入物理服务器对原有的物理服务器的影响越保持一致,但是太多又会影响性能。那么在实践中,一台物理服务器虚拟为多少个虚拟服务器节点合适呢?一般说来,经验值是150,当然根据集群规模和负载均衡的精度需求,这个值应该根据具体情况具体对待。
数据存储层设计
数据存储服务器集群的伸缩性对数据的持久性和可用性提出了更高的要求,因为数据存储服务器在任何情况下都必须保证数据的可用性和正确性。
关系数据库集群的伸缩性设计
目前,市面上主要的关系数据都支持数据复制功能,使用这个功能可以对数据库进行简单伸缩。下图为使用数据复制的MySQL集群伸缩性方案。
在这种架构中,多台MySQL实例有主从之分,数据写操作都在主服务器上,由主服务器将数据同步到集群中其他从服务器,数据读操作及数据分析等离线操作在从服务器上进行。
除了数据库主从读写分离,前面提到的业务分割模式也可以用在数据库,不同业务数据表部署在不同的数据库集群上,即俗称的数据分库。
在大型系统中,即使进行了分库和主从复制,对一些单表数据仍然很大的表,还需要进行分片,将一张表拆开分别存储在多个数据库中。目前市场上常用的分库分表的中间件是Mycat。
相比关系数据库本身功能的强大,目前各类分库分表中间件的功能都显得非常简陋,限制了关系数据库某些功能的使用。但是当网站业务面临不停增长的海量业务数据存储压力时,又不得不利用分布式关系数据库的集群伸缩能力,这时就必须从业务上回避分布式关系数据库的各种缺点:避免事务或利用事务补偿机制代替数据库事务;分解数据访问逻辑避免JOIN操作等。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。