系统怎样做到高可用?
高可用系统设计的思路
一个成熟系统的可用性需要从系统设计和系统运维两方面来做保障,两者共同作用,缺一不可。那么如何从这两方面入手,解决系统高可用的问题呢?
- 系统设计
在构建高可用系统时,“为故障设计”是我们坚持的首要原则。对于每秒请求量达百万的高并发系统,集群中的机器通常成百上千,单台服务器故障是司空见惯的情况,几乎每天都可能遇到。因此,提前规划故障应对方案,才能在关键时刻从容应对。系统设计中,故障处理应当是一个重要的考量,需预先思考如何自动检测故障并在故障发生后实现自动化解决。除了前瞻性的思维,我们还需要掌握具体的优化方法,比如故障转移(failover)、超时控制、降级和限流等。
一般来说,failover 过程可能涉及两种场景:1. 在完全对等的节点之间执行 failover。2. 在不对等的节点之间执行 failover,例如存在主节点和备节点的场景。在对等节点之间的 failover 相对简单。在这样的系统中,所有节点均承接读写流量,且节点无状态,因此每个节点都可以作为另一个节点的备选镜像。这种情况下,如果访问某个节点失败,可以简单地随机访问其他节点以确保服务的连续性。
举个例子,Nginx 可以配置当某一个 Tomcat 出现大于 500 的请求的时候,重试请求另一个 Tomcat 节点,就像下面这样:
在不对等节点之间的 failover 机制会更为复杂。例如,当我们拥有一个主节点和多个备用节点时,这些备用节点可以是热备(同样在线提供服务的备用节点)或冷备(仅作备份使用)。在这种场景下,代码中需要实现故障检测并控制主备切换。最常用的故障检测机制是“心跳检测”:客户端可以定期向主节点发送心跳包,或者从备份节点定期发送心跳包。如果在一段时间内未收到心跳包,就可以认为主节点可能出现故障,触发选主流程。选主操作需确保多个备份节点之间达成一致,通常通过分布式一致性算法实现,如 Paxos 或 Raft。
除了故障转移,系统间调用的超时控制也是高可用设计的一个关键考量。在复杂的高并发系统中,通常包含多个系统模块,并且依赖缓存、队列等各种组件和服务。在这样的架构中,调用的最大隐患是延迟而非失败。失败通常是瞬间的,通过重试可解决;但一旦调用某模块或服务发生显著延迟,调用方就会因阻塞在该调用上而无法释放已占用的资源。当大量请求因阻塞积压时,调用方可能耗尽资源而导致系统崩溃。
在系统开发的初期,超时控制通常不被重视,或者是没有方式来确定正确的超时时间。
我曾经参与过一个项目,模块之间通过 RPC 框架进行调用,默认的超时时间为 30 秒。平时系统运行稳定,但当遇到高流量时,RPC 服务端出现了一些慢请求,导致 RPC 客户端线程大量阻塞,最长达 30 秒。这种情况导致客户端耗尽调用线程,最终宕机。在故障复盘中,我们发现了这个问题,于是对 RPC、数据库、缓存以及第三方服务的调用超时时间进行了调整。这样一来,当出现慢请求时可以触发超时,避免系统发生连锁反应导致崩溃。
确定合理的超时时间是个难点:超时时间过短,可能产生大量超时错误,影响用户体验;超时时间过长,又无法有效防止阻塞。我建议通过分析系统的调用日志,统计如 99% 的响应时间,以此来设定超时时间。如果没有调用日志,则可参考经验值。无论选择哪种方式,超时时间都应随着系统的维护进行调整。超时控制的核心在于:在请求超过一定时间后主动让它失败,从而释放资源给后续请求使用。尽管这会导致部分请求失败,但通过牺牲少量请求确保系统整体的可用性是必要的。
另外还有两种有损策略也能提高系统的高可用性,即降级和限流。降级是在高负载下为保证核心服务稳定,临时停用非核心服务。例如,发布一条微博时,系统会先经过反垃圾服务检测,确保内容合法后再写入数据库。然而,反垃圾检测涉及多种策略匹配,尽管在日常流量下可以处理,但在高并发下可能成为瓶颈。鉴于反垃圾检测并非发布微博的核心流程,因此在流量高峰时可以暂时关闭该检测,确保核心流程的顺畅。
限流完全是另外一种思路,它通过对并发的请求进行限速来保护系统。
比如对于 Web 应用,我限制单机只能处理每秒 1000 次的请求,超过的部分直接返回错误给客户端。虽然这种做法损害了用户的使用体验,但是它是在极端并发下的无奈之举,是短暂的行为,因此是可以接受的。
实际上,无论是降级还是限流,在细节上还有很多可供探讨的地方,我会在后面的课程中,随着系统的不断演进深入地剖析,在基础篇里就不多说了。
- 系统运维
在系统设计阶段,我们可以通过超时控制、降级、限流等方法提升系统的可用性,而在系统运维层面,也有不少手段可用,如灰度发布和故障演练。一般来说,系统在业务平稳运行时故障较少,90% 的故障往往发生在上线和变更阶段。例如,某次功能上线后,由于设计问题导致数据库慢请求数量翻倍,从而拖慢了系统请求,进而引发故障。如果没有变更,数据库通常不会突然产生大量慢请求。因此,为了提升系统的可用性,做好变更管理至关重要。除了要有清晰的回滚方案,能够在问题发生时迅速恢复,灰度发布则是另一个关键手段。
灰度发布的核心在于,系统变更不是一次性全量推送,而是按照比例逐步推进,通常以机器为单位逐步发布。比如,先在 10% 的机器上部署变更,并观察系统性能指标和错误日志。如果一段时间后,系统指标保持稳定、错误日志正常,才逐步推广到全部机器。灰度发布让开发和运维团队能够在真实线上流量下监测变更影响,是保障系统高可用的重要步骤。
灰度发布是保障系统在正常运行下高可用的重要运维手段,但要了解系统在发生故障时的表现,还需要依赖另一个手段:故障演练。故障演练是通过对系统施加一些破坏性操作,观察在局部故障情况下系统的整体表现,以此发现潜在的可用性问题。
在复杂的高并发系统中,涉及的组件种类繁多,例如磁盘、数据库、网卡等,而这些组件随时可能发生故障。一旦故障发生,会不会像蝴蝶效应般导致整个系统服务不可用?我们无法预知,因此故障演练尤为重要。
故障演练的理念与当前流行的“混沌工程”高度契合。作为混沌工程的先驱,Netflix 在 2010 年推出的“Chaos Monkey”工具就是一个典型的故障演练工具。Chaos Monkey 通过随机关闭线上节点来模拟故障,帮助工程师了解系统在此类故障下的表现及其影响。当然,故障演练需要确保系统具备一定的容错能力。如果系统尚未具备这些能力,建议先搭建一套与生产环境相同的测试环境,在该环境中进行故障演练,以避免对生产系统造成影响。
总结
综上所述,从开发和运维的角度来看,提升系统可用性的方法各不相同。开发侧的关键在于如何处理故障,核心在于“冗余”和“取舍”。冗余是指通过备用节点和集群等方式来替代发生故障的服务,例如前面提到的故障转移和多活架构;而取舍则是在必要时丢卒保车,确保核心服务的安全和稳定。
而运维侧更倾向于保守策略,注重预防性措施,尤其关注变更管理和故障演练,通过严格的变更控制和模拟故障来降低故障发生的概率。
将开发和运维策略结合起来,才能构建出一套完整的高可用体系。然而需要注意的是,提升系统可用性往往需要在用户体验或系统性能上做出一定的妥协,同时也需要投入大量的人力和资源来建立健全的机制,因此必须适度,不应一味地追求极致优化。就像前面提到的,核心系统达到 99.99% 的可用性(四个九)已能满足需求,没有必要追求更高的五个九甚至六个九。
另外,大多数系统或组件都追求极致的性能,但也有些特殊系统并不以性能为重,而是追求极致的可用性。例如,配置下发系统虽然仅需在其他系统启动时提供配置数据,因此允许返回时间较长——秒级甚至十秒钟都可以,但却对可用性有极高要求(甚至可能达到六个九),因为它的配置可以获取慢,但不能获取不到。
这个例子说明了在系统设计中,可用性与性能有时是需要权衡的,不同系统的需求决定了取舍的方向,不能一概而论。