利用 etcd 进行 leader 选举实现服务高可用

原文地址: https://www.tony-yin.site/2019/05/15/Etcd_Service_HA/#

etcd logo

本文介绍如何经过etcd进行leader选举,从而实现服务高可用。html

概述

Etcd 是什么?

Etcd是一个分布式的,一致的key-value存储,主要用于共享配置和服务发现。Etcd是由CoreOS开发并维护,经过Raft一致性算法处理日志复制以保证强一致性。Raft是一个来自Stanford的新的一致性算法,适用于分布式系统的日志复制,Raft经过选举的方式来实现一致性,在Raft中,任何一个节点均可能成为leaderGoogle的容器集群管理系统Kubernetes、开源PaaS平台Cloud FoundryCoreOSFleet都普遍使用了etcdnode

Etcd 的特性?

在分布式系统中,如何管理节点间的状态一直是一个难题,etcd像是专门为集群环境的服务发现和注册而设计,它提供了数据TTL失效、数据改变监视、多值、目录监听、分布式锁原子操做等功能,能够方便的跟踪并管理集群节点的状态。Etcd的特性以下:python

  • 简单: curl可访问的用户的APIHTTP+JSON
  • 安全: 可选的SSL客户端证书认证
  • 快速: 单实例每秒1000次写操做
  • 可靠: 使用Raft算法保证一致性

为何须要 Etcd?

全部的分布式系统,都面临的一个问题是多个节点之间的数据共享问题,这个和团队协做的道理是同样的,成员能够分头干活,但老是须要共享一些必须的信息,好比谁是leader, 都有哪些成员,依赖任务之间的顺序协调等。因此分布式系统要么本身实现一个可靠的共享存储来同步信息(好比Elasticsearch),要么依赖一个可靠的共享存储服务,而Etcd就是这样一个服务。git

Etcd主要提供如下能力:github

  • 提供存储以及获取数据的接口,它经过协议保证etcd集群中的多个节点数据的强一致性。用于存储元信息以及共享配置。
  • 提供监听机制,客户端能够监听某个key或者某些key的变动(v2v3的机制不一样)。用于监听和推送变动。
  • 提供key的过时以及续约机制,客户端经过定时刷新来实现续约(v2v3的实现机制也不同)。用于集群监控以及服务注册发现。
  • 提供原子的CASCompare-and-Swap)和CADCompare-and-Delete)支持(v2经过接口参数实现,v3经过批量事务实现)。用于分布式锁以及leader选举。

第三方库和客户端工具

目前有不少支持etcd的库和客户端工具,好比命令行客户端工具etcdctlGo客户端go-etcdJava客户端jetcdPython客户端python-etcd等等。算法

背景

回归正题,一块儿谈谈如何借助etcd进行leader选举实现高可用吧。api

首先说下背景,如今集群上有一个服务,我但愿它是高可用的,当一个节点上的这个服务挂掉以后,另外一个节点就会起一个一样的服务,从而保证服务不中断。安全

这种场景应该比较常见,好比MySQL高可用、NFS高可用等等,而这些高可用的实现方式每每是经过keepalivedctdb等组件,对外暴露一个虚拟IP提供服务。架构

技术选型

那为何不采用上面提到的技术实现高可用呢?首先这些技术很成熟,的确很好用,可是不适用于全部场景,它们比较适合对外提供读写服务的场景,而并非全部服务都是对外服务的;其次,在已经存在etcd的集群环境上而且借助etcd能够达到高可用的状况下没有必要再引入其余组件;而后,在第三方库和客户端工具上,etcd有很大优点;最后,由于Raft算法的关系,在一致性上面etcd作的也比上面这几个要好。curl

核心:TTL & CAS

Etcd进行leader选举的实现主要依赖于etcd自带的两个核心机制,分别是 TTLAtomic Compare-and-SwapTTLtime to live)指的是给一个key设置一个有效期,到期后这个key就会被自动删掉,这在不少分布式锁的实现上都会用到,能够保证锁的实时有效性。Atomic Compare-and-SwapCAS)指的是在对key进行赋值的时候,客户端须要提供一些条件,当这些条件知足后,才能赋值成功。这些条件包括:

  • prevExistkey当前赋值前是否存在
  • prevValuekey当前赋值前的值
  • prevIndexkey当前赋值前的Index

这样的话,key的设置是有前提的,须要知道这个key当前的具体状况才能够对其设置。

设计原理

因此咱们能够这样设计:

  • 先定义一个key,用做于选举;定义key对应的value,每一个节点定义的value须要可以惟一标识;
  • 定义TTL周期,各节点客户端运行周期为TTL/2,这样能够保证key能够被及时建立或更新;
  • 启动时,每一个客户端尝试cas create key,并设置TTL,若是建立不成功,则表示抢占失败;若是建立成功,则抢占成功,而且给key赋值了能够惟一标识本身的value,并设置TTL
  • 客户端TTL/2按期运行,每一个客户端会先get这个keyvalue,跟本身节点定义的value相比较,若是不一样,则表示本身角色是slave,因此接下来要作的事就是周期去cas create key,并设置TTL;若是相同,则表示本身角色是master,那么就不须要再去抢占,只要更新这个keyTTL,延长有效时间;
  • 若是master节点中途异常退出,那么当TTL到期后,其余slave节点则会抢占到并选举出新的master

具体实现

环境参数:

etcd:v2
client:python-etcd

定义参数

  • 定义存储目录名称为etcd_watcheretcd_watcher全部相关的key都在该目录下;
  • 定义用于选举的key名称为master,定义每一个节点赋该key的值为本节点的hostname用做惟一标识;
  • 定义TTL60s,这样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

Etcd支持httpsssl认证,因此etcd集群作了这些安全配置的话,须要在实例化Client的时候配置protocolcertca_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采用两个条件:prevValueprevExist,下面是三个最基础的函数:

  • 建立master-key:争抢master
  • 获取master-key:获取master的值
  • 更新master-key:更新masterTTL
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()))

Refer

  1. Etcd V2 API
  2. Python-etcd doc
  3. etcd使用经验总结
  4. Etcd 架构与实现解析
  5. ETCD实现leader选举
  6. 主从系统的实现
  7. 利用ETCD进行多Mater模块容灾
  8. python使用etcd来实现配置共享及集群服务发现 【上】

总结

本文经过etcd自带的特性进行选举,而后经过选举机制实现了服务的高可用。只要etcd集群正常运行,服务就能够达到主备容灾的效果。该watcher能够应用于任意服务,而且能够一对多,多个服务能够共用一套选举机制,避免使用多套高可用组件。你们对该项目有兴趣的话,能够去Github上详细阅读,喜欢的话请点个赞哦(#^.^#)。

项目地址:https://github.com/tony-yin/e...

相关文章
相关标签/搜索