etcd一知半解

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 存储

就像在标准文件系统中一样,将数据存储在分层组织的目录中。
在这里插入图片描述

特点

  1. 完全复制:集群中的每个节点都可以使用完整的存档
  2. 高可用性:Etcd可用于避免硬件的单点故障或网络问题
  3. 一致性:每次读取都会返回跨多主机的最新写入
  4. 简单:包括一个定义良好、面向用户的API(gRPC)
  5. 安全:实现了带有可选的客户端证书身份验证的自动化TLS
  6. 快速:每秒10000次写入的基准速度
  7. 可靠:使用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 算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁。锁服务有两种使用方式,一是保持独占,二是控制时序。

在这里插入图片描述

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

对比zk

  1. 简单。使用 Go 语言编写部署简单;支持HTTP/JSON API,使用简单;使用 Raft 算法保证强一致性让用户易于理解。
  2. etcd 默认数据一更新就进行持久化。
  3. etcd 支持 SSL 客户端安全认证。
  4. zk部署维护复杂,其使用的Paxos强一致性算法复杂难懂。官方只提供了Java和C两种语言的接口。
  5. zk使用Java编写引入大量的依赖。运维人员维护起来比较麻烦。
  6. zk最近几年发展缓慢,不如etcd和consul等后起之秀。

Etcd 提供什么能力

  1. 提供存储以及获取数据的接口,它通过协议保证 Etcd 集群中的多个节点数据的强一致性。用于存储元信息以及共享配置。
  2. 提供监听机制,客户端可以监听某个key或者某些key的变更(v2和v3的机制不同)。用于监听和推送变更。
  3. 提供key的过期以及续约机制,客户端通过定时刷新来实现续约(v2和v3的实现机制也不一样)。用于集群监控以及服务注册发现。
  4. 提供原子的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. 计算机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}
  1. 计算机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}
  1. 计算机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整体架构

在这里插入图片描述

  1. 消息入口 一个etcd节点运行以后,有3个通道接收外界消息,以kv数据的增删改查请求处理为例,介绍这3个通道的工作机制。

  2. client的http调用:会通过注册到http模块的keysHandler的ServeHTTP方法处理。解析好的消息调用EtcdServer的Do()方法处理。(图中2)

  3. client的grpc调用:启动时会向grpc server注册quotaKVServer对象,quotaKVServer是以组合的方式增强了kvServer这个数据结构。grpc消息解析完以后会调用kvServer的Range、Put、DeleteRange、Txn、Compact等方法。kvServer中包含有一个RaftKV的接口,由EtcdServer这个结构实现。所以最后就是调用到EtcdServer的Range、Put、DeleteRange、Txn、Compact等方法。(图中1)

  4. 节点之间的grpc消息:每个EtcdServer中包含有Transport结构,Transport中会有一个peers的map,每个peer封装了节点到其他某个节点的通信方式。包括streamReader、streamWriter等,用于消息的发送和接收。streamReader中有recvc和propc队列,streamReader处理完接收到的消息会将消息推到这连个队列中。由peer去处理,peer调用raftNode的Process方法处理消息。(图中3、4)

  5. EtcdServer消息处理 对于客户端消息,调用到EtcdServer处理时,一般都是先注册一个等待队列,调用node的Propose方法,然后用等待队列阻塞等待消息处理完成。Propose方法会往propc队列中推送一条MsgProp消息。 对于节点间的消息,raftNode的Process是直接调用node的step方法,将消息推送到node的recvc或者propc队列中。 可以看到,外界所有消息这时候都到了node结构中的recvc队列或者propc队列中。(图中5)

  6. 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)

  7. raftNode的处理 raftNode的start()方法另外启动了一个协程,处理readyc队列(图中6)。取出需要发送的message,调用transport的Send方法并将其发送出去(图中4)。调用storage的Save方法持久化存储日志条目或者快照(图中9、10),更新kv缓存。 另外需要将已经同步好的日志应用到状态机中,让状态机更新状态和kv存储,通知等待请求完成的客户端。因此需要将已经确定同步好的日志、快照等信息封装在一个apply消息中推送到applyc队列。

  8. 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将一致性问题分解为了三个子问题:

  1. Leader 选举:当已有的leader故障时必须选出一个新的leader。
  2. 日志复制:leader接受来自客户端的命令,记录为日志,并复制给集群中的其他服务器,并强制其他节点的日志与leader保持一致。
  3. 安全 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实现服务注册和服务发现

  1. 编写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);
    }
    
  2. 服务端实现注册

    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
    }
    
  3. 客户端服务发现:由于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)
    		}
    	}
    
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Generalzy

文章对您有帮助,倍感荣幸

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值