文明记者 發表於 2020-1-13 11:21:00

go操作etcd

<h3>文章转自</h3>
<div class="container">&nbsp;</div>
<div id="body">
<div class="container">
<div class="col-group">
<div id="main" class="col-8">
<div class="res-cons">
<h1 class="post-title">go操作etcd</h1>
<div class="post-content">
<h1 id="etcd">etcd</h1>
<h2 id="etcd介绍">etcd介绍</h2>
<p>etcd是使用Go语言开发的一个开源的、高可用的分布式key-value存储系统,可以用于配置共享和服务的注册和发现。</p>
<p>类似项目有zookeeper和consul。</p>
<p>etcd具有以下特点:</p>
<ul>
<li>完全复制:集群中的每个节点都可以使用完整的存档</li>
<li>高可用性:Etcd可用于避免硬件的单点故障或网络问题</li>
<li>一致性:每次读取都会返回跨多主机的最新写入</li>
<li>简单:包括一个定义良好、面向用户的API(gRPC)</li>
<li>安全:实现了带有可选的客户端证书身份验证的自动化TLS</li>
<li>快速:每秒10000次写入的基准速度</li>
<li>可靠:使用Raft算法实现了强一致、高可用的服务存储目录</li>
</ul>
<h2 id="etcd应用场景">etcd应用场景</h2>
<h3 id="服务发现">服务发现</h3>
<p>服务发现要解决的也是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接。本质上来说,服务发现就是想要了解集群中是否有进程在监听 udp 或 tcp 端口,并且通过名字就可以查找和连接。</p>
<p><img src="https://www.liwenzhou.com/images/Go/etcd/etcd_01.png"></p>
<h3 id="配置中心">配置中心</h3>
<p>将一些配置信息放到 etcd 上进行集中管理。</p>
<p>这类场景的使用方式通常是这样:应用在启动的时候主动从 etcd 获取一次配置信息,同时,在 etcd 节点上注册一个 Watcher 并等待,以后每次配置有更新的时候,etcd 都会实时通知订阅者,以此达到获取最新配置信息的目的。</p>
<h3 id="分布式锁">分布式锁</h3>
<p>因为 etcd 使用 Raft 算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁。锁服务有两种使用方式,一是保持独占,二是控制时序。</p>
<ul>
<li><strong>保持独占即所有获取锁的用户最终只有一个可以得到</strong>。etcd 为此提供了一套实现分布式锁原子操作 CAS(<code>CompareAndSwap</code>)的 API。通过设置<code>prevExist</code>值,可以保证在多个节点同时去创建某个目录时,只有一个成功。而创建成功的用户就可以认为是获得了锁。</li>
<li>控制时序,即所有想要获得锁的用户都会被安排执行,但是<strong>获得锁的顺序也是全局唯一的,同时决定了执行顺序</strong>。etcd 为此也提供了一套 API(自动创建有序键),对一个目录建值时指定为<code>POST</code>动作,这样 etcd 会自动在目录下生成一个当前最大的值为键,存储这个新的值(客户端编号)。同时还可以使用 API 按顺序列出所有当前目录下的键值。此时这些键的值就是客户端的时序,而这些键中存储的值可以是代表客户端的编号。</li>
</ul>
<p><img src="https://www.liwenzhou.com/images/Go/etcd/etcd_02.png"></p>
<h2 id="为什么用-etcd-而不用zookeeper">为什么用 etcd 而不用ZooKeeper?</h2>
<p>etcd 实现的这些功能,ZooKeeper都能实现。那么为什么要用 etcd 而非直接使用ZooKeeper呢?</p>
<h3 id="为什么不选择zookeeper">为什么不选择ZooKeeper?</h3>
<ol>
<li>部署维护复杂,其使用的<code>Paxos</code>强一致性算法复杂难懂。官方只提供了<code>Java</code>和<code>C</code>两种语言的接口。</li>
<li>使用<code>Java</code>编写引入大量的依赖。运维人员维护起来比较麻烦。</li>
<li>最近几年发展缓慢,不如<code>etcd</code>和<code>consul</code>等后起之秀。</li>
</ol>
<h3 id="为什么选择etcd">为什么选择etcd?</h3>
<ol>
<li>简单。使用 Go 语言编写部署简单;支持HTTP/JSON API,使用简单;使用 Raft 算法保证强一致性让用户易于理解。</li>
<li>etcd 默认数据一更新就进行持久化。</li>
<li>etcd 支持 SSL 客户端安全认证。</li>
</ol>
<p>最后,etcd 作为一个年轻的项目,正在高速迭代和开发中,这既是一个优点,也是一个缺点。优点是它的未来具有无限的可能性,缺点是无法得到大项目长时间使用的检验。然而,目前 <code>CoreOS</code>、<code>Kubernetes</code>和<code>CloudFoundry</code>等知名项目均在生产环境中使用了<code>etcd</code>,所以总的来说,etcd值得你去尝试。</p>
<h2 id="etcd集群">etcd集群</h2>
<p>etcd 作为一个高可用键值存储系统,天生就是为集群化而设计的。由于 Raft 算法在做决策时需要多数节点的投票,所以 etcd 一般部署集群推荐奇数个节点,推荐的数量为 3、5 或者 7 个节点构成一个集群。</p>
<h3 id="搭建一个3节点集群示例">搭建一个3节点集群示例:</h3>
<p>在每个etcd节点指定集群成员,为了区分不同的集群最好同时配置一个独一无二的token。</p>
<p>下面是提前定义好的集群信息,其中<code>n1</code>、<code>n2</code>和<code>n3</code>表示3个不同的etcd节点。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">TOKEN=token-01
CLUSTER_STATE=new
CLUSTER=n1=http://10.240.0.17:2380,n2=http://10.240.0.18:2380,n3=http://10.240.0.19:2380
</pre>
</div>
<p>  </p>
<p>在<code>n1</code>这台机器上执行以下命令来启动etcd:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">etcd --data-dir=data.etcd --name n1 \
        --initial-advertise-peer-urls http://10.240.0.17:2380 --listen-peer-urls http://10.240.0.17:2380 \
        --advertise-client-urls http://10.240.0.17:2379 --listen-client-urls http://10.240.0.17:2379 \
        --initial-cluster ${CLUSTER} \
        --initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
