01-企业应用系统架构设计的原则一(高可用设计)
xiangliheart
xiangliheart
发布于 2022-01-01 / 201 阅读 / 0 评论 / 3 点赞

01-企业应用系统架构设计的原则一(高可用设计)

高可用(High Availability,HA)是指不间断提供服务的能力,无论是因为服务器宕机、网络异常,还是程序bug(漏洞)等任何原因所导致的故障,都应该尽量地将服务不可用的时间缩短到最小,将损失降到最低,这就是高可用设计的目的。

例如,一个系统一年只有一次宕机和一个系统每月宕机一次,在可用性上是存在巨大差异的。想要达到高可用也是有模式可循的,核心思想就是冗余、容错、故障转移和系统监控。

一、高可用指标

系统可用性一般使用年度可用性指标衡量,计算公式:p=(1-t1/t0)*100%.

p为可用性指标;t1为年度不可用小时数;t0为年度总小时数。

业界用几个九的方式对系统可用性等级划分如下:

指标简称

年度可用性指标

最大不可用时长

可用性级别

2个9

99%

88h(3.7d)

及格(基本可用)

3个9

99.9%

9h

中等(可用性较高)

4个9

99.99%

53min

优秀(可用性很高)

5个9

99.999%

5min

极佳(可用新极高)

二、冗余设计

系统中某个单节点故障可能会引起级联故障。如图 2-2所示,当D 服务因故障宕机时,就会造成B和C服务出现大量的交易失败和请求积压,问题会很快传遍所有依赖节点,造成大量业务功能无法使用甚至全面宕机。

图2-2 单节点级联调用故障

如图2-3所示,服务A使用了MQ、Redis、MongoDB、MySQL多种中间件,都是单节点部署,当MongoDB节点因故障宕机后,就可能会导致服务A无法正常启动、核心功能丧失、失去服务能力。

图2-3 单节点中间件故障

由此可见,如果每个服务节点都只有一台服务器,那么任意节点发生故障,都会导致整个系统不可用,这就是所谓的单节点故障,对系统的可用性具有极高的威胁。

冗余是高可用的核心思想,坚决避免服务出现单节点故障,做法就是增加备用节点,方案主要有两种:主备高可用和多活高可用。

(1)主备高可用方案:例如,某系统只有一台服务器A,如果宕机了就会引起服务不可用,那么就再准备一台服务器B,硬件配置、环境信息、应用部署都完全与服务器A保持一致。如果服务器A发生了故障,就立即把服务器B启动起来,顶替服务器A对外提供服务;看似简单,然而这也是一种高可用方案,同样是缩短了服务的不可用时长。

(2)多活高可用方案:为了避免单节点故障,可以部署多个同样的服务节点,同时提供服务。例如,使用的双卡双待的手机,一个电话卡欠费了,另一个还可用,两张卡是同时工作的。

做任何架构设计都不要拘泥于理论和技术,达到目标才最重要。

三、负载均衡架构

负载均衡架构是一种应用较为广泛的高可用手段,可以快速地为其他服务提供水平扩展能力。

1.什么是负载均衡架构?

如图2-4所示,无冗余结构服务端发生故障时,会直接导致系统无法访问;而使用负载均衡冗余结构,一个服务端发生故障后,依然有一个节点可以对外提供服务。如果服务端的压力过大,则可以通过增加多个服务节点来分担系统压力,这也是提高系统并发能力的直接手段。

图2-4 无冗余结构与负载均衡冗余结构的对比

负载设备可以使用硬件或软件实现,因此负载均衡可以分为硬负载(硬件设备)和软负载(软件)两种。硬负载主要使用F5、Radware 等硬件设备,软负载主要使用Nginx、LVS、HAProxy等软件。

硬负载具有性能高、稳定性强的特点,但是价格昂贵,而且不利于开发人员维护。软负载具有灵活性强,易于配置的特点,但是由于软负载工具运行在操作系统之上,所以性能和稳定性受操作系统限制,相较于硬负载要略差一些。

四、负载均衡高可用方案

这种架构方式虽然解决了服务端的单节点风险,但是负载设备本身又变为了单节点。下面介绍几种常用的负载均衡高可用方案。

(1)Nginx+Keepalive双机主备方案。

如图2-5所示,部署主备两台Nginx服务器,它们各自都有自己的IP 地址(10.1.8.3和10.1.8.4),在两台服务器上分别安装Keepalive服务,并绑定同一个虚拟IP(Virtual IP,VIP)地址(10.1.8.8),此IP地址要求没有被占用,客户端访问时通过VIP进行访问。

图2-5 Nginx+Keepalive双机主备架构

当主节点存活时,请求将发送至主节点处理。当主节点宕机后,

VIP会自动漂移到备份节点,由备机接管负载服务,成为新的主机,从而达到主备自动切换,具备自动故障转移的能力。当故障节点恢复后,这个新的主机自动变为备机使用。

双机主备需要注意时刻保持主备Nginx的配置完全相同,避免发生系统切换后,服务无法正常使用的情况。

(2)F5+LVS/HAProxy+Nginx多级负载方案。

Nginx+Keepalive的方案虽然可以保证Nginx的高可用,但缺点是只能由一个节点对外服务,单个节点的Nginx连接数和服务能力有限,如果想对Nginx进行水平扩展就无法满足。

可以使用开源免费的LVS或HAProxy+Nginx形成二级负载结构,从而让Nginx支持水平扩展,支持超高并发请求。LVS和HAProxy都可以使用Keepalive搭建双机主备高可用模式,如图2-6所示。

图2-6 LVS/HAProxy+Nginx二级负载架构

也可以使用硬负载+LVS/HAProxy+Nginx形成三级负载结构,从而让LVS/HAProxy也支持水平扩展,并发能力将再上升一个数量级。F5 和Radware可以搭建双机主备模式,保证高可用,如图2-7所示。

图2-7 硬负载+LVS/HAProxy+Nginx三级负载架构

3.四层与七层负载

LVS和HAProxy的性能比Nginx强很多,因为LVS和HAProxy专门用于四层负载,而Nginx多用于七层负载(新版本Nginx可以通过添加Stream模块,支持四层负载)。

LVS和HAProxy工作在第4层,而Nginx可以工作在第4层和第7层,如图2-8所示。图2-8 OSI七层网络模型

LVS和HAProxy更接近于操作系统底层,使用IP加端口的方式进行路由负载,让客户端和上游服务器直接建立通信,通过TCP、UDP的请求报文头中的IP地址或MAC地址,来达到转发的目的,对网络性能几乎无损耗。

使用Nginx的七层负载时,当客户端要与服务器建立连接时,必须先和Nginx建立连接,然后Nginx再和上游服务器建立连接,所有的请求和应答都由Nginx先接收再转发出去,所以存在一定的网络和性能损耗,对CPU和内存资源损耗较大,如图2-9所示。

图2-9 七层负载请求转发

LVS和HAProxy可以工作在四层网络之上,当客户端发起请求后, LVS/HAProxy会将请求报文的IP和端口直接修改为上游服务器的IP和端口,让客户端和服务器直接建立连接,因此几乎不占用流量,对CPU 和内存的消耗微乎其微,从而才具有更高的性能,如图2-10所示。

图2-10 四层负载请求转发

4.既然LVS和HAPr oxy这么强大,为什么还要使用Nginx呢?

