GO 中 ETCD 的編碼案例分享

小魔童哪吒發表於2021-06-19

我們來回顧一下上次我們說到的 服務註冊和發現

  • 分享了服務註冊和發現是什麼
  • CAP 定理是什麼
  • ETCD 是什麼,以及ETCD 和 Zookeeper的對比
  • ETCD 的分散式鎖實現的簡單原理

要是對 服務註冊與發現,ETCD 還有點興趣的話,歡迎檢視文章 服務註冊與發現之ETCD

今天我們來看看 GO 如何去操作 ETCD ,這個開源的、高可用的分散式key-value儲存系統

感興趣的小夥伴可以看看GO 的 ETCD 官方文件

pkg.go.dev/go.etcd.io/etcd/clientv...

根據官方文件,我們本次分享幾個點

  • ETCD 如何安裝
  • ETCD 裡面對於 KEY 的PUT 和GET操作
  • WATCH操作
  • Lease 租約
  • KeepAlive 保活
  • ETCD 分散式鎖的實現

ETCD 如何安裝

ETCD 的安裝和部署

這裡我們就做一個簡單的單機部署

  • github 上 下載最新的 etcd 包,github.com/etcd-io/etcd/releases/
  • 解壓後,將 etcd 和 etcdctl 拷貝到我們的 $GOBIN目錄下 , 或者加入我們系統的環境變數即可(目的是 可以直接鍵入etcd 系統能夠執行該可執行檔案)
  • 可以使用 etcd --version 檢視版本

關於 ETCD 的命令就不在此做過的分享了,今天主要是分享 GO 如何 使用 ETCD

包的安裝

本次我們使用的是 ETCD 的 clientv3 包 ,我們執行如下命令即可正確安裝 ETCD

 go get go.etcd.io/etcd/clientv3

無論你是直接執行上面的命令, 還是通過 go mod 的方式,去下載 ETCD 的 clientv3包, 可能會出現如下問題:

/root/go/pkg/mod/github.com/coreos/etcd@v3.3.25+incompatible/clientv3/balancer/picker/roundrobin_balanced.go:55:54: undefined: balancer.PickOptions
# github.com/coreos/etcd/clientv3/balancer/resolver/endpoint
/root/go/pkg/mod/github.com/coreos/etcd@v3.3.25+incompatible/clientv3/balancer/resolver/endpoint/endpoint.go:114:78: undefined: resolver.BuildOption
/root/go/pkg/mod/github.com/coreos/etcd@v3.3.25+incompatible/clientv3/balancer/resolver/endpoint/endpoint.go:182:31: undefined: resolver.ResolveNowOption

如上問題,是因為包衝突了 ,我們只需要將如下替換包的命令放到 我們 go.mod 下面即可

replace google.golang.org/grpc => google.golang.org/grpc v1.26.0

例如我的 go.mod 是這樣的

module my_etcd

go 1.15

require (
        github.com/coreos/etcd v3.3.25+incompatible // indirect
        github.com/coreos/go-semver v0.3.0 // indirect
        github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect
        github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
        github.com/gogo/protobuf v1.3.2 // indirect
        github.com/google/uuid v1.2.0 // indirect
        go.etcd.io/etcd v3.3.25+incompatible
        go.uber.org/zap v1.17.0 // indirect
        google.golang.org/grpc v1.38.0 // indirect
)

replace google.golang.org/grpc => google.golang.org/grpc v1.26.0

這裡順便插一句, go mod進行包管理的方式從 GO 1.14之後就開始有了,go mod管理包非常方便,這裡簡單分享一下如何使用

  • 在和 main.go 的同級目錄下,初始化一個go mod,執行如下命令
go mod init xxx
  • 寫好我們的程式碼在 main.go 檔案中 , 即可在 main.go 的同級目錄下執行 go build 進行編譯 go程式
  • 若編譯出現上述問題,那麼就可以在 生成的go.mod 檔案中 加入上述替換包的語句即可

包安裝好了,我們可以開始進行編碼了

