golang etcd簡明教程

etcd 是一個高可用強一致性的鍵值倉庫在不少分佈式系統架構中獲得了普遍的應用,本教程結合一些簡單的例子介紹golang版本的etcd/clientv3中提供的主要功能及其使用方法。git

若是還不熟悉etcd推薦先閱讀:github

看圖輕鬆瞭解etcdgolang

etcd經常使用操做介紹json

Let's get started now!segmentfault

安裝package

咱們使用v3版本的etcd client, 首先經過go get下載並編譯安裝etcd clinet v3架構

go get github.com/coreos/etcd/clientv3

該命令會將包下載到$GOPATH/src/github.com/coreos/etcd/clientv3中,全部相關依賴包會自動下載編譯,包括protobufgrpc等。mvc

官方文檔地址:https://godoc.org/github.com/...app

文檔中列出了Go官方實現的etcd client中支持的全部方法,方法仍是不少的,咱們主要梳理一下使用etcd時常常用到的主要API並進行演示。分佈式

鏈接客戶端

用程序訪問etcd首先要建立client,它須要傳入一個Config配置,這裏傳了2個選項:函數

  • Endpoints:etcd的多個節點服務地址。
  • DialTimeout:建立client的首次鏈接超時時間,這裏傳了5秒,若是5秒都沒有鏈接成功就會返回err;一旦client建立成功,咱們就不用再關心後續底層鏈接的狀態了,client內部會重連。
cli, err := clientv3.New(clientv3.Config{
   Endpoints:   []string{"localhost:2379"},
   // Endpoints: []string{"localhost:2379", "localhost:22379", "localhost:32379"}
   DialTimeout: 5 * time.Second,
})

返回的client,它的類型具體以下:

type Client struct {
    Cluster
    KV
    Lease
    Watcher
    Auth
    Maintenance
    // Username is a user name for authentication.
    Username string
    // Password is a password for authentication.
    Password string
    // contains filtered or unexported fields
}

類型中的成員是etcd客戶端幾何核心功能模塊的具體實現,它們分別用於:

  • Cluster:向集羣裏增長etcd服務端節點之類,屬於管理員操做。
  • KV:咱們主要使用的功能,即K-V鍵值庫的操做。
  • Lease:租約相關操做,好比申請一個TTL=10秒的租約(應用給key能夠實現鍵值的自動過時)。
  • Watcher:觀察訂閱,從而監聽最新的數據變化。
  • Auth:管理etcd的用戶和權限,屬於管理員操做。
  • Maintenance:維護etcd,好比主動遷移etcd的leader節點,屬於管理員操做。

咱們須要使用什麼功能,就去client裏獲取對應的成員便可。

