Etcd is a highly available and consistent repository of key values that is widely used in many distributed system architectures. This tutorial combines some simple examples to introduce the main features provided in the Golang version of ETCD/ClientV3 and how to use them.

If you are not familiar with etCD, it is recommended to read:

See pictures to easily understand etCD

This section describes common ETCD operations

Let’s get started now!

The installation package

Using the V3 version of the ETCD client, we first downloaded, compiled and installed ETCD Clinet V3 via go Get.

go get github.com/coreos/etcd/clientv3
Copy the code

This command will download the package to $GOPATH/src/github.com/coreos/etcd/clientv3, all related dependent package will automatically download compilation, including protobuf, GRPC, etc.

Official document address: godoc.org/github.com/…

The documentation lists all the methods supported in the ETCD Client, which is officially implemented by Go. There are a lot of methods, so let’s take a look at the main apis that are often used when using ETCD and demonstrate them.

Connecting to the client

To access the ETCD programmatically, we first create a client, which needs to pass in a Config configuration. Here we pass in two options:

  • Endpoints: service addresses of multiple etcD nodes.
  • DialTimeout: indicates the timeout period for creating the first connection of the client. This interval is 5 seconds. If no connection succeeds within 5 seconds, an err is returned. Once the client is created, we don’t have to worry about the state of the underlying connection, and the client will reconnect internally.
cli, err := clientv3.New(clientv3.Config{
   Endpoints:   []string{"localhost:2379"},
   // Endpoints: []string{"localhost:2379", "localhost:22379", "localhost:32379"}
   DialTimeout: 5 * time.Second,
})
Copy the code

Returns a client of the following type:

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
}
Copy the code

The members in the type are concrete implementations of the etCD client geometry core function modules, which are used for:

  • Cluster: Adding ETCD service nodes to a Cluster is an administrator operation.
  • KV: The main function we use, that is, the operation of the K-V key library.
  • Lease: Lease related operations, such as applying a Lease with TTL=10 seconds (applied to the key to automatically expire the key value).
  • Watcher: Watch subscriptions to listen for the latest data changes.
  • Auth: Manages etCD users and permissions, which belong to the administrator.
  • Maintenance: Maintains the ETCD. For example, the administrator actively migrates the ETCD leader node.

We need to use what function, go to the client to obtain the corresponding member.

Client.KV is an interface that provides all the methods of k-V operation:

type KV interface {

	Put(ctx context.Context, key, val string, opts ... OpOption) (*PutResponse, error) Get(ctx context.Context, keystring, 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
}
Copy the code

We obtain an implementation of the KV interface (which has a built-in error retry mechanism) by using the method clientv3.newkV () :

kv := clientv3.NewKV(cli)
Copy the code

Next, we will manipulate the data in the ETCD through KV.

Put

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

The first parameter is the Context of the goroutine. For etcd, key=/test/key1 is just a string, but for us it simulates the directory hierarchy.

The Put function is declared as follows:

// 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)Copy the code

In addition to the three arguments in the above example, there is support for a variable-length argument that can be passed controls to influence the behavior of the Put, such as a lease ID that can be carried to support key expiration.

The response structure returned by the Put operation is PutResponse. Different KV operations correspond to different response structures. All KV operations return response structures as follows:

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

After clientV3 is imported into the program code, the definition file of PutResponse can be quickly located in GoLand.PutResponse is just the type alias of Pb. PutResponse. You can see the detailed definition of PutResponse after jumping through GoLand.

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"`}Copy the code

The Header contains the latest revision information, and PrevKv can return Put to overwrite what the previous value was (currently nil). Print out the PutResponse returned:

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

// output
// PutResponse: &{cluster_id:14841639068965178418 member_id:10276657743932975437 revision:3 raft_term:7 
      
       }, err: 
       
        %
       
Copy the code

We need to determine err to determine whether the operation was successful.

Let’s Put the other two keys for a later demonstration:

kv.Put(context.TODO(),"/test/key2"."Hello World!")
// Write another interference item with the same prefix
kv.Put(context.TODO(), "/testspam"."spam")
Copy the code

There are now two keys in /test: key1 and key2, and /testspam does not belong in /test

Get

Use KV’s Get method to read the value of the given key:

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

The function is declared as follows:

// 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)Copy the code

Similar to Put, the comment in the function indicates that we can pass control arguments to influence the behavior of Get. For example, WithFromKey means reading all keys incrementing from the key argument, rather than reading a single key.

In the example above, I didn’t pass opOption, so I got the latest version of key=/test/key1.

Here err can’t tell whether the key exists (it can only tell whether the operation is abnormal due to various reasons), we need to judge whether the key exists by GetResponse (actually pb.RangeResponse) :

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"`
}
Copy the code

The Kvs field saves all the k-V pairs in the Get query. Since the example above only got a single key, we only need to determine whether len(Kvs) is equal to 1 to know whether the key exists.

Rangeresponse.more and Count, which come into play when we Get with options like withLimit(), are equivalent to paging queries.

Next, we Get all the child elements in the /test directory by adding the WithPrefix option to the Get query:

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

WithPrefix() looks for all keys prefixed by /test/, so you can simulate the effect of looking for subdirectories.

Etcd is an ordered K-V store, so keys prefixed by /test/ are always in order.

WithPrefix () actually translates to a range query, which generates an open key range based on the /test/ prefix: [” /test/ “, “/test0”, “/test/”, “/test0”, “/test/”)