ETCD 的 設定 KEY 和獲取 KEY 操作

ETCD 的預設埠是這樣的:

  • 2379

提供 HTTP API 服務

  • 2380

用來與 peer 通訊

我們開始寫一個 GET 和 PUT KEY 的DEMO

package main

import (
   "context"
   "log"
   "time"

   "go.etcd.io/etcd/clientv3"
)

func main() {

   // 設定 log 引數 ,列印當前時間 和 當前行數
   log.SetFlags(log.Ltime | log.Llongfile)

    // ETCD 預設埠號是 2379
   // 使用 ETCD 的 clientv3 包
   client, err := clientv3.New(clientv3.Config{
      Endpoints:   []string{"127.0.0.1:2379"},
      //超時時間 10 秒
      DialTimeout: 10 * time.Second,
   })

   if err != nil {
      log.Printf("connect to etcd error : %v\n", err)
      return
   }

   log.Printf("connect to etcd successfully ...")
   // defer 最後關閉 連線
   defer client.Close()

   // PUT KEY 為 name , value 為 xiaomotong
   ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   _, err = client.Put(ctx, "name", "xiaomotong")
   cancel()
   if err != nil {
      log.Printf("PUT key to etcd error : %v\n", err)
      return
   }

   // 獲取ETCD 的KEY
   ctx, cancel = context.WithTimeout(context.Background(), time.Second)
   resp, err := client.Get(ctx, "name")
   cancel()
   if err != nil {
      log.Printf("GET key-value from etcd error : %v\n", err)
      return
   }

   // 遍歷讀出 KEY 和對應的 value
   for _, ev := range resp.Kvs {
      log.Printf("%s : %s\n", ev.Key, ev.Value)
   }
}

感興趣的小夥伴可以將上述程式碼拷貝到你的環境中進行執行,即可看到你想要的答案

ETCD 的 WATCH操 作

WATCH操作就是拍一個哨兵監控某一個key對應值的變化,包括新增,刪除,修改

func main() {

   // 設定 log 引數 ,列印當前時間 和 當前行數
   log.SetFlags(log.Ltime | log.Llongfile)

    // ETCD 預設埠號是 2379
   // 使用 ETCD 的 clientv3 包
   client, err := clientv3.New(clientv3.Config{
      Endpoints:   []string{"127.0.0.1:2379"},
      DialTimeout: 10 * time.Second,
   })
   if err != nil {
      log.Printf("connect to etcd error : %v\n", err)
      return
   }

   log.Printf("connect to etcd successfully ...")

   defer client.Close()
   // 派一個哨兵 一直監視 name 的變化
   // respCh 是一個通道
   respCh := client.Watch(context.Background(), "name")
   // 若 respCh 為空,會阻塞在這裡
   for watchResp := range respCh {
      for _, v := range watchResp.Events {
         log.Printf("type =  %s , Key = %s , Value = %s\n", 
            v.Type, v.Kv.Key, v.Kv.Value)
      }
   }
}

上述程式碼因為 respCh是一個通道,若裡面沒有資料的話,下面的 for 迴圈,會阻塞的等,因此需要我們自己在終端上面模擬 新增,刪除,修改 name 對應的值,那麼,我們的程式就會做出對應的相應

例如,我在終端命令中敲入:etcdctl --endpoints=http://127.0.0.1:2379 put name "xiaomotong"

那麼,我們上述程式碼執行的程式就會輸出如下語句

./my_etcd
22:18:39 /home/xiaomotong/my_etcd/main.go:23: connect to etcd successfully ...
22:18:43 /home/xiaomotong/my_etcd/main.go:31:type =  PUT , Key = name , Value = xiaomotong

ETCD 的 LEASE 操作

LEASE ,租約,就是將自己的某一個 key 設定一個有效時間 / 過期時間,類似於 REDIS 裡面的 SETNX

