关于系统高可用的思考

大型分布式系统在实际的运行过程中面对的情况是非常复杂的,业务上的流量突增、依赖服务的不稳定、应用自身的瓶颈、物理资源的损坏等方方面面都会对系统的运行带来大大小小的的冲击。如何保障应用在复杂的工况环境下还能高效稳定运行,如何预防和面对突发问题,在设计和保障系统的时候应该从哪些方面着手?

本文将从单机性能优化、集群容量控制、分布式下高可用设计、系统容灾方面,探讨系统的高可用设计和保障。

1、单机性能优化


应用程序的性能度量标准一般分为吞吐量和延迟。吞吐量是指程序能够达到的最高性能指标。延迟是指请求到响应之间的整体延迟时间。分布式系统中,任一环节出现延迟变高时,都会导致该节点不可用,甚至不可用的状态会扩散至其它节点,引起整个分布式系统的“雪崩”,最终导致服务不可用。所以打造低延迟的应用程序,提升应用的单机性能对提升整个分布式系统可用性有很大的帮助。

对于性能优化可以参考关于服务器性能的思考关于服务器性能优化的思考和实践这两篇文章。

2、集群容量控制


有了单机性能的优化保障,系统是否已经是高枕无忧?不一定,性能优化只是提升了单机的处理能力,保障系统不会被某一次的请求而卡住。但面对汹涌而来的流量洪峰,即便集群部署了多台机器,整个系统也无法保障一定会稳定运行,某一次突然远高于预期的洪峰或者依赖的后端服务的一次持续抖动,就会导致整个集群的运行的不稳定。极端情况下,在高位持续运行机器可能会因为一次压力的加大,超过承受能力,带来整个服务的停顿,应用的Crash,进而可能将延迟传递给服务调用方造成整个系统的服务能力丧失而产生雪崩。

因此即使前期各种压测、容量规划、预案做的如何充分,面对不可预知的突发情况,依然无法做到真正的如丝般顺滑。为了防止自己的应用被打死或者被拖死,在系统设计的时候需要考虑到限流、降级和熔断,丢卒保车,以降级、暂停边缘服务、组件件为代价保障核心服务的资源,以系统不被突发流量击垮为第一要务。

2.1 限流

顾名思义就是对请求应用的流量进行限制,对于超过限流阈值的流量进行丢弃,用于保护系统处于一个合理的流量压力之下,不会因为突发的不可预知的大量请求打死。

典型的限流算法有漏桶(leaky bucket)算法和令牌桶(token bucket)算法。


漏桶算法

漏桶算法基本思路是有一个桶(会漏水),水以恒定速率滴出,上方会有水滴(请求)进入水桶。如果上方水滴进入速率超过水滴出的速率,那么水桶就会溢出,即请求过载。

令牌桶算法

令牌桶算法基本思路是同样也有一个桶,令牌以恒定速率放入桶,桶内的令牌数有上限,每个请求会acquire一个令牌,如果某个请求来到而桶内没有令牌了,则这个请求是过载的。

漏桶算法和令牌桶算法对比

令牌桶算法中会以恒定的速率如每秒n个放入桶中,如果桶的容量上限设置为1s放入的令牌大小,那么功能上令牌桶算法会退化为漏桶算法,其qps控速为n/s。

如果令牌桶的容量被设置为大于1s放入的令牌大小,如设置两秒,则其容量为2n,那么也就是说在流量突增的瞬间的qps控速会达到2n/s,然后恢复到n/s,很明显令牌桶会存在请求突发激增的问题。但一般系统运行都会在平均load以下,如4核机器,load一般在4以下(如果长时间处于4以上,则需要考虑系统运行是否有异常),偶尔短时间的流量激增是可以被系统慢慢消化的,只是不能长时间的处于流量超量的情况。所以令牌桶算法相比漏桶算法有更大的灵活性。

算法实现

常用的限流算法的实现有Guava RateLimiter,它提供了漏桶算法和令牌桶算法的实现。

2.2 降级

服务降级是一种典型的丢卒保车,二八原则实践,而降级的手段也无外乎关闭,下线等“简单粗暴”的操作。降级是对业务有损的,因此什么能降,什么不能降,什么时候能降,什么时候不能降,都需要提前和业务方确认,做好强弱依赖梳理。

2.3 熔断

熔断类似电力系统中的保险丝,当负载过大,或者电路发生故障或异常时,电流会不断升高,为防止升高的电流有可能损坏电路中的某些重要器件或贵重器件,烧毁电路甚至造成火灾。保险丝会在电流异常升高到一定的高度和热度的时候,自身熔断切断电流,从而起到保护电路安全运行的作用。

同样,在分布式系统中,如果调用的远程服务或者资源由于某种原因无法使用时,如果没有这种过载保护,就会导致请求的资源阻塞在服务器上等待从而耗尽系统或者服务器资源。很多时候刚开始可能只是系统出现了局部的、小规模的故障,然而由于种种原因,故障影响的范围越来越大,最终导致了全局性的后果。而这种过载保护就是大家俗称的熔断器(Circuit Breaker)。

