# 简介

# 分布式系统

:是指将一个软件系统的各个进程部署不同主机上。

  • 小型的软件系统通常只部署在一台主机上,属于集中式系统。而大型的软件系统通常部署成分布式系统,从而提高性能。
  • 优点:
    • 便于横向增加系统节点,提高系统容量、性能,比如处理高并发流量。
    • 可以将同一个程序运行多个实例,一个实例挂掉了就用其它实例,实现服务的高可用。
    • 可以将一个数据存储多个副本。
  • 难点:
    • 系统规模变大、结构变复杂,维护麻烦。比如要考虑共识、一致性、可用性。
    • 一个程序要运行多个实例,成本变高。

# 共识

:Consensus ,指系统中不同节点作出相同的决策。分布式系统的难点之一是如何达成共识。

  • 例如 MySQL 主从集群采用一种很简单的共识方案:部署一个主节点、多个从节点,主节点每执行一个事务,就让从节点复制执行该事务。
    • 这样只有主节点有权作出决策。如果主节点故障,则整个系统不可用。
  • 脑裂(brain split)
    • :指一个系统中存在多个有权决策的节点,并且作出了不同决策。
  • 拜占庭将军问题(Byzantine Generals Problem)
    • :拜占庭帝国进行战争时,多个将军会自主观察敌情,然后通过投票决定进攻还是撤退。但可能存在不诚实的将军,或者投票信件被丢失、篡改。
    • 该问题常用于比喻:分布式系统中某些节点传播虚假的信息,导致其它节点作出了错误决策。

# 2PC

:两阶段提交(two-phase commit),是 80 年代发布的一个共识算法。

  • 流程:
    1. Voting 阶段:一个节点担任 coordinator 角色,发送一个提议(比如想 commit 一个事务)给其它节点。其它各个节点对该提议进行投票,回复一个表示同意或否决的消息。
    2. Commit 阶段:如果所有节点都投票同意,则 coordinator 执行该提议,并通知其它节点跟随该决策。
      • 如果投票不通过,则 abort 该提议。并且如果该提议已经修改了一些数据,则 rollback 到之前状态。
  • 缺点:
    • 如果 coordinator 在发送提议之后故障,则其它节点可能不知道投票结果,即该提议是被 commit 还是 abort ,导致其它节点阻塞在当前阶段,该问题称为 fail-stop 。
    • 在 Commit 阶段,coordinator 需要阻塞等待其它节点都行动完毕,才能确定该提议的执行结果。
    • 要求所有节点都投票同意,没有容错性,存在单点故障的风险。

# 3PC

:三阶段提交(three-phase commit),是 80 年代发布的一个共识算法。

  • 流程:
    1. Voting 阶段
    2. PreCommit 阶段:如果所有节点都投票同意,则 coordinator 将投票结果发送给其它节点。其它节点收到投票结果之后,回复一个表示确认的消息。
    3. Commit 阶段:如果其它节点已确认,则 coordinator 执行该提议,并通知其它节点跟随该决策。
  • 优点:
    • 3PC 在 2PC 的基础上增加了 PreCommit 阶段,能解决 fail-stop 问题。
    • 在 Commit 阶段,coordinator 不必阻塞等待其它节点都行动完毕。
  • 缺点:
    • 发生网络分区时,3PC 依然不能达成共识。例如一半的节点收到了 PreCommit 消息,另一半的节点没收到,导致作出不同的决策。

# Paxos

:一个共识算法,于 1990 年发布。

  • 系统中一些节点担任 Proposer ,有权发出提议(Proposal)、投票。
    • 其它节点担任 Acceptor ,有权投票。
  • 一个提议需要超过半数的节点投票同意,才能通过。
    • 用公式描述:假设节点总数为 N ,要求法定成员数为 Quorum = N/2 + 1 ,其中 / 为整除运算符。当一个提议被至少 Quorum 个节点投票同意时,才能通过。
    • 这属于多数派(Majority)策略,可容忍不超过半数的节点不同意、离线。

# Raft

