etcd学习笔记

本文作者:陈进坚
个人博客:https://jian1098.github.io
CSDN博客:https://blog.csdn.net/c_jian
:https://www.jianshu.com/u/8ba9ac5706b6
联系方式:[email protected]

关于etcd

简介


etcd是使用Go语言开发的一个开源的、高可用的分布式key-value存储系统,可以用于配置共享和服务的注册和发现。

特点


  • 完全复制:集群中的每个节点都可以使用完整的存档
  • 高可用性:Etcd可用于避免硬件的单点故障或网络问题
  • 一致性:每次读取都会返回跨多主机的最新写入
  • 简单:包括一个定义良好、面向用户的API(gRPC)
  • 安全:实现了带有可选的客户端证书身份验证的自动化TLS
  • 快速:每秒10000次写入的基准速度
  • 可靠:使用Raft算法实现了强一致、高可用的服务存储目录

集群


etcd 作为一个高可用键值存储系统,天生就是为集群化而设计的。由于 Raft 算法在做决策时需要多数节点的投票,所以 etcd 一般部署集群推荐奇数个节点,推荐的数量为 3、5 或者 7 个节点构成一个集群。

服务发现


服务发现要解决的是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接。本质上来说,服务发现就是想要了解集群中是否有进程在监听 udp 或 tcp 端口,并且通过名字就可以查找和连接。要解决服务发现的问题,需要有下面三大支柱,缺一不可

  • 强一致性、高可用的服务存储目录。基于 Raft 算法的 etcd 就是一个强一致性高可用的服务存储目录。

  • 一种注册服务和监控服务健康状态的机制。用户可以在 etcd 中注册服务,并且对注册的服务设置 key TTL,定时保持服务的心跳以达到监控健康状态的效果。

  • 一种查找和连接服务的机制。通过在 etcd 指定的主题(由服务名称构成的服务目录)下注册的服务也能在对应的主题下查找到。

核心组件

  • HTTP Server:用于处理用户发送的 API 请求以及其它 etcd 节点的同步与心跳信息请求。

  • Store:用于处理 etcd 支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等,是 etcd 对用户提供的大多数 API 功能的具体实现。

  • Raft:Raft 强一致性算法的具体实现,是 etcd 的核心。

  • WAL:Write Ahead Log(预写式日志),是 etcd 的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引以外,etcd 就通过 WAL 进行持久化存储。WAL 中,所有的数据提交前都会事先记录日志。Snapshot 是为了防止数据过多而进行的状态快照;Entry 表示存储的具体日志内容。

安装etcd

https://github.com/etcd-io/etcd/releases获取最新版本,下载解压得到etcd以及etcdctl两个程序(linux和windows相同)。其中etcd就是运行etcd服务的二进制文件,etcdctl是官方提供的命令行etcd客户端,使用etcdctl可以在命令行中访问etcd服务。

为了方便操作可以将两个文件添加软连接到系统环境变量中

ln -fs /root/eosio/2.0/bin/etcd /usr/local/bin/etcd
ln -fs /root/eosio/2.0/bin/etcdctl /usr/local/bin/etcdctl

查看版本

etcd --version

启动etcd


etcd

可选参数:

-name 节点名称,默认是UUID

-data-dir 保存日志和快照的目录,默认为当前工作目录

-addr 公布的ip地址和端口。 默认为127.0.0.1:2379

-bind-addr 用于客户端连接的监听地址,默认为-addr配置

-peers 集群成员逗号分隔的列表,例如 127.0.0.1:2380,127.0.0.1:2381

-peer-addr 集群服务通讯的公布的IP地址,默认为 127.0.0.1:2380.

-peer-bind-addr 集群服务通讯的监听地址,默认为-peer-addr配置

上述配置也可以设置配置文件,默认为etcd目录/etcd.conf

键值库操作


写(put)

etcdctl put name "hello world"  //新增和更新都是put

读(get)

etcdctl get name

根据前缀查询

etcdctl get name --prefix   //查找前缀为name的

删除(del)

etcdctl del name

事务(txn)

etcd中事务是原子执行的,只支持类似if … then … else …这种表达

//先赋值
etcdctl put user1 bad

//开启事务
etcdctl txn --interactive
compares:
// 输入以下内容,注意=号两边要有空格,输入结束按两次回车
value("user1") = "bad"      

//如果 user1 = bad,则执行 get user1 
success requests (get, put, del):
get user1

//如果 user1 != bad,则执行 put user1 good
failure requests (get, put, del):
put user1 good

// 运行结果,执行 success
SUCCESS

user1
bad

监听(watch)

// 当 stock1 的数值改变( put 方法)的时候,watch 会收到通知,在这之前进程会阻塞
$ etcdctl watch stock1

// 新打开终端etcd 多台服务器集群
$ export ETCDCTL_API=3
$ etcdctl put stock1 1000

//在watch 终端显示
PUT
stock1
1000

