etcd介绍
etcd 是一个分布式键值对存储,设计用来可靠而快速的保存关键数据并提供访问。
etcd 基于 Raft 协议,通过复制日志文件的方式来保证数据的强一致性。
客户端应用写一个 key 时,首先会存储到 etcd Leader 上,然后再通过 Raft 协议复制到 etcd 集群的所有成员中,以此维护各成员(节点)状态的一致性与实现可靠性。
虽然 etcd 是一个强一致性的系统,但也支持从非 Leader 节点读取数据以提高性能,而且写操作仍然需要 Leader 支持,所以当发生网络分时,写操作仍可能失败。
etcd 具有一定的容错能力,假设集群中共有N个节点,即便集群中( n-1) /2个节点发生了故障,只要剩下的( n+1) /2 个节点达成一致, 也能操作成功,因此,它能够有效地应对网络分区和机器故障带来的数据丢失风险。
etcd 默认数据一更新就落盘持久化,数据持久化存储使用 WAL (write ahead log) ,预写式日志。
格式 WAL 记录了数据变化的全过程,在 etcd 中所有数据在提交之前都要先写入 WAL 中; etcd Snapshot (快照)文件则存储了某一时刻 etcd 的所有数据,默认设置为每 10 000 条记录做一次快照,经过快照后WAL 文件即可删除。
key-val 存储
就像在标准文件系统中一样,将数据存储在分层组织的目录中。
特点
- 完全复制:集群中的每个节点都可以使用完整的存档
- 高可用性:Etcd可用于避免硬件的单点故障或网络问题
- 一致性:每次读取都会返回跨多主机的最新写入
- 简单:包括一个定义良好、面向用户的API(gRPC)
- 安全:实现了带有可选的客户端证书身份验证的自动化TLS
- 快速:每秒10000次写入的基准速度
- 可靠:使用Raft算法实现了强一致、高可用的服务存储目录
相关术语
术语 | 描述 |
---|---|
Node(节点) | Node(节点) 是 raft 状态机的一个实例。它有唯一标识,如果它是leader,会内部记录其他节点的信息。 |
Member(成员) | Member(成员是) etcd 的一个实例。它承载一个 node/节点,并为 client (客户端)提供服务。 |
Cluster(集群) | Cluster(集群)由多个 member(成员)组成。每个成员的节点遵循 raft 一致性协议来复制日志。集群从成员中接收提案,提交他们并应用到本地存储。 |
Peer(同伴) | Peer(同伴)是同一个集群中的其他成员。 |
Proposal(提议) | 提议是一个需要完成 raft 协议的请求(例如写请求,配置修改请求)。 |
Client(客户端) | Client(客户端)是集群 HTTP API 的调用者。对于 V3 版本,应该包括 gRPC API。 |
Machine(机器) | 该术语已弃用。在 2.0 版本之前,在 etcd 中的成员备选。 |
etcd HTTP Server | 用于处理用户发送的 API 请求以及其它 etcd 节点的同步与心跳信息请求。 |
etcd Store | 用于处理 etcd 支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等,是 etcd 对用户提供的大多数 API 功能的具体实现。 |
Raft | Raft 强一致性算法的具体实现,是 etcd 的核心。 |
WAL | Write Ahead Log(预写式日志),是 etcd 的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引以外,etcd 就通过 WAL 进行持久化存储。WAL 中, 所有的数据提交前都会事先记录日志。Snapshot 是为了防止数据过多而进行的状态快照;Entry 表示存储的具体日志内容。 |
应用场景
etcd 可用于:
- 共享配置
- 服务发现
- 分布式锁或一致性保障
- 分布式数据队列
- 分布式通知和协调
- 集群选举
服务发现
服务发现要解决的也是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接。
本质上来说,服务发现就是想要了解集群中是否有进程在监听 udp 或 tcp 端口,并且通过名字就可以查找和连接。
配置中心
将一些配置信息放到 etcd 上进行集中管理。
应用在启动的时候主动从 etcd 获取一次配置信息,同时,在 etcd 节点上注册一个 Watcher 并等待,以后每次配置有更新的时候,etcd 都会实时通知订阅者,以此达到获取最新配置信息的目的。
分布式锁
因为 etcd 使用 Raft 算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁。锁服务有两种使用方式,一是保持独占,二是控制时序。
- 保持独占:
即所有获取锁的用户最终只有一个可以得到
。etcd 为此提供了一套实现分布式锁原子操作 CAS(CompareAndSwap)的 API。通过设置prevExist值,可以保证在多个节点同时去创建某个目录时,只有一个成功。而创建成功的用户就可以认为是获得了锁。 - 控制时序:即所有想要获得锁的用户都会被安排执行,但是
获得锁的顺序也是全局唯一的,同时决定了执行顺序
。etcd 为此也提供了一套 API(自动创建有序键),对一个目录建值时指定为POST动作,这样 etcd 会自动在目录下生成一个当前最大的值为键,存储这个新的值(客户端编号)。同时还可以使用 API 按顺序列出所有当前目录下的键值。此时这些键的值就是客户端的时序,而这些键中存储的值可以是代表客户端的编号。
对比zk
- 简单。使用 Go 语言编写部署简单;支持HTTP/JSON API,使用简单;使用 Raft 算法保证强一致性让用户易于理解。
- etcd 默认数据一更新就进行持久化。
- etcd 支持 SSL 客户端安全认证。
- zk部署维护复杂,其使用的Paxos强一致性算法复杂难懂。官方只提供了Java和C两种语言的接口。
- zk使用Java编写引入大量的依赖。运维人员维护起来比较麻烦。
- zk最近几年发展缓慢,不如etcd和consul等后起之秀。
Etcd 提供什么能力
- 提供存储以及获取数据的接口,它通过协议保证 Etcd 集群中的多个节点数据的强一致性。用于存储元信息以及共享配置。
- 提供监听机制,客户端可以监听某个key或者某些key的变更(v2和v3的机制不同)。用于监听和推送变更。
- 提供key的过期以及续约机制,客户端通过定时刷新来实现续约(v2和v3的实现机制也不一样)。用于集群监控以及服务注册发现。
- 提供原子的CAS(Compare-and-Swap)和 CAD(Compare-and-Delete)支持(v2通过接口参数实现,v3通过批量事务实现)。用于分布式锁以及leader选举。
etcd可以扮演两大角色:持久化的键值存储系统 分布式系统数据一致性服务提供者
etcd集群搭建
etcd 作为一个高可用键值存储系统,天生就是为集群化而设计的。由于 Raft 算法在做决策时需要多数节点的投票,所以 etcd 一般部署集群推荐奇数个节点,推荐的数量为 3、5 或者 7 个节点构成一个集群。
静态启动——搭建一个3节点集群
在每个etcd节点指定集群成员,为了区分不同的集群最好同时配置一个独一无二的token。
下面是提前定义好的集群信息,其中n1、n2和n3表示3个不同的etcd节点。
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
二进制启动
参数说明:
1. --name:etcd节点名称
2. --initial-advertise-peer-urls:列出为集群成员之间通信的 URLs 地址
3. --listen-peer-urls:列出用于建立 peer 监听的 URLs 列表
4. --advertise-client-urls:要向公众公布的此成员的客户端 URLs 的列表。与 etcd 集群通信的机器可以访问该的客户端 url 。etcd 客户端解析这些 url 以连接到集群。
5. --listen-client-urls:客户端访问监听的 URLs 列表
6. --initial-cluster:集群初始成员配置
7. --initial-cluster-state:初始化集群状态('new' 或 'existing')
8. --initial-cluster-token:在集群启动期间初始化的 token 值
- 计算机1
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}
- 计算机2
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}
- 计算机3
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}
docker搭建
# For each machine
ETCD_VERSION=v3.0.0
TOKEN=my-etcd-token
CLUSTER_STATE=new
NAME_1=etcd-node-0
NAME_2=etcd-node-1
NAME_3=etcd-node-2
HOST_1=192.168.71.128
HOST_2=192.168.71.129
HOST_3=192.168.71.130
CLUSTER=${NAME_1}=http://${HOST_1}:2380,${NAME_2}=http://${HOST_2}:2380,${NAME_3}=http://${HOST_3}:2380
# For node 1
THIS_NAME=${NAME_1}
THIS_IP=${HOST_1}
sudo docker run --net=host --name etcd quay.io/coreos/etcd:${ETCD_VERSION} \
/usr/local/bin/etcd \
--data-dir=data.etcd --name ${THIS_NAME} \
--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \
--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
# For node 2
THIS_NAME=${NAME_2}
THIS_IP=${HOST_2}
sudo docker run --net=host --name etcd quay.io/coreos/etcd:${ETCD_VERSION} \
/usr/local/bin/etcd \
--data-dir=data.etcd --name ${THIS_NAME} \
--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \
--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
# For node 3
THIS_NAME=${NAME_3}
THIS_IP=${HOST_3}
sudo docker run --net=host --name etcd quay.io/coreos/etcd:${ETCD_VERSION} \
/usr/local/bin/etcd \
--data-dir=data.etcd --name ${THIS_NAME} \
--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \
--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \
--initial-cluster ${CLUSTER} \
--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
检验集群健康并可以到达:
ETCDCTL_API=3 etcdctl --endpoints=http://192.168.71.128:2379,http://192.168.71.129:2379,http://192.168.71.130:2379 endpoint health
动态发现机制搭建
基于 etcd 的发现比较适合在部署时还无法确定节点 IP 等信息的场景,例如公有云环境的云主机。
在使用该方法之前要求,先要已经有一个 etcd 集群可用了。
定制 etcd 发现服务
发现使用已有集群来启动自身。如果使用私有的 etcd 集群,可以创建像这样的 URL:
curl -X PUT https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83/_config/size -d value=3
etcd 成员将使用 https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83 目录来注册。
每个成员必须有指定不同的名字标记。 Hostname 或者 machine-id 是个好选择。否则发现会因为重复名字而失败:
$ etcd --name infra0 --initial-advertise-peer-urls http://10.0.1.10:2380 \
--listen-peer-urls http://10.0.1.10:2380 \
--listen-client-urls http://10.0.1.10:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://10.0.1.10:2379 \
--discovery https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
$ etcd --name infra1 --initial-advertise-peer-urls http://10.0.1.11:2380 \
--listen-peer-urls http://10.0.1.11:2380 \
--listen-client-urls http://10.0.1.11:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://10.0.1.11:2379 \
--discovery https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
$ etcd --name infra2 --initial-advertise-peer-urls http://10.0.1.12:2380 \
--listen-peer-urls http://10.0.1.12:2380 \
--listen-client-urls http://10.0.1.12:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://10.0.1.12:2379 \
--discovery https://myetcd.local/v2/keys/discovery/6c007a14875d53d9bf0ef5a6fc0257c817f0fb83
公共 etcd 发现服务
公共的 discovery 就是通过 CoreOS 提供的公共 discovery 服务申请 token。
如果没有现成的集群可用,可以使用托管在 discovery.etcd.io 的公共发现服务。为了使用"new" endpoint来创建私有发现URL,使用命令:
curl https://discovery.etcd.io/new?size=3
以上命令会生成一个链接样式的 token,参数 size 代表要创建的集群大小,即: 有多少集群节点。
返回结果类似
https://discovery.etcd.io/14104b8f05d179cf4d59b61af5366e95You
这将创建带有初始化预期大小为3个成员的集群。如果没有指定大小,将使用默认值3。
在 etcd 服务启动时,可以指定该 token
使用环境变量的方式
ETCD_DISCOVERY=https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
等价的命令行启动参数
--discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
然后执行相同的命令:
$ etcd --name infra0 --initial-advertise-peer-urls http://10.0.1.10:2380 \
--listen-peer-urls http://10.0.1.10:2380 \
--listen-client-urls http://10.0.1.10:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://10.0.1.10:2379 \
--discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
…
使用 DNS 启动 etcd 集群
etcdctl客户端
etcd 大版本分为 V2 和 V3 版本,两个版本的 API 不兼容,因此在访问 etcd 时需要指定通讯的 API 版本
// 设置接口为v3版本
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
常用命令
key-val操作:
# 帮助命令
etcdctl -h
# 设置key-val
etcdctl put /user.rpc/127.0.0.1:8000; 127.0.0.1:8000
# 获取key
etcdctl get /user.rpc/127.0.0.1:8000
# 以16进制返回
etcdctl get /user.rpc/127.0.0.1:8000 --hex
# 获取范围内的值,[左,右)
etcdctl get /user.rpc/127.0.0.1:8000 /user.rpc/127.0.0.1:8004
# 获取指定前缀的key
etcdctl get --prefix /user.rpc
# limit限制返回的key数量
etcdctl get --prefix /user.rpc --limit 3
# 读取字典顺序大于或等于name的key
etcdctl get --from-key /user.rpc
# 获取key的版本号
etcdctl get /user.rpc/127.0.0.1:8000 -w=json
# 访问历史的key第八个版本
etcdctl get -rev=8 /user.rpc/127.0.0.1:800
# 返回字段解析
1. cluster_id:请求的集群ID
2. member_id:请求的节点ID
3. revision:etcd服务端全局数据版本号,对key的put和delete都会导致revision自增1
4. raft_term:etcd当前raft主节点任期号
5. mod_revision:当前key最后一次修改时全局数据版本号revision的值
6. create_revision:key创建时revision的值
7. version:当前key的版本号,创建时为1,put会使version++
# 删除一个key
etcdctl del /user.rpc/127.0.0.1:8000
# 删除范围内的key
etcdctl del /user.rpc/127.0.0.1:8000 /user.rpc/127.0.0.1:8004
# 删除同时获取值
etcdctl del --pre-kv /user.rpc/127.0.0.1:8000
watch和租约操作:
# watch一个key-val的变化
etcdctl watch /user.rpc/127.0.0.1:8000
# 获取租约
etcdctl lease grant 30
# 赋给key
etcdctl put --lease=******** key val
# 撤销租约
etcdctl lease revoke **********
# 刷新租约
etcdctl lease keep-alive **********
# 查询租约
etcdctl lease timetolive *********
# 查看租约和赋给的key(一个租约可以绑定多个key)
etcdctl lease timetolive --keys *********
权限管理(RBAC)
# 查看权限是否开启
etcdctl auth status
# 开启权限控制
etcdctl auth enable
# 创建用户
etcdctl user add root
>Password:123456
>Again Password:123456
# 创建test2角色
etcdctl role add test2
# 给角色赋予name目录读写权限
etcdctl role grant-permission test2 readwrite /name
# 用户关联角色,root关联test2权限
etcdctl user gran-role root test2
# 操作目录
etcdctl put /name val --user=root
# 回收权限
etcdctl role revoke role_name /name
# 删除角色
etcdctl role del test
# 查看角色列表
etcdctl role list
# 查看指定角色的权限
etcdctl role get test
# 查看用户列表
etcdctl user list --user=root
集群权限管理
# 添加集群root用户,默认拥有所有权限
etcdctl --endpoints node1(ip:port),node2,node3 user add root
# 开启身份认证
etcdctl --endpoints node1,node2,node3 --user=root --password=123456 auth enable
# 操作
etcdctl --endpoints node1,node2,node3 --user==root --password=123456 put key val
# 设置普通用户
etcdctl --user=root --password=123456 --endpoints=node1,node2,node3 user add user1
其余操作与上文一致,只需要添加--endpoints
选项options
租约
etcd 架构
网络层:提供网络数据读写功能,监听服务端口,完成集群节点之间数据通信,收发客户端数据。
Raft模块:Raft强一致性算法的具体实现。
存储模块:涉及KV存储、WAL文件、Snapshot管理等,用户处理etcd支持的各类功能的事务,包括数据索引 节点状态变更、监控与反馈、事件处理与执行 ,是 etcd 对用户提供的大多数 API 功能的具体实现。
复制状态机:这是一个抽象的模块,状态机的数据维护在内存中,定期持久化到磁盘,每次写请求都会持久化到 WAL 文件,并根据写请求的内容修改状态机数据。
etcd整体架构
-
消息入口 一个etcd节点运行以后,有3个通道接收外界消息,以kv数据的增删改查请求处理为例,介绍这3个通道的工作机制。
-
client的http调用:会通过注册到http模块的keysHandler的ServeHTTP方法处理。解析好的消息调用EtcdServer的Do()方法处理。(图中2)
-
client的grpc调用:启动时会向grpc server注册quotaKVServer对象,quotaKVServer是以组合的方式增强了kvServer这个数据结构。grpc消息解析完以后会调用kvServer的Range、Put、DeleteRange、Txn、Compact等方法。kvServer中包含有一个RaftKV的接口,由EtcdServer这个结构实现。所以最后就是调用到EtcdServer的Range、Put、DeleteRange、Txn、Compact等方法。(图中1)
-
节点之间的grpc消息:每个EtcdServer中包含有Transport结构,Transport中会有一个peers的map,每个peer封装了节点到其他某个节点的通信方式。包括streamReader、streamWriter等,用于消息的发送和接收。streamReader中有recvc和propc队列,streamReader处理完接收到的消息会将消息推到这连个队列中。由peer去处理,peer调用raftNode的Process方法处理消息。(图中3、4)
-
EtcdServer消息处理 对于客户端消息,调用到EtcdServer处理时,一般都是先注册一个等待队列,调用node的Propose方法,然后用等待队列阻塞等待消息处理完成。Propose方法会往propc队列中推送一条MsgProp消息。 对于节点间的消息,raftNode的Process是直接调用node的step方法,将消息推送到node的recvc或者propc队列中。 可以看到,外界所有消息这时候都到了node结构中的recvc队列或者propc队列中。(图中5)
-
node处理消息 node启动时会启动一个协程,处理node的各个队列中的消息,当然也包括recvc和propc队列。从propc和recvc队列中拿到消息,会调用raft对象的Step方法,raft对象封装了raft的协议数据和操作,其对外的Step方法是真正raft协议状态机的步进方法。当接收到消息以后,根据协议类型、Term字段做相应的状态改变处理,或者对选举请求做相应处理。对于一般的kv增删改查数据请求消息,会调用内部的step方法。内部的step方法是一个可动态改变的方法,将随状态机的状态变化而变化。当状态机处于leader状态时,该方法就是stepLeader;当状态机处于follower状态时,该方法就是stepFollower;当状态机处于Candidate状态时,该方法就是stepCandidate。leader状态会直接处理MsgProp消息。将消息中的日志条目存入本地缓存。follower则会直接将MsgProp消息转发给leader,转发的过程是将先将消息推送到raft的msgs数组中。 node处理完消息以后,要么生成了缓存中的日志条目,要么生成了将要发送出去的消息。缓存中的日志条目需要进一步处理(比如同步和持久化),而消息需要进一步处理发送出去。处理过程还是在node的这个协程中,在循环开始会调用newReady,将需要进一步处理的日志和需要发送出去的消息,以及状态改变信息,都封装在一个Ready消息中。Ready消息会推行到readyc队列中。(图中5)
-
raftNode的处理 raftNode的start()方法另外启动了一个协程,处理readyc队列(图中6)。取出需要发送的message,调用transport的Send方法并将其发送出去(图中4)。调用storage的Save方法持久化存储日志条目或者快照(图中9、10),更新kv缓存。 另外需要将已经同步好的日志应用到状态机中,让状态机更新状态和kv存储,通知等待请求完成的客户端。因此需要将已经确定同步好的日志、快照等信息封装在一个apply消息中推送到applyc队列。
-
EtcdServer的apply处理 EtcdServer会处理这个applyc队列,会将snapshot和entries都apply到kv存储中去(图中8)。最后调用applyWait的Trigger,唤醒客户端请求的等待线程,返回客户端的请求。
读写请求执行过程
读请求
raft算法
Paxos 协议和 Raft 协议都是为了解决分布式场景下的数据一致性问题。
etcd 集群使用 Raft 协议保障多节点集群状态下的数据一致性。etcd 是使用 Go 语言对 Raft 协议一种实现方式。
在 Raft 体系中,有一个强 leader,由它全权负责接收客户端的请求命令,并将命令作为日志条目复制给其他服务器,在确认安全的时候,将日志命令提交执行。当 leader 故障时,会选举产生一个新的 leader。在强 leader 的帮助下,Raft将一致性问题分解为了三个子问题:
- Leader 选举:当已有的leader故障时必须选出一个新的leader。
- 日志复制:leader接受来自客户端的命令,记录为日志,并复制给集群中的其他服务器,并强制其他节点的日志与leader保持一致。
- 安全 safety 措施:通过一些措施确保系统的安全性,如确保所有状态机按照相同顺序执行相同命令的措施。
解这三个子问题的过程,保障了数据的一致。
etcd 只是 Raft 协议的一种实现机制,可视化界面。
Go语言操作etcd
依赖包
go get go.etcd.io/etcd/clientv3
安装报错:在mod中加 replace google.golang.org/grpc => google.golang.org/grpc v1.26.0
put和get
put命令用来设置键值对数据,get命令用来根据key获取值。
func main() {
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"127.0.0.1:2379"}, // 节点
DialTimeout: 5 * time.Second, //超时时间
})
if err != nil {
// 错误处理
}
fmt.Println("[INFO] connect to etcd success")
defer cli.Close()
// put
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
// 1秒钟没有put上去就取消
_, 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)
}
}
watch操作
watch用来获取未来更改的通知。
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") // <-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)
}
}
}
将上面的代码保存编译执行,此时程序就会等待etcd中q1mi这个key的变化。
打开终端执行以下命令修改、删除、设置q1mi这个key。
etcd> etcdctl.exe --endpoints=http://127.0.0.1:2379 put q1mi "dsb2"
OK
etcd> etcdctl.exe --endpoints=http://127.0.0.1:2379 del q1mi
1
etcd> etcdctl.exe --endpoints=http://127.0.0.1:2379 put q1mi "dsb3"
OK
接到通知
watch>watch.exe
connect to etcd success
Type: PUT Key:q1mi Value:dsb2
Type: DELETE Key:q1mi Value:
Type: PUT Key:q1mi Value:dsb3
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{"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)
}
}
keepAlive
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 := <-ch
fmt.Println("ttl:", ka.TTL)
}
}
基于etcd实现分布式锁
go.etcd.io/etcd/clientv3/concurrency
在etcd之上实现并发操作,如分布式锁、屏障和选举。
导入该包:
import "go.etcd.io/etcd/clientv3/concurrency"
基于etcd实现的分布式锁示例:
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")
<-m2Locked
fmt.Println("acquired lock for s2")
输出:
acquired lock for s1`在这里插入代码片`
released lock for s1
acquired lock for s2
基于etcd实现服务注册和服务发现
-
编写pb文件:
syntax = "proto3"; option go_package = "./pb"; message HelloRequest{ string name = 1; string msg = 2; } message HelloResponse{ string name = 1; string msg = 2; } service Hello{ rpc SayHello(HelloRequest)returns(HelloResponse); }
-
服务端实现注册
package main import ( "context" "flag" "fmt" "github.com/generalzy/tmp/etcd" "github.com/generalzy/tmp/server/pb" "google.golang.org/grpc" "net" ) type HelloService struct { pb.UnimplementedHelloServer } func NewHelloService() *HelloService { return new(HelloService) } func (h *HelloService) SayHello(ctx context.Context, helloRequest *pb.HelloRequest) (*pb.HelloResponse, error) { fmt.Println(helloRequest.Msg) return &pb.HelloResponse{ Name: "Biden", Msg: fmt.Sprintf("Hello, %s , My name is John.Biden.", helloRequest.Name), }, nil } var port = flag.String("port", "8000", "service port") func main() { flag.Parse() addr := fmt.Sprintf("127.0.0.1:%s", *port) fmt.Println(addr) etcdCli, err := etcd.NewEtcdResolver([]string{"127.0.0.1:2379"}, 5) if err != nil { fmt.Println(err) return } lis, err := net.Listen("tcp", addr) if err != nil { fmt.Println(err) return } s := grpc.NewServer() // 创建gRPC服务器 pb.RegisterHelloServer(s, NewHelloService()) err = etcdCli.Register("hello.rpc", addr) if err != nil { fmt.Println(err) return } defer etcdCli.UnRegister("hello.rpc", addr) err = s.Serve(lis) if err != nil { fmt.Println(err) return } }
type EtcdResolver struct { hosts []string Client *clientv3.Client timeout int64 } func NewEtcdResolver(hosts []string, timeout int64) (*EtcdResolver, error) { client, err := clientv3.New(clientv3.Config{ Endpoints: hosts, }) if err != nil { return nil, err } return &EtcdResolver{ hosts: hosts, Client: client, timeout: timeout, }, nil } func (r *EtcdResolver) Register(service, addr string) error { manager, err := endpoints.NewManager(r.Client, service) if err != nil { os.Exit(1) } // 租约 lease, err := r.Client.Grant(context.Background(), r.timeout) if err != nil { return err } // 添加端点 err = manager.AddEndpoint(context.Background(), fmt.Sprintf("%s/%s", service, addr), endpoints.Endpoint{Addr: addr, Metadata: service}, clientv3.WithLease(lease.ID)) if err != nil { return err } // keepAlive LeaseKeepAliveResponseC, err := r.Client.KeepAlive(context.Background(), lease.ID) if err != nil { return err } go func() { for { select { case LeaseKeepAliveResponse, ok := <-LeaseKeepAliveResponseC: if !ok { r.UnRegister(service, addr) return } fmt.Println(LeaseKeepAliveResponse.ID, "keep alive") } } }() return nil } func (r *EtcdResolver) UnRegister(service, addr string) error { manager, err := endpoints.NewManager(r.Client, service) if err != nil { return err } err = manager.DeleteEndpoint(context.Background(), fmt.Sprintf("%s/%s", service, addr)) if err != nil { return err } return nil }
-
客户端服务发现:由于etcd已经实现了grpc.resolver的Builder和Resolver接口,所以可以直接使用:
package main import ( "context" "fmt" "github.com/generalzy/tmp/client/pb" "github.com/generalzy/tmp/etcd" "go.etcd.io/etcd/client/v3/naming/resolver" "google.golang.org/grpc/balancer/roundrobin" "time" "google.golang.org/grpc" ) func main() { etcdCli, _ := etcd.NewEtcdResolver([]string{"127.0.0.1:2379"}, 5) etcdResolver, err := resolver.NewBuilder(etcdCli.Client) if err != nil { fmt.Println(err) return } // 禁用tls 轮询 conn, err := grpc.Dial("etcd:///hello.rpc", grpc.WithResolvers(etcdResolver), grpc.WithInsecure(), grpc.WithDefaultServiceConfig(fmt.Sprintf(`{"loadBalancingPolicy":"%s"}`, roundrobin.Name))) if err != nil { fmt.Println(err) return } fmt.Println(conn.Target()) client := pb.NewHelloClient(conn) tk := time.NewTicker(time.Second) for { select { case <-tk.C: resp, err := client.SayHello(context.Background(), &pb.HelloRequest{ Name: "generalzy", Msg: "你好", }) if err != nil { fmt.Println(err) return } fmt.Println(resp) } } }