:一个共识算法,于 2013 年发布,在 Paxos 算法的基础上作了改进,是目前流行的共识算法。

  • 系统中有且仅有一个节点担任 Leader ,有权发出提议(Proposal)、投票。
    • 其它节点担任 Follower ,有权投票。
    • Leader 会定期发送心跳包给其它 Follower 节点。如果其它节点超过一定时长没有收到心跳包,则变为 Candidate 状态,选举一个节点担任新 Leader 。
      • 每次选出新 Leader ,就开始一个新任期,称为 Term 。
  • 每次准备修改集群数据时,Leader 会将该提议发送给所有 Follower ,等超过半数的节点同意并执行之后,才通过该提议,从而达成共识、数据一致性。
    • Quorum 必须超过半数。如果允许 Quorum 等于集群半数,甚至小于半数,则集群可能同时存在不止一个 Leader ,发生脑裂。
    • Leader 并不会等待所有节点都同意,因此属于最终一致性。
    • 每个节点都信任其它节点发来的信息,因此不能实现拜占庭容错。
  • 部署更多节点,增加 Quorum 的值,可以提高系统可用性,但是会增加每次达成共识的耗时。
    • Raft 集群应该部署奇数个节点。例如从节点总数从 3 增加到 4 , Quorum 依然为 2 ,并不会提高可用性。
    • 至少需要部署 1 个节点,但此时没有冗余节点,存在单点故障的风险。
    • 建议部署 3 或 5 个 节点,此时允许 1 或 2 个节点故障,可用性较高。
    • 如果部署 7 个或更多节点,可用性虽然很高,但会明显增加写操作的耗时。

# Bully

:一个共识算法,与 Raft 算法相似。

  • 如果 Leader 节点故障,则由 ID 最大的一个节点担任新 Leader 。

# Gossip

:一个广播消息的协议,常用于 P2P 服务。

  • 每个节点定期散播一次消息,最多发送给 k 个节点。
    • 发送消息之后,不必确保对方接收。
    • 其它节点收到消息之后,会散播给除了源节点之外的其它节点。
  • 消息被病毒式传播,能实现最终一致性。

# PoW

:工作量证明(Proof of Work),区块链的一个共识算法。

  • 例如比特币采用基于哈希函数的 PoW 算法:
    • 以区块为单位写入数据。
    • 一些节点担任矿工,负责生成新区块,即写入数据到区块链。
      • 每个矿工节点需要进行大量哈希运算,穷举猜测下一个区块的 nonce 随机数,第一个猜出来的节点有权生成该区块,然后广播给其它节点。
      • 生成新区块的矿工,可以获得该区块产生的一定量 BTC 作为奖励。
    • 非矿工的普通节点,只能读取区块链,或者向矿工发送写入请求。
  • 可实现顺序一致性,并实现拜占庭容错。

# PoS

:权益证明(Proof of Stake),区块链的一个共识算法。

  • 例如以太坊采用 PoS 算法:
    • 一些节点质押 32 个 ETH ,获得成为验证者的资格,负责投票验证新区块是否有效。
      • 如果行为不诚实,或者没有准时验证新区块,则质押的 ETH 销毁一部分,作为惩罚。
      • 如果工作称职,则质押的 ETH 少量增加,作为奖励。
    • 每个出块周期,随机选取一个验证者来打包新区块,然后广播给其它节点。
  • 与 PoW 相比,PoS 去掉了工作量成本,大幅降低了挖矿消耗的计算机硬件、电能。

# 一致性

:Consistency ,指系统中不同节点拥有的数据副本一致(通常还应该是最新的数据)。

  • 通常,需要各节点先实现数据一致性,才能掌握相同的情报,做出同样的决策,达成共识。
    • 数据不一致性时,各节点可能因为不同的数据副本而作出不同的决策,不容易达成共识。
  • 每个写操作之后,如果等所有节点复制完新数据,才开始下一个读操作,则称为同步复制,否则称为异步复制。
  • 常见的几种一致性模型:
    • 强一致性:采用同步复制,保证各节点的一致性。
      • 严格一致性(Strict consistency)
        • :每执行一个写操作,要求所有节点在一个 CPU 时钟周期内完成同步,实现数据一致。
        • 它要求实时同步,使得该分布式系统变成一个单机系统,即使客户端并行一些写操作,也会变成串行操作。
        • 这是最强的一致性模型,但只是理论上存在,不能在工程中实现,因为节点之间的网络通信总是存在延迟。
      • 线性一致性
        • :每执行一个写操作,要等所有节点都完成同步,才开始下一个操作。
        • 这是工程中能实现的最强的一致性模型。与严格一致性相比,不要求实时同步。
        • 又称为可线性化(Linearizability)、原子一致性。
      • 顺序一致性(Sequential consistency)
        • :执行一连串写操作时,所有节点会按相同顺序执行,但不保证同时执行。因此某些节点可能因为执行慢而数据滞后,但顺序并不会出错。
        • 强弱程度:严格一致性 > 线性一致性 > 顺序一致性
    • 弱一致性:采用异步复制,因此各节点可能不一致。
      • 因果一致性(Causal consistency)
        • :具有因果关系的多个操作(比如读写同一个数据),才保证顺序一致性,而其它并发操作则不限制。
      • 最终一致性(Eventual consistency)
        • :允许各节点暂时不一致,但保证在一定时间内实现一致。

