Go 語言中使用 ETCD

bean發表於2020-06-23

> 關於ETCD的簡介可以在百度上或者google上查詢。可能我們對他的應用環境更加感興趣,下面我列舉一些經典的應用場景:

  1. 服務發現(Service Discovery)

    服務發現要解決的也是分散式系統中最常見的問題之一,即在同一個分散式叢集中的程式或服務,要如何才能找到對方並建立連線。

  2. 訊息釋出與訂閱

    在分散式系統中,最適用的一種元件間通訊方式就是訊息釋出與訂閱

  3. 負載均衡

    分散式系統中,為了保證服務的高可用以及資料的一致性,通常都會把資料和服務部署多份,以此達到對等服務,即使其中的某一個服務失效了,也不影響使用。由此帶來的壞處是資料寫入效能下降,而好處則是資料訪問時的負載均衡。

  4. 分散式通知與協調

    與訊息釋出和訂閱有些相似

  5. 分散式鎖

    因為etcd使用Raft演算法保持了資料的強一致性,某次操作儲存到叢集中的值必然是全域性一致的,所以很容易實現分散式鎖。鎖服務有兩種使用方式,一是保持獨佔,二是控制時序

  6. 分散式佇列

    分散式佇列的常規用法與場景五中所描述的分散式鎖的控制時序用法類似,即建立一個先進先出的佇列,保證順序。

  7. 叢集監控與Leader競

    透過etcd來進行監控實現起來非常簡單並且實時性強。

更多應用場景需要我們自己多探索

一. 安裝 ETCD

由於 etcd 是Golang編寫,所以安裝和部署都非常簡單,直接啟動編譯好的檔案即可。但我們一般都會使用叢集的etcd,這裡推薦使用Docker。下面是我在GitHub上找到的Docker啟動單機版etcd的docker-compose.yaml檔案,如果專案中還有使用其他服務,可以自己新增在檔案中一起啟動。叢集版的可以自己在GitHub上找對應的版本。

version: '3.8'

networks:
  app-tier:
    driver: bridge

services:
  etcd:
    image: 'bitnami/etcd:latest'
    environment:
      - ALLOW_NONE_AUTHENTICATION=yes
      - ETCD_ADVERTISE_CLIENT_URLS=http://etcd:2379
    ports:
      - 2379:2379
      - 2380:2380
    networks:
      - app-tier
  myapp:
    image: 'bitnami/etcd:latest'
    networks:
      - app-tier

以上檔案儲存為 docker-compose.yaml 然後使用命令: docker-compse up -d 啟動服務。

一個不錯的一直維護的ETCD的Dockerfile和docker-compose的GitHub倉庫:

> Github 地址:github.com/bitnami/bitnami-docker-...

二. 安裝 Go 語言 ETCD 包

> 這裡我使用Mac作為開發環境,Golang版本為 1.14.3

我們使用ETCD官方提供的ETCD的包,因為本身ETCD就是使用GOlang編寫的,所以在Go中使用也是更加方便。

go get github.com/coreos/etcd

這裡因為etcd依賴的包版本變更,導致不能執行,所以需要修改 go.mod 檔案讓ETCD跑起來。go.mod 檔案如下:

    github.com/coreos/etcd v3.3.22+incompatible
    github.com/coreos/go-semver v0.3.0 // indirect
    github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
    //github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
    github.com/coreos/go-systemd/v22 v22.1.0
    github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
    github.com/gogo/protobuf v1.3.1 // indirect
    github.com/google/uuid v1.1.1 // indirect
    github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75
    go.etcd.io/bbolt v1.3.4 // indirect
    go.etcd.io/etcd v3.3.22+incompatible
    go.uber.org/zap v1.15.0 // indirect
    //google.golang.org/grpc v1.29.1 // indirect
    google.golang.org/grpc v1.26.0

> 上面被註釋掉的兩個包就是版本過高,導致etcd不能正常執行而報錯,所以換成對應的版本。

三. 建立 ETCD 連線

func mian(){
  var (
        client *clientv3.Client
        config clientv3.Config
    err error
    )

    config = clientv3.Config{
    // 這裡的 Endpoints 是一個字串陣列切片,支援配合多個節點
    Endpoints:   []string{"127.0.0.1:2379"},
    // DialTimeout 連線超時設定
        DialTimeout: time.Duration(5) * time.Millisecond,
    }
    if client, err = clientv3.New(config); err != nil {
        return
    }
}