</pre>
</div>
<p>  </p>
<p>在<code>n2</code>这台机器上执行以下命令启动etcd:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">etcd --data-dir=data.etcd --name n2 \
        --initial-advertise-peer-urls http://10.240.0.18:2380 --listen-peer-urls http://10.240.0.18:2380 \
        --advertise-client-urls http://10.240.0.18:2379 --listen-client-urls http://10.240.0.18:2379 \
        --initial-cluster ${CLUSTER} \
        --initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
</pre>
</div>
<p>  </p>
<p>在<code>n3</code>这台机器上执行以下命令启动etcd:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">etcd --data-dir=data.etcd --name n3 \
        --initial-advertise-peer-urls http://10.240.0.19:2380 --listen-peer-urls http://10.240.0.19:2380 \
        --advertise-client-urls http://10.240.0.19:2379 --listen-client-urls http://10.240.0.19:2379 \
        --initial-cluster ${CLUSTER} \
        --initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
</pre>
</div>
<p>  </p>
<p>etcd 官网提供了一个可以公网访问的 etcd 存储地址。你可以通过如下命令得到 etcd 服务的目录,并把它作为<code>-discovery</code>参数使用。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">curl https://discovery.etcd.io/new?size=3
https://discovery.etcd.io/a81b5818e67a6ea83e9d4daea5ecbc92

# grab this token
TOKEN=token-01
CLUSTER_STATE=new
DISCOVERY=https://discovery.etcd.io/a81b5818e67a6ea83e9d4daea5ecbc92