func main() {

   // 設定 log 引數 ,列印當前時間 和 當前行數
   log.SetFlags(log.Ltime | log.Llongfile)
   // ETCD 預設埠號是 2379
   // 使用 ETCD 的 clientv3 包
   client, err := clientv3.New(clientv3.Config{
      Endpoints:   []string{"127.0.0.1:2379"},
      DialTimeout: 10 * time.Second,
   })
   if err != nil {
      log.Printf("connect to etcd error : %v\n", err)
      return
   }

   log.Printf("connect to etcd successfully ...")

   defer client.Close()

   // 我們建立一個 20秒鐘的租約
   resp, err := client.Grant(context.TODO(), 20)
   if err != nil {
      log.Printf("client.Grant error : %v\n", err)
      return
   }

   // 20秒鐘之後, /name 這個key就會被移除
   _, err = client.Put(context.TODO(), "/name", "xiaomotong", clientv3.WithLease(resp.ID))
   if err != nil {
      log.Printf("client.Put error : %v\n", err)
      return
   }
}

上述 name , 20 秒鐘之後 就會自動失效

ETCD 的保活操作

順便說一下,keepalived 也是一個開源的元件,用作高可用,感興趣的可以深入瞭解一下

此處的 keepalived 是 保活, 這裡是 ETCD 的保活, 可以在上述程式碼中做一個調整,上述的 name ,不失效

func main() {

   // 設定 log 引數 ,列印當前時間 和 當前行數
   log.SetFlags(log.Ltime | log.Llongfile)
   // ETCD 預設埠號是 2379
   // 使用 ETCD 的 clientv3 包
   client, err := clientv3.New(clientv3.Config{
      Endpoints:   []string{"127.0.0.1:2379"},
      DialTimeout: 10 * time.Second,
   })
   if err != nil {
      log.Printf("connect to etcd error : %v\n", err)
      return
   }

   log.Printf("connect to etcd successfully ...")

   defer client.Close()

   // 我們建立一個 20秒鐘的租約
   resp, err := client.Grant(context.TODO(), 20)
   if err != nil {
      log.Printf("client.Grant error : %v\n", err)
      return
   }

   // 20秒鐘之後, /name 這個key就會被移除
   _, err = client.Put(context.TODO(), "/name", "xiaomotong", clientv3.WithLease(resp.ID))
   if err != nil {
      log.Printf("client.Put error : %v\n", err)
      return
   }

   // 這個key  name ,將永久被儲存
   ch, kaerr := client.KeepAlive(context.TODO(), resp.ID)
   if kaerr != nil {
      log.Fatal(kaerr)
   }
   for {
      ka := <-ch
      log.Println("ttl:", ka.TTL)
   }
}

我們可以看看 keepalived 的官方說明 ,

KeepAlive 使給定的租約永遠存活。如果傳送到通道的 keepalive 響應沒有立即被使用,租期客戶端將至少每秒鐘繼續向 etcd 伺服器傳送keepalive 請求,直到使用最新的響應。

// KeepAlive keeps the given lease alive forever. If the keepalive response
// posted to the channel is not consumed immediately, the lease client will
// continue sending keep alive requests to the etcd server at least every
// second until latest response is consumed.
//
// The returned "LeaseKeepAliveResponse" channel closes if underlying keep
// alive stream is interrupted in some way the client cannot handle itself;
// given context "ctx" is canceled or timed out. "LeaseKeepAliveResponse"
// from this closed channel is nil.
//
// If client keep alive loop halts with an unexpected error (e.g. "etcdserver:
// no leader") or canceled by the caller (e.g. context.Canceled), the error
// is returned. Otherwise, it retries.
//
// TODO(v4.0): post errors to last keep alive message before closing
// (see https://github.com/coreos/etcd/pull/7866)
KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAliveResponse, error)

來看看 ETCD 的分散式鎖實現

這裡需要引入一個新的包,"github.com/coreos/etcd/clientv3/concurrency"

不過使用go mod 管理方式的小夥伴就不用操心了, 寫完程式碼,直接 go buildGO 工具會直接幫我們下載相關包,並編譯好