由于LVS和HAProxy工作在四层网络之上,功能也比较单一,只负责请求分发,因此流量本身并不穿过它们。不支持URL路径匹配,不能做动静分离,可配置性不强。由于更接近于操作系统和网络层面,所以一般开发人员难以配置,需要专门的运维人员负责。

Nginx工作在七层网络之上,可以作为静态资源服务器、文件服务器,支持POP、SMTP协议,支持HTTP缓存、URL路径匹配,在服务器配置较好的情况下,也能够承载单机2万以上的并发,稳定性很好,配置文件简单、语法友好,开发人员可以直接配置。同时,插件众多,社区活跃度较高,通过插件可支持TCP反向代理、访问限流等功能。基于以上原因,Nginx具有极高的市场覆盖率。当然,LVS、HAProxy、Nginx都在不断地发展,未来功能差异可能会进一步缩小。

F5、Radware等硬负载设备,也是工作在四层网络之上,借助独立硬件的能力,性能最为出众,但是价格也十分高昂。

5.负载策略

客户端请求到达负载设备,需要根据一定的算法决定分发给哪一个上游服务器,这个分发算法就是负载策略,如图2-11所示。

图2-11 负载分发原理

对于不同的负载软件,支持的负载策略稍有差异,但是大体相同,有如下10种。

(1)轮询策略:请求被依次循环分发给上游服务器,一般属于默认策略,最常用。

(2)权重轮询策略:可以设置权重,权重越大,分发的请求越多,配置高的服务器权重应该大一些,配置低的服务器权重应该小一些。例如,如果设置4:1的权重,则配置高的服务器会收到4次请求,配置低的服务器会收到1次请求。

(3)动态权重策略:请求分发交由系统控制,负载均衡器会收集每个上游服务器的状态,如CPU、内存、磁盘I/O、繁忙程度等各项指标,从而计算出权重,给相对空闲的服务器分发更多请求,给繁忙的服务器分发少量请求。

(4)最小连接数策略:由于所有的连接都经过负载设备,所以它可以统计出每个上游服务器的连接数情况,每次都将请求转发给连接数最小的服务器。

(5)最短响应时长策略:将请求分发给响应时间最短的服务器。(6)IP哈希策略:将相同来源IP的请求转发到同一个上游服务器,可用于Session保持等场景。

(7)URL哈希策略:与IP哈希策略类似,使用请求URL进行哈希计算,将相同哈希值的请求转发到相同的上游服务器。

(8)最小会话策略:根据当前的Session保持情况,将请求分发给会话最小的服务器。

(9)趋势分析策略:根据一段时间内的请求分发情况、连接数、会话、服务器状态等信息判断出每个上游服务器未来的流量上升和下降趋势,将请求转发给趋势上升的服务器。

(10)   随机策略:将请求随机分发给某一个上游服务器。

五、 DNS轮询负载设计

DNS(Domain Name System,域名系统)是互联网的一种服务,它将域名和IP地址进行映射存储,当用户访问某个域名时能够快速找到对应的IP地址,使用户更加方便地访问互联网。

互联网应用访问流程包括以下3个步骤,如图2-12所示。

(1) 客户端使用域名(yinhongliang.com)请求服务器时,需先请求DNS服务进行域名解析。

(2)DNS服务通过查找域名注册表,找到域名所对应的IP地址(10.3.1.33),返回给客户端。

(3)客户端使用IP地址(10.3.1.33)再去访问服务器的应用。

图2-12 DNS解析

然而,单个服务器节点不但存在单节点风险,而且能够支撑的最大并发连接数十分有限,可以借助DNS进行负载,从而使服务支持水平扩展。

将域名与多个IP地址进行绑定(例如,yinhongliang.com这个域名与10.3.1.33、10.3.1.34、10.3.1.35三个域名绑定),用户每次发起请求时DNS服务都按照顺序依次返回域名所对应的IP地址,从而达到了服务轮询负载的目的,如图2-13所示。

图2-13 DNS轮询负载

也可以针对不同的地区、运营商绑定不同的IP地址。这样,全国各地的用户进行访问时,可以尽量将用户的请求解析到距离最近的服务器,从而加快响应速度。这种架构虽然解决了水平扩展问题,却将服务器直接暴露在互联网上,并且吞吐量依然不足,可以采用DNS+多级负载的方式架构。

可以将域名绑定在负载均衡器LVS的IP地址上,这样通过DNS解析后,用户的请求就会通过LVS和Nginx的多级负载,无须暴露真正的后端服务,极大地提高了系统的安全性、可用性和并发能力,如图2-14所示。

DNS解析负载的最大问题是,没有自动监测能力,无法帮助应用自动完成故障转移。例如,某个服务节点宕机,由于DNS没有主动探测机制,不会自动将无法访问的服务节点IP从DNS列表中移除,所以用户依然会请求到宕机的服务。

当系统发生故障时,需要手动修改域名解析配置,由于DNS存在缓存,并不能立即生效,所以还会有一段时间服务不可用。

图2-14 DNS+多级负载架构

六、 两地三中心容灾设计

两地三中心是一种容灾方案。在同一个城市或邻近的城市(如北京、天津)建立两个数据中心(Data Center,DC),由于距离较近,网络延迟低,因此数据几乎可以达到实时同步。当其中一个数据中心发生故障,如火灾、地震等毁灭性事故时,可以快速将服务切换到另外一个数据中心,保证服务可以短时间内恢复。

由于两个数据中心在邻近的城市,对于一些地震、洪水等不可抗力因素,可能会导致两个数据中心都不可用,因此可以在更远的位置再部署一个数据中心,只进行数据同步备份。例如,当北京、天津的数据中心都不可用时,可以使用上海的数据进行应用重建,如图2-15所示。

图2-15 两地三中心容灾架构

两地三中心容灾架构的特点是同一时间只有一个数据中心对外提供服务。如图2-16所示,北京和天津的两个数据中心内的服务保持完全相同,同时处于运行状态, 但是只有主数据中心(北京)对外提供服务,而从数据中心(天津)只是空跑而已。

图2-16 双中心灾备

在日常工作中,任何的系统发布(代码发布、数据库DDL、服务器环境配置等)都需要在两个数据中心执行,目的是确保环境完全一致。

数据库采用主从模式进行数据同步,保证双方数据一致,但是跨地区的数据同步会出现较大延迟,可以增加专线降低延迟程度。

将域名解析到北京机房,只让主数据中心对外提供服务。当主数据中心发生灾难不可用时,先将天津机房的从库升级为主库,然后将

DNS解析到天津机房即可。两地三中心模式,相当于做了一个数据中心级别的热备份;因为始终有一个数据中心处于闲置状态,无法充分利用,所以对成本存在极大的浪费。由于一个数据中心始终处于冷却状态(没有任何真实业务发生),当真正发生灾备切换时,并不一定能够正常运转起来,所以两地三中心只是看起来美好而已。

为了节省成本,可以只将核心业务做灾备,当发生灾难时优先保证企业或政府机构的核心业务可以正常运转。

七、 异地多活架构设计

两地三中心容灾设计带来极大的浪费,并且没有解决访问效率问题。两个可用的数据中心都部署在同一个城市或相邻的城市,例如,服务器部署在北京,则只有华北、华中等周边地区访问速度较快,而华南地区、海外地区访问速度较慢。

