利用 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...

相關文章
相關標籤/搜索