Client.KV是一個interface`,提供了關於K-V操做的全部方法:

type KV interface {

    Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)

    Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)

    // Delete deletes a key, or optionally using WithRange(end), [key, end).
    Delete(ctx context.Context, key string, opts ...OpOption) (*DeleteResponse, error)

    // Compact compacts etcd KV history before the given rev.
    Compact(ctx context.Context, rev int64, opts ...CompactOption) (*CompactResponse, error)

    Do(ctx context.Context, op Op) (OpResponse, error)

    // Txn creates a transaction.
    Txn(ctx context.Context) Txn
}

咱們經過方法clientv3.NewKV()來得到KV接口的實現(實現中內置了錯誤重試機制):

kv := clientv3.NewKV(cli)

接下來,咱們將經過kv操做etcd中的數據。

Put

putResp, err := kv.Put(context.TODO(),"/test/key1", "Hello etcd!")

第一個參數是goroutine的上下文Context。後面兩個參數分別是key和value,對於etcd來講,key=/test/key1只是一個字符串而已,可是對咱們而言卻能夠模擬出目錄層級關係。

Put函數的聲明以下:

// Put puts a key-value pair into etcd.
// Note that key,value can be plain bytes array and string is
// an immutable representation of that bytes array.
// To get a string of bytes, do string([]byte{0x10, 0x20}).
Put(ctx context.Context, key, val string, opts ...OpOption) (*PutResponse, error)

除了上面例子中的三個的參數,還支持一個變長參數,能夠傳遞一些控制項來影響Put的行爲,例如能夠攜帶一個lease ID來支持key過時。

Put操做返回的是PutResponse,不一樣的KV操做對應不一樣的response結構,全部KV操做返回的response結構以下:

type (
   CompactResponse pb.CompactionResponse
   PutResponse     pb.PutResponse
   GetResponse     pb.RangeResponse
   DeleteResponse  pb.DeleteRangeResponse
   TxnResponse     pb.TxnResponse
)

程序代碼裏導入clientv3後在GoLand中能夠很快定位到PutResponse的定義文件中,PutResponse只是pb.PutResponse的類型別名,經過Goland跳轉過去後能夠看到PutResponse的詳細定義。

type PutResponse struct {
   Header *ResponseHeader `protobuf:"bytes,1,opt,name=header" json:"header,omitempty"`
   // if prev_kv is set in the request, the previous key-value pair will be returned.
   PrevKv *mvccpb.KeyValue `protobuf:"bytes,2,opt,name=prev_kv,json=prevKv" json:"prev_kv,omitempty"`
}

Header裏保存的主要是本次更新的revision信息,而PrevKv能夠返回Put覆蓋以前的value是什麼(目前是nil,後面會說緣由),把返回的PutResponse打印出來看一下:

fmt.Printf("PutResponse: %v, err: %v", putResp, err)

// output
// PutResponse: &{cluster_id:14841639068965178418 member_id:10276657743932975437 revision:3 raft_term:7  <nil>}, err: <nil>%

咱們須要判斷err來肯定操做是否成功。

咱們再Put其餘2個key,用於後續演示:

kv.Put(context.TODO(),"/test/key2", "Hello World!")
// 再寫一個同前綴的干擾項
kv.Put(context.TODO(), "/testspam", "spam")

如今/test目錄下有兩個鍵: key1和key2, 而/testspam並不歸屬於/test目錄

Get

使用KV的Get方法來讀取給定鍵的值:

getResp, err := kv.Get(context.TODO(), "/test/key1")

其函數聲明以下:

// Get retrieves keys.
// By default, Get will return the value for "key", if any.
// When passed WithRange(end), Get will return the keys in the range [key, end).
// When passed WithFromKey(), Get returns keys greater than or equal to key.
// When passed WithRev(rev) with rev > 0, Get retrieves keys at the given revision;
// if the required revision is compacted, the request will fail with ErrCompacted .
// When passed WithLimit(limit), the number of returned keys is bounded by limit.
// When passed WithSort(), the keys will be sorted.
Get(ctx context.Context, key string, opts ...OpOption) (*GetResponse, error)

和Put相似,函數註釋裏提示咱們能夠傳遞一些控制參數來影響Get的行爲,好比:WithFromKey表示讀取從參數key開始遞增的全部key,而不是讀取單個key。

在上面的例子中,我沒有傳遞opOption,因此就是獲取key=/test/key1的最新版本數據。

這裏err並不能反饋出key是否存在(只能反饋出本次操做由於各類緣由異常了),咱們須要經過GetResponse(其實是pb.RangeResponse)判斷key是否存在:

type RangeResponse struct {
    Header *ResponseHeader `protobuf:"bytes,1,opt,name=header" json:"header,omitempty"`
    // kvs is the list of key-value pairs matched by the range request.
    // kvs is empty when count is requested.
    Kvs []*mvccpb.KeyValue `protobuf:"bytes,2,rep,name=kvs" json:"kvs,omitempty"`
    // more indicates if there are more keys to return in the requested range.
    More bool `protobuf:"varint,3,opt,name=more,proto3" json:"more,omitempty"`
    // count is set to the number of keys within the range when requested.
    Count int64 `protobuf:"varint,4,opt,name=count,proto3" json:"count,omitempty"`
}

Kvs字段,保存了本次Get查詢到的全部k-v對,由於上述例子只Get了一個單key,因此只須要判斷一下len(Kvs)是否等於1便可知道key是否存在。

RangeResponse.MoreCount,當咱們使用withLimit()等選項進行Get時會發揮做用,至關於翻頁查詢。

接下來,咱們經過給Get查詢增長WithPrefix選項,獲取/test目錄下的全部子元素:

rangeResp, err := kv.Get(context.TODO(), "/test/", clientv3.WithPrefix())

WithPrefix()是指查找以/test/爲前綴的全部key,所以能夠模擬出查找子目錄的效果。

etcd是一個有序的k-v存儲,所以/test/爲前綴的key老是順序排列在一塊兒。

withPrefix()實際上會轉化爲範圍查詢,它根據前綴/test/生成了一個前閉後開的key range:[「/test/」, 「/test0」),爲何呢?由於比/大的字符是0,因此以/test0做爲範圍的末尾,就能夠掃描到全部以/test/爲前綴的key了。

在以前,咱們Put了一個/testspam鍵值,由於不符合/test/前綴(注意末尾的/),因此就不會被此次Get獲取到。可是,若是查詢的前綴是/test,那麼/testspam就會被返回,使用時必定要特別注意。

打印rangeResp.Kvs能夠看到得到了兩個鍵值:

[key:"/test/key1" create_revision:2 mod_revision:13 version:6 value:"Hello etcd!"  key:"/test/key2" create_revision:5 mod_revision:14 version:4 value:"Hello World!" ]

Lease

etcd客戶端的Lease對象能夠經過如下的代碼獲取到

lease := clientv3.NewLease(cli)

lease對象是Lease接口的實現,Lease接口的聲明以下:

type Lease interface {
    // Grant 建立一個新租約
    Grant(ctx context.Context, ttl int64) (*LeaseGrantResponse, error)

    // Revoke 銷燬給定租約ID的租約
    Revoke(ctx context.Context, id LeaseID) (*LeaseRevokeResponse, error)

    // TimeToLive retrieves the lease information of the given lease ID.
    TimeToLive(ctx context.Context, id LeaseID, opts ...LeaseOption) (*LeaseTimeToLiveResponse, error)

    // Leases retrieves all leases.
    Leases(ctx context.Context) (*LeaseLeasesResponse, error)

    // KeepAlive keeps the given lease alive forever.
    KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAliveResponse, error)

    // KeepAliveOnce renews the lease once. In most of the cases, KeepAlive
    // should be used instead of KeepAliveOnce.
    KeepAliveOnce(ctx context.Context, id LeaseID) (*LeaseKeepAliveResponse, error)

    // Close releases all resources Lease keeps for efficient communication
    // with the etcd server.
    Close() error
}

Lease提供瞭如下功能:

  • Grant:分配一個租約。
  • Revoke:釋放一個租約。
  • TimeToLive:獲取剩餘TTL時間。
  • Leases:列舉全部etcd中的租約。
  • KeepAlive:自動定時的續約某個租約。
  • KeepAliveOnce:爲某個租約續約一次。
  • Close:釋放當前客戶端創建的全部租約。

要想實現key自動過時,首先得建立一個租約,下面的代碼建立一個TTL爲10秒的租約:

grantResp, err := lease.Grant(context.TODO(), 10)

返回的grantResponse的結構體聲明以下:

// LeaseGrantResponse wraps the protobuf message LeaseGrantResponse.
type LeaseGrantResponse struct {
    *pb.ResponseHeader
    ID    LeaseID
    TTL   int64
    Error string
}

在應用程序代碼中主要使用到的是租約ID。

接下來咱們用這個Lease往etcd中存儲一個10秒過時的key:

kv.Put(context.TODO(), "/test/vanish", "vanish in 10s", clientv3.WithLease(grantResp.ID))

這裏特別須要注意,有一種狀況是在Put以前Lease已通過期了,那麼這個Put操做會返回error,此時你須要從新分配Lease。

當咱們實現服務註冊時,須要主動給Lease進行續約,一般是以小於TTL的間隔循環調用Lease的KeepAliveOnce()方法對租約進行續期,一旦某個服務節點出錯沒法完成租約的續期,等key過時後客戶端即沒法在查詢服務時得到對應節點的服務,這樣就經過租約到期實現了服務的錯誤隔離。

keepResp, err := lease.KeepAliveOnce(context.TODO(), grantResp.ID)

或者使用KeepAlive()方法,其會返回<-chan *LeaseKeepAliveResponse只讀通道,每次自動續租成功後會向通道中發送信號。通常都用KeepAlive()方法

KeepAlive和Put同樣,若是在執行以前Lease就已通過期了,那麼須要從新分配Lease。etcd並無提供API來實現原子的Put with Lease,須要咱們本身判斷err從新分配Lease。

Op

Op字面意思就是」操做」,Get和Put都屬於Op,只是爲了簡化用戶開發而開放的特殊API。

KV對象有一個Do方法接受一個Op:

// Do applies a single Op on KV without a transaction.
// Do is useful when creating arbitrary operations to be issued at a
// later time; the user can range over the operations, calling Do to
// execute them. Get/Put/Delete, on the other hand, are best suited
// for when the operation should be issued at the time of declaration.
Do(ctx context.Context, op Op) (OpResponse, error)

其參數Op是一個抽象的操做,能夠是Put/Get/Delete…;而OpResponse是一個抽象的結果,能夠是PutResponse/GetResponse…

能夠經過Client中定義的一些方法來建立Op:

  • func OpDelete(key string, opts …OpOption) Op
  • func OpGet(key string, opts …OpOption) Op
  • func OpPut(key, val string, opts …OpOption) Op
  • func OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op

其實和直接調用KV.Put,KV.GET沒什麼區別。

下面是一個例子:

cli, err := clientv3.New(clientv3.Config{
    Endpoints:   endpoints,
    DialTimeout: dialTimeout,
})
if err != nil {
    log.Fatal(err)
}
defer cli.Close()

ops := []clientv3.Op{
    clientv3.OpPut("put-key", "123"),
    clientv3.OpGet("put-key"),
    clientv3.OpPut("put-key", "456")}

for _, op := range ops {
    if _, err := cli.Do(context.TODO(), op); err != nil {
        log.Fatal(err)
    }
}

把Op交給Do方法執行,返回的opResp結構以下:

type OpResponse struct {
    put *PutResponse
    get *GetResponse
    del *DeleteResponse
    txn *TxnResponse
}

你的操做是什麼類型,你就用哪一個指針來訪問對應的結果。

Txn事務

etcd中事務是原子執行的,只支持if … then … else …這種表達。首先來看一下Txn中定義的方法:

type Txn interface {
    // If takes a list of comparison. If all comparisons passed in succeed,
    // the operations passed into Then() will be executed. Or the operations
    // passed into Else() will be executed.
    If(cs ...Cmp) Txn

    // Then takes a list of operations. The Ops list will be executed, if the
    // comparisons passed in If() succeed.
    Then(ops ...Op) Txn

    // Else takes a list of operations. The Ops list will be executed, if the
    // comparisons passed in If() fail.
    Else(ops ...Op) Txn

    // Commit tries to commit the transaction.
    Commit() (*TxnResponse, error)
}

Txn必須是這樣使用的:If(知足條件) Then(執行若干Op) Else(執行若干Op)。

If中支持傳入多個Cmp比較條件,若是全部條件知足,則執行Then中的Op(上一節介紹過Op),不然執行Else中的Op。

首先,咱們須要開啓一個事務,這是經過KV對象的方法實現的:

txn := kv.Txn(context.TODO())

下面的測試程序,判斷若是k1的值大於v1而且k1的版本號是2,則Put 鍵值k2和k3,不然Put鍵值k4和k5。

kv.Txn(context.TODO()).If(
 clientv3.Compare(clientv3.Value(k1), ">", v1),
 clientv3.Compare(clientv3.Version(k1), "=", 2)
).Then(
 clientv3.OpPut(k2,v2), clentv3.OpPut(k3,v3)
).Else(
 clientv3.OpPut(k4,v4), clientv3.OpPut(k5,v5)
).Commit()

相似於clientv3.Value()用於指定key屬性的,有這麼幾個方法:

  • func CreateRevision(key string) Cmp:key=xxx的建立版本必須知足…
  • func LeaseValue(key string) Cmp:key=xxx的Lease ID必須知足…
  • func ModRevision(key string) Cmp:key=xxx的最後修改版本必須知足…
  • func Value(key string) Cmp:key=xxx的建立值必須知足…
  • func Version(key string) Cmp:key=xxx的累計更新次數必須知足…

Watch

Watch用於監聽某個鍵的變化, Watch調用後返回一個WatchChan,它的類型聲明以下:

type WatchChan <-chan WatchResponse

type WatchResponse struct {
    Header pb.ResponseHeader
    Events []*Event

    CompactRevision int64

    Canceled bool

    Created bool
}

當監聽的key有變化後會向WatchChan發送WatchResponse。Watch的典型應用場景是應用於系統配置的熱加載,咱們能夠在系統讀取到存儲在etcd key中的配置後,用Watch監聽key的變化。在單獨的goroutine中接收WatchChan發送過來的數據,並將更新應用到系統設置的配置變量中,好比像下面這樣在goroutine中更新變量appConfig,這樣系統就實現了配置變量的熱加載。

type AppConfig struct {
  config1 string
  config2 string
}

var appConfig Appconfig

func watchConfig(clt *clientv3.Client, key string, ss interface{}) {
    watchCh := clt.Watch(context.TODO(), key)
    go func() {
        for res := range watchCh {
            value := res.Events[0].Kv.Value
            if err := json.Unmarshal(value, ss); err != nil {
                fmt.Println("now", time.Now(), "watchConfig err", err)
                continue
            }
            fmt.Println("now", time.Now(), "watchConfig", ss)
        }
    }()
}

watchConfig(client, "config_key", &appConfig)

golang etcd clientv3的主要功能就是這些,但願能幫你們梳理出學習脈絡,這樣工做中應用到etcd時再看官方文檔就會容易不少。

相關文章
相關標籤/搜索