熔断可以看着是自动化的降级,因此也可能是对业务有损的,在使用熔断服务时需要确定好,被熔断的服务不是应用的核心链路。

熔断服务的核心在于如何判断是否要熔断、何时进行熔断恢复。基于滑动窗口的消息静默限流,介绍了Neflix实现的Hystrix,并借鉴其思想在后端定时服务不稳定时,主动停止对定时服务的请求,通过消息中间蓄洪,等待后端服务恢复稳定后再开始请求。

2.4 容量控制的代价

限流的确是保障自身应用稳定的利器,在应用的维护方看来,保证自己的业务不被打死是第一要务,但是限流却会给依赖方带来影响,对他们的业务是有损的。在复杂业务中,往往是许多应用相互依赖,你限别人的同时,别人也会限制你,所以在大促的前期需要多方确认限流的场景和限流的值,有时甚至会反复讨论和讨价还价,付出巨大的沟通和时间成本。

特别是一些核心应用,比如交易系统作为整个大促的核心,不仅承担着大量的写入请求,实际还承担着天量读取请求,大量其他应用强烈依赖对交易系统的反查来获取交易的详情信息和状态,如物流、订单、优惠等。但是交易系统的处理能力也不是无限的,所以在大促前的准备工作中,对依赖交易的应用进行梳理,确定限流值、限流场景、限流时长成为一项异常繁重的工作,往往需要一个个应用的排查,一个个业务去沟通讨论。

3、分布式高可用系统设计


对于无状态的服务,如果做好了单机性能优化和容量控制,基本上可以面对大部分突发情况,无非就是流量过大先限流再扩容,后端服务不稳定则降级,即便是出现应用crash、机器损坏等极端情况,也只需要做好流量隔离即可。

但是对于有状态的应用,则必须考虑极端情况下数据的完整性和服务的可用性,而为了保证数据的完整性通常又会引入数据备份,而引入数据备份则又会导致一致性问题。在分布式的高可用设计中,这类问题被归结为CAP,即一致性、可用性和分区容错性三者无法在分布式系统中被同时满足,并且最多只能满足其中两个。

对于分布式系统的高可用方案,业界有一些通用的解决方案:


分布式高可用方案对比

其中横轴代表了分布式系统中通用的高可用解决方案,如:冷备、Master/Slave、Master/Master、两阶段提交以及基于Paxos算法的解决方案;纵轴代表了分布式系统所关心的各项指标,包括数据一致性、事务支持的程度、数据延迟、系统吞吐量、数据丢失可能性、故障自动恢复时间。

对于Paxos算法感兴趣的朋友,可以看看raft算法在软负载中的研究,raft算法假定了Paxos算法的特定使用场景,可以看成是Paxos的简化版。

3.1 RocketMQ的高可用设计

这里对RocketMQ的高可用设计进行介绍和分析。

RocketMQ消息引擎基于多机房部署结构,依托于Zookeeper的分布式锁和通知机制,引入Controller组件负责Broker状态的监控以及主备状态机转换,设计了一套Master/Slave结构的高可用架构。

RocketMQ高可用设计

Zookeeper Server作为分布式服务框架,需要至少在A、B、C三个机房部署以保证其高可用,为RocketMQ高可用架构提供如下功能:

  • 维护持久节点(PERSISTENT),用来保存主备状态机
  • 维护临时节点(EPHEMERAL),用来保存RocketMQ Broker的当前状态
  • 当Broker进程消失,该临时节点也将不复存在
  • 当主备状态机、服务端当前状态发生变更时,通知对应的观察者

RocketMQ Broker作为直接面向用户提供高可靠消息服务的数据节点,首先需要保证以Master/Slave结构实现多机房对等部署,即机房A中的Master对应的Slave会部署在另外一个机房B,并且一个集群中的Master会均衡地分布到所有机房中;消息的写请求会命中Master,然后通过同步或者异步方式复制到Slave上进行持久化存储;消息的读请求会优先命中Master,特殊情况下(消息堆积读消息导致磁盘压力大)读请求会转移至Slave。

RocketMQ Broker直接与Zookeeper Server进行交互。体现在:

  • 以临时节点的方式向Zookeeper汇报Broker当前状态;
  • 作为观察者监听Zookeeper上以持久节点方式保存的主备状态机的变更;

当监听发现Zookeeper上的主备状态机发生变化时,根据最新的状态机更改Broker当前状态;RocketMQ HA Controller是RocketMQ高可用架构中为了降低系统故障恢复时间而引入的无状态组件,在A、B、C三个机房分布式部署,其主要职责体现在:

  • 作为观察者监听Zookeeper上以临时节点方式保存的Broker当前状态的变更;
  • 根据集群中所有Broker的当前状态,控制主备状态机的切换并以持久节点的方式向Zookeeper汇报最新主备状态机。
  • 出于对系统复杂性以及软件本身对CAP原则的适配考虑,RocketMQ高可用架构的设计采用了Master/Slave结构,在提供低延迟、高吞吐量消息服务的基础上,采用主备同步复制的方式避免故障时消息的丢失,同时引入故障自动恢复机制以降低故障恢复时间,提升整个系统的SLA。

