赞
踩
etc (基于 Go 语言实现)在 Linux 系统中是配置文件目录名;etcd 就是配置服务;
etcd 诞生于 CoreOS 公司,最初用于解决集群管理系统中 os 升级时的分布式并发控制、配置文件的存储与分发等问题。基于此,etcd 设计为提供高可用、强一致性的小型** kv 数据存储**服务。项目当前隶属于 CNCF 基金会,被包括 AWS、Google、Microsoft、Alibaba 等大型互联网公司广泛使用;
etcd 是一个可靠的分布式 KV 存储,其底层使用 Raft 算法保证一致性,主要用于共享配置、服务发现、集群监控、leader 选举、分布式锁等场景;
1)共享配置:配置文件的存储与分发,将配置文件存储在 etcd 中,其它节点加载配置文件;如需修改配置文件,则修改后,其它节点只需重新加载即可;
2)服务发现:客户端请求经过 etcd 分发给各个服务端,此时如果增加一个服务端并连接到 etcd,由于客户端在一直监听着 etcd,所以客户端能够快速拉去新增服务端地址,并将客户端请求通过 etcd 下发到新增的服务端;
3)集群监控:客户端会通过 etcd 监控所有的 master、slave 节点,一旦有节点发生宕机,客户端能够及时发现;
4)leader 选举:假设一个 master 节点连接多个 slave 节点,如果 master 节点发生宕机,此时 etcd 会从 slave 节点中选取一个节点作为 master 节点;
5)分布式锁:常规情况下锁的持有者和释放者是进程中的线程,而在分布式情况下,锁的持有者和释放者可以是微服务或进程;
1)安装 golang 环境;
2)下载并编译安装 etcd;
- // 下载源代码
- git clone https://gitee.com/k8s_s/etcd.git
-
- // 设置源代理
- go env -w GO111MODULE=on
- go env -w GOPROXY=https://goproxy.cn,direct
-
- // 进入 etcd 目录
- cd etcd
-
- // 切换最新分支
- git checkout release-3.5
-
- go mod vendor
- ./build
-
- 在 etcd/bin 目录生成对应的执行文件 etcd、etcdctl 和 etcdutl
-
- // 查看 etcd 版本
- ./etcdctl version
说明:可以到 Gitee - 基于 Git 的代码托管和研发协作平台 网站搜 etcd 下载最新的即可!
执行结果如下:
etcd 的启动与使用
- cd etcd/bin
-
- // 启动 etcd
- nohup ./etcd > ./start.log 2>&1 &
-
- // 使用 v3 版本 api
- export ETCDCTL_API=3
-
- // ./etcdctl + etcd 命令即可
- ./etcdctl put key val
执行结果如下所示:
扩展:一般情况下一个请求需要建立一条连接,比较浪费资源,所以有了 http + json 通信模式(json 是一种协议),但 json 加解密非常慢;
扩展:
1)文件系统的存储结构
- /node
- /node/node1
- /node/node2
- /node/node1/sub1
- /node/node1/sub2
2)扁平的 kv 结构
- node
- node1
- node2
- node3
- 使用 get node --prefix 命令获取对应文件
etcd 体系结构如下所示:
示例分析:
- # ./etcdctl put key2 val2
- OK
- # ./etcdctl get key2
- key2
- val2
- # ./etcdctl get key2 -w json
- {"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":9,"raft_term":7},"kvs":[{"key":"a2V5Mg==","create_revision":9,"mod_revision":9,"version":1,"value":"dmFsMg=="}],"count":1}
参数说明:
- cluster_id:集群 id;
- member_id:当前 etcd 节点 id;
- revision:整个 etcd 的版本 id,且只要 key 发生变更(增、删、改),则 revision 加一;全局单调递增,64bits;
- raft_term:leader 任期,leader 切换时 term 加一;全局单调递增,64bits;
- kvs:
- create_revision 创建数据时,对应的版本号;
- mod_revision 数据修改时,对应的版本号;
- version 当前的版本号;标识该 val 被修改了多少次;
注:"key":"a2V5Mg==" 和 "value":"dmFsMg==" 是因为值被加密了,在 get 时会对其进行解密!
执行结果:
设置即存储共享配置信息;
- NAME:
- put - Puts the given key into the store
-
- USAGE:
- etcdctl put [options] <key> <value> (<value> can also be given fromstdin) [flags]
-
- DESCRIPTION:
- Puts the given key into the store.
- When <value> begins with '-', <value> is interpreted as a flag.
- Insert '--' for workaround:
-
- $ put <key> -- <value>
- $ put -- <key> <value>
-
- If <value> isn't given as a command line argument and '--ignorevalue' is not specified,this command tries to read the value from standard input.
-
- If <lease> isn't given as a command line argument and '--ignorelease' is not specified,this command tries to read the value from standard input.
-
- For example,
- $ cat file | put <key>
- will store the content of the file to <key>.
-
- OPTIONS:
- -h, --help[=false] help for put
- --ignore-lease[=false] updates the key using its current lease
- --ignore-value[=false] updates the key using its current value
- --lease="0" lease ID (in hexadecimal) to attach to thekey
- --prev-kv[=false] return the previous key-value pair beforemodification
语法命令:
- put key val
-
- // 存储 key value 的同时返回上一次存储的 key value
- put key val --prev-kv
删除 key vla;
- NAME:
- del - Removes the specified key or range of keys [key, range_end)
-
- USAGE:
- etcdctl del [options] <key> [range_end] [flags]
-
- OPTIONS:
- --from-key[=false] delete keys that are greater than or equal to the given key using byte compare
-
- -h, --help[=false] help for del
- --prefix[=false] delete keys with matching prefix
- --prev-kv[=false] return deleted key-value pairs
语法命令:
- del key
-
- // 删除成功,返回 1
- // 若 key 不存在,则返回 0
获取 key vla;
- NAME:
- get - Gets the key or a range of keys
-
- USAGE:
- etcdctl get [options] <key> [range_end] [flags]
-
- OPTIONS:
- --consistency="l" Linearizable(l) or Serializable(s)
- --count-only[=false] Get only the count
- --from-key[=false] Get keys that are greater than or equal to the given key using byte compare
- -h, --help[=false] help for get
- --keys-only[=false] Get only the keys
- --limit=0 Maximum number of results
- --order="" Order of results; ASCEND or DESCEND(ASCEND by default)
- --prefix[=false] Get keys with matching prefix
- --print-value-only[=false] Only write values when using the "simple" output format
- --rev=0 Specify the kv revision
- --sort-by="" Sort target; CREATE, KEY, MODIFY, VALUE, or VERSION
语法命令:
- get key
-
- // 获取前缀匹配 key 的所有 key val
- get key --prefix
-
- // 获取字符串小于 key2 的所有 key val
- get key key2
-
- // 获取字符串大于等于 key2 的所有 key val
- get key2 --from-key
-
- // 只获取字符串等于 key2 的 key
- get key2 --keys-only
-
- // 获取前缀匹配 key 的所有 key
- get key --prefix --keys-only
-
- // 获取前缀匹配 key 的前两个 key
- get key --prefix --keys-only --limit=2
-
- // 先排序,再获取前缀匹配 key 的前两个 key
- get key --prefix --keys-only --limit=2 --sort-by=value
- // 获取所有前缀和 key 匹配的 key val
- # ./etcdctl get key --prefix
- key
- val2023
- key1
- val1
- key2
- val2
- key20
- val20
- key2024
- val2024
-
- // 范围查询,获取 key2 之前(范围区间为左闭右开)的 key val
- # ./etcdctl get key key2
- key
- val2023
- key1
- val1
注:比较范围区间时是按字符串进行比较的,如:key、key1、key2、key20、key2024 中只有 key、key1 小于 key2;
执行结果:
- # ./etcdctl get key --prefix
- key
- val2023
- key1
- val1
- key2
- val2
- key20
- val20
- key2024
- val2024
- # ./etcdctl get key2 --from-key
- key2
- val2
- key20
- val20
- key2024
- val2024
执行结果:
用来实现监听和推送服务;
- NAME:
- watch - Watches events stream on keys or prefixes
-
- USAGE:
- etcdctl watch [options] [key or prefix] [range_end] [--] [execcommand arg1 arg2 ...] [flags]
-
- OPTIONS:
- -h, --help[=false] help for watch
- -i, --interactive[=false] Interactive mode
- --prefix[=false] Watch on a prefix if prefix is set
- --prev-kv[=false] get the previous key-value pair before the event happens
- --progress-notify[=false] get periodic watch progress notification from server
- --rev=0 Revision to start watching
语法命令:
- // 监听 key 的变动
- watch key
-
- 1) 启两个 session
- 2) 在 session A 中执行:WATCH key
- 3) 在 session B 中执行操作 key 的命令,如:PUT key val,DEL key 等,则同时会在 session A 中显示具体操作
-
- // 当前事件发生前先获取前一个 key val
- watch key --prev-kv
-
- // 监听多个 key 的变动
- watch key --prefix
说明:监听时也可以指定监听范围和版本等信息;
用于分布式锁以及 leader 选举;保证多个操作的原子性;确保多个节点数据读写的一致性;
有关数据版本号信息请参考上述:数据版本号机制 部分;
- NAME:
- txn - Txn processes all the requests in one transaction
-
- USAGE:
- etcdctl txn [options] [flags]
-
- OPTIONS:
- -h, --help[=false] help for txn
- -i, --interactive[=false] Input transaction in interactive mode
-
- 事务
- 1. 比较
- 1. 比较运算符 > = < !=
- 2. create 获取key的create_revision
- 3. mod 获取key的mod_revision
- 4. value 获取key的value
- 5. version 获取key的修改次数
- 2. 比较成功,执行下述代码
- 1. 成功后可以操作多个 del put get
- 2. 这些操作保证原子性
- 3. 比较失败,执行下述代码
- 1. 成功后可以操作多个 del put get
- 2. 这些操作保证原子性
语法命令:
TXN if/ then/ else ops
- # ./etcdctl put key val1995
- OK
- # ./etcdctl get key -w json
- {"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":12,"raft_term":7},"kvs":[{"key":"a2V5","create_revision":2,"mod_revision":12,"version":5,"value":"dmFsMTk5NQ=="}],"count":1}
-
- # ./etcdctl put key val2024
- OK
- # ./etcdctl get key -w json
- {"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":13,"raft_term":7},"kvs":[{"key":"a2V5","create_revision":2,"mod_revision":13,"version":6,"value":"dmFsMjAyNA=="}],"count":1}
- # ./etcdctl txn -i
- compares:
- mod("key")="9"
- Error: malformed comparison: mod("key")="9"; got mod("key") ""
- # ./etcdctl txn -i
- compares:
- mod("key") = "12"
-
- success requests (get, put, del):
- get key
-
- failure requests (get, put, del):
- get key --rev=12
-
- FAILURE
-
- key
- val1995
从上述执行结果来看,代码走的是 比较失败 的逻辑;
注:mod("key") = "12" 等号前后要有空格,不然会报错!
执行结果:
- # ./etcdctl txn -i
- compares:
- create("key") = "2"
-
- success requests (get, put, del):
- get key
-
- failure requests (get, put, del):
- del key
-
- SUCCESS
-
- key
- val2024
执行结果:
- # ./etcdctl put key val2020
- OK
- # ./etcdctl get key -w json
- {"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":14,"raft_term":7},"kvs":[{"key":"a2V5","create_revision":2,"mod_revision":14,"version":7,"value":"dmFsMjAyMA=="}],"count":1}
- # ./etcdctl put key val2023
- OK
- # ./etcdctl get key -w json
- {"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":15,"raft_term":7},"kvs":[{"key":"a2V5","create_revision":2,"mod_revision":15,"version":8,"value":"dmFsMjAyMw=="}],"count":1}
- # ./etcdctl txn -i
- compares:
- version("key") = "7"
-
- success requests (get, put, del):
- get key
-
- failure requests (get, put, del):
- get key --rev=14
-
- FAILURE
-
- key
- val2020
执行结果:
用于集群监控以及服务注册发现;
- etcdctl lease grant <ttl> [flags] 创建一个租约
- etcdctl lease keep-alive [options] <leaseID> [flags] 续约
- etcdctl lease list [flags] 枚举所有的租约
- etcdctl lease revoke <leaseID> [flags] 销毁租约
- etcdctl lease timetolive <leaseID> [options] [flags] 获取租约信息
-
- OPTIONS:
- --keys[=false] Get keys attached to this lease
语法命令:
- // 创建一个 100 秒的租约
- lease grant 100
-
- // 如果租约创建成功会显示如下输出
- lease 694d7b82c54a9309 granted with TTL(100s)
-
- // 将多个 key 绑定到租约
- put key1 vla1 --lease=694d7b82c54a9309
- put key2 vla2 --lease=694d7b82c54a9309
- put key3 vla3 --lease=694d7b82c54a9309
-
- // 获取具有匹配前缀的 key(包括:绑定租约的 key 和未绑定租约的 key)
- get key --prefix
-
- // 输出结果
- key1
- vla1
- key2
- vla2
- key3
- vla3
-
- // 销毁租约
- lease revoke 694d7b82c54a9309
-
- // 获取具有匹配前缀的 key(因为租约已被销毁,所以此时返回的只有未绑定租约的 key)
- get key --prefix
-
- // 获取租约信息(如果租约未过期,则输出结果会显示租约的剩余日期;如果租约已过期,则显示已过期)
- lease timetolive 694d7b82c54a9309
-
- // 输出结果(租约已过期)
- lease 694d7b82c54a9309 already expired
-
- // 续约(可以让租约剩余日期一直保持在设定时间;续约前提是当前租约未过期)
- lease keep-alive 694d7b82c54a9309
锁
- USAGE:
- etcdctl lock <lockname> [exec-command arg1 arg2 ...] [flags]
-
- OPTIONS:
- -h, --help[=false] help for lock
- --ttl=10 timeout for session
不能直接 go get go.etcd.io/etcd/clientv3(官方提供驱动包)不然会报错的;因为 gRPC 版本过新的缘故;
这里我们需要指定 gRPC 的版本信息;
- # 指定 gRPC 版本为 v1.26.0
- go mod edit --require=google.golang.org/grpc@v1.26.0
-
- # 下载安装 gRPC 驱动包
- go get -u -x google.golang.org/grpc@v1.26.0
-
- # 下载安装 etcd 驱动包
- go get go.etcd.io/etcd/clientv3
启动 etcd
1)方式一
- nohup ./etcd > ./start.log 2>&1 &
-
- // 查看端口对外开放情况(etcd 默认端口为 2379)
- lsof -i:2379
执行结果:
从上述执行结果可知,使用方式一启动时,etcd 的端口号只能在本地连接。
2)方式二
- nohup ./etcd --listen-client-urls 'http://0.0.0.0:2379' --advertise-client-urls 'http://0.0.0.0:2379' > ./start.log 2>&1 &
-
- // 查看端口对外开放情况(etcd 默认端口为 2379)
- lsof -i:2379
执行结果:
从上述执行结果可知,使用方式一启动时,etcd 的端口号可以被外部连接。
注:使用方式二启动 etcd!
注:如果 etcd 所在机器是公司内部机器,需要把安全组对应端口号放开,即需要放开 2379!
- package main
-
- import (
- "context"
- "fmt"
- "time"
-
- "github.com/coreos/etcd/clientv3"
- )
-
- func main() {
- // 创建连接
- cli, err := clientv3.New(clientv3.Config{
- // Endpoints 是一个切片,可同时连接多个服务器
- Endpoints: []string{"120.92.144.250:2379"},
- DialTimeout: 5 * time.Second, // 连接超时时间
- })
- if err != nil {
- panic(err)
- }
-
- // 程序执行结束前释放连接资源
- defer cli.Close()
-
- // v3 通讯服务使用的是 gRPC,需设置超时控制(即如果 put 命令执行后,在超时时间内没有返回结果,则取消 put 命令的执行)
- ctx, cancel := context.WithTimeout(context.Background(), time.Second)
- _, err = cli.Put(ctx, "key", "mark")
- cancel()
- if err != nil {
- panic(err)
- }
-
- // 获取 key
- ctx, cancel = context.WithTimeout(context.Background(), time.Second)
- /*
- 此处的 get 等同于在终端执行 ./etcdctl get key -w json
- 输出结果:
- {"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":17,"raft_term":7},
- "kvs":[{"key":"a2V5","create_revision":2,"mod_revision":15,"version":8,"value":"dmFsMjAyMw=="}],"count":1}
- */
- resp, err := cli.Get(ctx, "key")
- cancel()
- if err != nil {
- panic(err)
- }
-
- for _, ev := range resp.Kvs {
- fmt.Printf("%s:%s\n", ev.Key, ev.Value)
- }
- }
- package main
-
- import (
- "context"
- "fmt"
-
- "github.com/coreos/etcd/clientv3"
- )
-
- func main() {
- // 创建连接
- cli, err := clientv3.NewFromURL("120.92.144.250:2379")
- if err != nil {
- panic(err)
- }
- defer cli.Close()
-
- // watch key 的操作
- //watch := cli.Watch(context.Background(), "key")
-
- // watch 大于等于 key3 的操作,监听对象由第三个参数控制
- watch := cli.Watch(context.Background(), "key", clientv3.WithFromKey())
-
- for resp := range watch {
- for _, ev := range resp.Events {
- fmt.Printf("Type: %s Key: %s Value: %s\n", ev.Type, ev.Kv.Key, ev.Kv.Value)
- }
- }
- }
执行上述代码后会阻塞等待其它用户操作 etcd,如下所示:
1)在终端执行 etcd 操作
2)在 go 客户端查看监听情况
- package main
-
- import (
- "context"
- "fmt"
-
- "github.com/coreos/etcd/clientv3"
- )
-
- func main() {
- // 创建连接
- cli, err := clientv3.NewFromURL("120.92.144.250:2379")
- if err != nil {
- panic(err)
- }
- defer cli.Close()
-
- // 创建租约
- lease, err := cli.Grant(context.Background(), 5)
- if err != nil {
- panic(err)
- }
- fmt.Println("lease id", lease.ID)
-
- // 把 key-val 绑定到租约
- _, err = cli.Put(context.Background(), "key", "mark", clientv3.WithLease(lease.ID))
- if err != nil {
- panic(err)
- }
-
- // 续租:长期续租、短期续租
- // 长期续租:不停的续租
- if false {
- ch, err := cli.KeepAlive(context.Background(), lease.ID)
- if err != nil {
- panic(err)
- }
- for {
- recv := <-ch
- fmt.Println("time to live", recv.TTL)
- }
- }
- // 短期续租:只续租一次
- if true {
- res, err := cli.KeepAliveOnce(context.Background(), lease.ID)
- if err != nil {
- panic(err)
- }
- fmt.Println("time to live", res.TTL)
- }
- }
- package main
-
- import (
- "context"
- "fmt"
-
- "github.com/coreos/etcd/clientv3"
- "github.com/coreos/etcd/clientv3/concurrency"
- )
-
- func main() {
- // 创建连接
- cli, err := clientv3.New(clientv3.Config{
- Endpoints: []string{"127.0.0.1:2379"},
- })
- if err != nil {
- panic(err)
- }
- defer cli.Close()
-
- // 创建 session1
- s1, err := concurrency.NewSession(cli, concurrency.WithContext(context.Background()), concurrency.WithTTL(10))
- if err != nil {
- panic(err)
- }
- defer s1.Close()
-
- // 为 session1 创建锁
- m1 := concurrency.NewMutex(s1, "lock")
-
- // 创建 session2
- s2, err := concurrency.NewSession(cli, concurrency.WithContext(context.Background()))
- if err != nil {
- panic(err)
- }
- defer s2.Close()
-
- // 为 session2 创建锁
- m2 := concurrency.NewMutex(s2, "lock")
-
- // 对 session1 加锁
- if err := m1.Lock(context.Background()); err != nil {
- panic(err)
- }
- fmt.Println("s1 acquired lock")
-
- // 创建管道
- m2ch := make(chan struct{})
-
- // 开启协程,对 session2 加锁,但由于已经被 session1 锁住,所以 session2 的加锁操作,阻塞等待
- go func() {
- defer close(m2ch)
- if err := m2.Lock(context.Background()); err != nil {
- panic(err)
- }
- }()
-
- // session1 释放锁
- if err := m1.Unlock(context.Background()); err != nil {
- panic(err)
- }
- fmt.Println("s1 released lock")
-
- // 通知 session2 session1 已经释放锁,此时 session2 可执行加锁操作
- <-m2ch
- fmt.Println("s2 acquired lock")
- }
注:Go 项目在创建好之后,需要在终端执行:go mod init 项目名称,生成 go.mod 文件。
etcd 为每个 key 创建一个索引;一个索引对应着一个 B+ 树;B+ 树 key 为 revision,B+ 树节点存储的值为 value;B+ 树存储着 key 的版本信息从而实现了 etcd 的 mvcc;etcd 不会任由版本信息膨胀,通过定期的 compaction 来清理历史数据;
etcd 为了加速索引数据,在内存中维持着一个 B 树;B 树 key 为 key-val 中的 key,value 为该 key 的 revision;示意图如下:
etcd 不同命令执行流程:
问题:mysql 的 mvcc 是通过什么实现的?
答:undolog;
问题:mysql B+ 树存储什么内容?
答:具体分为聚簇索引和二级索引;
问题:mysql 为了加快索引数据,采用什么数据结构?
答:MySQL 采用自适应 hash 来加速索引;
扩展:B-树和 B+ 树区别?
- B-树和 B+ 树都是多路平衡搜索树;采用中序遍历的方式会得到一个有序的结构;都是通过 key 的方式来维持树的有序性;
- B-树一个节点中 n 个元素对应着 n+1 个指针;而 B+ 树一个节点中 n 个元素对应着 n 个指针;
- B-树每个节点都存储节点信息,B+ 树只有叶子节点存储节点信息,非叶子节点只存储索引信息;
- B+ 树叶子节点之间通过双向链表连接,对于范围查询速度更快,这样减少了磁盘 io;
etcd 是串行写(避免不必要的加锁),并发读;
并发读写时(读写同时进行),读操作是通过 B+ 树 mmap 访问磁盘数据;写操作走日志复制流程;可以得知如果此时读操作走 B 树出现脏读幻读问题;通过 B+ 树访问磁盘数据其实访问的事务开始前的数据,由 mysql 可重复读隔离级别下 MVCC 读取规则可智能避免脏读和幻读问题;
并发读时,可走内存 B 树;
注:由于 etcd 写的时候是先写到内存中的 B 树,然后再写到磁盘上的 B+ 树,因此并发读写时需要读 B+ 树数据,否则容易出现脏读幻读问题;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。