因此,最好的架构应该是异地多活架构,就是全国乃至全球部署数据中心,这些数据中心同时对外提供服务,让不同地区的用户访问距离他们最近的数据中心。但是,部署成本极高,并且技术复杂,资金和人员投入巨大。

例如,分别设立北京、上海、香港数据中心,通过域名解析将不同地区的用户请求分发到不同的数据中心,如图2-17所示。

图2-17 异地多活架构

异地双活是异地多活的特例,是指在两个距离较远的城市建立IDC(Internet Data Center,互联网数据中心),同时对外提供服务。

异地多活架构主要存在以下两个问题。

(1)跨数据中心访问,导致处理速度慢。用户的一次请求,对于分布式系统,可能在服务器发生数十次的调用,如果这些调用不能发生在同一个IDC内,延迟就会较高。

例如,某北京用户的一次购买行为,服务器需要200个服务调用完成,如果这200次调用全部在北京IDC内完成,每次调用耗时2毫秒,则全部串行化总计需要200×2=400(毫秒)给用户应答。如果这200次调用中有100次发生在北京IDC内,每次调用耗时2毫秒,另外有100次发生在上海IDC内(发生跨IDC调用),每次调用耗时20毫秒,则全部串行化总计需要(100×2+100×20)/1000=2.2(秒)给用户应答,会造成极差的用户体验,甚至有些页面直接超时,如图2-18所示。

所以,异地多活的第一个要求就是尽量让所有的交易发生在同一个IDC内,避免出现跨区访问。

图2-18 一个IDC内完成与跨IDC调用的对比

(2)数据无法实时同步。由于各个IDC的距离较远,网络延迟是无法避免的,因此数据无法达到实时复制。如果不进行特殊的控制,则会产生很严重的不一致性问题。

例如,某用户购买商品时,交易是在北京机房完成,订单数据也存储在北京机房内。紧接着用户去查看订单信息,这时请求到了上海机房,而此时数据还未同步过来,就会导致用户无法看到订单,这是用户无法容忍的错误。

为了解决数据同步延迟问题,最简单的方式就是数据库不拆分也不同步,都集中在一个IDC中,多个IDC中的服务共享一套数据源,即伪异地多活架构,如图2-19所示。

图2-19 伪异地多活架构

每个IDC中除数据库外均正常部署,所有服务都对一个数据源(并非一个数据库)进行读写。这样就保证了数据的一致性。这种方式部署实现相对简单,但是数据库压力过大,并且跨区访问时由于数据库远程访问,依然会造成交易效率很低。这种方案依然只适合多个IDC同城或邻城部署,因此通常作为异地多活的一种过渡方案。

真正的异地多活架构则是数据库都是完全独立拆分部署的,通过同步的方式进行数据同步,但是复制延迟是无法避免的,因此这个问题无须纠结(因为现代的通信技术还无法彻底解决长距离数据传输的延迟问题)。所以,异地多活架构并不是将所有业务都进行异地部署,对于一些账户余额、转账等一致性、实时性要求极高的业务并不适合,而且对于一些非常用的交易也没有必要进行异地多活设计。例如,登录操作是使用极其频繁的,用户可能一天要登录10次,而用户信息修改可能一个月才修改1次,并且用户登录十分重要,是一切业务的起点,而用户信息修改并不重要。

用户的位置几乎是固定的,不会出现现在在北京登录,而10分钟之后在上海登录。所以,对于登录业务、用户注册、产品订单等数据只需要异步复制到其他IDC即可,达到最终的数据一致性。

所以,将同一个地区的用户交易控制在一个IDC内,数据存储在一个IDC内,再将数据异步复制到其他IDC是一种较好的方案。

八、MongoDB高可用架构

如果读者对MongoDB不了解,则可以先阅读以下基础知识内容。

1. MongoDB基础知识

(1)MongoDB的概念。

MongoDB 是一个基于分布式文件存储的数据库,由C++语言编写,旨在为 Web 应用提供可扩展的高性能数据存储解决方案。

MongoDB 是一个介于关系型数据库和非关系型数据库之间的产品,是非关系型数据库中功能最丰富、最像关系型数据库的产品。

MongoDB并不是按照表(Table)存储的,而是按照文档(Document)存储的,将数据以JSON形式存储在数据库中,因此MongoDB也是一种NoSQL数据库,或者称之为文档型数据库。

(2) MongoDB的优势。

MongoDB的优势在于使用JSON文档结构,适合整合复杂数据、整体查询的场景;自带主从模式、副本集模式和分片模式,可以很方便地进行读写分离、高可用和负载均衡,其他关系型数据库往往需要借助其他中间件才能做到,同时支持海量数据的存储和查询。

2. MongoDB的3种高可用架构

为保证MongoDB的高可用,官方提供了3种模式,分别为主从模式、副本集模式和分片模式。其中主从模式官方不推荐使用,并且在

MongoDB 3.6中彻底废弃。

(1)主从模式。

主从模式的第一种用法是用作备份,主库提供读写能力,从库不可以进行读写,数据实时地从主库备份到从库,从库只作为单纯的备份使用。从库无法分担主库的压力,只是避免了数据丢失的风险。

主从模式的第二种用法是实现读写分离,主库提供读写能力,从库只提供查询能力。从库除了可以起到数据备份的作用,还可以分担部分主库的压力。另外,也可以部署多个从节点,形成一主多从的架构,进一步分担查询压力。

主从模式被废弃的原因是没有自动故障转移能力,当主库发生故障后,必须手动操作将从库切换为主库,应用程序也要修改连接信息,如图2-20所示。

图2-20 MongoDB主从模式

(2)副本集模式。

副本集模式是采用选举算法,在多个MongoDB节点中选举出一个主节点(也叫作Master节点),负责数据的读写操作,其余节点作为副本节点,从主节点同步数据,起到数据冗余、备份、服务高可用的目的,从节点也可以承担读操作进而减轻主库压力。

当主节点宕机或网络故障时,集群会根据选举算法重新推举出新的主节点,承担数据的读写操作,其他从节点将从该节点继续复制数据。当故障节点修复后,将重新作为整个集群中的从节点继续工作,以这种方式达到了故障自动转移的目的,如图2-21所示。

图2-21 MongoDB副本集模式

MongoDB集群的各个节点之间通过心跳机制进行检查,从而来判定节点是否可用,是否需要进行重新选举,如图2-22所示。

值得注意的是,在副本集模式下,每个节点都具有数据库的全量数据。由于副本集需要大量的存储空间,对性能也有较高要求,因此如果只是为了保证MongoDB的高可用,为了可以正常地 选举,则可以只部署仲裁节点,而不部署数据副本节点。仲裁节点只负责投票,选举出主节点,而不存储业务数据,如图2-23所示。

图2-22 MongoDB检查机制

图2-23 MongoDB仲裁节点

(3)分片模式。

主从模式、副本集模式中每个节点存储的依然是全量数据,存储量存在上限,随着数据量的增长,读写操作性能会急剧下降,因此主从模式、副本集模式更适用于中小型系统、数据量增长缓慢的系统,单个集合数据不超过千万条,可以不考虑分片。切记不要过度设计,此乃设计师的大忌。

