美文网首页
etcd学习笔记

etcd学习笔记

作者: 不能吃的坚果j | 来源:发表于2020-08-31 17:34 被阅读0次

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

    关于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  <nil>}
    

    普通查询

    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学习笔记

          本文链接:https://www.haomeiwen.com/subject/rxhpsktx.html