$ etcdctl watch stock --prefix
$ etcdctl put stock1 10
$ etcdctl put stock2 20

租约(lease)

lease 可以设置访问的失效时间

$ etcdctl lease grant 300   //创建一个300秒的租约
# lease 326963a02758b527 granted with TTL(300s)

$ etcdctl put sample value -- lease=326963a02758b527    //写入操作时将租约id为326963a02758b527的租约分配给sample
OK

$ etcdctl get sample

$ etcdctl lease keep-alive 326963a02758b520 //续约
$ etcdctl lease revoke 326963a02758b527     //手动释放租约
lease 326963a02758b527 revoked

# or after 300 seconds                            //自动释放租约
$ etcdctl get sample

Lease提供了几个功能:

  • Grant:分配一个租约。
  • Revoke:释放一个租约。
  • TimeToLive:获取剩余TTL时间。
  • Leases:列举所有etcd中的租约。
  • KeepAlive:自动定时的续约某个租约。
  • KeepAliveOnce:为某个租约续约一次。
  • Close:貌似是关闭当前客户端建立的所有租约。

分布式锁(lock)

分布式锁,一个人操作的时候,另外一个人只能看,不能操作

# 第一终端
$ etcdctl lock mutex1
mutex1/326963a02758b52d

# 第二终端
$ etcdctl lock mutex1

# 当第一个终端结束了,第二个终端会显示
mutex1/326963a02758b53

选举(elect)

选举节点为leader,只有leader节点才有写入的权限,普通节点只有读权限,保证数据一致性;leader节点会定时向普通节点发送心跳,当普通节点收不到心跳时会自动选举新的leader

$ etcdctl elect one p1

one/326963a02758b539
p1

# another client with the same name blocks
$ etcdctl elect one p2

//结束第一终端,第二终端显示
one/326963a02758b53e
p2

集群状态监控(endpoint)

集群健康状态检查

$ etcdctl --write-out=table endpoint status

$ etcdctl endpoint health

快照(snapshot)

用于保存etcd数据库的快照

etcdctl snapshot save my.db

Snapshot saved at my.db

etcdctl --write-out=table snapshot status my.db

集群成员管理(Member)

用于查看、添加,删除,更新成员

export ENDPOINTS=127.0.0.1:2379,127.0.0.1:2479,127.0.0.1:2579   //windows下export换成set
etcdctl member list -w table    //成员列表-w可省略
etcdctl --endpoints=$ENDPOINTS member remove b9057cfdc8ff17ce   //删除成员
etcdctl --endpoints=$ENDPOINTS member add cd3 --peer-urls=http://127.0.0.1:2180 //添加成员cd3为成员名

启动新节点

etcd --name cd3 --listen-client-urls http://127.0.0.1:2179 --advertise-client-urls http://127.0.0.1:2179 --listen-peer-urls http://127.0.0.1:2180 --initial-advertise-peer-urls http://127.0.0.1:2180 --initial-cluster-state existing --initial-cluster cd2=http://127.0.0.1:2580,cd0=http://127.0.0.1:2380,cd3=http://127.0.0.1:2180,cd1=http://127.0.0.1:2480 --initial-cluster-token etcd-cluster-1

go语言操作etcd

连接

下载驱动包

go get github.com/coreos/etcd/clientv3

连接服务

cli, err := clientv3.New(clientv3.Config{
   Endpoints:   []string{"localhost:2379"},
   // Endpoints: []string{"localhost:2379", "localhost:22379", "localhost:32379"}
   DialTimeout: 5 * time.Second,
})

读写

第一个参数是goroutine的上下文Context,后面两个参数分别是key和value。

kv := clientv3.NewKV(cli)
putResp, err := kv.Put(context.TODO(),"/test/key1", "Hello etcd!")  
// PutResp: &{cluster_id:14841639068965178418 member_id:10276657743932975437 revision:3 raft_term:7  }

普通查询

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

返回结构体

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是否存在。

按前缀查询

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

分页查询

RangeResponse.MoreCount,当我们使用withLimit()等选项进行Get时会发挥作用,相当于翻页查询。

op操作

Op字面意思就是”操作”,Get和Put都属于Op,只是为了简化用户开发而开放的特殊API。

其参数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)
    }
}

租约

创建一个租约

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

分配租约

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

如果在Put之前Lease已经过期了,那么这个Put操作会返回error,此时你需要重新分配Lease

续约

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

如果在执行之前Lease就已经过期了,那么需要重新分配Lease。etcd并没有提供API来实现原子的Put with Lease,需要我们自己判断err重新分配Lease。

事务

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

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调用后返回一个WatchChan,它的类型声明如下:

type WatchChan <-chan WatchResponse

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

    CompactRevision int64

    Canceled bool

    Created bool
}

参考文章

https://juejin.im/post/5dba5bedf265da4d461eb8ff#heading-3

https://zhuanlan.zhihu.com/p/38300827

你可能感兴趣的:(etcd学习笔记)