MongoDB的分片模式,能够保证数据存储量可以无限增长,同时保证读写操作的性能,当然系统的复杂度也会进一步提升。

分片的目的是将数据切分存储。例如,1000万条数据如果拆分存储在5个分片节点中,则每个节点只需要存储200万条数据,每个分片节点都同时对外提供读写服务,从而提高了MongoDB的存储能力和服务性能,这与分库分表的理念是相同的。

分片集群架构中存在3个重要的角色,分别为分片(Shard)、路由器(Router)、配置服务(Config Servers),图2-24所示为MongoDB分片集群架构。

图2-24 MongoDB分片集群架构

①MongoDB分片存储原理。

分片节点:并不需要把数据库中所有的集合都进行分片,可以只针对部分数据量较大,需要水平扩展的集合进行分片。如图2-25所示,将Collection1分为3片,分布在分片A、B、C中,每一部分都是

Collection1的子集,而对Collection2、Collection3不进行分片。不进行分片的集合必须存储在数据库的主分片上(图2-25中标星的分片A),每个数据库都包含一个主分片。

为了保证每个分片的高可用,可以对分片节点采用副本集方式部署。

图2-25 MongoDB Collection分片

②MongoDB分片路由原理。

路由节点:作为应用程序与分片节点之间的路由器,起到代理和桥梁的作用。对于客户端发送的请求,按照一定的算法,将请求转发到特定的分片集合执行读写操作。如图2-26所示,由于Collection1已经分片,因此对于Collection1的读写操作,可能会被转发到分片A、B、C上,而对于Collection2、Collection3的读写操作,只会被转发到分片A 上。

图2-26 MongoDB分片路由示例

配置节点:用于存储路由节点、分片节点的元数据信息及集群的配置信息,配置节点必须以高可用方式部署(Config Server ReplicaSet,简称CSRS,配置服务副本集)。

③MongoDB数据分片算法。

分片集群的部署并不难,参照官方手册即可,难点是如何进行合理的分片。分片就要选择分片键(Shard Key),需要选择被分片集合的某一个字段,或者某几个字段的组合。一个集合只能有一个分片键,分片后分片键不可更改,所以创建时一定要慎重。

MongoDB提供两种分片算法,一种是Hash(哈希)算法,另一种是Range算法。

a.  Hash算法。Hash算法就是将分片键进行哈希计算,从而将数据分配到不同的分片上。

如图2-27所示,用户ID等于1、3、5的数据存储在分片A上,ID等于2、23、55的数据存储在分片B上。根据用户ID可以快速查询某一条数据,如果查询ID大于等于3小于等于60的数据,则由于无法确定这些数据都存储在哪个分片上,因此需要将查询请求广播到所有节点上,找到符合条件的数据,再将各个分片节点的结果集汇总在一起。

图2-27 Hash算法

Hash算法的优点是数据分布比较均匀,缺点是数据不连续。对于按照键值查询的数据可以快速定位,但是对于区间查询的数据,则需要广播到所有节点,效率降低。

b. Range算法。Range算法就是将分片键分为不同的连续区间,从而将数据存储在不同的分片上。

如图2-28所示,用户ID在0~100(不含100)之间的数据存储在A分片节点上,100~200(不含200)之间的数据存储在B分片节点上,

200~300(不含300)之间的数据存储在C分片节点上。如果ID等于50,则存入分片A;如果ID等于220,则存入分片C。如果需要查询ID在20~80的区间数据,则只需要查询分片A即可;就算存在跨分片的查询,也可以快速定位这些数据在哪些分片中。例如,查询ID在20~150的区间数据,通知分片A、B即可,不需要广播到所有分片节点。

图2-28 Range算法

Range算法的优点是数据分布连续,对于区间查询友好;缺点是使用不当会造成数据分布不均匀,比较适合单调递增的主键设计。Range 算法包含下边界,不包含上边界,也就是大于等于最小值,小于最大值。

如果使用单调递增的分片键,则Range算法会将新插入的数据都分配到最后的分片节点上,造成最后的分片成为写热点(写操作量较大)。分片键的选择会直接影响分片集群的性能、效率和可伸缩性。

九、 Redis高可用架构

如果读者对Redis不了解,则可以先阅读以下相关基础知识内容。

1. Redis基础知识

(1)Redis的概念。

Redis(Remote Dictionary Server,远程字典服务器)是一个开源

的、使用C语言编写、遵守BSD协议、支持网络、可基于内存、可持久化的Key-Value(键-值)数据库,并提供多种语言的API。由于Redis不使用SQL语句,因此它也被称为NoSQL数据库,与之类似的产品还有

Memcached。

Redis支持丰富的数据类型,如string、list、set、zset(sorted set)、hash。

(2) Redis的作用。

由于Redis属于内存型数据库,所有数据都在内存中存储(也可以持久化磁盘中)和查询,因此其具有极高的存储和查询性能,远远超过关系型数据库。

Redis通常作为缓存中间件使用,减少对数据库的访问次数,从而减少磁盘的读写次数,提高服务性能。

(3)Redis的基本使用流程。

当系统服务需要从数据库中查询数据时,并不是直接到数据库中查询数据,而是先到Redis中查询。Redis中查询不到,才到数据库中查询。Redis的使用流程如图2-29所示。

① 第1.1~1.2步:系统服务向Redis发起查询数据的请求,Redis中没有查询到对应数据。

② 第1.3~1.4步:由于Redis中没有对应数据,所以请求数据库查询数据,并返回了结果。

③  第1.5步:系统服务在得到数据库返回的结果后,将结果数据存储到Redis中。

④ 第2.1~2.2步:当系统服务再次要查询这个数据时,可以直接从 Redis中获取到结果,不需要访问数据库了。Redis为内存型数据库,查询速度更快,从而减轻了数据库的访问压力,同时提高了系统性能。

图2-29 Redis的使用流程

(4)Redis是否可以完全替代关系型数据库?

首先,Redis基于内存存储和查询,虽然速度快,但是内存资源有限,无法像磁盘一样存储海量数据;其次,Redis使用Key-Value形式存储,无法表达复杂的数据关系,所以Redis无法完全替代关系型数据库。

2.持久化

Redis是一个内存型数据库,存储空间有限,同时数据容易丢失(重启丢失)。因此,Redis提供了持久化功能,将内存数据写入磁盘,在Redis启动时可以从磁盘加载,提供备份和恢复能力,这也属于高可用范畴。

Redis的持久化是异步进行的,主进程不阻塞并实时对外提供服务,同时异步写入磁盘,保证服务不间断的同时,达到了数据备份的效果。Redis的持久化有两种模式:RDB模式和AOF模式。

(1)RDB模式。

RDB模式是一种周期性备份模式,与定时批处理的原理相同,每隔一段时间备份一份快照文件,因此会产生多个备份文件,每个备份文件都代表某一时间点的数据。可以将RDB文件存储到外部设备或远程存储设备中,以保证数据的安全性,同时方便转移和存储。

RDB模式是在Redis的主进程上Fork(分叉)出一个子进程专门用于持久化,因此对于Redis的性能影响较低,使用RDB文件进行恢复和重启速度更快,如图2-30所示。

图2-30 Redis的RDB备份模式

RDB模式的缺点是会造成数据丢失。例如,5分钟进行一次持久化备份,当发生服务器宕机后,就会丢失上次备份到停机时间之间的数据,这一点不如AOF模式。