# 可用性

  • 服务可用(Available)

    • :指客户端发出请求时,能收到正常的响应。
      • 响应时长不能超过正常范围。
      • 响应的内容不能是错误的,但可以不是最新的数据。
    • 服务不可用时,又称为服务中断、故障。
  • 可用性(Availability)

    • :又称为可用率。如果服务可用的时长,占提供服务的总时长的比例接近 100% ,则称为可用性高,否则称为可用性低。
    • 采用负载均衡、健康检查等措施可以实现服务的高可用性(High Availability ,HA)。
  • SLA (Service Level Agreement ,服务等级协议)

    • :由服务提供商承诺的服务质量指标,如果未达到则给客户一定赔偿。
    • 比如承诺服务的全年可用性为 99% ,即不可用的时长低于 3.65 天;全年可用性为 99.9% ,即不可用时长低于 0.365*24=8.76 小时。
  • 提高系统性能的常见方案:

    • 垂直扩展:增加单个服务的性能,比如增加服务器的 CPU 、内存等资源。
    • 水平扩展:增加服务实例的数量,比如在一组服务器上分别部署一个服务实例。

# 常见问题

  • 单点故障(Single Point of Failure)

    • :单个模块不可用,导致整个服务不可用。或者单个服务不可用,导致整个系统不可用。
  • 级联故障(Cascading failure)

    • :上游服务故障,导致下游调用它的服务故障。
  • 服务雪崩

    • :级联故障导致大量服务不可用。

# 常见措施

  • 服务熔断

    • :当上游服务可用性降低时,下游服务停止调用它,避免级联故障。
    • 服务熔断之后,下游服务可以拒绝提供服务,也可以开始服务降级。
  • 服务降级

    • :降低服务给客户端的响应质量,避免服务完全不可用。
    • 例如:
      • 通过 API 网关限制所有服务的被调用次数、网速,避免负载过大。
      • 当服务负载过大时,停止次要功能、延时处理请求(这会增加响应耗时)、减少响应内容、使用旧的响应内容,甚至拒绝服务。
      • 调用上游服务时都应该设置超时时间,如果超时,则当前服务返回降级的响应。
      • 可以在配置中心增加一个参数开关,开启它之后,各个服务会开始服务降级。也可以让各个服务自动判断是否需要降级。
  • 服务限流

    • :属于服务降级的一种措施。指限制一定时间内服务的被调用次数,超过限制时拒绝新的请求,避免负载过大。
  • 重试

    • 一种业务操作执行之后,如果用户重复请求,是否允许重试,这需要实现幂等性。
    • 一种业务操作执行失败之后,如果用户不重复请求,是否自动重试。
  • 例如 Sentinel 是阿里巴巴公司开源的一个熔断框架。

    • GitHub (opens new window)
    • 用法:
      1. 部署 Sentinel 。目前只需部署 sentinel-dashboard 这个 Java 进程,同时提供 API 端口和 Web 管理页面。
      2. 修改一些 Java 业务进程的代码,定义一些 Sentinel 资源,然后配置熔断规则。
      3. 启动 Java 业务进程,连接到 Sentinel ,获取熔断规则。
    • 默认只会将熔断规则保存在 Java 业务进程的内存中,因此重启进程后会丢弃。建议二次开发 Sentinel ,将熔断规则推送到 Nacos 等位置存储。
    • 功能:
      • 支持在流量过大时自动熔断。例如 QPS、线程数超过阈值。
        • 建议先进行压力测试,确定业务服务的负载上限,然后据此设置熔断阈值。
      • 支持在服务降级时自动熔断。例如平均响应时间(RT)、抛出异常数超过阈值。
      • 支持多种熔断措施。例如:
        • 抛出 FlowException 异常,拒绝新的 HTTP 请求。
        • 让新请求排队等待被处理,如果等待超时则拒绝请求,从而对流量数削峰。
        • 预热:逐渐增加熔断阈值,避免业务服务突然从空载变成满载。

# 健康检查

:Health Check ,通过软件检查集群中各个服务器的状态,自动发现故障的服务器。

  • 发现故障节点之后,需要及时将它下线,避免客户端访问它而服务不可用。或者通过重启等方式修复。

# 负载均衡

