分布式协同服务 系列 etcd 概述

Posted by lichao modified on October 25, 2020

etcd 是一个 Go 语言编写的高可用的一致性分布式键值存储系统,用于提供配置共享和服务发现等功能。常见的 etcd 使用场景包括:服务发现、分布式锁、分布式数据队列、分布式通知和协调、主备选举等。

etcd 这个名字由两部分组成: etc 和 d ,即 UNIX/Linux 操作系统的“/etc”目录和分布式(distributed)首字母的“d” 。我们都知道,/etc 目录一般用于存储 UNIX/Linux 操作系统的配置信息因此 etc 和 d 合起来就是一个分布式的 “/etc” 目录。由此可见,etcd 的寓意是为大规模分布式系统存储配置信息。

etcd 基于 Raft 协议保证了数据的强一致性,它将复杂的一致性问题分解成 Leader 选举、日志同步、安全性三个相对独立的子问题,只要集群一半以上节点存活就可提供服务,具备良好的可用性。通过复制日志文件的方式来保证数据的强一致性。当客户端应用写一个 key 时,首先会存储到 etcd 的 Leader 上,然后再通过 Raft 协议复制到 etcd 集群的所有成员中,以此维护各成员(节点)状态的一致性与实现可靠性。虽然 etcd 是一个强一致性的系统,但也支持从非 Leader 节点读取数据以提高性能,而且写操作仍然需要 Leader 的支持,所以当发生网络分区时,写操作仍可能失败。

etcd 具有一定的容错能力,假设集群中共有 n 个节点,即便集群中(n-1)/2 个节点发生了故障,只要剩下的(n+1)/2个节点达成一致,也能操作成功。因此,它能够有效地应对网络分区和机器故障带来的数据丢失风险。

etcd 默认数据一更新就落盘持久化,数据持久化存储使用 WAL (write ahead log,预写式日志)格式。WAL 记录了数据变化的全过程,在 etcd 中所有数据在提交之前都要先写入WAL 中; etcd 的 Snapshot(快照)文件则存储了某一时刻 etcd 的所有数据,默认设置为每 10 000 条记录做一次快照,经过快照后 WAL 文件即可删除。

应用场景

典型的 etcd 应用场景包括但不限于分布式数据库、服务注册与发现、分布式锁、分布式消息、队列、分布式系统选主等。

服务注册与发现

服务发现( Service Discovery )要解决的是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务如何才能找到对方并建立连接。 从本质上说,服务发现就是要了解集群中是否有进程在监听 UDP 或者 TCP 端口,并且通过名字就可以进行查找和链接。要解决服务发现的问题,需要具备如下三个条件。

  1. 一个强一致性、高可用的服务存储目录。而基于 Raft 算法的 etcd 天生就是这样一个强一致性、高可用的服务存储目录。
  2. 一种注册服务和健康服务健康状况的机制。用户可以在 etcd 中注册服务,并且对注册的服务配置 key TTL ,定时保持服务的心跳以达到监控健康状态的效果。
  3. 一种查找和连接服务的机制。在etcd 指定的主题下注册的服务业能在对应的主题下查找到。为了确保连接,我们可以在各个服务机器上都部署一个代理模式的 etcd,这样就可以确保访问etcd 集群的服务都能够互相连接。

消息发布和订阅

在分布式系统中,最为适用的组件间通信的机制是消息的发布和订阅机制。 具体而言就是,设置一个配置共享中心,消息提供者在这个配置中心发布消息, 而消息使用者则订阅他们关心的主题,一旦所关心的主题有消息发布,就会实时通知订阅者。通过这种方式,我们可以实现发布式系统配置的集中式管理和实时动态更新。

负载均衡

在分布式系统中,为了保证服务的高可用以及数据的一致性,通常都会把数据和服务部署为多份,以此达到对等服务,即使其中的某一个服务失效了,也不会影响使用。 这样的实现虽然会导致一定程度上数据写入性能的下降,但是却能够实现数据访问时的负载均衡。因为每个对等服务节点上都存储有完整的数据,所以所有用户的访问流量都可以分流到不同的机器上。

分布式通知与协调

这里讨论的分布式通知和协调,与消息发布和订阅有点相似。 两者都使用了 etcd 的 Watcher 机制,通过注册与异步通知机制,实现分布式环境下不同系统之间的通知与协调,从而对数据变更进行实时处理。

