ETCD 的理解和简单使用(一)

ETCD 的理解和简单使用

1. etcd介绍

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

特点:

完全复制:集群中的每个节点都可以使用完整的存档。

高可用性:etcd可用于避免硬件的单点故障和网络问题。

一致性:每次读取都会返回跨多主机的最新写入。

简单:保罗一个定义良好、面向用户的API。

安全:实现了带有可选的客户端证书身份验证的自动化TLS。

快速:每秒1W次写入的基准速度。

可靠: RAFT算法实现了强一致、高可用的服务存储目录。

2. etcd的应用场景

2.1 服务发现

服务发现:即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立了解?本质上来说,服务发现就是想要了解进群众是否有进程监听UDP和TCP端口,并且通过域名就可以查找和连接。


etcd.png

配置中心
将一些配置信息放到etcd上进行集中管理。
应用在启动的时候主动从etcd获取一次配置信息,同事,在etcd节点上注册一个WATCHER并等待,以后每次配置有更新的时候,etcd都会实时通知订阅者,一次达到获取配置信息的目的。

分布式锁

详细请参考:

  1. https://www.jianshu.com/p/3be460bf9e0f

https://www.cnblogs.com/jiujuan/p/12147809.html

https://segmentfault.com/a/1190000021603215?utm_source=tag-newest

因为etcd使用Raft算法保持了数据的强一致性,其次操作存储到集群中的值必然是全局一直的,所以很容易实现分布式锁。锁服务有两种使用方式。

  • 保持独占即所有获取锁的用户最终之后一个用户可以得到。etcd为此提供了一套实现分布式锁算子操作CAS(CompareAndswap)的API. 通过设置prevExist值,可以保证在多个节点同事创建某个目录时,只有一个成功。而创建成功的用户就可以认为是获得了锁。
  • 控制时序: 即所有想要获得所的用户都会被安排执行,但是获得所的顺序也是全局唯一的,同事决定了执行顺序。etcd也提供了一套API(自动创建有序键),对一个目录建值同时指定为POST动作,这样etcd会自动在目录下生一个当前最大值为键,存储这个新的值(客户端编号)。同时还可以使用API按顺序列出所有当前目录下的键值。此时这些建的值就是客户端的时序,而这些键中存储的值可以代表客户端的编号。

etcd集群

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

为什么用etcd而不用zookeeper?

  • etcd简单,使用Go语言编写部署简单,支持HTTP/JSON API,使用简单:使用Raft算法保证强一致性,让用户易于理解。
  • etcd默认数据一更新就进行持久化。
  • etcd支持SSL客户端安全认证。
  • zookeeper部署维护复杂,其使用的PAXOS强一致性算法难懂。官方只提供了JAVA和C两种语言的接口。
  • zookeeper 使用JAVA编写引入大量依赖。运维人员维护起来比较麻烦。

拓展:Raft

etcd下载与安装

  • 源码码下载地址https://github.com/etcd-io/etcd/releases

3. etcd命令简单使用

1. 启动etcd

`./etcd`

2. etcdclt 交互

2.1 put

通过put将key和value存储到etcd集群中。 每个存储的key都通过Raft协议复制到所有etcd集群成员,以实现一致性和可靠性。

# etcdctl put name dong    
OK
2.2 get

通过get可以从一个etcd集群中读取key的值。
现有K-V对:

age = 18
age2 = 19
name = dong
name2 = zhang
name3 = zhao
name4 = gan
- 1. 通过key来直接读取valu
$ etcdctl get name        
name # key
dong # value

- 2. 通过key来直接读取value,只显示value
$ etcdctl get name --print-value-only
dong # value

- 3. 通过key来直接读取value,只显示key
$ etcdctl get name --keys-only
name # key

- 4. 读取指定范围的key,从name~name3 左闭右开区间
$ etcdctl get name name3
name # k1
dong # v1
name2 # k2
zhang # v2