(2)AOF模式。

AOF模式是一种日志模式,对于每一条命令都以操作日志的形式记录。当Redis重启时,通过日志回放的方式进行数据恢复,因此AOF 可以更好地降低数据丢失情况,工作模式如图2-31所示。

图2-31 Redis的AOF备份模式

可以利用AOF文件做更灵活的数据恢复。例如,由于误操作对某些数据进行了删除或清空,可以修改AOF文件,将删除和清空指令删去,再使用修改后的AOF文件进行恢复。

AOF文件的写入有3种策略,可以在redis.conf配置文件中使用 appendfysnc参数进行配置。

①always:代表每次写入操作完毕后,立即执行fsync操作,进行 AOF文件写入。这种方式可以保证数据基本不丢失,但是对Redis的性能有较大影响,一般不采用。

②no:代表每次写入操作完毕后,不会执行fsync操作,而是由操作系统进行调度,性能最好,但是数据丢失风险最高,具有不可控性,一般不采用。

③everysec:每秒执行一次fsync操作,对性能影响不大,同时能够保证即使数据丢失也只丢失最近1秒的数据,推荐采用。

由于AOF采用操作日志的形式存储,因此相同数据量的AOF文件会比RDB文件大。为了解决AOF文件过大的问题,Redis提供了重写操作。例如,有连续100个重复的set操作,则可以合并为一条日志存储,从而减小AOF文件的体积。AOF采用日志回放的方式恢复,而RDB采用一次性整体恢复的方式,所以RDB的恢复速度会更快一些。

RDB和AOF模式各有优缺点,又相互补足。Redis支持同时开启两种持久化模式,所以推荐同时开启两种模式,用AOF保证数据完整性,用RDB作为冷备份补足。在恢复时优先采用AOF进行恢复,当AOF文件损坏无法使用时,再使用RDB文件恢复。

3. Redis的3种高可用架构缓存数据库是提高程序性能和承压能力的重要工具,如果缓存服务器宕机,则很容易引起雪崩问题,导致系统级联故障,甚至大面积宕机。因此,缓存服务器的可用性也是至关重要的。下面对企业中常用的Redis高可用架构进行分析讲解。

Redis的高可用也有3种模式:主从模式、哨兵模式和集群模式,原理与MongoDB基本相同,只是名称不同而已,对应关系如表2-2所示。

表2-2 Redis与MongoDB高可用架构的对比

(1)主从模式。

持久化虽然解决了内存数据不丢失的问题,但是当Redis服务器因为故障宕机而无法重启时,需要手动复制RDB或AOF文件到新服务器,然后再进行数据恢复,不仅延长了服务不可用时间,一旦备份文件丢失或损坏,还可能无法恢复,如图2-32所示。

图2-32 Redis主备模式数据恢复

Redis的主从模式与MongoDB的主从模式基本相同,主库对外提供读写服务,从库可以只用于备份,而不对外提供服务,这就是主备模式(热备份)。如果从库对外提供查询服务,减轻主库的压力,则为读写分离模式。主从模式主要有3种架构方式,如图2-33所示。一个主库一个从库为一主一从模式,最为简单,但是读写分离,承压能力有限。一个主库多个从库,所有从库均从主库同步数据,为一主多从模式,从库的节点越多,读压力就能够进一步减轻,但是所有从节点均从主节点同步数据,势必会对主节点造成一定的影响。因此,可以不让所有从节点与主节点保持同步,而是与主节点的从节点保持同步即可,为主从从模式。

图2-33 Redis的3种主从架构

无论采用哪种部署模式,主从模式的核心是如何将数据从主节点高效、准确地复制到从节点。主从同步主要分为全量同步、增量同步和部分同步3种方式,同步流程如图2-34所示。

①  全量同步。

第1~2步:从节点启动后,与主节点建立连接,并主动发送SYNC 指令给主节点,请求全量数据同步。

第3~4步:主节点收到SYNC指令后,单独Fork出一个进程生成

RDB文件,在文件生成期间,如果有新的写请求,则主进程会将这些命令先缓存起来。RDB文件生成后,会发送给从服务器。

第5步:从节点接收快照文件后,加载快照进行全量初始化操作。

②  增量同步。

第6~7步:在全量同步之后,只需要将主节点缓存的写指令发送给从节点即可,从节点接收指令后执行指令,即可与主节点保持同步。

后续主节点会将写指令异步实时地同步给从节点。

③  部分同步。

第8~10步:如果主节点与从节点之间因为某些原因断线重连,就需要重新执行SYNC指令,做全部数据的复制和初始化,这显然效率太低了,因此Redis引入了部分同步PSYNC指令。在从节点断线重连后,只需要从上次已经同步到的位置继续同步即可。因此,主节点需要记录每一个从节点已经同步到了哪个位置,记录和更新偏移量。

图2-34 Reids主从同步流程

Redis采用异步复制的方式,主从节点在数据同步的过程中依然可以对外提供服务。如果从节点正在进行数据同步,这时有查询请求,则从节点会以旧数据作为查询依据返回给客户端。如果担心用户查询到脏数据,则也可以配置为数据正在同步时给客户端返回错误。在一主多从的模式下,如果数据量较大,则全量同步对主节点性能消耗极其严重。因此,如果有多个从节点,则应该逐一启动,间隔时间要长一些。

与MongoDB相同,Redis的主从模式最大的问题是无法自动故障转移,当主节点宕机后,必须手动设置从节点为主节点,才可以对外提供读写服务。虽然这个过程比复制RDB和AOF文件要快,但是依然不够理想,Redis主从切换过程如图2-35所示。

图2-35 Redis主从切换过程

(2)哨兵模式。

哨兵模式通过主动监控、主观下线、客观下线、Master选举保证了故障的自动转移,达到服务的高可用目的。

哨兵模式是在主从模式的基础上增加了额外的哨兵节点,多个哨兵节点组成一个“仲裁委员会”,它们负责监控主从节点的状态,从而判定其是否存活,进而决定是否将主节点下线,再通过选举算法决定将哪个从节点升级为主节点。

推荐至少采用3+3的结构部署,即3个哨兵节点+3个Redis节点。需要注意的是,哨兵节点并不存储实际的业务数据,如图2-36所示。

图2-36 Redis哨兵集群架构

因为主节点知道哪些节点是从节点,所以哨兵节点每隔10秒向主节点发送一次info指令,用于获取Redis的网络拓扑图。使用info指令可以获得所有节点的IP地址、端口、状态等信息。

哨兵节点每隔1秒向主节点、从节点、其他哨兵节点发送一次ping 命令,来判断各个节点的存活状态,如果在指定的时间内被ping节点没有应答,则认为此节点已经下线。这种被称为主观下线(Subjective

Down,SDOWN),如图2-37所示。

图2-37 Redis哨兵集群信息同步和监控

主观下线是指单独某一个哨兵节点做出的判断,无法代表真实情况。例如,哨兵A无法ping通主节点,则主观地认为主节点已经下线,但可能主节点并没有下线,而是哨兵A到主节点之间的网络出现故障所导致的,故障转移流程如下。

①  哨兵A探测到主节点不通,则将其标记为主观下线。但是,哨兵A判断的不一定准确,也不算数。