3.2 RocketMQ数据可靠性的分析

消息系统的数据可靠性包括数据不丢失和数据不重复。

RocketMQ作为一款消息型中间件,它提供At least Once的特性,即保证消息不丢失,但是不提供Exactly Only Once的特性,即不保证消息不重复。从之前的分析知道,即便消息在m/s上都保存成功,但是如果
master在回ack给producer的时候失败,producer会重新进行投递,在分布式环境中要保证消息的不重复需要付出巨大的代价,因此RocketMQ不提供Exactly Only Once的特性。

RocketMQ的这个问题的解决方案是交给消息中间件的使用者,在消费端需要做消息幂等。

3.3 数据可靠性和性能之间的选择?

消息型中间件最重要的特性之一就是保证消息的不丢失,然而消息不丢失是以牺牲应用性能为代价的。为了保证严格的消息不丢失,一条消息在产生和消费的过程中由producer、broker、consumer三者来共同保证。

其中最核心的是broker端保证消息不能丢失,为此在写master时需要同步将消息复制到slave,并且等slave上持久化成功并通知master,master在确认master和slave都持久化成功之后才能发ack到producer确认消息保存成功。

在严格数据可靠性的保障下,主备之间的同步备份和持久化是一个比较耗时的过程,这会使整个消息系统的吞吐量大大降低。因此RocketMQ提供了异步备份的选项,在不需要严格数据可靠性的业务中,可以选择消息的异步备份,比如:包裹的状态变更消息,一个包裹状态由发货变更为运输、派送、签收,缺失其中一个状态变更的消息并不会引起灾难性的后果。

3.4 扩展 - mysql(innodb)的数据可靠性对性能的影响

mysql(innodb)也是采用了典型的Master/Slave结构来保证数据的可靠性,同时为了实现事务,mysql(innodb)实现了很多日志,如:binlog、redolog、undolog,其日志和同步策略也分为异步和同步,分别由以下参数控制:

  • innodb_flush_log_at_trx_commit

    0:log buffer将每秒一次地写入log file中,并且log file的flush(刷到磁盘)操作同时进行.该模式下,在事务提交的时候,不会主动触发写入磁盘的操作。

    1:每次事务提交时MySQL都会把log buffer的数据写入log file,并且flush(刷到磁盘)中去.

    2:每次事务提交时MySQL都会把log buffer的数据写入log file.但是flush(刷到磁盘)操作并不会同时进行。该模式下,MySQL会每秒执行一次 flush(刷到磁盘)操作。

  • sync_binlog

    0:MySQL不控制binlog的刷新,由文件系统自己控制它的缓存的刷新。

    1:每次事务提交,MySQL都会把binlog刷到磁盘。

    >0:每次事务提交,MySQL调用文件系统的刷新操作将缓存刷下去。

在正常的运行过程中,这两个参数一般设置为1,即双1设置,数据可靠性最高但是性能最低。但在大促和压测期间可以选择性的设置为双0,异步去刷盘。对比发现,对性能的提升在一倍以上,对db io的使用下降约一倍。

通过允许少量的数据丢失带来的性能提升还是很客观的,当然如果对数据可靠性要求极其严格,如交易金融级别的,还是需要同步刷盘。

db性能监控
db磁盘io

零点左右的峰值是压测流量。

4、系统容灾


系统容灾是在指在高可用设计的架构下,当应用出现问题的时候,如何保障系统继续正常稳定提供服务。

应用运行过程中可能出现的问题:

  • 应用正常关闭
  • 应用异常 Crash
  • OS Crash
  • 机器掉电,但是能立即恢复供电情况
  • 机器无法开机(可能是cpu、主板、内存等关键设备损坏)
  • 磁盘设备损坏

在这些情况下,需要考虑如何对外提供稳定的服务,以RocketMQ为例:

RocketMQ高可用状态切换
  • 第一个节点启动后,Controller控制状态机切换为单主状态,通知启动节点以Master角色提供服务。
  • 第二个节点启动后,Controller控制状态机切换成异步复制状态。Master通过异步方式向Slave复制数据。
  • 当Slave的数据即将赶上Master,Controller控制状态机切换成半同步状态,此时命中Master的写请求会被Hold住,直到Master以异步方式向Slave复制了所有差异的数据。
  • 当半同步状态下Slave的数据完全赶上Master时,Controller控制状态机切换成同步复制模式,Mater开始以同步方式向Slave复制数据。该状态下任一节点出现故障,其它节点能够在秒级内切换到单主状态继续提供服务。

Controller组件控制RocketMQ按照单主状态,异步复制状态,半同步状态,同步复制状态的顺序进行状态机切换。中间状态的停留时间与主备之间的数据差异以及网络带宽有关,但最终都会稳定在同步复制状态下。

推荐阅读更多精彩内容