- 5. 按前缀读取
$ etcdctl get n --prefix --keys-only
name
name2
name3
name4

- 6. 读取数量限制
$ etcdctl get n --prefix --keys-only --limit 3
name
name2
name3

- 7. 读取大于或等于指定键的字节值的键(从字母b开始,包含b)
$ etcdctl get --from-key b --keys-only
name
name2
name3
name4

- 8.

2.3 del
- 1. 删除指定的 key
$ etcdctl del name
1 # 返回值,影响的个数

- 2. 删除指定的键值对
$ etcdctl del --prev-kv name
1 # 返回值,影响的个数
name # k
dong # v

- 3. 删除指定范围的key
$ etcdctl del name name3
2 # 返回受影响的个数

- 4. 删除具有前缀的key
$ etcdctl del --prefix a
2

-5. 删除大于或等于键的字节值的键的命令
$ etcdctl del --from-key b
4
2.4 watch

Watch 用于监测一个 key-value 的变化,一旦 key-value 发生更新,就会输出最新的值。

  1. 在新的终端输入etcdctl watch key监听对应的key。
  1. $ etcdctl put key 001
    OK
    $ etcdctl put key 002
    OK
    $ etcdctl del key
    1
    
    ###### 以下是etcdctl watch key的输出
    $ etcdctl watch key
    PUT # TYPE
    key # K
    001 # V
    PUT
    key
    002
    DELETE
    key
    
    
2.5 lock(分布式锁)具体可参考:

etcd 的 lock 指令对指定的 key 进行加锁。注意,只有当正常退出且释放锁后,lock 命令的退出码是 0,否则这个锁会一直被占用直到过期(默认 60 秒)。

  • 在第一个终端输入如下命令:
$ etcdctl lock mutex1
mutex1/694d7a4c4cf36947
  • 在第二个终端输入同样的命令:
$ etcdctl lock mutex1

在此可以发现第二个终端发生了阻塞,并未返回类似 mutex1/694d7a4c4cf36947的输出。此时,如果我们使用 Ctrl+C 结束了第一个终端的 lock,然后第二个终端的显示如下:

mutex1/694d7a4c4cf3694b

可见,这就是一个分布式锁的实现。

2.6 transactions(事务)

txn支持从标准输入中读取多个请求,并将他们看作一个原子性的事务执行。 事务是由条件列表,条件判断成功时的执行列表(条件列表中全部条件为真表示成功)和条件判断失败时的执行列表(条件列表中有一个为假即为失败)组成的。

$ etcdctl txn -i # 交互
compares:  
value("name") = "dong" # 条件,可以写多个

success requests (get, put, del): # 条件为true,执行的命令
put result ok

failure requests (get, put, del): # 条件为fales,执行的命令
put result failed

SUCCESS

OK


####### 因为value("name") = "dong" 为TRUE,所以命令put result ok 执行
$ etcdctl get result
result
ok


2.7 compact(压缩)

etcd 会保存数据的修订版本,以便用户可以读取旧版本的 key。但是为了避免累积无尽头的版本历史,就需要压缩过去的修订版本。压缩后,etcd 会删除历史版本并释放资源。

$ etcdctl compact 5
compacted revision 5

$ etcdctl get --rev=4 foo
Error: etcdserver: mvcc: required revision has been compacted
2.8 lease(租约)

