原文地址: https://www.tony-yin.site/2019/05/15/Etcd_Service_HA/#
本文介绍如何经过etcd
进行leader
选举,从而实现服务高可用。html
Etcd
是一个分布式的,一致的key-value
存储,主要用于共享配置和服务发现。Etcd
是由CoreOS
开发并维护,经过Raft
一致性算法处理日志复制以保证强一致性。Raft
是一个来自Stanford
的新的一致性算法,适用于分布式系统的日志复制,Raft
经过选举的方式来实现一致性,在Raft
中,任何一个节点均可能成为leader
。Google
的容器集群管理系统Kubernetes
、开源PaaS
平台Cloud Foundry
和CoreOS
的Fleet
都普遍使用了etcd
。node
在分布式系统中,如何管理节点间的状态一直是一个难题,etcd
像是专门为集群环境的服务发现和注册而设计,它提供了数据TTL
失效、数据改变监视、多值、目录监听、分布式锁原子操做等功能,能够方便的跟踪并管理集群节点的状态。Etcd
的特性以下:python
curl
可访问的用户的API
(HTTP
+JSON
)SSL
客户端证书认证1000
次写操做Raft
算法保证一致性全部的分布式系统,都面临的一个问题是多个节点之间的数据共享问题,这个和团队协做的道理是同样的,成员能够分头干活,但老是须要共享一些必须的信息,好比谁是leader
, 都有哪些成员,依赖任务之间的顺序协调等。因此分布式系统要么本身实现一个可靠的共享存储来同步信息(好比Elasticsearch
),要么依赖一个可靠的共享存储服务,而Etcd
就是这样一个服务。git
Etcd
主要提供如下能力:github
etcd
集群中的多个节点数据的强一致性。用于存储元信息以及共享配置。key
或者某些key
的变动(v2
和v3
的机制不一样)。用于监听和推送变动。key
的过时以及续约机制,客户端经过定时刷新来实现续约(v2
和v3
的实现机制也不同)。用于集群监控以及服务注册发现。CAS
(Compare-and-Swap
)和CAD
(Compare-and-Delete
)支持(v2
经过接口参数实现,v3
经过批量事务实现)。用于分布式锁以及leader
选举。目前有不少支持etcd
的库和客户端工具,好比命令行客户端工具etcdctl
、Go
客户端go-etcd
、Java
客户端jetcd
、Python
客户端python-etcd
等等。算法
回归正题,一块儿谈谈如何借助etcd
进行leader
选举实现高可用吧。api
首先说下背景,如今集群上有一个服务,我但愿它是高可用的,当一个节点上的这个服务挂掉以后,另外一个节点就会起一个一样的服务,从而保证服务不中断。安全
这种场景应该比较常见,好比MySQL
高可用、NFS
高可用等等,而这些高可用的实现方式每每是经过keepalived
、ctdb
等组件,对外暴露一个虚拟IP
提供服务。架构
那为何不采用上面提到的技术实现高可用呢?首先这些技术很成熟,的确很好用,可是不适用于全部场景,它们比较适合对外提供读写服务的场景,而并非全部服务都是对外服务的;其次,在已经存在etcd
的集群环境上而且借助etcd
能够达到高可用的状况下没有必要再引入其余组件;而后,在第三方库和客户端工具上,etcd
有很大优点;最后,由于Raft
算法的关系,在一致性上面etcd
作的也比上面这几个要好。curl
Etcd
进行leader
选举的实现主要依赖于etcd
自带的两个核心机制,分别是 TTL 和 Atomic Compare-and-Swap。TTL
(time to live
)指的是给一个key
设置一个有效期,到期后这个key
就会被自动删掉,这在不少分布式锁的实现上都会用到,能够保证锁的实时有效性。Atomic Compare-and-Swap
(CAS
)指的是在对key
进行赋值的时候,客户端须要提供一些条件,当这些条件知足后,才能赋值成功。这些条件包括:
prevExist
:key
当前赋值前是否存在prevValue
:key
当前赋值前的值prevIndex
:key
当前赋值前的Index
这样的话,key
的设置是有前提的,须要知道这个key
当前的具体状况才能够对其设置。
因此咱们能够这样设计:
key
,用做于选举;定义key
对应的value
,每一个节点定义的value
须要可以惟一标识;TTL
周期,各节点客户端运行周期为TTL/2
,这样能够保证key
能够被及时建立或更新;cas create key
,并设置TTL
,若是建立不成功,则表示抢占失败;若是建立成功,则抢占成功,而且给key
赋值了能够惟一标识本身的value
,并设置TTL
;TTL/2
按期运行,每一个客户端会先get
这个key
的value
,跟本身节点定义的value
相比较,若是不一样,则表示本身角色是slave
,因此接下来要作的事就是周期去cas create key
,并设置TTL
;若是相同,则表示本身角色是master
,那么就不须要再去抢占,只要更新这个key
的TTL
,延长有效时间;master
节点中途异常退出,那么当TTL
到期后,其余slave
节点则会抢占到并选举出新的master
。环境参数:
etcd:v2 client:python-etcd
etcd_watcher
,etcd_watcher
全部相关的key
都在该目录下;key
名称为master
,定义每一个节点赋该key
的值为本节点的hostname
用做惟一标识;TTL
为60s
,这样etcd_watcher
按期执行的时间为30s
;定义etcd_watcher
六种角色,分别为:
Master
:上一次运行角色为Master
,当前运行角色仍为Master
Slave
:上一次运行角色为Slave
,当前运行角色仍为Slave
ToMaster
:上一次运行角色为Slave
,当前运行角色为Master
ToSlave
:上一次运行角色为Master
,当前运行角色为Slave
InitMaster
:上一次运行角色为None
,当前运行角色为Master
InitSlave
:上一次运行角色为None
,当前运行角色为Slave
class EtcdClient(object): def __init__(self): self.hostname = get_hostname() self.connect() self.ttl = 60 self.store_dir = '/etcd_watcher' self.master_file = '{}/{}'.format(self.store_dir, 'master') self.master_value = self.hostname # node role self.Master = 'Master' self.Slave = 'Slave' self.ToMaster = 'ToMaster' self.ToSlave = 'ToSlave' self.InitMaster = 'InitMaster' self.InitSlave = 'InitSlave' # node basic status: master or slave self.last_basic_status = None self.current_basic_status = None
Etcd
支持https
和ssl
认证,因此etcd
集群作了这些安全配置的话,须要在实例化Client
的时候配置protocol
、cert
、ca_cert
选项。
这里不得不吐槽一下python-etcd
的官方文档,连这些配置都未说明,仍是笔者去翻源码才找到的。。。
def connect(self): try: self.client = etcd.Client( host='localhost', port=2379, allow_reconnect=True, protocol='https', cert=( '/etc/ssl/etcd/ssl/node-{}.pem'.format(self.hostname), '/etc/ssl/etcd/ssl/node-{}-key.pem'.format(self.hostname) ), ca_cert='/etc/ssl/etcd/ssl/ca.pem' ) except Exception as e: logger.error("Connect etcd failed: {}".format(str(e)))
CAS
采用两个条件:prevValue
和prevExist
,下面是三个最基础的函数:
master-key
:争抢master
master-key
:获取master
的值master-key
:更新master
的TTL
def create_master(self): logger.info('Create master.') try: self.client.write( self.master_file, self.master_value, ttl=self.ttl, prevExist=False ) except Exception as e: logger.error("Create master failed: {}".format(str(e))) def get_master(self): try: master_value = self.get(self.master_file) return master_value except etcd.EtcdKeyNotFound: logger.error("Key {} not found.".format(self.master_file)) except Exception as e: logger.error("Get master value failed: {}".format(str(e))) def update_master(self): try: self.client.write( self.master_file, self.master_value, ttl=self.ttl, prevValue=self.master_value, prevExist=True ) except Exception as e: logger.error("Update master failed: {}".format(str(e)))
获取当前节点基本状态是Master
仍是Slave
def get_node_basic_status(self): node_basic_status = None try: master_value = self.get_master() if master_value == self.master_value: node_basic_status = self.Master else: node_basic_status = self.Slave except Exception as e: logger.error("Get node basic status failed: {}".format(str(e))) return node_basic_status
获取当前节点基本状态,跟上一次的基本状态做比较,获得最终的角色,这里的角色分为上述的六种。
def get_node_status(self): self.last_basic_status = self.current_basic_status self.current_basic_status = self.etcd_client.get_node_basic_status() node_status = None if self.current_basic_status == self.Master: if self.last_basic_status is None: node_status = self.InitMaster elif self.last_basic_status == self.Master: node_status = self.Master elif self.last_basic_status == self.Slave: node_status = self.ToMaster else: logger.error("Invalid last basic status for master: {}".format( self.last_basic_status) ) elif self.current_basic_status == self.Slave: if self.last_basic_status is None: node_status = self.InitSlave elif self.last_basic_status == self.Master: node_status = self.ToSlave elif self.last_basic_status == self.Slave: node_status = self.Slave else: logger.error("Invalid last basic status for slave: {}".format( self.last_basic_status) ) else: logger.error("Invalid current basic status: {}".format( self.current_basic_status) ) return node_status
根据当前节点的角色,协调服务作对应的工做,保证高可用。
def run(self): try: logger.info("===== Init Etcd Wathcer =====") self.etcd_client.create_master() while True: node_status = self.get_node_status() logger.info("node status : {}".format(node_status)) if node_status == self.etcd_client.ToMaster: self.do_ToMaster_work() self.etcd_client.update_master() elif node_status == self.etcd_client.InitMaster: self.do_InitMaster_work() self.etcd_client.update_master() elif node_status == self.etcd_client.Master: self.etcd_client.update_master() elif node_status == self.etcd_client.ToSlave: self.do_ToSlave_work() self.etcd_client.create_master() elif node_status == self.etcd_client.InitSlave: self.do_InitSlave_work() self.etcd_client.create_master() elif node_status == self.etcd_client.Slave: self.etcd_client.create_master() else: logger.error("Invalid node status: {}".format(node_status)) time.sleep(self.interval) self.etcd_client = EtcdClient() except Exception: logger.error("Etcd watcher run error:{}".format(traceback.format_exc()))
本文经过etcd
自带的特性进行选举,而后经过选举机制实现了服务的高可用。只要etcd
集群正常运行,服务就能够达到主备容灾的效果。该watcher
能够应用于任意服务,而且能够一对多,多个服务能够共用一套选举机制,避免使用多套高可用组件。你们对该项目有兴趣的话,能够去Github
上详细阅读,喜欢的话请点个赞哦(#^.^#)。