如何在github上面找到想要的sdk以及Python使用Etcd叢集基礎教程

画个一样的我發表於2024-06-22

1、背景介紹

最近接手了一個專案,專案是使用Python開發的,其中使用到了Etcd,但是專案之前開發的方式,只能夠支援單節點連線Etcd,不能夠在Etcd節點發生故障時,自動轉移。因此需要實現基於現有etcd sdk 開發一個能夠實現故障轉移的功能,或者更換etcd sdk來實現故障轉移等功能。

先來看看專案之前使用到的 etcd 庫,即 python-etcd3,透過給出的示例,沒有看到可以連線多節點的方式,深入到原始碼後,也沒有發現可以連線多節點的方式,基本上可以斷定之前使用到的 etcd sdk 不支援叢集方式了。因為專案中不僅僅是使用到了簡單的 get、put、delete 等功能,還用到了 watch、lock等功能,所以最好是找到一個可以替換的 sdk,這樣開發週期可以縮短,並且也可以減少工作量。

2、尋找可替換的SDK

網上搜了下,發現用的比較多的幾個庫都不支援叢集方式連線,而且也蠻久沒有更新了。比如: etcd3-pypython-etcd3

那重新找一個 etcd 的sdk 吧,然後在 github 上面搜尋,最開始按照預設推薦順序看了好幾個原始碼,都是不支援叢集方式連線的。

都有點心灰意冷了,突然想到可以換一下 github 的推薦順序,換成最近有更新的,然後我換成了 Recently updated 搜尋,然後從前往後看,在第二頁看到了一個庫,點選去看了下原始碼,發現是透過 grpc 方式呼叫的 etcd server,點進去看 client.py 檔案,看到有一個類是: MultiEndpointEtcd3Client,突然眼前一亮,難道可以,然後更加文件安裝了對於的 sdk ,測試發現可以叢集連線。

發現可以叢集連線後,接下來就是看看專案中用到的其他功能,可以正常使用不,比如: watch、lock 。測試發現都可以正常使用。

接下來就是整合到專案中了,這裡就不仔細介紹,大家根據自己實際情況自行調整。

3、etcd-sdk-python 連線叢集

官方教程

etcd-sdk-python 連線叢集方式比較簡單,需要先建立 Endpoint,然後作為引數,傳給 MultiEndpointEtcd3Client。

from pyetcd import MultiEndpointEtcd3Client, Endpoint
from pyetcd.exceptions import ConnectionFailedError

# time_retry 的意思是,當這個節點連線失敗後,多少秒後再次去嘗試連線
e1 = Endpoint(host="192.168.91.66", port=12379, secure=False, time_retry=30)
e2 = Endpoint(host="192.168.91.66", port=22379, secure=False, time_retry=30)
e3 = Endpoint(host="192.168.91.66", port=32379, secure=False, time_retry=30)

# failover 的意思是,當節點發生故障時,是否進行故障轉移,這個引數一定要設定為True,否則當一個節點發生故障時,會報錯
c = MultiEndpointEtcd3Client([e1, e2, e3], failover=True)

l = c.lease(10)

data = {"data": 8000}
c.put("/test_ttl", json.dumps(data).encode("utf-8"), lease=l)

time.sleep(5)
b = c.get("/test_ttl")
print(dir(b))

print(dir(b[0]))
print(dir(b[1]))
print(b[1].lease_id)

4、實現一個簡約的自動續約的分散式鎖

import math
from threading import Thread
import time

from pyetcd import MultiEndpointEtcd3Client, Endpoint
from pyetcd.exceptions import ConnectionFailedError

e1 = Endpoint(host="192.168.91.66", port=12379, secure=False, time_retry=2)
e2 = Endpoint(host="192.168.91.66", port=22379, secure=False, time_retry=2)
e3 = Endpoint(host="192.168.91.66", port=32379, secure=False, time_retry=2)

c = MultiEndpointEtcd3Client([e1, e2, e3], failover=True)