etcd --data-dir=data.etcd --name n1 \
        --initial-advertise-peer-urls http://10.240.0.17:2380 --listen-peer-urls http://10.240.0.17:2380 \
        --advertise-client-urls http://10.240.0.17:2379 --listen-client-urls http://10.240.0.17:2379 \
        --discovery ${DISCOVERY} \
        --initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}


etcd --data-dir=data.etcd --name n2 \
        --initial-advertise-peer-urls http://10.240.0.18:2380 --listen-peer-urls http://10.240.0.18:2380 \
        --advertise-client-urls http://10.240.0.18:2379 --listen-client-urls http://10.240.0.18:2379 \
        --discovery ${DISCOVERY} \
        --initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}


etcd --data-dir=data.etcd --name n3 \
        --initial-advertise-peer-urls http://10.240.0.19:2380 --listen-peer-urls http://10.240.0.19:2380 \
        --advertise-client-urls http://10.240.0.19:2379 --listen-client-urls http:/10.240.0.19:2379 \
        --discovery ${DISCOVERY} \
        --initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
</pre>
</div>
<p>  </p>
<p>到此etcd集群就搭建起来了,可以使用<code>etcdctl</code>来连接etcd。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">export ETCDCTL_API=3
HOST_1=10.240.0.17
HOST_2=10.240.0.18
HOST_3=10.240.0.19
ENDPOINTS=$HOST_1:2379,$HOST_2:2379,$HOST_3:2379

etcdctl --endpoints=$ENDPOINTS member list
</pre>
</div>
<p>  </p>
<h2>&nbsp;</h2>
<p>etcd下载:</p>
<p>  windows   </p>
<p>    官网自行下载运行二进制文件即可</p>
<p>  mac&nbsp;</p>
<p>    $ brew install etcd</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<h2>终端操作</h2>
<p>etcd 提供了&nbsp;<code>etcdctl</code>&nbsp;命令行工具 和 HTTP API 两种交互方法。<code>etcdctl</code>命令行工具用 go 语言编写,也是对 HTTP API 的封装,日常使用起来也更容易。所以这里我们主要使用&nbsp;<code>etcdctl</code>&nbsp;命令行工具演示。</p>
<p><strong>put</strong></p>
<p>应用程序通过 put 将 key 和 value 存储到 etcd 集群中。每个存储的密钥都通过 Raft 协议复制到所有 etcd 集群成员,以实现一致性和可靠性。</p>
<p>这里是设置键的值的命令&nbsp;<code>foo</code>&nbsp;到&nbsp;<code>bar</code>:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl put foo bar
OK
</pre>
</div>
<p>  </p>
</div>
<p><strong>get</strong></p>
<p>应用程序可以从一个 etcd 集群中读取 key 的值。</p>
<p>假设 etcd 集群已经存储了以下密钥:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">foo = bar
foo1 = bar1
foo2 = bar2
foo3 = bar3
a = 123
b = 456
z = 789 </pre>
</div>
<pre class="brush:go;gutter:true;">读取键为 foo 的命令:</pre>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl get foo
foo// key
bar// value </pre>
</div>
<pre class="brush:go;gutter:true;">上面同时返回了 key 和 value,怎么只读取 key 对应的值呢:</pre>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl get foo --print-value-only
bar</pre>
</div>
<pre class="brush:go;gutter:true;">以十六进制格式读取键为foo的命令:</pre>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl get foo --hex
\x66\x6f\x6f
\x62\x61\x72</pre>
</div>
<pre class="brush:go;gutter:true;">查询可以读取单个key,也可以读取一系列key:</pre>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl get foo foo3
foo
bar
foo1
bar1
foo2
bar2
请注意,foo3由于范围超过了半开放时间间隔 [foo, foo3),因此不包括在内foo3。</pre>
</div>
<pre class="brush:go;gutter:true;">按前缀读取:</pre>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl get --prefix foo
foo
bar
foo1
bar1
foo2
bar2
foo3
bar3
</pre>
</div>
<pre class="brush:go;gutter:true;">按结果数量限制读取
</pre>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl get --limit=2 --prefix foo
foo
bar
foo1
bar1</pre>
</div>
<pre class="brush:go;gutter:true;">读取大于或等于指定键的字节值的键:</pre>
<p> </p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl get --from-key b
b
456
z
789
</pre>
</div>
<pre class="brush:go;gutter:true;">应用程序可能希望通过访问早期版本的 key 来回滚到旧配置。由于对 etcd 集群键值存储区的每次修改都会增加一个 etcd 集群的全局修订版本,因此应用程序可以通过提供旧的 etcd 修订版来读取被取代的键。</pre>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">假设一个 etcd 集群已经有以下 key:

