Skip to the content.

领导着与跟随者(Leader And Followers)

描述:用单个服务器在跨域一组服务器之间来协作备份。

问题

为了在管理数据的系统中实现容错,需要在多个服务器上复制数据。

有给到一些在客户端的一致性的保证这是很重要的。当数据在多个服务器更新时,让它何时对于客户端可见是必要的的一个决定。写和读 Quorum 并不充分,在一些失败的场景中会引起客户端对数据的不一致性。在仲裁中,每个独立的服务并不知道在其它服务器上的数据的状态,只有在数据被多个服务器读取时,数据的不一致性一定要解决。有些时候,这还不够。关于发送给客户端的数据强一致性时必须的。

解决方案

多数选择一个服务器作为 leader。leader 的职责就是代表整个集群做决定以及给其它服务器传递这个决定。

在开始阶段,每个服务器都会去寻找已经存在的 leader。如果没有 leader,就会触发 leader 选举。这些服务器只有在成功选举出一个 leader 服务器之后才接收请求。只有 leader 处理客户端的请求。如果一个请求发送给追随者服务器的,那么这些 followers 就会转发给 leader 服务器。

Leader 选举

​ 选举示意图

​ 投票

​ leader 心跳检查

对于 3-5 个节点的小集群,像在系统中实现一致性,在数据集群内不依赖于任何外部系统,能够实现 leader 选举 。Leader 选举时发生在服务器启动阶段。每个服务器在启动时开始一个 leader 选举并尝试选举一个 leader。这个时候系统时不会接收任何客户端请求直到选出了 leader。在生成时钟模式中解释过,每个 leader 选举都需要更新生成的数值。这些总是处于在 Leader,Follower 或是 Looking For Leader(或是 Candidate)这三个状态的一个。

public enum ServerRole {
    LOOKING_FOR_LEADER,
    FOLLOWING,
    LEADING;
}

心跳检查机制是用在探测已经存在的 leader 是否发生故障,以至可以选举新的 leader。

新的 leader 选举开始于发送给每个服务器一个投票的请求消息。

class ReplicationModule…

  private void startLeaderElection() {
      replicationState.setGeneration(replicationState.getGeneration() + 1);
      registerSelfVote();
      requestVoteFrom(followers);
  }

选举算法

当选举一个 leader 有两个因素要考虑到。

从大多数服务器中接受到投票的服务器会转换为 Leader 状态。这大多数是在 Quorum 中讨论决定的。一旦选举,这个 leader 会继续发送心跳检查给所有追随者。如果追随者在指定的时间间隔内没有收到心跳检查,那么就会触发新的 leader 选举。

使用外部[可线性化]的存储来 Leader 选举

对于小集群,运行一个 leader 选举在数据集群上能工作的很好。对于大数据集群,它至少有上千个节点,通过外部的存储如 Zookeeper 或 etcd 也容易工作。(这些内部使用了一致性和提供了线性保证)。这些典型的大集群有一个被标记为主节点或控制器节点,它能代表整个集群的所有决定。实现一个 leader 选举机制需要具备三个功能:

对于 Leader 选举,每个服务器使用 compareAndSwap 指令在外部存储器创建一个 key ,并且谁第一个成功,谁就是被选举为 leader。取决于使用的外部存储,创建 key 的时间很短。在 key 生存之前 leader 选举会一致反复更新 key [The elected leader repeatedly updates the key before the time to live value]。每个服务器都可以对这个 key 监听,如果 key 过期,服务器会得到通知,而现有的 leader 在激活时间设置内没有更新该 key [Every server can set a watch on this key, and servers get notified if the key expires without getting updated from the existing leader within the time to live setting]。如 etcd 允许一个 compareAndSwap 操作,只有当键不存在之前,允许通过设置 key 操作。在 Zookeeper 这个就没有显式的 compareAndSwap 类别的指令支持,但是它可以通过创建一个新的节点来实现,如果这个节点不存在就会预期一个异常。也没有明确的生存时间,但 zookeeper 有一个临时节点的概念。这个节点一直存在,直到服务器与 zookeeper 有一个活动的会话,其它节点被删除以及每个监听的节点没有被通知。例如,利用 zookeeper 实现下面的 leader 选举:

class ServerImpl…

  public void startup() {
      zookeeperClient.subscribeLeaderChangeListener(this);
      elect();
  }

  public void elect() {
      var leaderId = serverId;
      try {
          zookeeperClient.tryCreatingLeaderPath(leaderId);
          this.currentLeader = serverId;
          onBecomingLeader();
      } catch (ZkNodeExistsException e) {
          //back off
          this.currentLeader = zookeeperClient.getLeaderId();
      }
  }

其它所有的服务器都会观察这个存在的 leader 的存活情况。当探测到这个 leader 宕机的时候,新的 leader 选举就会触发。失败探测器会使用与 leader 选举相同外部线性化的存储。此外部存储还具有实现组成员关系和故障检测机制的功能。例如,拓展上面基于 zookeeper 的实现,Zookeeper 能配置一个变更监听器,它在已经存在的 leader 节点发生改变时触发。

class ZookeeperClient…

  public void subscribeLeaderChangeListener(IZkDataListener listener) {
      zkClient.subscribeDataChanges(LeaderPath, listener);
  }

集群中的每个服务器都订阅这个变更,只要 callback 被触发,一个新的 leader 选举就会触发,以上面所述相同的的方式触发。

class ServerImpl…

  @Override
  public void handleDataDeleted(String dataPath) throws Exception {
      elect();
  }

​ zookeeper 选举

像 Consul 或 etcd 这样的系统能使用相同的方式来实现 leader 选举。

为什么 Quorum 读/写不足以保证强一致性

它可能看起来像是由 Dynamo 风格的数据库(如 Cassandra)提供的 Quorum 读/写,足以在服务器出现故障时获得强大的一致性。但其实不是这样的。思考下面例子。我们现在有一个三个服务器的集群,变量 x 存在所有的服务器上,(复制因子是 3)。启动时值从 1 开始。

两次连续的读取值就会显示最新的值。一旦 server1 重新恢复,随后的一次读取就会获取最新的值。假设读修复或反熵(Anti-Entropy)过程正在运行,其余的服务器也会“最终”得到最新的值。但是集群存储不提供保证会确保一次指定值就在所有客户端可见,即使服务器失败,所有后续读取都将继续获取该值。

反熵:在一个最终一致性性的数据库中,随着时间流逝,应该被精确复制的节点都会慢慢互相偏差。这个偏差可以被认为是系统的“entropy”。反熵 就是让节点之间互相同步的过程。

例子

原文链接

https://martinfowler.com/articles/patterns-of-distributed-systems/leader-follower.html