Before, we Put a /testspam key, because it does not match the /test/ prefix (note the/at the end), so it will not be picked up by this Get. However, if the query is prefixed with /test, /testspam will be returned, so be careful when using it.

Print rangeresp.kvs and you can see that you get two keys:

[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!" ]
Copy the code

Lease

The Lease object of the ETCD client can be obtained using the following code

lease := clientv3.NewLease(cli)
Copy the code

The lease object is an implementation of the lease interface, which is declared as follows:

typeGrant(CTX context. context, TTL int64) (*LeaseGrantResponse, Revoke(CTX context. context, ID LeaseID) (*LeaseRevokeResponse, *LeaseRevokeResponse, *LeaseRevokeResponse, *LeaseRevokeResponse, *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 keepsfor efficient communication
	// with the etcd server.
	Close() error
}
Copy the code

Lease provides the following functions:

  • Grant: Assign a lease.
  • Revoke: Releases a lease.
  • TimeToLive: Obtains the remaining TTL time.
  • Leases: Lists Leases in all ETcds.
  • KeepAlive: automatically and periodically renew a lease.
  • KeepAliveOnce: To renew a lease once.
  • Close: releases all leases established by the current client.

To automatically expire a key, you must first create a lease. The following code creates a lease with a TTL of 10 seconds:

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

The structure of the returned grantResponse is declared as follows:

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

The lease ID is primarily used in the application code.

Next we use this Lease to store a key in the ETCD that expires in 10 seconds:

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

In particular, if the Lease expires before the Put, the Put operation will return error and you will need to reassign the Lease.

When we implement service registration, we need to proactively renew the Lease. Usually, we call the KeepAliveOnce() method of Lease in a loop with a smaller than TTL interval to renew the Lease. Once a service node fails to complete the renewal of the Lease, After the key expires, the client cannot obtain the service of the corresponding node when querying the service. In this way, service error isolation is realized by the lease expiration.

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

Or use the KeepAlive() method, which returns the <-chan *LeaseKeepAliveResponse read-only channel and sends a signal to the channel each time the automatic lease is renewed successfully. The KeepAlive() method is usually used

KeepAlive is the same as Put. If the Lease expires before execution, the Lease needs to be reallocated. Etcd does not provide an API to implement the atomic Put with Lease. We need to determine the ERR to reassign the Lease.

Op

Op literally means “action”. Both Get and Put are Op specific apis that are opened up to simplify user development.

KV objects have a Do method that accepts an 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)
Copy the code

The Op argument is an abstract operation that can be Put/Get/Delete… ; Whereas OpResponse is an abstract result, which can be PutResponse/GetResponse…

An Op can be created using some methods defined in the Client:

  • 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

It’s no different than calling kv.put, kv.get.

Here’s an example:

cli, err := clientv3.New(clientv3.Config{
    Endpoints:   endpoints,
    DialTimeout: dialTimeout,
})
iferr ! =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)
    }
}
Copy the code

Give the Op to the Do method and return the opResp structure as follows:

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

The pointer you use to access the result is what type of operation you are doing.

Txn transaction

Transactions in ETCD are executed atomically and only if… Then… The else… This expression. First look at the methods defined in Txn:

type Txn interface {
	// If takes a list of comparison. If all comparisons passed insucceed, // 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 inIf() succeed. Then(ops ... Op) Txn // Else takes a list of operations. The Ops list will be executed,if the
	// comparisons passed inIf() fail. Else(ops ... Op) Txn // Commit tries to commit the transaction. Commit() (*TxnResponse, error) }Copy the code

Txn must be used like this: If(satisfies a condition) Then(performs several ops) Else(performs several ops).

Multiple Cmp comparison conditions are passed in the If. If all conditions are met, the Op in the Then (described in the previous section) is executed; otherwise, the Op in the Else is executed.

First, we need to open a transaction, which is done using the KV object method:

txn := kv.Txn(context.TODO())
Copy the code

The following test program determines that if k1 is greater than v1 and K1’s version number is 2, Put the keys K2 and K3, otherwise Put the keys K4 and 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()
Copy the code

Similar to clientv3.value ()\ for specifying the key attribute, there are several methods:

  • Func CreateRevision(key string) Cmp: key= XXX
  • Func LeaseValue(key string) Cmp: key= XXX Lease ID must satisfy…
  • Func ModRevision(key String) Cmp: key= XXX The last modified version must meet…
  • Func Value(key string) Cmp: key= XXX
  • Func Version(key string) Cmp: key= XXX The cumulative number of updates must be…

Watch

Watch is used to listen for key changes. When called, Watch returns a WatchChan with the following type declaration:

type WatchChan <-chan WatchResponse

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

    CompactRevision int64

    Canceled bool

    Created bool
}
Copy the code

A WatchResponse is sent to the WatchChan when the listening key changes. The typical application scenario of Watch is the hot loading of system configuration. After the system reads the configuration stored in the ETCD key, we can use Watch to listen for the change of the key. The system hot-loads the configuration variables by receiving the data from the WatchChan in a separate Goroutine and applying updates to the configuration variables of the system Settings, such as updating the variable appConfig in the Goroutine as follows.

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(a) {
		for res := range watchCh {
			value := res.Events[0].Kv.Value
			iferr := 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)
Copy the code

Golang ETCD ClientV3 is the main function of these, hope to help you to sort out the learning context, so that the work of etCD application and then look at the official documents will be much easier.