②哨兵A将此消息通知给哨兵B和C:“我发现主节点不可用了,你们也去看看是不是真的。”③ 哨兵B和C收到消息后就去探测主节点状态,探测之后沟通一下,如果超过半数(可配置)的哨兵节点都认为主节点已经不可用,则标记为客观下线(Objective Down,

ODOWN)。

④ 哨兵A发出消息,告知哨兵B、C,其要作为哨兵的头领,主持本次的故障转移。如果超过半数的哨兵节点同意,则哨兵A可以成为仲裁委员会的主节点,负责本次的故障转移工作。当然,其他哨兵节点也可以不同意,如果不同意,则再次投票进行选举。

⑤ 将其中一个Redis从节点(ID较小的)脱离,并升级为主节点。

将另外一个从节点指向新的主节点。然后通知客户端,主节点已经更换完毕。

⑥ 如果原来的主节点恢复后重新启动,则将自动变为从节点,指向新的主节点。哨兵节点通过info指令就可以感知到新的拓扑结构。

哨兵模式中每个Redis节点都存储着全量的业务数据,本质是在主从模式的基础上增加了高可用策略。因此,依然只有主节点能够对外提供读写服务,从节点只提供读服务,可以认为哨兵模式是一种高可用的读写分离架构。

(3)集群模式。

Redis集群模式是一个分布式系统,与MongoDB的分片模式类似,每个节点中都存储着部分数据,当部分节点不可用时,依然可以对外提供服务。

集群模式主要用在海量数据存储、高并发、高可用的场景,如果数据量不大,只有几吉字节,那么就没必要使用集群模式。

集群模式中有多个主节点,每个主节点都可以有多个从节点,当某个主节点发生故障时,从节点可以被选举为主节点,继续工作。这需要使用大量的服务器资源,相当于将多个主从集群连接在一起,为每个数据分片都提供了高可用能力,如图2-38所示。

图2-38 Redis集群模式架构

如何将客户端写入的数据分配到不同的Redis节点上,既要数据分布均匀,又要保证数据查询时可以快速准确地找到数据存储节点?对比MongoDB分片集群,只要有合适的分片算法就可以了。

在Redis提供集群模式之前,都是采用Twemproxy、Codis这种代理中间件来实现数据分片存储的,如图2-39所示。原理很简单,这些中间件负责将客户端请求的数据Key,经过哈希算法、一致性哈希算法、取模等算法进行计算,再将数据分配到不同的节点上。由于官方提供了Redis 集群,因此这种方式也慢慢地被弃用了,但是分片的原理依然万变不离其宗。

图2-39 Redis代理模式架构

Redis集群使用哈希槽(Hash Slot)的方式来实现数据分片存储,值得在其他设计场景中借鉴。Redis集群有固定的16384个哈希槽,每个Redis节点负责管理一部分哈希槽。如图2-40所示,如果有3个节点,则节点1负责0~5500的槽位,节点2负责5501~11000的槽位,节点3负责11001~16384的槽位。

图2-40 Redis集群架构

当客户端发出读写请求时,Redis 集群根据Key进行计算,计算出 Key对应的槽位值是多少,计算公式为CRC16(Key)%16384。例如,某个Key经过计算后得到的槽位值为6000,由于在5501~11000区间内,因此可以确认请求应该交由节点2处理。

十、Kafka高可用架构

1.消息队列和Kafka基础知识

(1)消息队列。

消息队列(Message Queue,MQ)采用队列结构,用于在多个系

统之间传递消息,可以支持点对点模式和订阅模式。

如图2-41所示,消息队列中有生产者和消费者两个角色。生产者负

责生产消息,并将消息发送到消息队列中。消费者负责从消息队列中

接收并处理消息,以这样的方式完成了系统之间的解耦。

图2-41 消息队列

① 点对点模式。生产者发布消息到消息队列中,多个监听该队列的消费者中只有一个消费者可以消费该消息,这种模式就是点对点模式。如图2-42所示,生产者1发布消息,只有消费者3消费了该消息。

图2-42 点对点模式

② 订阅模式。生产者发布消息到消息队列的某个主题中,只要是订阅了这个主题的消费者都可以接收到该消息。如图2-43所示,生产者1将消息发送至队列主题中,订阅了该主题的消费者1、2、3都可以接收到该消息。

图2-43 订阅模式

(2)Kafka基础知识。

与ActiveMQ、RabbitMQ、RocketMQ一样,Kafka是一款高性能的消息队列中间件,主要提供以下3个功能:发布和订阅消息;以容错的方式记录消息流;可以在消息发布时进行处理。

① Kafka的使用场景。在系统或应用程序之间构建可靠的、用于传输实时数据的管道,提供消息队列功能。构建实时的流数据处理程序来变换或处理数据流,在大数据流式计算场景下应用比较广泛。

② Kafka的优势。

a.高吞吐量、低延迟:Kafka每秒可以处理数十万条消息,延迟最低只有几毫秒,所以它是大型高并发互联网架构的首选消息中间件。

b.可用性、扩展性:Kafka支持集群高可用模式,支持热扩展(不停机扩展)。

c. 持久性、可靠性:消息可被持久化到本地磁盘上,并且支持数据备份。

d.容错性:集群中可以有部分节点故障,而不影响使用。

e. 高并发:支持数千个客户端同时读写。

2. Kafka集群高可用设计

Kafka集群搭建要借助于Zookeeper来存储节点的元数据信息,从而达到消息代理(Broker)的高可用。

每个Kafka主题都可以设定指定数量的分区,消息会根据算法分配到不同的分区中,如果分区不可用,则会导致数据丢失。Kafka通过分区副本来保证分区的高可用。

如图2-44所示,设置分区的数量为3,则每个主题都会有3个分区。同时,设置分区副本数也为3,则每个分区都会有3个副本。其中一个为Leader副本(主副本),另外两个为Follower副本(从副本)。

图2-44 Kafka集群高可用架构

例如,P1分区有3个副本,分别是1个主分区P1_Leader、2个从分区P1_Follower。同理, P2、P3分区也有3个副本。

如果将P1分区的3个副本都放在Broker1中,当Broker1发生宕机时,则整个P1分区消息全部丢失。因此,Kafka会尽量均匀地将副本分散到各个Broker中,防止Broker不可用导致分区消息全部丢失。其中一个Broker宕机,其他Broker中的副本依然可用。

生产者发送的消息都会写入Leader副本中,再由Leader同步复制给其他Follower副本。当Leader副本发生故障时,会通过选举算法将其中一个Follower副本升级为Leader副本来提供服务。

例如,P1分区的3个副本,P1_Leader副本位于Broker1中,2个P1_Follower副本分别位于Broker1和Broker2中,Leader副本实时将消息复制给2个Follower副本,当Leader副本发生故障时,会从其他的Follower副本中重新选举出新的Leader副本作为主分区。

十一、数据库高可用架构

这里主要是指关系型数据库的高可用架构,主要采用的思想还是冗余和故障转移的设计思路。

1. 数据库的3种高可用架构

关系型数据库一般都支持3种高可用架构:主备架构、主从架构和互为主从架构。

(1)主备架构。

一个主库一个备库,主库实时将数据同步到备库中。主备架构主要是为了实时备份数据,防止数据丢失,解决了冷备份导致的备份数据不全和不及时的问题。主备架构可以同城部署,也可以异地部署,以达到容灾的目的,如图2-45所示。