foo = bar         # revision = 2
foo1 = bar1       # revision = 3
foo = bar_new   # revision = 4
foo1 = bar1_new   # revision = 5
以下是访问以前版本 key 的示例:

$ etcdctl get --prefix foo # 访问最新版本的key
foo
bar_new
foo1
bar1_new

$ etcdctl get --prefix --rev=4 foo # 访问第4个版本的key
foo
bar_new
foo1
bar1

$ etcdctl get --prefix --rev=3 foo #访问第3个版本的key
foo
bar
foo1
bar1

$ etcdctl get --prefix --rev=2 foo #访问第3个版本的key
foo
bar

$ etcdctl get --prefix --rev=1 foo #访问第1个版本的key</pre>
</div>
<p><strong>del</strong></p>
<p>应用程序可以从一个 etcd 集群中删除一个 key 或一系列 key。</p>
<p>假设一个 etcd 集群已经有以下key:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">foo = bar
foo1 = bar1
foo3 = bar3
zoo = val
zoo1 = val1
zoo2 = val2
a = 123
b = 456
z = 789
</pre>
</div>
<p>删除 key 为&nbsp;<code>foo</code>&nbsp;的命令:</p>
</div>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl del foo
1
</pre>
</div>
<p>  </p>
</div>
<p>删除键值对的命令:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl del --prev-kv zoo
1
zoo
val</pre>
</div>
</div>
<p>删除从&nbsp;<code>foo</code>&nbsp;到&nbsp;<code>foo9</code>&nbsp;的命令:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl del foo foo9
2
</pre>
</div>
<p>删除具有前缀的键的命令:</p>
</div>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl del --prefix zoo
2
</pre>
</div>
<p>  </p>
</div>
<p>删除大于或等于键的字节值的键的命令:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl del --from-key b
2
</pre>
</div>
<p>  </p>
</div>
<p><strong>watch</strong></p>
<p>应用程序可以使用<code>watch</code>观察一个键或一系列键来监视任何更新。</p>
<p>打开第一个终端,监听&nbsp;<code>foo</code>的变化,我们输入如下命令:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl watch foo
</pre>
</div>
<p>  </p>
</div>
<p>再打开另外一个终端来对 foo 进行操作:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl put foo 123
OK
$ etcdctl put foo 456
OK
$ ./etcdctl del foo
1
</pre>
</div>
<p>  </p>
</div>
<p>第一个终端结果如下:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl watch foo
PUT
foo
123
PUT
foo
456
DELETE
foo
</pre>
</div>
<p>  </p>
</div>
<p>除了以上基本操作,<code>watch</code>&nbsp;也可以像&nbsp;<code>get</code>、<code>del</code>&nbsp;操作那样使用 prefix、rev、 hex等参数,这里就不一一列举了。</p>
<p>lock</p>
<blockquote>Distributed locks: 分布式锁,一个人操作的时候,另外一个人只能看,不能操作</blockquote>
<p><code>lock</code>&nbsp;可以通过指定的名字加锁。注意,只有当正常退出且释放锁后,lock命令的退出码是0,否则这个锁会一直被占用直到过期(默认60秒)</p>
<p>在第一个终端输入如下命令:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl lock mutex1
mutex1/326963a02758b52d
</pre>
</div>
<p>  </p>
</div>
<p>在第二个终端输入同样的命令:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl lock mutex1
</pre>
</div>
<p>  </p>
</div>
<p>从上可以发现第二个终端发生了阻塞,并未返回像&nbsp;<code>mutex1/326963a02758b52d</code>&nbsp;的字样。此时我们需要结束第一个终端的&nbsp;<code>lock</code>&nbsp;,可以使用 Ctrl+C 正常退出<code>lock</code>命令。第一个终端&nbsp;<code>lock</code>&nbsp;退出后,第二个终端的显示如下:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl lock mutex1                                                                                                                        
mutex1/694d6ee9ac069436
</pre>
</div>
<p>  </p>
</div>
<p>txn</p>
<p>txn 从标准输入中读取多个请求,将它们看做一个原子性的事务执行。事务是由条件列表,条件判断成功时的执行列表(条件列表中全部条件为真表示成功)和条件判断失败时的执行列表(条件列表中有一个为假即为失败)组成的。</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl put user frank
OK