实现方式通常如下不同的系统都在 etcd 上对同一个目录进行注册, 同时设置 Watcher 监控该目录的变化(如果对子目录的变化也有需求, 那么可以设置成递归模式)。若某个系统更新了etcd 的目录,那么设置了 Watcher 的系统就会收到通知,并做出相应的通知,然后进行相应的处理。

分布式锁

因为 etcd 使用 Raft 算法保持了数据的强一致性,某次操作存储到集群中的值就必然是全局一致的,所以 etcd 很容易实现分布式锁。锁服务包含两种使用方式,一是保持独占,二是控制时序。

  1. 保持独占。保持独占即所有试图获取锁的用户最终只有一个可以得到。etcd 为此提供了一套实现分布式锁原子操作 CAS (ComparaAndSwap)的API 。通过设置 prevExist 值,可以保证在多个节点上同时创建某个目录时,只有一个节点能够成功,而成功的那个即可获得分布式锁。
  2. 控制时序。试图获取锁的所有用户都会进入等待队列,获得锁的顺序是全局唯一的,同时还能决定队列的执行顺序。etcd 为此也提供了一套 API (自动创建有序键),它会将一个目录的键值指定为 POST 动作,这样, etcd 就会在目录下生成一个当前最大的值作为键,并存储这个新的值(客户端编号)。同时还可以使用API 按顺序列出所有目录下的键值。此时这些键的值就是客户端的时序,而这些键中存储的值则可以是代表客户端的编号。

分布式队列

分布式队列的常规用法与分布式锁的控制时序用法类似,即通过创建一个先进先出的队列来保证顺序。另一种比较有意思的实现是在保证队列达到某个条件时再统一按顺序执行。要实现这种方法,可以在“/queue”目录中另外建立一个“/queue/condition”节点。关于 condition 节点,具体说明如下。

  1. condition 可以表示队列的大小。比如一个大的任务若需要在很多小任务都就绪的情况下才能执行,那么每当有一个小任务就绪时,就将这个 condition 的数值加 1,直到达到大任务规定的数字,然后再开始执行队列里的一系列小任务,直至最终执行大任务。
  2. condition 可以表示某个任务不在队列中。这个任务既可以是所有排序任务的首个执行程序,也可以是拓扑结构中没有依赖的点。通常必须在执行这些任务之后才能执行队列中的其他任务。
  3. condition 还可以表示开始执行任务的通知。可以由控制程序来指定,当 condition 发生变化时,开始执行队列任务。

集群监控与 Leader 竞选

通过 etcd 来进行监控的功能实现起来非常简单并且实时性较强,主要会用到如下两点特性。

  1. 前面几个场景已经提到了 Watcher 机制,当某个节点消失或发生变动时, Watcher 会第一时间发现并告知用户。
  2. 节点可以设置 TTL key ,比如每隔 30s 向 etcd 发送一次心跳信号,以此代表该节点依然存活着,否则就说明节点已经消失了。

这样就可以第一时间检测到各节点的健康状态,以完成集群的监控要求。另外,使用分布式锁,还可以完成Leader 竞选。对于一些需要长时间进行 CPU 计算或使用 I/O 的操作,只需要由竞选出的 Leader 计算或处理一次,再把结果复制给其他的 Follower 即可,从而避免重复劳动,节省计算资源。 Leader 应用的经典场景是在搜索系统中建立全量索引。如果各个机器分别进行索引的建立,那么将很难保证索引的一致性。通过 etcd 的 CAS 机制竞选 Leader ,再由 Leader 进行索引计算,最后将计算结果分发到其他节点即可。

存储引擎

etcd 使用简单内存树,它的节点数据结构精简后如下,含节点路径、值、孩子节点信息。这是一个典型的低容量设计,数据全放在内存,无需考虑数据分片,只能保存 key 的最新版本,简单易实现。 dubbo

1
2
3
4
5
6
7
type node struct {
  Path string //节点路径
  Parent *node //关联父亲节点
  Value string //key的value值
  ExpireTime time.Time //过期时间
  Children map[string]*node //此节点的孩子节点
}

Raft 算法提供了成员变更算法,可基于此实现成员在线、安全变更,同时此协调服务使用 Go 语言编写,无依赖,部署简单。