我們來回顧一下上次我們說到的 服務註冊和發現
- 分享了服務註冊和發現是什麼
- CAP 定理是什麼
- ETCD 是什麼,以及ETCD 和 Zookeeper的對比
- ETCD 的分散式鎖實現的簡單原理
要是對 服務註冊與發現,ETCD 還有點興趣的話,歡迎檢視文章 服務註冊與發現之ETCD
今天我們來看看 GO 如何去操作 ETCD ,這個開源的、高可用的分散式key-value儲存系統
感興趣的小夥伴可以看看GO 的 ETCD 官方文件
根據官方文件,我們本次分享幾個點
- 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 build
,GO 工具會直接幫我們下載相關包,並編譯好
Go 這一點真的相當不戳
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 協議》,轉載必須註明作者和本文連結