$ ./etcdctl txn -i
compares:
value("user") = "frank"

success requests (get, put, del):
put result ok

failure requests (get, put, del):
put result failed

SUCCESS

OK

$ etcdctl get result                                                                                                                           
result
ok
</pre>
</div>
<p>  </p>
</div>
<p>解释如下:</p>
<ol>
<li>先使用 etcdctl put user frank 设置 user 为 frank</li>
<li>etcdctl txn -i 开启事务(-i表示交互模式)</li>
<li>第2步输入命令后回车,终端显示出 compares:</li>
<li>输入 value("user") = "frank",此命令是比较 user 的值与 frank 是否相等</li>
<li>第 4 步完成后输入回车,终端会换行显示,此时可以继续输入判断条件(前面说过事务由条件列表组成),再次输入回车表示判断条件输入完毕</li>
<li>第 5 步连续输入两个回车后,终端显示出 success requests (get, put, delete):,表示下面输入判断条件为真时要执行的命令</li>
<li>与输入判断条件相同,连续两个回车表示成功时的执行列表输入完成</li>
<li>终端显示 failure requests (get, put, delete):后输入条件判断失败时的执行列表</li>
<li>为了看起来简洁,此实例中条件列表和执行列表只写了一行命令,实际可以输入多行</li>
<li>总结上面的事务,要做的事情就是 user 为 frank 时设置 result 为 ok,否则设置 result 为 failed</li>
<li>事务执行完成后查看 result 值为 ok</li>
</ol>
<p><strong>compact</strong></p>
<p>正如我们所提到的,etcd保持修改,以便应用程序可以读取以前版本的 key。但是,为了避免累积无限的历史,重要的是要压缩过去的修订版本。压缩后,etcd删除历史版本,释放资源供将来使用。在压缩版本之前所有被修改的数据都将不可用。</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl compact 5
compacted revision 5

$ etcdctl get --rev=4 foo
Error: etcdserver: mvcc: required revision has been compacted
</pre>
</div>
<p>  </p>
</div>
<p><strong>lease 与 TTL</strong></p>
<p>etcd 也能为 key 设置超时时间,但与 redis 不同,etcd 需要先创建 lease,然后 put 命令加上参数 –lease= 来设置。lease 又由生存时间(TTL)管理,每个租约都有一个在授予时间由应用程序指定的最小生存时间(TTL)值。</p>
<p>以下是授予租约的命令:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl lease grant 30                                                                                                                        
lease 694d6ee9ac06945d granted with TTL(30s)

$ etcdctl put --lease=694d6ee9ac06945d foo bar
OK
</pre>
</div>
<p>  </p>
</div>
<p>以下是撤销同一租约的命令:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl lease revoke 694d6ee9ac06945d                                                                                                               
lease 694d6ee9ac06945d revoked

