面向故障的系统设计

2019-05-20

在一个分布式系统中,故障是无法避免的。网络故障、光纤被挖断、人为配置错误等等都可能导致系统出现异常,在这种情况下我们需要做好应对措施。

如何对待故障和可用性要求有关。举个例子,在发生区域地震导致某个数据中心不可用时,一个高可用系统需要自动切换到另外一个数据中心,这就是通常所说的『异地多活』架构,这样做的成本很高,所以可用性要求背后其实是『收益&成本』权衡。上面说所举例子是一个非常小概率的事件,除此之外系统中一些经常发生的小故障,如网络抖动、数据库连接不可用等等故障其实是更常见的,下面提供的策略可以更好的应对这些常见故障。

面对故障通常有三板斧。

  1. 故障感知
  2. 优雅的故障处理
  3. 日志和实时监测,以供问题的排查和修复

以下是一些具体设计原则。

  1. 重试。重试是非常有效的策略。系统时常需要引入外部依赖,外部依赖因为诸多因素而影响其可靠性,如代码质量、网络等;对于外部依赖,要用『防御式编程』来对待,假定依赖是不可靠的,当错误发生后首要策略就是重试。很多服务SDK内部已经包含重试逻辑,对于调用一个REST api接口,我们可以用自带重试逻辑的HTTP Client访问。
  2. 断路器。就像电路里保险丝一样,当电流异常升高到一定高度和热度时,自身熔断切断电流,保证电路安全运行。在设计系统时也可以借鉴这个思想,以保障我们系统安全。当故障是暂时的,重试可以很好解决系统的弹性问题,当故障持续,我们需要通过断路器来停止对故障组件的调用,避免系统资源耗尽以及下游服务雪崩。
  3. 关键系统隔离。当一个系统故障时,通常会引发异常,如线程和socket不能及时回收,导致内存被耗尽,因此我们需要将关键服务和其它服务分开部署以达到故障隔离。举个例子,由于A服务数据库和B服务数据库混布在一个物理机上,在某个业务高峰期,A服务数据库占用了大量CPU、内存等系统资源,导致B服务出现数据库连接异常进而导致B服务不可用,而B服务是关键业务系统,后续将B服务数据库单独部署来避免该问题。
  4. 队列异步化。系统在业务高峰时会遇到突发流量高峰,而这种高峰有时会超过系统处理能力。为避免该问题,我们可以在系统内部引入队列,将处理异步化,到达削峰的效果。
  5. 故障转移。当一个实例不可达,需要将请求转移到其它实例上。对于一些无状态服务,比如web服务器,可以在一个负载均衡后接入多个实例。对于有状态服务,比如数据库,需要使用副本和自动故障转移。如redis中master-slave+sentinel就是对于有状态服务进行故障转移的一种方式。对于有状态服务,如何在故障转移的同时保证一致性值得进一步探讨。
  6. 服务降级。有时候故障无法转移,我们可以提供一个有损服务,但不影响核心功能。如一个标签查询服务,假如服务不可用,可以返回一组默认标签提供优雅降级。
  7. 容量规划。我们需要知道硬件资源是否满足现有流量,可以通过定期评估、重大活动提前评估等方式进行,对于日常使用需要有一个合理冗余如 30%CPU空闲,不应该满负载运行系统。
  8. 使用方限流、屏蔽。你永远不知道用户会怎么调用系统,所以需要给使用方调用频率加一个上限,避免小部分用户异常行为引发整体系统异常。屏蔽和限流一个道理,但是对滥用系统更严厉的惩罚。
  9. 测试时需要考虑异常输入。通常来说,正常输入总是经过完善测试,但是对于异常输入却没有测试到位。关于这一点,可以看看c标准库中atoi这个函数是如何考虑异常输入情况。这里有一个笑话可以供你体会。
  10. 混沌工程(Chaos Engineering)。chaos是对系统的终极考验,通过主动引入一些平时极少概率发生的故障,从而提前发现系统潜在问题。

以上十条面向故障的系统设计原则,希望能对你有启发。