這樣就可以得到一個etcd的客戶端例項

四. ETCD 的 KV 操作

ETCD 中 KV 的操作需要一個KV例項,建立KV例項需要 etcd 的客戶端例項來建立

kv := clientv3.NewKV(client)

拿到 KV 例項之後就可以對 KV 進行操作了,下面就是 KV 的三個基本操作, etcd 裡面沒有update操作,因為PUT操作已經滿足我們建立和更新操作了,所以只有PUT。

1. KV 的 PUT 操作

我們先嚐試往 etcd 中 put 一個資料

// 第一個引數為上下文,第二個為 KEY, 第三個為 VALUE。也可以傳第四個引數 option,比如:給這個KEY 加一個過期時間, 在KEY過期機制裡面會有詳細記錄
putResponse, err := kv.Put(context.TODO(),"/testDir/User/user1","user info")

etcd 的各種操作中都會有對應的 Response, put 操作也不例外,同樣返回 putResponse,這是一個物件,裡面包含Header, PrevKv。 PrevKv提供了 Put 之前的這個 key 的 KV 值。Header 提供了etcd 的 Revision 和其他 etcd 的資訊和方法。

2. KV 的 GET 操作

我們依然使用剛剛拿到的 KV 例項進行操作

// 用法一: 第一個引數為上下文,第二個為 KEY,即可獲取對應 Key 的 Value
getResponse, err := kv.Gut(context.TODO(),"/testDir/User/user1")
// 用法二:  第一個引數為上下文,第二個為 KEY,第三個為可選引數  option,這個操作會返回字首為 "/testDir/User/" 下所有 Key 的 Value, 相當於獲取一個列表
gutResponse, err := kv.Gut(context.TODO(),"/testDir/User/", clientv3.WithPrefix())

同樣,getResponse 也返回了很多資訊,但最主要的是我們獲取的 Key 的 Value,如果是使用第二種用法,會返回一個 KVs,是一個 KV 的陣列切片。也包含了Header, Count, More, More是一個bool值,指示是否有更多鍵可以返回要求的範圍。 Count 返回返回資料的數量

3. KV 的 DELETE 操作

// 用法一: 第一個引數為上下文,第二個為 KEY,即可獲取對應 Key 的 Value
deleteResponse, err := kv.Delete(context.TODO(),"/testDir/User/user1")
// delete 也可以刪除 "/testDir/User/" 下所有的key,類似get的操作,但第三個引數要傳 WithPrefix()

deleteResponse 返回 Header, Deleted,PrevKvs, Header中包含資訊與之前差不多,後面的Deleted 是一個int64值,代表刪除數量。PrevKvs 返回刪除之前的 KV 值

五. ETCD 的租約機制

Etcd 中支援類似 Redis 中的 key 過期機制,使用這一功能配合其他etcd功能可以實現非常多的強大的功能,例如:分散式鎖,服務發現等。

1. 獲取租約例項

> 要使用 etcd 的租約需要獲得租約的 Lease 例項,我們先建立一個:

// 使用clientv3 建立一個lease的例項,傳入 etcd 的 client 例項
lease := clientv3.New(client)

2. 申請租約

獲得例項申請一個租約,然後拿到租約 ID

// 申請租約使用lease例項的Grant方法,第一個引數還是上下文,第二個是TTL,過期時間
grant, err := lease,Grant(context.TODO(), 5)
// 拿到租約 ID, 拿到這個租約 ID 之後,就可以使用 kv 例項進行 put 操作,加上option引數的 WithLease, 就可以給一個 key 設定過期時間
leaseID := grant.ID

3. 續租

既然是租約,當然是可以續約的,續租有兩種方式,一種是自動續租,一種是手動續租。

自動續租

// 自動續租時需要傳入要續租的租約 ID,lease的 KeepAlive 會啟動一個協程執行自動續約,每次續約事件是我們第一次申請租約時設定的時間。
    aliveChan, err := lease.KeepAlive(context.TODO(), leaseID)

> 自動續租返回一個續租的結果,是一個channel,裡面放著續租應答。下面是一個完整的續租程式碼

package main

import (
    "context"
    "fmt"
    "go.etcd.io/etcd/clientv3"
    "time"
)

