分布式系统下的可用性与可伸缩性

背景

最近我们组开展了一个针对异地多活场景的通用解决方案——单元化,旨在解决分布式系统的灾备问题,在调查中了解到了很多业务有趣的解决方案,记录在本文中,方便回顾。

写下本文时,我正就职于字节跳动的中台架构组,负责为字节的各个业务中台的共性问题提供解决方案,比如 异地多活多租户治理 等。

为什么要讨论这个话题

  • 通过输出本文,让自己将杂乱的相关知识进行整理
  • 随着互联网市场逐渐饱和,未来各个软件都将会更加注重自己的服务质量,比如可用性、性能、可拓展性等等,所以将会有越来越多的团队关注自己的服务质量,其中首当其冲的就是可用性,希望我的一些经验和见解能够帮助到后来的人

概念对齐

在开始正文前,我们有必要先对齐几个本文涉及到的概念:可用性与可伸缩性。

可用性

可用性是软件系统在一定时间内持续可用的时间,比如IT服务(像一些云平台or基础设施)会对他们的用户承诺 3个9, 4个9 之类,可用性相关的SLA,它们是什么含义? 这里的几个9指的是在一年时间内,可用时间所占比的百分率,比如 3个9 指, 在一年时间内,99.9% 时间都是可用的,那么对应的服务中断时间就是:

3个9:(1-99.9%)36524=8.76小时,表示该系统在连续运行1年时间里最多可能的业务中断时间是8.76小时。 4个9:(1-99.99%)36524=0.876小时=52.6分钟,表示该系统在连续运行1年时间里最多可能的业务中断时间是52.6分钟。 5个9:(1-99.999%)36524*60=5.26分钟,表示该系统在连续运行1年时间里最多可能的业务中断时间是5.26分钟。

有人可能会有疑问,软件跑的好好的为啥会中断?中断的原因可能会有很多种:

  • 服务更新:如果版本升级时没有实现平滑升级,那么会造成短暂的中断
  • 机器故障:比如机器太老,需要裁撤,突然网卡坏了,机房散热器故障等等,如果你的服务没有健全的健康检查以及切流机制,往往就会带来灾难性的影响

通常来说,软件设计层面上的缺陷都是可以避免的,比如服务更新造成的中断。所以一般评估可用性都是在物理层面的影响范围上来看待,因为物理层面上的故障原因是不可控的,比如机器老化、自然灾害等。 按照范围来说,我们可以简单对软件的可用性进行一个分级

  • 单实例:顾名思义,单个实例运行,最低的可用性级别,没法防范任何范围的故障
  • 机房级别容灾:可以抵御机房级别的故障,通常来说都是多个实例分散在不同机房来实现
  • 数据中心级别容灾:一般一个数据中心由多个机房组成,这个级别的容灾可以容忍一个数据中心故障
  • 城市级别容灾:跟名字一样,能够容忍一个城市的所有数据中心出现灾害
  • 国家级别容灾:不解释了。

这几个级别容灾能力从上往下依次递增,所需要的解决的问题和付出的代价也有很大不同,这里多说几句:

  • 机房级别容灾 一般很少见,因为多数基础设施的提供商都不会暴露机房信息给你,而且这个粒度也偏细了
  • 数据中心级别容灾 在各个云上可以类比成可用区(AZ:AvailiableZone),每个可用区都是独立的物理资源,以尽量做到故障隔离,但实际以我在腾讯游戏维护容器平台时的经验来看,其实很难做到

可伸缩性

可伸缩性其实比较容易理解,就是指一个服务能否扩缩容,其中又包含了两个方向:

  • 垂直伸缩: 简单来说就是加资源,4CPU换8CPU,8G换16G,这在虚拟机时代是个比较重的操作,但是在容器环境下已经非常轻量了,但是受限于单台服务器的配置,你不可能无限大地垂直扩容
  • 水平伸缩:需要架构支持,能够通过添加实例来分担负载,这里架构上要克服的问题就是当多个实例同时运行时,并发所带来的各种bad case。 垂直伸缩vs水平伸缩

这两种伸缩方式,前者是不需要软件进行任何修改的,天然支持;而后者是处理海量数据、请求时所必须实现的。 可伸缩性的评估可以按耗时:

  • 不可伸缩:顾名思义,只能单实例运行,不具备可伸缩性
  • 秒级:扩缩容能够在秒级别完成
  • 分钟级:扩缩容能够在分钟级别完成
  • 小时级:扩缩容能够在小时级别完成
  • 天级:扩缩容能够在天级别完成