图2-45 主备架构

主备架构的缺点:① 备库不提供任何服务,始终处于闲置状态,对资源是一种浪费;② 当主库发生故障时,无法自动故障转移;③ 不能自动将备库切换为主库,需由DBA(数据库管理员)手动处理。

(2)主从架构。

主从架构可以细分为一主一从架构、一主多从架构和主从从架构。

① 一主一从架构。一个主库一个从库,主库提供读写,从库只提供读,主库实时将数据同步给从库,如图2-46所示。

图2-46 一主一从架构

主从架构是一种读写分离架构,与主备架构不同的是,从库依然可以对外提供服务,但是只提供读服务,不提供写服务。对于读多写少的情况,可以大大减轻主库的压力。主库一般只用于写,对于实时性要求较高的业务,也可以强制从主库读取。

② 一主多从架构。一个从库虽然能够分担主库压力,但是当查询量过大时从库也会成为瓶颈。如图2-47所示,一个主库多个从库,每个从库都可以支持读操作,均从主库同步数据,从而进一步分担数据库压力,解决单个从库的压力问题。

图2-47 一主多从架构

③ 主从从架构。对于一主多从架构,由于所有从库都从主库同步数据,因此会对主库造成较大压力,可以使用主从从架构来解决这个问题。

如图2-48所示,一个主库多个从库,但并不是所有从库都从主库同步数据,而是一个从库A从主库同步数据,而其他从库都从这个从库A继续同步数据。这样做的目的是减轻主库同步的压力,同时也提供多个从库的读能力来分担系统压力。

图2-48 主从从架构

虽然看似完美,但是主从从架构的延迟一定比一主一从、一主多从架构高。对于实时性要求较高的业务,就会不适用。

(3)互为主从架构。

主从架构分担了数据库的读压力,但是对于主库的写入压力并没有减轻。如图2-49所示,两个数据库服务器互为主从架构,两个数据库节点都支持数据的读写操作,两个主节点的数据相互同步,从而同时提高了数据库的读写能力。难点是两个节点数据的主键不能够冲突,一旦出现数据不一致将很难恢复。

图2-49 互为主从架构

2.数据同步策略

数据库的高可用架构主要依靠数据的同步机制,以MySQL数据库为例,同步策略主要有3种,即异步复制策略、全同步复制策略和半同步复制策略。

(1)异步复制策略。

① MySQL默认的数据同步策略就是异步复制策略。

② 主库在执行完客户端提交的事务后会立即将结果返回给客户端,并不关心从库是否已经完成了同步。

③ 优点是响应速度最快,客户端延迟最低,用户体验最好。

④ 缺点是一旦数据还未同步到从库,而主库宕机了,就会导致从库数据丢失。当把从库切换为主库时,就会存在数据缺失。

(2)全同步复制策略。

① 当主库执行完一个事务,所有的从库也都执行了该事务才返回给客户端,确保数据已经同步成功。

② 优点是主库与从库的数据保持严格的一致,避免了数据不一致的问题。

③ 缺点是因为需要等待所有从库执行完该事务才能返回,所以客户端响应速度变慢,性能下降,尤其当从节点有很多时,性能下降就会更加严重。

(3)半同步复制策略。

① 半同步复制策略是介于异步复制与全同步复制之间的一种策略,主库只需要等待至少一个从库节点成功接收了要同步的数据,就可以立即给客户端应答,不需要等待所有从库给主库反馈。

② 同时,这里只是一个从库收到数据的反馈,而不是从库已经完全完成数据持久化的反馈,因此节省了很多时间。

③ 半同步复制策略比较适合于主从从架构使用。

十二、高并发访问限流设计

淘宝、京东等大型的互联网电商系统,在春节、“618”“双11”等促销节点,系统访问量会出现暴增,导致服务器压力过大。如果发生大面积服务器宕机,导致用户无法完成购买,就会带来巨大的经济损失。

限流的目的是保证系统的可用性,虽然有些用户会遇到“系统繁忙,请稍后再试”的情况,但是最重要的是保证系统不宕机,让系统中的大多数核心功能是可用的,降低一些用户体验,以此来顾全大局。就像玩游戏一样,将游戏的画质调低,来保障操作的流畅性是一样的道理。

需要限流的场景有:系统资源有限,承载能力有限;大量并发访问导致系统性能下降甚至宕机,避免引起雪崩问题;系统某些接口遭受攻击导致整个系统无法访问。

访问限流需要在客户端、服务端、负载设备、网关、应用系统等多个服务节点做处理,涉及漏桶和令牌桶两种算法,目标就是减轻后端服务和数据库的访问压力,保证系统的可用性。

1.倒金字塔限流原则

一次完整的客户端请求是,用户通过客户端发送请求到负载服务,负载服务将请求分发到网关服务集群,网关再将请求路由到某一个后端服务,后端服务再操作数据库,最终把操作结果原路返回给客户端,如图2-50所示。

图2-50 客户端请求

限流的最终目的是保证后端服务、数据库不被压垮,因此流量应该从左到右逐层递减。遵循倒金字塔限流原则,可将一个完整的系统分为四层,分别是客户端层、负载层、服务层和数据层。越靠近上层事务处理压力越小,反而应对高并发能力越强;越靠近底层处理事务的能力越强,反而应对高并发能力越弱,如图2-51所示。

图2-51 倒金字塔限流原则

上层限流的目的是保护下层服务安全,使到达下层的请求不超过其处理能力范围,电商大促、秒杀等场景所使用的各种限流、熔断、降级、缓存、排队等手段,都是为了减轻后端服务和数据库的访问压力,避免系统崩溃。

根据倒金字塔限流原则,越在上层的服务限流效果越好,控制范围越大。

2.客户端限流设计

客户端限流主要针对两种请求,一种来自真实用户的正常请求,另一种是攻击性请求。用户正常请求量突增,这种情况一般是由特殊的时间节点和事件导致的,如节假日、活动大促造成的请求量突增。

99%的系统用户是不懂技术的,更不懂得如何进行服务攻击,而客户端限流主要针对的就是这个群体,因此具有投入低、效果好的特点。

客户端限流主要采用如下4种手段:纯前端验证码、禁用按钮、调用限制和假排队。

(1)纯前端验证码:如果某个用户的请求过于频繁,则自动弹出图片验证码,要求用户完成验证,如图2-52所示。这样做的目的是打断用户操作,验证当前属于人为操作还是机器操作,这个验证码可以完全由前端页面生成(如JavaScript生成),不需要考虑安全性问题。

(2)禁用按钮:当用户点击了页面内的按钮后,则立即禁用此按钮,直到收到服务端应答为止。这样做的目的是防止用户因为急躁而反复点击按钮,造成重复请求。

(3)调用限制:可以在客户端限制用户在1秒内可以发起请求的次数,从而限定用户的操作频率,减轻服务端压力。如果用户点击过于频繁,则可以提示“您的操作过于频繁,请您少安毋躁”。

(4)假排队:当用户发起的请求过于频繁时,弹出提示“当前业务繁忙,正在排队中...1/10人”或“当前业务繁忙,预计等待5秒...”,如图2-53所示。排队的人数及等待的秒数都是随机的,目的就是让请求不要过于集中在一个时间点,减少并发请求量。