$ etcdctl get foo
</pre>
</div>
<p>  </p>
</div>
<p>应用程序可以通过刷新其TTL来保持租约活着,因此不会过期。</p>
<p>假设我们完成了以下一系列操作:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl lease grant 10
lease 32695410dcc0ca06 granted with TTL(10s)
</pre>
</div>
<p>  </p>
</div>
<p>以下是保持同一租约有效的命令:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl lease keep-alive 32695410dcc0ca06
lease 32695410dcc0ca06 keepalived with TTL(10)
lease 32695410dcc0ca06 keepalived with TTL(10)
lease 32695410dcc0ca06 keepalived with TTL(10)
...
</pre>
</div>
<p>  </p>
</div>
<p>应用程序可能想要了解租赁信息,以便它们可以续订或检查租赁是否仍然存在或已过期。应用程序也可能想知道特定租约所附的 key。</p>
<p>假设我们完成了以下一系列操作:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl lease grant 200
lease 694d6ee9ac06946a granted with TTL(200s)

$ etcdctl put demo1 val1 --lease=694d6ee9ac06946a
OK

$ etcdctl put demo2 val2 --lease=694d6ee9ac06946a
OK
</pre>
</div>
<p>  </p>
</div>
<p>以下是获取有关租赁信息的命令:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl lease timetolive 694d6ee9ac06946a
lease 694d6ee9ac06946a granted with TTL(200s), remaining(178s)
</pre>
</div>
<p>  </p>
</div>
<p>以下是获取哪些 key 使用了租赁信息的命令:</p>
<div class="highlight">
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">$ etcdctl lease timetolive --keys 694d6ee9ac06946a
lease 694d6ee9ac06946a granted with TTL(200s), remaining(129s), attached keys()
</pre>
</div>
<p>  </p>
</div>
<h2>&nbsp;</h2>
<h2 id="go语言操作etcd">Go语言操作etcd</h2>
<p>这里使用官方的etcd/clientv3包来连接etcd并进行相关操作。</p>
<h3 id="安装">安装</h3>
<pre><code class="language-bash">$ go get go.etcd.io/etcd/clientv3
</code></pre>
<h3 id="put和get操作">put和get操作</h3>
<p><code>put</code>命令用来设置键值对数据,<code>get</code>命令用来根据key获取值。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">package main

import (
        "context"
        "fmt"
        "time"

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

// etcd client put/get demo
// use etcd/clientv3

func main() {
        cli, err := clientv3.New(clientv3.Config{
                Endpoints:   []string{"127.0.0.1:2379"},
                DialTimeout: 5 * 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.Background(), time.Second)
        _, err = cli.Put(ctx, "q1mi", "dsb")
        cancel()
        if err != nil {
                fmt.Printf("put to etcd failed, err:%v\n", err)
                return
        }
        // get
        ctx, cancel = context.WithTimeout(context.Background(), time.Second)
        resp, err := cli.Get(ctx, "q1mi")
        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)
        }
}
</pre>
</div>
<p>  </p>
<h3 id="watch操作">watch操作</h3>
<p><code>watch</code>用来获取未来更改的通知。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">package main

import (
        "context"
        "fmt"
        "time"

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

// watch demo

func main() {
        cli, err := clientv3.New(clientv3.Config{
                Endpoints:   []string{"127.0.0.1:2379"},
                DialTimeout: 5 * 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 key:q1mi change
        rch := cli.Watch(context.Background(), "q1mi") // &lt;-chan WatchResponse
        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)
                }
        }
}
</pre>
</div>
<p>  </p>
<p>将上面的代码保存编译执行,此时程序就会等待etcd中<code>q1mi</code>这个key的变化。</p>
<p>例如:我们打开终端执行以下命令修改、删除、设置<code>q1mi</code>这个key。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">etcd&gt; etcdctl.exe --endpoints=http://127.0.0.1:2379 put q1mi "dsb2"
OK

etcd&gt; etcdctl.exe --endpoints=http://127.0.0.1:2379 del q1mi
1

