系统的伸缩性以及扩展性设计
最近读了李智慧老师的《大型网站技术架构》,让我对架构设计里的一些概念有了比较清晰的认知。
书中把网站系统演化过程中可能遇到的问题比较全面地整理了出来,遇到每个问题时可以用什么手段和技术来解决也写得比较清楚。
虽然书中没有给出实操时的具体细节,实际接入时可能还会遇到其他细节的坑。可是看完这本书之后,当工作中要对系统做性能优化的时候,就能知道系统现在是遇到什么问题,应当使用哪种技术来处理。
读书习惯划重点以及写笔记。读完这本书后发现画得密密麻麻的,真是整本书都是重点啊哈哈。
这篇文章针对书中伸缩性以及扩展性两章做个记录。
伸缩性以及扩展性这两个名词比较容易混淆,那么什么是伸缩性,什么是扩展性?
伸缩性
简单的说是指系统能通过简单的增减服务器数量等手段来提高系统处理业务的能力。
扩展性
简单来说就是当系统要增加新功能时,已有的代码以及系统架构需不需要进行修改,或者说需不需要进行成本很高的修改。
伸缩性
系统为什么需要伸缩性?
因为网站的发展是渐进的,如果网站架构具备线性伸缩能力,那么我们用户访问量达到某个规模的时候,就能通过简单地增加服务器数量来提高系统的处理能力,而不必面临着重构之类这么重的手段。所以这就要求我们的架构设计要有“能伸”的能力。
而在某些时候,例如电商网站做活动的时候,可能某段时间内访问量激增,那么就需要活动期间增加服务器数量,而活动结束后下架服务器。所以这就要求我们的架构设计同样也要有“能缩”的能力。
网站架构的伸缩性设计
网站的伸缩性可以分成两类:一类是根据功能进行物理分离实现伸缩,一类是单一功能通过集群实现伸缩。
简单来解释一下,就是将单一应用服务按模块拆分成多个独立的服务,每个独立的服务都使用集群部署的方式对外提供服务。
举个例子:
刚开始是一个应用走遍天下,后来逐步将数据库拆分出去,包装成可复用的分布式服务提供对外调用。然后把缓存也分离出去,封装成分布式缓存服务器。
经过这样的模块拆分之后,对系统的伸缩性设计就变成对各个细分模块的伸缩性设计了。
每一种模块都能使用服务器集群的方式来提高系统伸缩性,伸缩性主要是说服务器集群的伸缩性来说的,集群中通过添加新服务器来提供集群整体的能力。
集群伸缩性又可以分为应用服务器集群伸缩性和数据服务器集群伸缩性。
数据服务器集群又可以分为缓存服务器集群以及存储数据服务器集群。各种集群的伸缩性设计是大不相同的。
应用服务器集群的伸缩性设计
首先,应用服务器要设计成无状态的,这样每次用户请求都可以发送到集群中任意一台服务器上去处理。当新增服务器时,能迅速分摊已有服务器的压力。
应用服务器集群的伸缩性主要是通过负载均衡来实现的。负载均衡装置能感知和配置服务器数量,能将用户请求按一定的规则分发到不同的服务器去处理,以下是几种负载均衡的实现:
一、HTTP 重定向负载均衡
利用一台服务器作为 HTTP 重定向服务器,请求到达的时候计算真实的 web 服务器地址,然后响应 302 重定向给用户。
优点是实现简单。缺点是用户访问需要两次请求才能到达服务器,性能较差;重定向服务器有可能成为系统的瓶颈,整个集群的伸缩性规模有限;使用 302 重定向,会影响seo排名。因此实际中这种方式不多见。
二、DNS域名解析负载均衡
在 DNS 服务器中配置多个 A记录(可以理解为域名的指向 IP)。每次域名解析请求都会根据负载均衡算法计算返回集群中某个服务器的地址,从而利用 DNS 来实现负载均衡。
优点是将负载均衡的工作交给 DNS,省去管理负载均衡服务器的维护成本,同时大部分 DNS 还支持返回离用户最近的服务器地址,改善用户访问性能。缺点是当下架服务器后,修改 A记录 可能需要一段时间才能生效,导致部分用户访问失败;而且 DNS 负载均衡的控制权不在自己手里,没办法做更多的改善和管理。
事实上大型网站总是会使用 DNS 来做第一级负载均衡,不过返回给用户的是另一组内部负载均衡服务器的地址,在内部负载均衡服务器上再做第二级负载均衡。
三、反向代理负载均衡
在 web 服务器集群的前面,部署一台反向代理服务器,负责连接用户和 web 服务器。用户的请求通过反向代理服务器转发到集群中的某个服务器,web 服务器的处理结果也是通过反向代理服务器返回给用户。
因此 web 服务器只需要使用内部IP地址,而反向代理服务器则需要配置双网卡以及内部外部两套IP地址。
优点是部署简单。缺点是需要经过转发,反向代理服务器可能行为性能瓶颈。
四、IP 负载均衡
在网络层通过修改请求目标地址进行负载均衡,负载均衡服务器接收到请求后,直接修改数据目的地的 IP,改为集群中某一台服务器的地址。
跟上面反向代理负载均衡的区别是,IP 负载均衡是在内核进程完成数据分发,不需要经过服务器里的应用程序(例如nginx)分发,会有较好的处理性能。
五、数据链路层负载均衡
这是目前大型网站中使用最广的一种负载均衡手段。
在数据链路层直接修改 max 地址,web 服务器直接返回数据给用户。通过配置 web 服务器集群的虚拟 IP 地址和负载均衡服务器的 IP 地址一致,实现负载均衡数据分发时不需要修改 IP 地址。
负载均衡服务器的实现可以分为两个部分:
根据负载均衡算法,计算得到 web 服务器集群中的某一台服务器地址。
将用户的请求发送到该服务器上。
下面说下常见的几种负载均衡算法:
一、轮询
每台服务器需要处理的请求数目都相同,适合于所有服务器硬件都相同的场景。
二、加权轮询
依照服务器的性能不同给每台服务器都配置一个权重,按照权重来轮询。
三、随机
请求随机分配,成本较低,许多场合下都很实用。可以实现加权随机。
四、最少连接
记录服务器正在处理的请求数,将最新的请求分配给最少连接的服务器上。同样也能实现加权最少连接。
五、来源 IP 计算 hash
使用来源 IP 计算 hash 得到服务器地址。
适合于有状态的应用服务器,实现来源同一个 IP 地址的请求,都分发到同一个应用服务器来处理。
分布式缓存集群的伸缩性设计
分布式缓存服务器集群中不同的服务器缓存的数据各不相同,缓存的访问请求不能在任意一台服务器处理。需要先确认缓存在哪台服务器,然后才能访问。
因此分布式缓存集群的伸缩性设计不能使用简单的负载均衡来实现。
分布式缓存服务器集群伸缩性的最主要目标:新上线服务器时,已经缓存的数据要尽可能还能被访问到。
首先,先看下分布式缓存是怎么访问数据的:
以 key-value 的形式缓存数据。
缓存组件维护可用的缓存服务器列表,组件利用 key 使用路由算法得到对应的某一台服务器地址。
应用程序跟对应的服务器通信,访问数据。
路由算法至关重要,路由算法选择不当有可能会在上线新的缓存服务器时导致系统瘫痪。
在系统设计中,数据库的负载能力很多时候是以有缓存为前提来设计的。
当大部分已有缓存不能被正确访问时,压力就会落到数据库上。
超过数据库负载能力时,可能会导致数据库宕机,从而引发系统的瘫痪。
所以分布式缓存的伸缩,要么选择凌晨这种访问量最少的时候进行扩容,要么想办法改进路由算法。
目前比较流行的解决这个问题的路由算法是一致性 Hash 算法。
一致性 Hash 算法是通过一个叫做一致性 Hash 环的数据结构实现 Key 到缓存服务器的 Hash 映射。
环上的 Key,会根据他的 Hash 值来查找到离他最近的顺时针方向的节点,而节点上映射着服务器地址,从而实现了寻址。
这样当新增一个节点(例如图中的 Node3)时,这时候影响的数据是 Node2 到 Node3 这段距离之间的数据,因为这段数据原本该映射到 Node1 地址的,现在被映射到 Node3 节点上了。
可见,如果节点越多,则受影响的数据就越少。
可是这个算法过程还有一个问题。
新增节点 Node3 后,Node1 的压力被分摊了,可是 Node0 以及 Node2 的压力并没有变。
意味着新增服务器后,各个服务器的负载压力不平均了,这显然不是我们想看见的。
解决方法如下:
将每台物理缓存服务器虚拟为一组虚拟缓存服务器,将虚拟服务器的 Hash 值放置在 Hash 环上,Key 在环上先找到虚拟服务器节点,再得到物理服务器的信息。
这时候新增服务器(例如 Node3 )时,在环上加的就不再是一个节点了,而是一组虚拟节点( V30、V31、V32 )了。
而影响到的数据,都是上一个节点到他之间的数据,因此理想情况下能平均地减轻每个已有服务器的压力。
数据存储服务器集群的伸缩性设计
数据存储服务器集群的伸缩性对数据的持久性和可用性有更高的要求。
因为缓存服务器数据没了还能从数据库访问,如果数据库服务器中数据没了,影响就太大了。
一、关系型数据库集群的伸缩性设计
实现关系型数据库集群的伸缩性的常用方式:
数据库主从复制,并读写分离。
不同的数据表进行分库,同一个数据表进行分片。
利用数据库访问代理组件,来进行分片数据库的访问。
分片数据库的伸缩流程:
数据迁移,利用一致性 Hash 算法使受影响的数据尽量少。
修改路由算法,将迁移后的数据访问改到新的服务器地址。
过程中保留旧数据作为兼容,伸缩完成之后删除旧数据。
这里分片之后可能需要业务上的协调。分表分库分片之后的数据查询,他们的join操作、排序操作等可能会受影响。
二、NoSQL 数据库的伸缩性设计
NoSQL 数据库产品强化了高可用性和可伸缩性,放弃了关系数据库的两大重要基础:
以关系代数为基础的结构化查询语言( SQL )
事务一致性保证( ACID )
NoSQL 数据库,例如 HBase 的伸缩性主要依赖可分裂的 HRegion 和可伸缩的分布式文件系统 HDFS 来实现。
当进行服务器的伸缩时,HRegion 是能连带数据 HFile 一起进行合并或分裂迁移的。
最后,网站伸缩性往往和可用性、正确性、性能等耦合在一起,改善伸缩性可能会影响一些网站的其他特性,所以需要做好平衡。
扩展性
扩展性主要是为了应对市场需求,快速开发迭代,不让技术成为产品的拖累。
网站的扩展性设计,核心思想是模块化。降低模块间的耦合性,提高模块的复用性。
利用分布式消息队列降低系统耦合性
系统模块化之后,利用消息队列能降低模块之间的耦合性。
模块跟模块之间不再直接通信,而是通过消息队列服务器来进行通信。
利用分布式服务打造可复用的业务平台
分布式服务可以通过接口分解系统耦合性,不同子系统通过相同的接口描述进行服务调用。因此一个服务是可以给多个子系统调用的,实现复用。
如果不进行分布式服务拆分,在一个单一应用中进行部署和开发,则可能会有如下问题:
编译、部署困难。代码太大,耗时非常久。
代码分支管理困难,代码都在一个项目中。
数据库连接耗尽,集群中每个服务器都连接一个数据库连接池,消耗大量数据库的系统资源。
新增业务困难,牵一发而动全身,逻辑臃肿耦合,改动代码时不知道哪里有坑。
分布式的服务拆分分为两种:
将一个大应用拆分为多个小应用,部署为多个独立的 Web 应用系统。
可复用的服务拆分出来部署为独立的分布式服务,例如上面的数据库连接,各个子系统可以通过分布式服务访问数据库。
现在比较流行的分布式服务框架例如 Dubbo,大多会有如下特性:
服务注册、服务发现、服务调用
负载均衡
心跳检测、失效转移
高效的远程通信
对应用最少侵入
版本管理,更新时的兼容
实时监控
可扩展的数据结构
关系型数据库中,当因为新功能需要改动原有表的结构时,如果表中数据量很大,这时候过程可能会比较痛苦。需要考虑的问题很多,例如改表过程中的数据写入和数据访问等。
而另一种方案,就是在创建表时就预留冗余字段,不过这种预留不确定性很高,而且命名上也有很大的问题,显然不是好的数据库设计。
而在支持 ColumnFamily 结构的 NoSQL 数据库中,创建表时不需要指定字段,只需要指定 ColumnFamily 的名字,在数据写入时再指定字段名,因此可以随意扩展。查询的时候还能指定字段名称和值来查询。
那么关系型数据库中,借鉴的做法是可以使用一个大字段来存 json 格式的数据,从而实现扩展。不过弊端就是查询操作的时候特别费劲。
最后,扩展性设计谨记两个词:低耦合、可复用。
李智慧老师的《大型网站技术架构》,推荐大家一定要去读一下。
版权声明: 本文为 InfoQ 作者【Janenesome】的原创文章。
原文链接:【http://xie.infoq.cn/article/2f0393b3e1ab6ee43ff4cdd8e】。文章转载请联系作者。
评论 (1 条评论)