图2-52 人机验证

图2-53 排队

客户端限流的特点是不会真正向服务端发起请求,而是完全由客户端进行自我控制。客户端限流的缺点是会降低用户体验,所以怎样在保证用户体验的同时,减轻服务端压力,这是对产品设计的一种考验。

漏桶算法的原理如图2-54所示,将服务器想象成一个大木桶,上方有一个水龙头,水(令牌Token)会以固定速度流入木桶中。那么,木桶的容量就是服务器的最大可处理能力。在木桶下方有一个漏孔,水(令牌Token)以固定速度流出,当流入速度大于流出速度时,木桶中的水就有可能被加满,这时再有水(令牌Token)流进来,水就会溢出(超出服务器处理能力)。

图2-54 漏桶算法的原理

在桶的下方就是接收到的客户端请求,当请求到达后先进入请求队列中,客户端请求获取到桶中流出的令牌,则可以执行。如果获取不到,则无法出队列,即无法执行。

当请求量很小时,每个请求都可以得到令牌,都可以处理;当请求量巨大时,请求被阻塞到队列中等待执行。

如果队列被放满了,则拒绝执行,客户端得到“系统繁忙,请稍后再试”的应答消息。因此,通过这种方式达到了限流的目的。

漏桶算法的缺点是无法处理突发请求。一般系统的请求量在白天会大于晚上,当白天有大量的请求到达时,也只能以固定速度处理,就算桶中还有大量的水(令牌Token),也依然只能以固定速度获得。

为了应对突发请求,衍生出了令牌桶算法,这也是目前绝大多数限流技术所采用的算法。

4.令牌桶算法

令牌桶算法的原理如图2-55所示,依然将服务器想象成一个大木桶,同样桶上方的水龙头以固定速度向桶中流入令牌,当桶满了(超出服务器处理能力)之后依然溢出。桶下方依然有一个漏孔可以流出令牌,但区别是此令牌并不是以固定速度流出,而是只要桶中还有足够的剩余令牌就可以取出来,不需要以固定速度流出。

图2-55 令牌桶算法的原理

与漏桶算法相同,在桶的下方就是接收到的客户端请求,当请求到达后先进入请求队列中,客户端请求获取到桶中流出的令牌,则可以执行。如果获取不到,则无法出队列,即无法执行。

在夜间服务器并没有什么请求,因此可以积累大量的令牌在桶中,例如,已经积累了5万个令牌。在早上9点时突发了3万个请求,当任务到达后就可以直接从令牌桶中获得3万个令牌,然后再去执行任务。

客户端的请求永远是曲线的,总会有高有低,而不是直线。因此,令牌桶算法可以在请求量少时积累令牌,在请求量大时快速释放令牌,从而达到了动态限流的目的。

注意

限流的目的是保证服务器的可用性,使请求压力不超过服务器的处理能力。当然,同时客户端请求的处理速度要越快越好,因此令牌桶算法的适用性更好。

5.服务端负载限流

服务端负载限流是指负载设备的限流,一般会采用F5进行硬负载,使用Nginx、LVS、HAProxy做软负载,这些负载设备既然是流量的入口,那么就可以对流量做限制,下面以Nginx的访问限流为例进行说明。

Nginx提供了ngx_http_limit_req_module和ngx_http_limit_conn_module两个模块来支持限流配置,如图 2-56所

示。

图2-56 Nginx限流

(1)使用ngx_http_limit_req_module模块可以对请求速率进行控制,从而达到限流的目的。

例如,上面的配置中,在http模块下配置“limit_req_zone $binary_remote_addr zone=rateZone:10m rate=10r/s;”,详细含义如表2-3所示。

表2-3 限流参数说明(1)

在server模块下配置“limit_req zone=rateZone;”,详细含义如表2-4所示。

表2-4 限流参数说明(2)

对于以上的配置,虽然能够起到限制请求速率的目的,但是却无法处理突发请求,当请求速率突然大于每100毫秒1个请求时则无法处理,这种方式过于死板。

如上所示,可以增加burst=100这样一个配置,相当于增加了一个队列,当有101个请求瞬间到达时,Nginx会先处理第一个请求,然后将100个请求放入队列中,再按照100毫秒处理1个请求的速度进行处理,但是如果同时收到102个请求,则第102个请求到达时,就会直接拒绝,并返回503错误(Service Unavailable,服务不可用)。

这样只是接收了突发请求,而没有立即处理,因此排队越靠后的请求处理速度越慢,用户等待时间越长。例如,队列中第100个请求,需要等待100×100毫秒,也就是10秒之后才能处理,这样会极大地降低用户体验。

因此,burst参数一般与nodelay参数结合使用(如上),代表Nginx收到100个突发请求时立即处理,不会再排队处理。但是,即使这些请求都被处理了,等待队列却不会被立即释放,而是每100毫秒释放一个。这样就达到了限制请求速率的同时,也能够处理突发请求。

(2)使用ngx_http_limit_conn_module模块可以对连接数进行限制,从而达到限流的目的。

例如,上面的配置中,“limit_conn_zone $binary_remote_addrzone=connZone:10m; ”的含义与limit_req_zone完全相同,只是比limit_req_zone缺少了rate参数来控制速率。limit_conn参数可以配置在http、server、location中,用来限制最大连接数。

上例中limit_conn connZone 10,代表的是使用connZone共享内存空间,限制同一个客户端最大同时与服务端保持10个连接。

如上所示,将$binary_remote_addr改为$server_name,就可以根据服务端来限制连接数,代表同一个服务端最多允许20个连接,无论这些连接是否来源于同一个IP。

不同的负载均衡器都有不同的限流配置方式,但是原理都大体相同。

6.微服务网关限流

此处所说的网关是指微服务的网关(参见3.7节),如Zuul、SpringCloud Gateway、自定义网关应用等。网关限流需要进行自定义开发,这是一种应用级别的限流。

如图2-57所示,用户微服务、订单微服务、库存微服务面临的压力可能是不同的,因此可以根据自己的业务需要进行限流。例如,只针对订单微服务做限流,甚至可以只针对订单微服务中的某一个接口做限流,也可以针对某一个用户、IP、时间段做限流,灵活度非常高。

图2-57 微服务网关限流

例如,某一个接口受到攻击,或者某一个接口响应十分缓慢、长时间占用资源而不释放,就可以单独针对这个接口做限流。对单独接口进行限流是十分有必要的,服务器宕机大多数情况是由于某一个接口的执行性能不够、存在bug而导致整个服务受到影响,因为在一个服务内CPU、硬盘、内存等资源都是共享的,还无法做到接口级别的隔

离。

Zuul是Spring Cloud框架支持的第一代网关,推荐使用spring-cloudzuul-ratelimit进行限流集成,其优点是集成简单方便,通过配置就可以实现限流功能。对于复杂的限流要求,只需要继承DefaultRateLimitKeyGenerator,实现自己的KeyGenerator即可。

Spring Cloud Gateway是Spring Cloud出品的第二代网关,具有限流功能,与Redis集成后通过简单的配置就可以支持访问限流。

当然,无论是自定义网关,还是Zuul、Spring Cloud Gateway等第三方网关,都可以自己按照漏桶算法、令牌桶算法去实现限流。避免重复造轮子,也可以使用Google Guava RateLimiter工具类进行开发实现。


评论