可能有同学疑问,为啥伸缩性只看时间,不看成本,比如是全自动,还是半自动或者纯人肉扩容,确实对于可伸缩性来说,自动化也很重要,但自动化是比较难量化的工作,时间是可以量化的。 另外换个角度看待这个问题,能在分钟级别甚至秒级别完成的扩缩容机制,它能靠人肉实现吗?

有状态与无状态

我们经常听说的 有状态(Stateful) 和 无状态(Stateless) 是指什么?

  • 有状态:指服务需要保存过去的信息,比如各类数据库就是典型的有状态服务,这里想要补充的是,比如一些并行的任务执行引擎、有粘性Session的API服务都属于有状态的,它们存储了过去的部分上下文以开展工作。
  • 无状态:服务不需要保存任何过去的信息,比如将所有数据都存储到数据库的API服务、纯计算的服务等等。

有状态和无状态对我们今天要讨论的话题有什么影响呢?简单来说,无状态的服务多数时候你只需解决好并发就可以支持多实例(集群)部署,而有状态服务还需要考虑多实例间的数据一致性这样的复杂问题,因此两者在软件架构上差别很大,而当今业界多数方案主要解决的就是 有状态服务的高可用 这个方向。

部署模式

对齐了概念后,我们知道了:要实现高可用,首选就要支持多实例部署,那么我们现在抛开具体的架构方案,假设我们已经具备了多实例部署的能力,来看看我们有几种部署方式。

Tips

这里主要讨论有状态服务,作为无状态服务来说,只要你满足下面章节中介绍的 集群架构的必须能力,那么可以任意部署到任意国家、城市、数据中心。

同城多活

同城多活就是多个实例都是部署在同一个城市,根据部署的情况而言,又可以分为:

  • 同机房
  • 同数据中心不同机房
  • 不同数据中心

我在上面的概念里已经分析过后两种情况,一般都是选择不同数据中心部署,第一种情况一般适合简单的实例扩容以解决并发压力,区域容灾能力弱极端情况下可能某个机架的故障都会导致服务中断。

  • 容灾规模:机房级别
  • 冗余成本:取决于实例数量

两地三中心

在两个城市的三个数据中心部署实例,为什么是三个数据中心?是因为在有状态的分布式系统中为了解决实例数据一致性问题,基本都会引入选主机制,而选主就要满足多数派才能成功,就算一些非强一致性的db(比如m3db),按照quorum机制,也需要存在多数派才能保证最终一致性。 在这个模式下,最多允许一个中心的故障,理想情况下允许城市的故障(一个中心的那个城市,另一个部署两个中心的城市一旦故障则实例会因为无法保障一致性而停止服务)

  • 容灾规模:中心级别
  • 冗余成本:三份

三地五中心

类似两地三中心,但是可以容忍一个城市的故障(无论哪个城市发生灾害,能可以保持多数派)

  • 容灾规模:中心级别
  • 冗余成本:五份

集群架构解析

看了下上面的部署模式,一定有同学会有疑问

就这?

别急,上面的只是我省略了很多信息的简化图,我们会在下面的 常见的分布式存储架构 结合以上的部署模式来展开细节。

必须能力

首先我们先看看,服务要支持集群架构需要哪些必须的能力。

流量路由

当只有一个实例时,无论有状态 or 无状态我们可以简单从 Client -> Server 端,但是当有多个实例时呢?我们如何让调用方知道它应该请求哪个实例。 这里有几个方式。

  • Client: 这里的 Client 分两种流量,第一种是南北向,Client一般是App、H5以及小程序等,这些流量的特点都是来自公网;第二种是东西向流量,它们来自私有网络内,一般是其他后端服务、脚本或者定时任务等。这两种流量都可以在发送时通过服务发现进行分流。
  • 代理(LB, Gateway):暴露一组代理,通过某些服务发现手段暴露给外网

服务发现 无论在Client端 or 代理进行分流,都需要一种服务发现的机制,服务发现即通过服务中心来获取目标的地址,通常有几种常见模式:

  • DNS: 整个外部网络上来看,使用 DNS是一个最理想的方式,但 DNS 存在一个缺点——更新很慢,各个递归服务器都可能缓存解析记录,导致我们在短时间内更新记录变得很困难,加上现在容器环境下实例IP经常会变动,因此很少会在外网网络直接使用服务IP,而通过接入网络地址相对固定的 LB 或 网关。
  • 自行实现:consul/ETCD/zookeepper 甚至 Redis等都可以用来实现服务中心,部分开源组件也会自行实现自己的 discover

一致性

常见的分布式存储架构

主备

主从

Quorum

Shard

单元化架构

如何落地

业务显式部署

通用解决方案

总结

小公司该如何做软甲架构的技术规划