func main() {
    cli, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"},
        DialTimeout: 5 * time.Second,
    })
    if err != nil {
        fmt.Println(err.Error())
    }
    // Get a lease instance
    lease := clientv3.NewLease(cli)
    grant, err := lease.Grant(context.TODO(), 10)
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    // lease id
    leaseID := grant.ID
    // 自動續租
    alive, err := lease.KeepAlive(context.TODO(), leaseID)
    if err != nil {
        fmt.Println(err)
        return
    }
    // 處理續租應答的協程
    go func() {
        for {
            select {
            case res := <-alive:
                if res == nil {
                    fmt.Println("租約已經失效")
                    goto END
                } else {
                    fmt.Println("自動續租應答:", res.ID)
                }
            }
        }
    END:
    }()
    // Get KV of client
    kv := clientv3.NewKV(cli)
    put, err := kv.Put(context.TODO(), "/testDir/User/user1", "11", clientv3.WithLease(leaseID))
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("寫入成功:", put.Header.Revision)

    for {
        get, err := kv.Get(context.TODO(),"/testDir/User/user1)
        if err != nil {
            fmt.Println(err)
            return
        }
        if get.Count == 0 {
            fmt.Println("租約過期了")
            break
        }
        fmt.Println("還沒過期", get.Kvs)
        time.Sleep(time.Second * 2)
    }
}

手動續租就不去記錄了。

六. ETCD 的 WATCHER

etcd 的 Watcher 可以監聽指定的 key 的各種操作,我們先獲取 watcher 例項

// 獲取 watcher 例項的方法跟之前的 lease 和 kv 一樣
watcher := clientv3.NewWatcher(cli)

拿到 watcher 例項之後,還需要指定從哪個 Revision 開始監聽哪個 Key,所以還需要拿一個Key的Revision。比如我們還是想監聽 “/testDir/User/”下面所有Key的變化,我們可以這樣。

// 先 Get 我們需要監聽的 "/testDir/User/" 的getResponse,
get, err = jm.Kv.Get(context.TODO(), "/testDir/User/", clientv3.WithPrefix())
// 從get操作的下一個Revision開始監聽,這就是下一個Revision
watchStartRevision = get.Header.Revision + 1
// 然後使用watcher物件對這個目錄進行監聽
watchChan := watcher.Watch(context.TODO(), "/testDir/User/", clientv3.WithRev(watchStartRevision), clientv3.WithPrefix())
// 監聽後返回一個 Channel 裡面傳回監聽到的 "/testDir/User/" 下面的 key 的事件變化,下面是對事件變化的處理
for w := range watchChan {
        for _, event := range w.Events {
            switch event.Type {
            case mvccpb.PUT:
                fmt.Println("修改為:", string(event.Kv.Value), "revision:", event.Kv.CreateRevision)
            case mvccpb.DELETE:
                fmt.Println("刪除:", event.Kv.ModRevision)
            }
        }
    }

七. ETCD 分散式鎖原理

因為etcd使用Raft演算法保持了資料的強一致性,某次操作儲存到叢集中的值必然是全域性一致的,所以很容易實現分散式鎖。鎖服務有兩種使用方式,一是保持獨佔,二是控制時序。

保持獨佔即所有獲取鎖的使用者最終只有一個可以得到。etcd為此提供了一套實現分散式鎖原子操作CAS(CompareAndSwap)的API。透過設定prevExist值,可以保證在多個節點同時去建立某個目錄時,只有一個成功。而建立成功的使用者就可以認為是獲得了鎖。

控制時序,即所有想要獲得鎖的使用者都會被安排執行,但是獲得鎖的順序也是全域性唯一的,同時決定了執行順序。etcd為此也提供了一套API(自動建立有序鍵),對一個目錄建值時指定為POST動作,這樣etcd會自動在目錄下生成一個當前最大的值為鍵,儲存這個新的值(客戶端編號)。同時還可以使用API按順序列出所有當前目錄下的鍵值。此時這些鍵的值就是客戶端的時序,而這些鍵中儲存的值可以是代表客戶端的編號。

八. ETCD 的服務發現

服務發現要解決的也是分散式系統中最常見的問題之一,即在同一個分散式叢集中的程式或服務,要如何才能找到對方並建立連線。本質上來說,服務發現就是想要了解叢集中是否有程式在監聽udp或tcp埠,並且透過名字就可以查詢和連線。透過服務發現機制,在etcd中註冊某個服務名字的目錄,在該目錄下儲存可用的服務節點的IP。在使用服務的過程中,只要從服務目錄下查詢可用的服務節點去使用即可。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章