:Load Balance ,将服务器部署多个实例,用一个反向代理服务器接收所有客户端的访问流量,然后按特定的策略分发给各个实例。

  • 优点:
    • 容易横向扩容。
    • 均衡各个服务器的负载压力,降低单点故障的风险。
    • 有的负载均衡服务器能对各个服务器进行健康检查,避免将流量转发给故障的服务器。

# 分区容错性

:Partition tolerance ,指系统出现网络分区时,能否继续提供服务。

  • 如果任意两个节点之间不能在指定时间内将数据同步一致(比如网络延迟较大、节点故障),则视作网络中断,出现了网络分区。

# CAP 定理

:一个流行的理论,认为在分布式系统中,一致性(C)、可用性(A)、分区容错性(P) 三种性能通常不能同时满足,最多满足两种。

  • 假设分布式系统中存在两个节点 N1、N2 ,两者的网络通信必然存在一定延迟。先在 N1 处写入数据 D ,然后在 N2 处读取数据 D 。此时:
    • 如果 N2 等同步 N1 的数据之后再返回响应,则满足了 C ,但不满足 A 。
    • 如果 N2 不同步 N1 的数据就返回响应,则必然是错误的响应,满足了 A ,但不满足 C 。
    • 如果 N1 或 N2 因为网络分区而不能提供服务,则满足了 C ,但不满足 A、P 。

# 部署架构

假设部署一个数据库系统,常见的架构如下:

  • 单实例
    • :将系统只部署一套实例,使用一个或多个主机。
    • 为了避免磁盘损坏,需要定期备份数据到其它主机。
    • 优点:
      • 架构简单,成本最低。
    • 缺点:
      • 没有冗余实例,每个组件都存在单点故障的风险。
      • 发生故障时,需要在新的主机上部署一套实例,再恢复数据,需要消耗大量人力、时间。
  • 主从集群
    • :将系统部署一个主实例、一个或多个从实例,并实时同步主实例的数据到从实例。当主实例故障时,能快速启用从实例。
    • 可以手动切换到从实例,也可以通过程序自动切换。
    • 优点:
      • 主实例负责处理用户的读、写请求,从实例可以不被用户访问,也可以处理用户的读请求,减轻主实例的负载。
      • 从实例会实时同步数据,有能力随时担任主实例。不必花时间、人力从备份点恢复数据。
    • 缺点:
      • 需要实现分布式一致性。
      • 需要运行多个实例,冗余多、成本高。
  • 多主集群
    • :将系统部署多个主实例。
    • 优点:
      • 每个实例都可供客户端访问,读写数据。容易分散负载,大幅提高可用性。
    • 缺点:
      • 多个主实例之间,实现分布式一致性的难度很大。
  • 副本集群
    • :将系统部署一个主实例、一个或多个副本实例。
    • 像主从集群,每个实例都拥有完整的一份数据。但区别在于,当主实例故障时,副本实例能根据某种共识算法,选出一个副本实例,担任新的主实例。
    • 优点:
      • 可用性比主从集群高。
    • 缺点:
      • 实现分布式一致性的难度,比主从集群高。
      • 客户端可能需要知道所有副本实例的 IP 地址,自动发现新的主实例。或者部署一个 proxy 服务器,自动发现新的主实例,并进行反向代理,然后让客户端访问 proxy ,而不是直接连接数据库实例。

# 容灾

  • 软件、硬件系统可能因为断电、断网、火灾、地震等不可抗力而故障,为了容忍灾难,通常将系统部署多套实例,当一套实例故障时能使用其它实例。

  • 多套实例通常部署在不同城市,地理位置较远,减小被同一个灾难同时波及的风险。比如:

    • 同城的不同机房
    • 异地的不同机房
      • 网络延迟较大,因此同步数据的难度大。
  • 根据实例是否被用户使用,分为几种情况:

    • 主从灾备
      • :部署一套主实例、一套或多套从实例。平时让用户只使用主实例,而从实例处于待机状态。
      • 主实例故障时,需要花时间启用从实例,并且从实例不一定可用,比如突然接收大量请求可能故障。
    • 双活
      • :部署两套实例,平时让用户同时使用。又称为双主实例。
      • 每个实例都实时可用,也减少了冗余成本,但实现分布式一致性的难度更大。
    • 多活
  • 相关概念:

    • RPO(Recovery Point Objective ,数据恢复点目标):发生灾难时,系统最多丢失最近多长时间的数据。
      • 例如系统每隔 1 小时备份一次,则最多丢失最近 1 小时内的数据,更早的数据可以从备份点恢复。
    • RTO(Recovery Time Objective ,恢复时间目标):发生灾难时,系统需要多长时间才能恢复。又称为故障恢复时间(Mean Time To Repair ,MTTR)。