etcd
的介绍与安装
-
主要用于微服务的配置中心和服务发现,数据可靠性比
redis
更强
在对外
api
的应用中,如何知道order服务的rpc
地址?如果服务的
ip
地址变化了怎么办?在传统的配置文件模式,修改配置文件,应用程序是需要重启才能解决的,所以引入etcd
-
windows安装
-
etcd-v3.5.16-windows-amd64.zip
-
-
docker
安装docker run --name etcd -d -p 2379:2379 -p 2380:2380 -e ALLOW_NONE_AUTHENTICATION=yes bitnami/etcd:3.3.11 etcd
安装完成后在下载路径启用cmd
(因为未加环境变量)
基本命令
// 设置或更新值 etcdctl put name 张三 // 获取值 etcdctl get name // 只要value etcdctl get name --print-value-only // 获取name前缀的键值对 etcdctl get name --prefix // 删除键值对 etcdctl del name // 监听键的变化 etcdctl watch name
// 例如 etcdctl put rpc.order 127.0.0.1:7001 etcdctl put rpc.user 127.0.0.1:7002 etcdctl get rpc --prefix // ------------ etcdctl watch rpc.user // 另起一个cmd etcdctl put rpc.user 127.0.0.1:7003
Go
操作 etcd
驱动包安装
不能直接
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 go mod tidy
启动 etcd
-
方式一:(
etcd
的端口号只能在本地连)start /B etcd > start.log 2>&1 // /B表示在后台运行程序 >将输出重定向到文件 将标准错误重定向到标准输出 netstat -an | findstr :2379 // 来验证端口是否被监听
// 用于 Linux 或类 Unix 系统:etcd --listen-client-transport-port 2379 nohup ./etcd > ./start.log 2>&1 & // 查看端口对外开放情况(etcd 默认端口为 2379) lsof -i:2379
-
方式二:(如果
etcd
所在机器是公司内部机器,需要把安全组对应端口号放开,即需要放开 2379)start /B etcd.exe --listen-client-urls 'http://0.0.0.0:2379' --advertise-client-urls 'http://0.0.0.0:2379' > start.log 2>&1// 查看端口对外开放情况(etcd 默认端口为 2379) netstat -ano | findstr :2379
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
// windows tasklist | findstr etcd taskkill /F /IM etcd.exe
put
、get
使用
package mainimport ("context""fmt""time""github.com/coreos/etcd/clientv3" )func main() {// 创建连接cli, err := clientv3.New(clientv3.Config{ // 创建etcd客户端实例// Endpoints 是一个切片,可同时连接多个服务器Endpoints: []string{"127.0.0.1:2379"},DialTimeout: 5 * time.Second, // 连接超时时间})if err != nil {panic(err)}// 程序执行结束前释放连接资源defer cli.Close()// 这里创建了一个带有超时的上下文ctx,用于控制Put操作的超时ctx, cancel := context.WithTimeout(context.Background(), time.Second)// 向etcd中写入数据,如果操作成功,cancel函数会被调用,取消上下文的超时控制_, err = cli.Put(ctx, "key", "mark")cancel()if err != nil { // 操作失败,服务终止panic(err)}// Get操作,有超时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 { // 遍历etcd返回的结果fmt.Printf("%s:%s\n", ev.Key, ev.Value)} }
watch
使用
package mainimport ("context""fmt""github.com/coreos/etcd/clientv3" )func main() {// 创建etcd客户端实例cli, err := clientv3.NewFromURL("127.0.0.1:2379")if err != nil {panic(err)}defer cli.Close()// 监听key 的操作//watch := cli.Watch(context.Background(), "key")// 创建了一个监听器来监听名为"key"的键或大于"key"的所有键的变化// clientv3.WithFromKey()选项表示监听的范围从"key"开始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 客户端查看监听情况
lease
使用
package mainimport ("context""fmt""github.com/coreos/etcd/clientv3" )func main() {// 创建连接cli, err := clientv3.NewFromURL("127.0.0.1:2379")if err != nil {panic(err)}defer cli.Close() // 关闭连接// 创建租约lease, err := cli.Grant(context.Background(), 5) // 5秒的租约if err != nil {panic(err)}fmt.Println("lease id", lease.ID) // 打印租约ID// 把 key-val 绑定到租约_, err = cli.Put(context.Background(), "key", "mark", clientv3.WithLease(lease.ID))if err != nil {panic(err)}// 续租:长期续租、短期续租, 续约到期删除键值对// 长期续租:不停的续租if true {ch, err := cli.KeepAlive(context.Background(), lease.ID)if err != nil {panic(err)}for {recv := <-ch // 接收续租结果fmt.Println("time to live", recv.TTL) // 打印剩余租期}}// 短期续租:只续租一次if false {res, err := cli.KeepAliveOnce(context.Background(), lease.ID)if err != nil {panic(err)}fmt.Println("time to live", res.TTL) // 打印剩余租期} }
lock 使用
package mainimport ("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()// 创建了第一个会话 s1,并设置了一个 TTL为 10 秒s1, err := concurrency.NewSession(cli, concurrency.WithContext(context.Background()), concurrency.WithTTL(10))if err != nil {panic(err)}defer s1.Close()// 使用会话 s1 创建了一个名为 "lock" 的互斥锁m1 := concurrency.NewMutex(s1, "lock")// 创建了第二个会话 s2,没有设置 TTL,将一直保持活跃,直到显式关闭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 可执行加锁操作<-m2chfmt.Println("s2 acquired lock") }
etcd
存储原理及读写机制
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
不同命令执行流程:
-
etcd
get 命令执行流程:etcd
在执行 get 获取数据时,先从内存中的 B 树中寻找,如果找不到,再从 B+ 树中寻找,从 B+ 树中找到数据后,将其缓存到 B 树并输出到客户端; -
etcd put
命令执行流程:etcd
在执行 put 插入或修改数据时,先从内存中的 B 树中寻找,如果找到了,则对其进行修改并将其写入到 B+ 树;
问题: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+ 树数据,否则容易出现脏读幻读问题;
关闭etcd
监听
# 使用 tasklist 命令来查找 etcd 相关的进程 tasklist | findstr etcd # 使用 taskkill 命令结束 /F 表示强制结束进程,/IM 后面跟上进程的名称 taskkill /F /IM etcd.exe # 通过服务的方式来启动 etcd (如:etcd.exe --service) net stop etcd # 使用 netstat 命令检查端口是否还在监听 netstat -an | findstr :2379