原文地址: 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
上詳細閱讀,喜歡的話請點個贊哦(#^.^#)。