Go 這一點真的相當不戳

img

package main

import (
    "context"
    "github.com/coreos/etcd/clientv3"
    "github.com/coreos/etcd/clientv3/concurrency"
    "log"
)
func main (){

   // 設定 log 引數 ,列印當前時間 和 當前行數
   log.SetFlags(log.Ltime | log.Llongfile)

   // ETCD 預設埠號是 2379
   // 使用 ETCD 的 clientv3 包
   // Endpoints 需填入 url 列表
   client, err := clientv3.New(clientv3.Config{Endpoints: []string{"/name"}})
   if err != nil {
      log.Printf("connect to etcd error : %v\n", err)
      return
   }
   defer client.Close()

   // 建立第一個 會話
   session1, err := concurrency.NewSession(client)
   if err != nil {
      log.Printf("concurrency.NewSession 1 error : %v\n", err)
      return
   }
   defer session1.Close()
   // 設定鎖
   myMu1 := concurrency.NewMutex(session1, "/lock")

   // 建立第二個 會話
   session2, err := concurrency.NewSession(client)
   if err != nil {
      log.Printf("concurrency.NewSession 2 error : %v\n", err)
      return
   }
   defer session2.Close()
   // 設定鎖
   myMu2 := concurrency.NewMutex(session2, "/lock")

   // 會話s1獲取鎖
   if err := myMu1.Lock(context.TODO()); err != nil {
      log.Printf("myMu1.Lock error : %v\n", err)
      return
   }
   log.Println("Get session1 lock ")


   m2Chan := make(chan struct{})
   go func() {
      defer close(m2Chan)
      // 如果加鎖不成功會阻塞,知道加鎖成功為止
      // 這裡是使用一個通道的方式來通訊
      // 當 myMu2 能加鎖成功,說明myMu1 解鎖成功
      // 當 myMu2 加鎖成功的時候,會關閉 通道
      // 關閉通道,從通道中讀出來的就是nil
      if err := myMu2.Lock(context.TODO()); err != nil {
         log.Printf("myMu2.Lock error : %v\n", err)
         return
      }
   }()

   // 解鎖
   if err := myMu1.Unlock(context.TODO()); err != nil {
      log.Printf("myMu1.Unlock error : %v\n", err)
      return
   }
   log.Println("Release session1 lock ")

   // 讀取到nil
   <-m2Chan

   log.Println("Get session2 lock")
}

在上述程式碼中,我們建立 2 個會話來模擬分散式鎖

我們先讓第 1 個會話拿到鎖, 並且第 2 個會話會去嘗試加鎖

當 第 2個會話,正確加鎖成功的時候, 會關閉一個通道,來確認自己真的加到鎖了

上述第 2 個會話加鎖的邏輯如下:

  • 如果加鎖不成功會阻塞,知道加鎖成功為止
  • 這裡是使用一個通道的方式來通訊
  • myMu2 能加鎖成功,說明 myMu1 解鎖成功
  • myMu2 加鎖成功的時候,會關閉 m2Chan 通道
  • 關閉通道,從 m2Chan通道中讀出來的就是nil , 確認會話 2 加鎖成功

總結

  • 分享了ETCD的簡單單點部署,ETCD 使用到的包安裝,以及會遇到的問題
  • ETCD 的設定 和 獲取KEY
  • ETCD 的WATCH 監控 KEY的簡化
  • ETCD 的租約 和保活機制
  • ETCD 的分散式鎖的簡單實現

如上的編碼案例,大家可以拿下來自己執行看看效果,一起學習,一起進步

若想更多的深入瞭解和學習,可以看文章最開始說到的官方文件,官方文件中的案例更加詳盡

具體的原始碼也是非常詳細的,就怕你學不會

歡迎點贊,關注,收藏

朋友們,你的支援和鼓勵,是我堅持分享,提高質量的動力

好了,本次就到這裡,下一次 分享GO 中 string 的實現原理

技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。

我是小魔童哪吒,歡迎點贊關注收藏,下次見~

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

相關文章