etcd&gt; etcdctl.exe --endpoints=http://127.0.0.1:2379 put q1mi "dsb3"
OK
</pre>
</div>
<p>  </p>
<p>上面的程序都能收到如下通知。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">watch&gt;watch.exe
connect to etcd success
Type: PUT Key:q1mi Value:dsb2
Type: DELETE Key:q1mi Value:
Type: PUT Key:q1mi Value:dsb3
</pre>
</div>
<p>  </p>
<h3 id="lease租约">lease租约</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">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{"127.0.0.1:2379"},
                DialTimeout: time.Second * 5,
        })
        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秒钟之后, /nazha/ 这个key就会被移除
        _, err = cli.Put(context.TODO(), "/nazha/", "dsb", clientv3.WithLease(resp.ID))
        if err != nil {
                log.Fatal(err)
        }
}
</pre>
</div>
<p>  </p>
<h3 id="keepalive">keepAlive</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">package main

import (
        "context"
        "fmt"
        "log"
        "time"

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

// etcd keepAlive

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

        resp, err := cli.Grant(context.TODO(), 5)
        if err != nil {
                log.Fatal(err)
        }

        _, err = cli.Put(context.TODO(), "/nazha/", "dsb", clientv3.WithLease(resp.ID))
        if err != nil {
                log.Fatal(err)
        }

        // the key 'foo' will be kept forever
        ch, kaerr := cli.KeepAlive(context.TODO(), resp.ID)
        if kaerr != nil {
                log.Fatal(kaerr)
        }
        for {
                ka := &lt;-ch
                fmt.Println("ttl:", ka.TTL)
        }
}
</pre>
</div>
<p>  </p>
<h3 id="基于etcd实现分布式锁">基于etcd实现分布式锁</h3>
<p><code>go.etcd.io/etcd/clientv3/concurrency</code>在etcd之上实现并发操作,如分布式锁、屏障和选举。</p>
<p>导入该包:</p>
<pre><code class="language-go">$ import "go.etcd.io/etcd/clientv3/concurrency"
</code></pre>
<p>基于etcd实现的分布式锁示例:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
if err != nil {
    log.Fatal(err)
}
defer cli.Close()

// 创建两个单独的会话用来演示锁竞争
s1, err := concurrency.NewSession(cli)
if err != nil {
    log.Fatal(err)
}
defer s1.Close()
m1 := concurrency.NewMutex(s1, "/my-lock/")

s2, err := concurrency.NewSession(cli)
if err != nil {
    log.Fatal(err)
}
defer s2.Close()
m2 := concurrency.NewMutex(s2, "/my-lock/")

// 会话s1获取锁
if err := m1.Lock(context.TODO()); err != nil {
    log.Fatal(err)
}
fmt.Println("acquired lock for s1")

m2Locked := make(chan struct{})
go func() {
    defer close(m2Locked)
    // 等待直到会话s1释放了/my-lock/的锁
    if err := m2.Lock(context.TODO()); err != nil {
      log.Fatal(err)
    }
}()

if err := m1.Unlock(context.TODO()); err != nil {
    log.Fatal(err)
}
fmt.Println("released lock for s1")

&lt;-m2Locked
fmt.Println("acquired lock for s2")
</pre>
</div>
<p>  </p>
<p>输出:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:go;gutter:true;">acquired lock for s1
released lock for s1
acquired lock for s2
</pre>
</div>
<p>  </p>
<p>查看文档了解更多</p>
<h3 id="其他操作">其他操作</h3>
<p>其他操作请查看etcd/clientv3官方文档。</p>
<p>参考链接:</p>
<ul>
<li>https://etcd.io/docs/v3.3.12/demo/</li>
<li>https://www.infoq.cn/article/etcd-interpretation-application-scenario-implement-principle/</li>
</ul>
</div>
<div id="gitalk-container">&nbsp;</div>
</div>
</div>
</div>
</div>
</div>

</div>
<div id="MySignature" role="contentinfo">
    Songzhibin<br><br>
来源:https://www.cnblogs.com/binHome/p/12186436.html
頁: [1]
查看完整版本: go操作etcd