KEY的TTL(time to live 生存时间)是etcd的重要特征之一,即设置KEY的超时时间。 与Redis不同,etcd需要先闯进lease(租约),通过put --lease=设置。 而lease又有TTL管理,以此来实现key的超时时间。

  1. 创建lease

    $ etcdctl lease grant 60 # 创建lease,注意时间是60s
    lease 694d7a4c4cf36969 granted with TTL(60s)
    $ etcdctl put --lease=694d7a4c4cf36969 name soo # 将lease设置到指定的key中
    OK
    $ etcdctl get name # 获取对应的key,以及返回值
    name
    soo
    $ etcdctl get name
    name
    soo
    $ etcdctl get name # 获取对应的key,超过存活时间,没有返回值
    
  2. 废除指定的lease

    $ etcdctl lease grant 60 # 创建lease
    lease 694d7a4c4cf36970 granted with TTL(60s)
    $ etcdctl put --lease=694d7a4c4cf36970 name soo # 将lease设置到指定的key中
    OK
    $ etcdctl lease revoke 694d7a4c4cf36970 #  撤销对应的lease
    lease 694d7a4c4cf36970 revoked
    $ etcdctl get name # 无法获取key对应的value
    
  3. 查看租约期内对应的key,以及租约的状态

    $ etcdctl lease grant 240 # 创建lease
    lease 694d7a4c4cf3697b granted with TTL(240s)
    
    $ etcdctl put --lease=694d7a4c4cf3697b name liu # 设置key
    OK
    
    $ etcdctl put --lease=694d7a4c4cf3697b name2 song # 设置key 
    OK
    
    $ etcdctl lease timetolive 694d7a4c4cf3697b # 获取租约信息
    lease 694d7a4c4cf3697b granted with TTL(240s), remaining(184s)
    
    $ etcdctl lease timetolive --keys 694d7a4c4cf3697b # 获取租约信息以及对应的key
    lease 694d7a4c4cf3697b granted with TTL(240s), remaining(165s), attached keys([name name2])
    
    $ etcdctl lease timetolive --keys 694d7a4c4cf3697b #租约已经过期所对应的返回值
    lease 694d7a4c4cf3697b already expired
    
    
  4. 续约

    $ etcdctl lease grant 30
    lease 694d7a4c4cf36984 granted with TTL(30s)
    
    $ etcdctl put --lease=694d7a4c4cf36984 name aaa
    OK
    
    $ etcdctl lease keep-alive 694d7a4c4cf36984 # 该命令不会退出,一直会续约
    lease 694d7a4c4cf36984 keepalived with TTL(30)
    lease 694d7a4c4cf36984 keepalived with TTL(30)
    lease 694d7a4c4cf36984 keepalived with TTL(30)
    lease 694d7a4c4cf36984 keepalived with TTL(30)
    

4. GO Client SDK 交互

下载对应的库:go get go get go.etcd.io/etcd/clientv3