class EtcdGlobalMutex(object):

    def __init__(self, etcd_client, lock_key, ttl=5, acquire_timeout=2):
        """

        :param etcd_client: 已連線的etcd客戶端
        :param lock_key: 分散式鎖key
        :param ttl: key的有效期
        :param acquire_timeout: 嘗試獲取鎖的最長等待時間
        """
        self.etcd_client = etcd_client
        self.lock_key = lock_key
        self.ttl = ttl if ttl else 5
        self.acquire_timeout = acquire_timeout if acquire_timeout else 2
        self.locker = etcd_client.lock(lock_key, ttl)

    def _acquire(self):
        self.locker.acquire(timeout=self.acquire_timeout)

    def _refresh_lock(self):
        """
        重新整理lock,本質上就是更新 key 的ttl
        :return:
        """
        # 向上取整
        seconds = math.ceil(self.ttl / 2)
        if seconds == 1 and self.ttl == 1:
            seconds = 0.5

        while True:
            try:
                self.locker.refresh()
            except ConnectionFailedError as e:
                # 測試發現,當etcd叢集一個節點故障時,可能會出現這個錯誤
                print(f"refresh_lock. lock_key:{self.lock_key}. ConnectionFailedError, err:{e}")

            except Exception as e1:
                # 非期望錯誤,退出,防止執行緒不能退出
                print(f"refresh_lock. lock_key:{self.lock_key}. unexpected error. err:{e1}")
                return

            time.sleep(seconds)

    def try_lock(self):
        """
        嘗試獲取鎖,當獲取不到鎖時,會監聽對應的key,當key消失時,會再次嘗試獲取鎖
        :return:
        """
        try:
            self._acquire()
        except ConnectionFailedError as e:
            print(f"try_lock. lock_key:{self.lock_key}. ConnectionFailedError. err:{e}")
            time.sleep(1)

            self.try_lock()

        if self.locker.is_acquired():
            print(f"try_lock. lock_key:{self.lock_key}. Lock acquired successfully")
            # 啟動重新整理鎖的執行緒
            t1 = Thread(target=self._refresh_lock)
            t1.start()
        else:
            print(f"try_lock. lock_key:{self.lock_key}. Failed to acquire lock")
            self._watch_key()

    def _watch_key(self):
        """
        監聽 key
        :return: 
        """
        # 寫入etcd的key
        real_key = f"/locks/{self.lock_key}"
        cancel = None
        try:
            print(f"watch_key. lock_key:{self.lock_key}")
            # watch 需要捕獲異常,這樣當一個etcd節點掛掉後,還能夠正常 watch
            events_iterator, cancel = self.etcd_client.watch(real_key)
            for event in events_iterator:
                print(f"watch_key. lock_key:{self.lock_key}. event: {event}")
                cancel()
                break
        except ConnectionFailedError as e:
            print(f"watch_key. lock_key:{self.lock_key}, ConnectionFailedError err:{e}")
            if cancel:
                cancel()
            time.sleep(1)
            
            self.etcd_client._clear_old_stubs()

            self._watch_key()

        self.try_lock()


def main():
    name = 'lock_name'
    e = EtcdGlobalMutex(c, name, ttl=10)
    e.try_lock()

    while True:
        print("Main thread sleeping")
        time.sleep(2)


if __name__ == "__main__":
    main()


5、watch key 如何實現?

如果只是單純的實現一個 watch key 功能,沒啥好說的,看看官方給的 api 就可以,因為測試的時候,發現如果一個 etcd 節點掛掉,而這個節點有正好是連線的節點,會出現報錯,這個時候需要做一些異常捕獲處理。

import math
from threading import Thread
import time

from pyetcd import MultiEndpointEtcd3Client, Endpoint
from pyetcd.exceptions import ConnectionFailedError
from pyetcd.events import PutEvent

e1 = Endpoint(host="192.168.91.66", port=12379, secure=False, time_retry=2)
e2 = Endpoint(host="192.168.91.66", port=22379, secure=False, time_retry=2)
e3 = Endpoint(host="192.168.91.66", port=32379, secure=False, time_retry=2)

c = MultiEndpointEtcd3Client([e1, e2, e3], failover=True)

look_key = "look_key"

def watch(self):
    print('xxx is watching')
    cancel = None
    try:
        events_iterator, cancel = c.watch_prefix(look_key)

        self.watch_key(events_iterator)
    except  ConnectionFailedError as e:
        # 重點就是這裡的異常處理
        print(f"xxx. ConnectionFailedError, err:{e}")
        if cancel:
            cancel()
        time.sleep(1)

        c._clear_old_stubs()
        watch()
    except Exception as e1:
        # 非期望錯誤,退出,防止執行緒不能退出
        print(f"xxx.  unexpected error. err:{e1}")
        if cancel:
            cancel()

        return


def watch_key(self, events_iterator):
    print("coming watch_key")
    for watch_msg in events_iterator:
        print(watch_msg)
        if type(watch_msg) != PutEvent:
            # 如果不是watch響應的Put資訊, 忽略
            continue

        # xxx 處理監聽到的資訊

透過上面的學習,對 etcd-sdk-python 有一個基礎的認識。

哈哈,再次感謝大佬的貢獻!

6、部署 etcd 叢集

叢集部署可以看我之前寫的文章 02、etcd單機部署和叢集部署

文章中是使用了 goreman 來管理多程序的,如何模擬一個節點掛掉了,其實為了方便測試,我們可以將 使用到的 profile 檔案,分成三個。profile_etcd1、profile_etcd2、profile_etcd3 。透過使用 goreman 來管理。

goreman -f profile_etcd1 start

相關文章