• GO微服务实战第十九节 如何保障分布式系统的高可用性?


    高可用性是我们经常提到的名词,指系统提供的服务要始终可用,无论是系统内部运行出现故障,还是系统的外部依赖出现问题,甚至遇到系统硬件损坏、停电等致命性打击,系统都要保证基本可用。

    因此,高可用系统关注用户使用体验,并且通过降低系统出现故障的概率,以及缩短系统因突发故障导致的宕机时间,减轻了开发运维人员的工作。

    目前,主流的互联网产品都采用了大量手段来保证系统可用性,比如淘宝在双十一时会采用限流和降级设计等手法,来保证系统能够承受住秒杀活动时产生的巨量瞬时流量;再比如,Kafka 采用冗余设计将消息备份到多个不同的 Broker 中,来避免消息丢失等。

    那么,为了让你对系统可用性有更直观的认识,我首先带你了解分布式系统中故障不可避免的原因,然后再来介绍衡量系统可用性的指标,最后介绍目前常用的高可用性设计,以帮助你学习后面的项目案例实践打下理论基础。希望通过本节课的学习,你能够对如何设计高可用性系统有个整体的认知。

    故障不可避免

    高可用性是指系统提供的服务要始终可用,然而故障不可避免,特别是在分布式系统下,面对不可控的用户流量和机房环境,系统故障将会显得更加复杂和不可预测。

    在大规模的分布式系统中,各个模块之间存在错综复杂的依赖调用关系,比如前端服务依赖于后端服务获取业务处理数据,后端服务依赖于数据库进行数据持久化处理,如果任一环节出现问题,都有可能导致雪崩式、多米诺骨牌式故障,甚至可以断言故障的出现成了常态。

    Drawing 0.png

    如上图的分布式系统中,用户请求查看系统中的某一个页面,请求经过服务网关转发给前端服务处理,前端服务向后端服务请求渲染页面需要的业务数据,后端服务也可能需要从数据库中查找相关的持久化数据,请求需要经过上述长长的调用链才能处理返回结果。也就是说,这时我们起码要保证网络连接正常、服务网关正常、前端服务正常、后台服务正常、数据库正常,请求才能被正常处理。如果调用链中的任一环节出现问题,就很可能请求出错,出现故障,这将直接影响到用户体验。

    系统出现故障的原因多种多样,主要有以下这些:

    • 网络问题,网络连接故障、网络带宽出现超时拥塞等;

    • 性能问题,数据库出现慢查询、Java Full GC 导致执行长时间等待、CPU 使用率过高、硬盘 IO 过载、内存分配失败等;

    • 安全问题,被网络攻击,如 DDoS 等;异常客户端请求,如爬虫等;

    • 运维问题,需求变更频繁不可控,架构也在不断地被调整,监控问题等;

    • 管理问题,没有梳理出关键服务以及服务的依赖关系,运行信息没有和控制系统同步;

    • 硬件问题,硬盘损坏导致数据读取失败、网卡出错导致网络 IO 处理失败、交换机出问题、机房断电导致服务器失联,甚至是人祸(比如挖掘机挖断机房光缆,导致一整片机房网络中断)等。

    面对如此多的可控和不可控的故障因素,系统的高可用性似乎变成不可能完成的任务,但是在日常开发运维中,我们可以采用一些有效的设计、实现和运维手段来提高系统的可用性,尽量交付一个在任何时候都基本可用的系统。

    系统可用性指标

    系统可用性指标是衡量分布式系统高可用性的重要因素,它通常是指系统可用时间与总运行时间之比,即Availability=MTTF/(MTTF+MTTR)。

    其中,MTTF(Mean Time To Failure)是指平均故障前的时间,一般是指系统正常运行的时间系统的可靠性越高,MTTF 越长,即系统正常运行的时间越长。

    MTTR(Mean Time To Recovery)是指平均修复时间,即从故障出现到故障修复的这段时间,也就是系统不可用的时间MTTR 越短说明系统的可用性越高

    系统可用性指标可以通过下表的 9999 标准衡量,现在普遍要求至少 2 个 9,最好 4 个 9 以上:

    Lark20200911-181411.png

    冗余设计

    通过前面基础知识的铺垫,我们已经了解了系统故障的必现性以及系统可用性指标的重要性。接下来,我们将介绍一些典型的高可用设计,以便你知晓如何降低系统故障对系统正常运行的影响。

    分布式系统中单点故障不可取的,而降低单点故障的不二法门就是冗余设计,通过多点部署的方式,并且最好部署在不同的物理位置上,避免单机房中多点同时失败。冗余设计不仅可以提高服务的吞吐量,还可以在出现灾难时快速恢复。目前常见的冗余设计有主从设计和对等治理设计,其中主从设计又可以细分为一主多从、多主多从。

    冗余设计中一个不可避免的问题是考虑分布式系统中的数据一致性,多个节点中冗余的数据追求强一致性还是最终一致性。即使节点提供无状态服务,也需要借助外部服务,比如数据库、分布式缓存等维护数据状态。

    CAP 是描述分布式系统下节点数据同步的基本原则,分别指:

    • Consistency,数据强一致性,各个节点中对于同一份数据在任意时刻都是一致的;

    • Availablity,可用性,系统在任何情况下接收到客户端请求后,都能够给出响应;

    • Partition Tolerance,分区容忍性,系统允许节点网络通信失败。

    分布式系统一般基于网络进行数据通信,所有 P 是必须满足。但是满足数据强一致性的系统无法保证可用性,最典型的例子就是 ZooKeeper。

    ZooKeeper 采用主从设计,服务集群由 Leader、Follower 和 Observer 三种节点角色组成,它们的职责如下表所示:

    Lark20200911-181407.png

    在 ZooKeeper 集群中,由于只有 Leader 角色的节点具备写数据的能力,所以当 Leader 节点宕机时,在新的 Leader 节点没有被选举出来之前,集群的写能力都是不可用的。在这样的情况下,虽然 ZooKeeper 保证了集群数据的强一致性,但是此时集群无法响应客户端的写请求,即不满足 C 可用性原则。

    对等治理设计中比较优秀的业内体现为 Netiflx 开源的Eureka 服务注册和发现组件。Eureka 集群由 Eureka Client 和 Eureka Server 两种节点角色组成,其中 Eureka Client 是指服务实例使用的服务注册和发现的客户端,各服务实例使用它来与 Eureka Server 进行通信,主要用于向 Eureka Server 请求服务注册表中的数据和注册自身服务实例信息; Eureka Server 作为服务注册中心,在注册表中存储了各服务实例注册的服务实例信息,并定时与服务实例维持心跳,剔除掉注册表中长时间心跳失败的服务实例。Eureka Server 采用多实例的方式保证高可用性部署。

    每一个 Eureka Server 都是对等的数据节点,Eureka Client 可以向任意的 Eureka Server 发起服务注册请求和服务发现请求。Eureka Server 之间的数据通过异步 HTTP 的方式同步,由于网络的不可靠性,不同 Eureka Server 中的服务实例数据不能保证在任意时间节点都相等,只能保证在 SLA 承诺时间内达到数据的最终一致性。Eureka点对点对等的设计保证了服务注册与发现中心的高可用性,但是由于 Eureka Server 数据同步的不可靠性,数据的强一致性降级为数据的最终一致性。

    熔断设计

    在分布式系统中,一次完整的请求可能需要经过多个服务模块的通力合作,请求在多个服务中传递,服务对服务的调用会产生新的请求,这些请求共同组成了这次请求的调用链。当调用链中的某个环节,特别是下游服务不可用时,将会导致上游服务调用方不可用,最终将这种不可用的影响扩大到整个系统,导致整个分布式系统的不可用,引发服务雪崩现象

    为了避免这种情况,在下游服务不可用时,保护上游服务的可用性显得极其重要。对此,我们可以参考电路系统的断路器机制,在必要的时候“壮士断腕”,当下游服务因为过载或者故障出现各种调用失败或者调用超时现象时,及时“熔断”服务调用方和服务提供方的调用链,保护服务调用方资源,防止服务雪崩现象的出现。

    断路器的基本设计图如下所示,由关闭、打开、半开三种状态组成。

    Drawing 2.png

    • 关闭(Closed)状态:此时服务调用者可以调用服务提供者。断路器中使用失败计数器周期性统计请求失败次数和请求总次数的比例,如果最近失败频率超过了周期时间内允许失败的阈值,则切换到打开(Open)状态。比如在查询历史订单数据时,订单服务出现短时间的宕机,该段时间内的查询历史订单的请求都会失败,100% 的调用失败率超过了断路器中的预设的失败阈值 50%,那么断路器就会打开。在关闭状态下,失败计数器基于时间周期运作,会在每个统计周期开始前自动重置,防止某次偶然错误导致断路器进入打开状态。

    • 打开(Open)状态:在该状态下,对应用程序的请求会立即返回错误响应或者执行预设的失败降级逻辑,而不调用服务提供者。接着刚才调用订单服务的例子,在断路器处于打开状态时,所有查询历史订单的请求都会执行预设的失败降级逻辑,直接返回“系统繁忙,稍后再试”的提示语,避免服务调用者浪费资源进行无效的请求。断路器进入打开状态后会启动超时计时器,在计时器到达后,断路器进入半开状态,给此时不可用的服务提供者一定的时间进行恢复。

    • 半开(Half-Open)状态:允许应用程序一定数量的请求去调用服务。如果这些请求对服务的调用成功,那么可以认为之前导致调用失败的错误已经修正,此时断路器切换到关闭状态,同时将失败计数器重置。如果这一定数量的请求存在调用失败的情况,则认为导致之前调用失败的问题仍然存在,断路器切回到打开状态,并重置超时计时器来给系统一定的时间修正错误。半开状态能够有效防止正在恢复中的服务被突然而来的大量请求再次打垮。比如订单服务在超时计时器达到之前还没修复好,从服务调用者过来的调用流量可能会破坏原先的问题环境,导致订单服务的问题排查处理更困难。半开状态也给服务调用恢复正常的机会,如果此时订单服务修复成功,半开状态尝试的请求都能够正常返回,那么就关闭断路器,查询历史订单数据的请求都恢复正常处理。

    使用断路器设计模式,能够有效地保护服务调用方的稳定性,它能够避免服务调用者频繁调用可能失败的服务提供者,防止服务调用者浪费 CPU 周期、线程和 IO 资源等,提高服务整体的可用性。

    小结

    鉴于分布式系统中各模块交互的复杂性和网络的不可靠性,系统出现故障的概率大大增加,对此如何提高系统的可用性是开发高质量软件系统必须考虑的。

    本节课我们首先介绍了系统可用性指标,接着阐述了分布式系统中故障不可避免的情况,最后介绍了两种常用的高可用设计:

    • 冗余设计, 如何降低分布式中出现单点故障的可能性;

    • 熔断设计, 如何防止服务雪崩,保护服务调用者的资源。

    除上述介绍的设计,还有其他针对不同场景使用的设计与方案,如限流设计等,我们都将在下篇中进行介绍。希望通过本节课的学习,你能了解可用性对于分布式系统的重要性,并初步掌握如何设计一个高可用的分布式系统。


    精选评论

    **裕:

    ”即不满足 C 可用性原则。“应该是”即不满足 A 可用性原则。“吧。

    *博:

    我理解断路器一般是网关层统一控制,另外对于整个链路的时间超时(比如一个api要经过 A-E),api调用A的请求整个链路为2s。如果在C节点发现整个链路已经超过2S(可能在发生在请求C初始阶段或者C处理过程中)了,那么C应该如何做?

    在上一篇文章中,我们首先介绍了系统可用性的相关概念——系统故障的必现性以及系统可用性指标,然后还详细说明了两种常用于提高分布式系统可用性的设计——冗余设计和熔断设计。

    冗余设计是通过多点部署的方式来提高系统的故障容错率,在故障发生时可以进行故障转移,从而避免单点故障,但它同样带来了多节点数据一致性的挑战,增加了系统设计的复杂性。熔断设计在服务提供方不可用时保护服务调用方的资源,减少服务调用方中无用的远程调用,但在系统出现瞬时巨量流量时却也无能为力。对此我们就需要使用限流设计和降级设计来保护服务提供方,进而保证服务提供方在大量访问流量冲击时依然能稳定提供服务。

    因此接下来我们就详细介绍下其他高可用设计和方案,包括限流设计、降级设计、无状态设计和重试设计等,希望能进一步加深你对如何设计高可用分布式系统的理解。

    限流设计

    熔断设计保护的是服务调用者,即上游服务的可用性,对于下游服务提供者,考虑到自身服务实例的负载能力,同样需要限流设计保护自己不被过量的流量冲垮。一般来讲有以下的限流策略:

    • 拒绝服务,把多出来的请求拒绝掉。一般来说,好的限流系统在经受流量暴增情况时,会暂时拒绝周期时间内请求数量最大的客户端,这样可以在一定程度上把一些不正常的或者是带有恶意的高并发访问挡在“门外”。

    • 服务降级,关闭或是把后端做降级处理,释放资源给主流程服务以支持更多的请求。降级有很多方式,一种是把一些不重要的服务给停掉,把 CPU、内存或是数据的资源让给更重要的功能;一种是数据接口只返回部分关键数据,减少数据查询处理链路;还有更快的一种是直接返回预设的缓存或者静态数据,不需要经过复杂的业务查询处理获取数据,从而能够响应更多的用户请求。

    • 优先级请求,是指将目前系统的资源分配给优先级更高的用户,优先处理权限更高的用户的请求。

    • 延时处理,在这种情况下,一般来说会使用缓冲队列来缓冲大量的请求,系统根据自身负载能力异步消费队列中的请求。如果该队列也满了,那么就只能拒绝用户请求。使用缓冲队列只是为了减缓压力,一般用于应对瞬时大量的流量削峰。

    • 弹性伸缩,采用自动化运维的方式对相应的服务做自动化的伸缩。这种方案需要应用性能监控系统,能够感知到目前最繁忙的服务,并自动伸缩它们;还需要一个快速响应的自动化发布、部署和服务注册的运维系统。如果系统的处理压力集中在数据库这类不易自动扩容的外部服务,服务弹性伸缩意义不大。

    限流设计最主要的思想是保证系统处理自身承载能力内的请求访问,拒绝或者延缓处理过量的流量,而这种思想主要依赖于它的限流算法。那接下来我们介绍两种常用的限流算法:漏桶算法和令牌桶算法。

    漏桶算法是网络世界中流量整形或速率限制时经常使用的一种算法,它的主要目的是控制数据进入系统的速率,平滑对系统的突发流量,为系统提供一个稳定的请求流量。如下图所示,水先流进漏桶(表示请求进入系统),而后以恒定速率流出(表示系统处理请求)。无论水龙头流入的水有多大,漏桶中的水总是以恒定的速率流出,这样就保证了系统不会处理超过自身负载能力的请求。当访问流量过大时漏桶中就会积水,如果水太多了就会溢出,此时溢出的请求将会被拒绝。当出现突发巨量流量时,溢出漏桶的请求也会被拒绝。

    图片2.png

    漏桶算法示意图

    例如,系统中用户注册的瓶颈是 100 QPS,即 1 秒钟最多只能同时注册 100 人,如果注册人数过多就会出现未知的错误。此时就可以采用漏桶算法,保证每秒钟系统中同时注册的人数不超过 100 人。

    令牌桶算法则是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。桶中存放的令牌数有最大上限,超出之后就被丢弃。一般来说令牌桶内令牌数量上限就是系统负载能力的上限,不建议超过太多。当流量或者网络请求到达时,每个请求都要获取一个令牌,如果能够从令牌桶中获取到令牌,请求将被系统处理,被获取到的令牌也会从令牌桶中移除;如果获取不到令牌,该请求就要被限流,要么直接丢弃,要么在缓冲区等待(如下图所示)。令牌桶限制了请求流量的平均流入速率,令牌以一定的速率添加到桶内,只要桶里有足够的令牌,所有的请求就能流入系统中被处理,这能够应对一定程度的突发巨量流量。

    图片3.png

    令牌桶算法示意图

    其他设计与方案

    限流方案在处理巨量瞬时流量时,大多数时候会拒绝掉系统无法处理的过量流量,服务的处理能力并没有过多改变,这就可能会导致拒绝掉一些关键业务请求的尴尬情况发生。而降级设计能够暂时提高系统某些关键服务的处理能力,从而承载更多的请求访问,当然它会牺牲其他次要功能的资源。除了上述详细介绍的几个设计方案外,还有无状态、幂等性、超时、重试等多种提高系统可用性的设计方案,我们接下来就对它们一一介绍,但限于篇幅,这里就只进行简要的介绍。

    降级设计

    在应对大流量冲击时,可以尝试对请求的处理流程进行裁剪,去除或者异步化非关键流程的次要功能,保证主流程功能正常运转。

    一般来说,降级时可以暂时“牺牲”的有:

    • 降低一致性。从数据强一致性变成最终一致性,比如说原本数据实时同步方式可以降级为异步同步,从而系统有更多的资源处理响应更多请求。

    • 关闭非关键服务。关闭不重要功能的服务,从而释放出更多的资源。

    • 简化功能。把一些功能简化掉,比如,简化业务流程,或是不再返回全量数据,只返回部分数据。也可以使用缓存的方式,返回预设的缓存数据或者静态数据,不执行具体的业务数据查询处理。

    无状态设计

    在分布式系统设计中,倡导使用无状态化的方式设计开发服务模块。这里“无状态”的意思是指对于功能相同的服务模块,在服务内部不维护任何的数据状态,只会根据请求中携带的业务数据从外部服务比如数据库、分布式缓存中查询相关数据进行处理,这样能够保证请求到任意服务实例中处理结果都是一致的。

    无状态设计的服务模块可以简单通过多实例部署的方式进行横向扩展,各服务实例完全对等,可以有效提高服务集群的吞吐量和可用性。但是如此一来,服务处理的性能瓶颈就可能出现在提供数据状态一致性的外部服务中。

    幂等性设计

    幂等性设计是指系统对于相同的请求,一次和多次请求获取到的结果都是一样的。幂等性设计对分布式系统中的超时重试、系统恢复有重要的意义,它能够保证重复调用不会产生错误,保证系统的可用性。一般我们认为声明为幂等性的接口或者服务出现调用失败是常态,由于幂等性的原因,调用方可以在调用失败后放心进行重新请求。

    举个简单的例子,在一笔订单的支付中,订单服务向支付服务请求支付接口,由于网络抖动或者其他未知的因素导致请求没能及时返回,那么此时订单服务并不了解此次支付是否成功。如果支付接口是幂等性的,那我们就可以放心使用同一笔订单号重新请求支付,如果上次支付请求已经成功,将会返回支付成功;如果上次支付请求未成功,将会重新进行金额扣费。这样就能保证请求的正确进行,避免重复扣费的错误。

    超时设计

    鉴于目前网络传播的不稳定性,在服务调用的过程中,很容易出现网络包丢失的现象。如果在服务调用者发起调用请求处理结果时出现网络丢包,在请求结果返回之前,服务调用者的调用线程会一直被操作系统挂起;或者服务提供者处理时间过长,迟迟没返回结果,服务调用者的调用线程也会被同样挂起。当服务调用者中出现大量的这样被挂起的服务调用时,服务调用者中的线程资源就可能被耗尽,导致服务调用者无法创建新的线程处理其他请求。这时就需要超时设计了。

    超时设计是指给服务调用添加一个超时计时器,在超时计时器到达之后,调用结果还没返回,就由服务调用者主动结束调用,关闭连接,释放资源。通过超时设计能够有效减少系统等待时间过长的服务调用,使服务调用者有更多的资源处理其他请求,提高可用性。但是需注意的是,要根据下游服务的处理和响应能力合理设置超时时间的长短,过短将会导致服务调用者难以获取到处理结果,过长将会导致超时设计失去意义。

    重试设计

    在很多时候,由于网络不可靠或者服务提供者宕机,服务调用者的调用很可能会失败。如果此时服务调用者中存在一定的重试机制,就能够在一定程度上减少服务失败的概率,提高服务可用性。

    比如业务系统在某次数据库请求中,由于临时的网络原因,数据请求超时了,如果业务系统中具备一定的超时重试机制,根据请求参数再次向数据库请求数据,就能正常获取到数据,完成业务处理流程,避免该次业务处理失败。

    使用重试设计的时候需要注意以下问题:

    • 待重试的服务接口是否为幂等性。对于某些超时请求,请求可能在服务提供者中执行成功了,但是返回结果却在网络传输中丢失了,此时若重复调用非幂等性服务接口就很可能会导致额外的系统错误。

    • 服务提供者是否只是临时不可用。对于无法快速恢复的服务提供者或者网络无法立即恢复的情况下,盲目的重试只会使情况更加糟糕,无脑地消耗服务调用方的 CPU 、线程和网络 IO 资源,过多的重试请求甚至可能会把不稳定的服务提供者打垮。在这种情况下建议你结合熔断设计对服务调用方进行保护。

    接口缓存

    接口缓存是应对大并发量请求,降低接口响应时间,提高系统吞吐量的有效手段。基本原理是在系统内部,对于某部分请求参数和请求路径完成相同的请求结果进行缓存,在周期时间内,这部分相同的请求结果将会直接从缓存中读取,减少业务处理过程的负载。

    最简单的例子是在一些在线大数据查询系统中,查询系统会将周期时间内系统查询条件相同的查询结果进行缓存,加快访问速度。

    但接口缓存同样有着它不适用的场景。接口缓存牺牲了数据的强一致性,因为它返回的过去某个时间节点的数据缓存,并非实时数据,这对于实时性要求高的系统并不适用。另外,接口缓存加快的是相同请求的请求速率,这对于请求差异化较大的系统同样无能为力,过多的缓存反而会大量浪费系统内存等资源。

    实时监控和度量

    由于分布式中服务节点众多,问题的定位变得异常复杂,对此建议对每台服务器资源使用情况和服务实例的性能指标进行实时监控和度量。最常见的方式是健康检查,通过定时调用服务提供给健康检查接口判断服务是否可用。

    目前业内也有开源的监控系统 Prometheus,它监控各个服务实例的运行指标,并根据预设的阈值自动报警,及时通知相关开发运维人员进行处理。

    常规化维护

    定期清理系统的无用代码,及时进行代码评审,处理代码中 bad smell,对于无状态服务可以定期重启服务器减少内存碎片和防止内存泄漏……这些都是非常有效的提高系统可用性的运维手段。

    小结

    虽然在分布式系统中,由于系统的复杂性,在很大程度上加大了服务错误的可能性,但是也有足够的方案保证系统可用性。

    紧接着上一课时,这节课我们介绍了其他的提高分布式系统可用性的设计与方案,包括:

    • 限流设计如何保证系统能够承受着巨量瞬时流量的冲击;

    • 其他设计与方案中的降级设计保障系统主要功能可用、接口缓存提高服务响应速率,等等。

    通过这两个课时的学习,相信你已经了解了可用性对分布式系统的重要性,以及常用的提高系统可用性的设计与方案。在系统运行过程中,无论是系统内部还是系统外部依赖都极可能出现故障导致不可用。在这种情况下,你就需要时时考虑在故障情况下如何保证系统的基本可用性,采用多种设计防止故障的产生或者在故障出现之后如何恢复和可用。希望在接下来的设计开发工作中,你能够有意识地想起这两个课时中介绍的高可用设计与方案,努力打造一个高可用的系统。

    最后,你工作中遇到的分布式系统还有其他问题吗?你当时是如何解决的呢?采用了哪些高可用的设计?欢迎你在留言区和我分享。

  • 相关阅读:
    SAP PA SD后台配置
    Hive--09---函数----窗口函数
    shell脚本
    JavaSE - 深度探讨继承与多态,私有成员是否被继承问题
    Spring Boot学习笔记(1)
    7天学完Spring:Spring AOP统一问题处理
    博客摘录「 hyperf使用jwt的redis储存驱动实现用户token认证」2024年4月9日
    jsp基本表格和简单算法表格
    MongoDB数据库新手入门
    【Redis 多机服务的简单认识】
  • 原文地址:https://blog.csdn.net/fegus/article/details/126461316