注: 这里开启mod可能会有error: undefined: balancer.PickOptions, 需要在mod文件中:replace google.golang.org/grpc => google.golang.org/grpc v1.26.0`

1. put、get 和 del
package main

import (
    "context"
    "fmt"
    "time"

    "go.etcd.io/etcd/clientv3"
)

func main() {

    // 创建链接
    cli, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"},
        DialTimeout: 10 * time.Second,
    })
    if err != nil {
        // handle error!
        fmt.Printf("connect to etcd failed, err:%v\n", err)
        return
    }
    fmt.Println("connect to etcd success")
    defer cli.Close()

    // put
    ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
    _, err = cli.Put(ctx, "name", "dong")
    cancel()
    if err != nil {
        fmt.Printf("put to etcd failed, err:%v\n", err)
        return
    }


    // get
    ctx, cancel = context.WithTimeout(context.TODO(), time.Second)
    resp, err := cli.Get(ctx, "name")
    cancel()
    if err != nil {
        fmt.Printf("get from etcd failed, err:%v\n", err)
        return
    }
    for _, ev := range resp.Kvs {
        fmt.Printf("%s:%s\n", ev.Key, ev.Value)
    }

    // del
    delresp, err := cli.Delete(context.TODO(), "name")
    if err != nil {
        fmt.Printf("del key failed, err:%v\n", err)
        return
    }

    if delresp.Deleted == 1{
        fmt.Println("del key success!")
    }
}

2. wathc
package main

import (
    "context"
    "fmt"
    "time"

    "go.etcd.io/etcd/clientv3"
)


func main() {
        // 创建连接  
    cli, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"},
        DialTimeout: 10 * time.Second,
    })
    if err != nil {
        fmt.Printf("connect to etcd failed, err:%v\n", err)
        return
    }
    fmt.Println("connect to etcd success")
    defer cli.Close()
  
  
    // watch
    rch := cli.Watch(context.TODO(), "name") // 返回一个channel

  // 一直监听key的变化
    for wresp := range rch {
        for _, ev := range wresp.Events {
            fmt.Printf("Type: %s Key:%s Value:%s\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
        }
    }
}
3. lease
package main

import (
    "fmt"
    "time"
)

// etcd lease

import (
    "context"
    "log"

    "go.etcd.io/etcd/clientv3"
)

func main() {
    // 创建连接
    cli, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"},
        DialTimeout: 10 * time.Second,
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("connect to etcd success.")
    defer cli.Close()

    // 创建一个5秒的租约
    resp, err := cli.Grant(context.TODO(), 5)
    if err != nil {
        log.Fatal(err)
    }

    // 5秒钟之后, name 这个key就会被移除
    _, err = cli.Put(context.TODO(), "name", "dong", clientv3.WithLease(resp.ID))
    if err != nil {
        log.Fatal(err)
    }


    // 设置 keep-alive
    ch, err := cli.KeepAlive(context.TODO(), resp.ID)
    if err != nil {
        log.Fatal(err)
    }
    for {
        ka := <-ch
        fmt.Println("ttl:", ka.TTL)
    }

4. 事务
package main

import (
    "fmt"
    "time"
)

// etcd lease

import (
    "context"
    "log"

    "go.etcd.io/etcd/clientv3"
)

func main() {
    // 创建连接
    cli, err := clientv3.New(clientv3.Config{
        Endpoints:   []string{"localhost:2379"},
        DialTimeout: 10 * time.Second,
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("connect to etcd success.")
    defer cli.Close()

  // 先设置了一个key
    _, _ = cli.Put(context.TODO(), "name", "dong")
  
 // 获取事务对象 
    txn := cli.Txn(context.TODO())
  
  // 这里的if是不成立的,执行else,会将name的值设置为liu
    txnResp, err := txn.If(clientv3.Compare(clientv3.Value("name"), "=", "liu")).
        Else(clientv3.OpPut("name", "liu")).Commit() 
    if err != nil{
        fmt.Printf("commit failed, err:%v\n", err)
        return
    }

    if !txnResp.Succeeded {
        getResp, _ := cli.Get(context.TODO(), "name")
        fmt.Println(string(getResp.Kvs[0].Value))  // 打印出liu
    }
}
5. 分布式锁(待研究)

参考:

  1. https://blog.csdn.net/wohu1104/article/details/108552649
  2. http://www.topgoer.com/%E6%95%B0%E6%8D%AE%E5%BA%93%E6%93%8D%E4%BD%9C/go%E6%93%8D%E4%BD%9Cetcd/etcd%E4%BB%8B%E7%BB%8D.html
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 0 专辑概述 etcd 是云原生架构中重要的基础组件,由 CNCF 孵化托管。etcd 在微服务和 Kuberna...
    aoho阅读 18,652评论 0 2
  • 官方链接: etcd命令行 快速入门单机启动etcd本地集群启动使用goreman启动本地三节点Procfileg...
    捞月亮的阿汤哥阅读 1,805评论 0 2
  • 概览 下图中展示了etcd如何处理一个客户端请求的涉及到的模块和流程。图中淡紫色的矩形表示etcd,它包括如下几个...
    神奇的考拉阅读 6,289评论 0 14
  • 部署环境三台机子:10.42.5.18710.42.5.18810.42.5.189 在10.42.5.187机子...
    SkTj阅读 769评论 0 0
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,501评论 28 53