注册服务中心etcd学习笔记

1、简介

etcd 诞生于 CoreOS 公司,最初用于解决集群管理系统中 os 升级时的分布式并发控制、配置文件的存储与分发等问题。基于此,etcd 设计为提供高可用、强一致性的小型 kv 数据存储服务。项目当前隶属于 CNCF 基金会,被包括 AWS、Google、Microsoft、Alibaba 等大型互联网公司广泛使用。
etcd 基于 Go 语言实现,主要用于共享配置和服务发现。
etc 在 Linux 系统中是配置文件目录名;etcd 就是配置服务

etcd是提供配置服务的中间件;存储少量的重要信息:高可用、强一致;适合读多写少的场景。

2、安装

(1)在github release page下载相应平台的软件压缩包;
(2)解压缩;
(3)如果是 linux 系统,进入解压目录,将 etcd 和 etcdctl 可执行文件移动到 /usr/local/bin 目录下;
(4)执行 etcd --version 如果能看到版本信息,说明安装成功。

3、v2和v3比较

  • 使用 gRPC + protobuf 取代 http + json 通信,提高通信效率;gRPC 只需要一条连接;http 是每个请求建立一条连接;protobuf 加解密比 json 加解密速度得到数量级的提升;包体也更小;

grpc是流式流程;一条长链接,多个请求、多个回应;区别于http的一请求、一回应。

  • v3 使用 lease (租约)替换 key ttl 自动过期机制;

redis有设置key的过期时间expire,但都需要独立设置(数量太大很费时间),租约lease就是组合设置

  • v3 支持事务和多版本并发控制(一致性非锁定读)的磁盘数据库;而v2 是简单的 kv 内存数据库;
  • v3 是扁平的kv结构;v2 是类型文件系统的存储结构。

etcd是扁平结构,组合拼接名称来操作。v2版本类似zookeeper,是树结构,以文件路径名称来操作。

4、体系结构

在这里插入图片描述

  • grpc server

里面还有grpc gateway网关代理层,把client http转换成grpc方式进行通信;grpc server除了跟client交互,跟其他grpc server之间也是进行grpc交互。

  • 强一致:raft算法保证一致性。

具体的:假如client端发送一个kv对上去(写),leader grpc server收到后,先写日志,再广播给follower grpc server,超过半数以上follow grpc server收到后(写成功),返回一个值给client,确保数据一致。

  • 数据同步
  • mysql redis主从复制,都是异步复制。客户端写完主redi server后立即返回,然后从redis再来主redis同步数据。
  • etcd同步复制:客户端写完leader grpc server、超过半数的follow grpc server同步之后,才会返回。原理就是通过raft算法保证强一致性,保证所有follow都同步后,再返回给客户端。
  • wal(write ahead log)预写式日志

执行写操作前先写日志,跟 mysql中 redo 类似,wal 实现的是顺序写,而若按照 B+ 树写,则涉及到多次 io 以及 随机写。
先是顺序写、追加写的方式写磁盘,提升写性能;然后再去修改内存,然后再异步写对应的数。
rocksdb、etcd里用wal文件、mysql用redolog文件:

  • rocksdb:有两块存储内存,一块是可变的、一块是不可变的,都是跳表;从可变跳表变成不可变跳表的这段时间,数据都写在wal文件中,等不可变跳表都落盘之后,才会删除相对应的wal。
  • mysql:通过redolog。写一个数据,本来是要写B+树的,需要经过多次磁盘io;所以先写redolog,写完之后,用异步的mmap映射的方式去刷磁盘,也是通过顺序写提升写性能。mysql磁盘数据是通过B+树映射,内存数据是通过hash组织数据
  • snap shot快照

用于其他节点同步主节点数据从而达到一致性地状态;类似 redis 中主从复制中 rdb 数据恢复;流程:1. leader 生成 snapshot;2. leader 向 follower 发送 snapshot;3.follower 接收并应用snapshot;
主要用在leader和 follow数据版本差异过大时,同步使用。跟redis数据同步类似,主redis中用rdb结构数据同步到从redis。

  • boltdb:负责kv存储

是一个单机的支持事务的 kv 存储,etcd 的事务是基于 boltdb 的事务实现的;boltdb 为每一个 key 都创建一个索引(B+树);该 B+ 树存储了 key 所对应的版本数据;
嵌入式kv数据库,跟rocksdb kv类似,但有本质差别:

  • rocksdb组织文件用lsm-tree多层级方式,解决写多读少问题,为了提升写性能,用在分布式关系数据库中;而boltdb用B+树组织文件,解决读多写少问题。
  • 内存组织方式:rocksdb用跳表,boltdb用B树(为什么不用红黑树或跳表?见下文)

5、etcd APIs

短链接:一次请求、一次返回,就断开。

(1)设置 put

# Puts the given key into the store 
PUT key val 
--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 the key 
--prev-kv[=false]        return the previous key-value pair before modification

put  get
put --prev-kv

(2)删除 del

成功返回1、失败返回0。

# Removes the specified key or range of keys [key, range_end) 
DEL key 
DEL keyfrom keyend 
--from-key[=false] #delete keys that are greater than or equal to the given key using byte compare 
--prefix[=false]   #delete keys with matching prefix --prev-kv[=false] #return deleted key-value pairs

在这里插入图片描述

(3)获取 get

get是重点,因为etcd解决读多写少的问题

# Gets the key or a range of keys 
GET key 
GET keyfrom keyend 
--consistency="l"          #Linearizable(l) or Serializable(s)  隔离级别,l是线性隔离,效率最高,但可能取到脏数据;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 
--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 # CREATE: 根据创建版本号create_version进行排序; MODIFY: 根据修改版本号mod_version进行排序; VERSION: 根据修改次数version进行排序

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

limit限定返回个数:
在这里插入图片描述

etcd版本号机制: 通过-rev=157 获取指定版本的kv
  • term: #进行了多少次的leader选取,etcd重启时+1,如果只有1个节点,term=重启次数
    leader 任期,leader 切换时 term 加一;全局单调递增,64 bits;
  • revision: #全局版本号,增加kv、修改kv,都会+1
    etcd 键空间版本号,key 发生变更,则 revision 加一;全局单调递增,64 bits;用来支持MVCC;
  • kv: #每个kv对应的版本号
    (1)create_revision #创建数据时,对应的版本号;
    (2)mod_revision #数据修改时,对应的版本号;
    (3)version #当前的版本号;标识该 val 被修改了多少次;

redis、rocksdb同一个key多次赋值,是覆盖前一次,而etcd是保留每次记录,用版本号区分,会维持一定数量的信息(v2=1000、v3是指定的数量级)。
在这里插入图片描述
get key -w json 打印key对应的值, -w fileds也是:
在这里插入图片描述

排序:

在这里插入图片描述
在这里插入图片描述

(4)监听 watch

用来实现监听和推送服务。

# Watches events stream on keys or prefixes 
WATCH key 
-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

在这里插入图片描述

(5)事务

用于分布式锁以及 leader 选举;保证多个操作的原子性;确保多个节点数据读写的一致性。
问题:通过事务实现 redis 中的 setnx ; //setnx :set no exist设置不存在的kv
注意:

  • 比较
    1. 比较运算符 > = < != ; 比较运算符两侧需要空格
    2. create 获取 key 的 create_revision
    3. mod 获取 key 的 mod_revision
    4. value 获取 key 的 value
    5. version 获取 key 的修改次数
    6.
  • 比较成功
    1. 成功后可以操作多个 del put get
    2. 这些操作保证原子性
  • 比较失败
    1. 失败后可以操作多个 del put get
    2. 这些操作保证原子性
# Txn processes all the requests in one transaction 
TXN if/ then/ else ops 
-i, --interactive[=false]     #Input transaction in interactive mode
  • redis中:setnx 设置不存在的kv:(setnx key value)
    (1)获取key
    (2)如果key不存在则设置kv
    (3)如果key存在,直接返回key对应的value值
  • etcd中事务就是类似redis的setnx操作:
    在这里插入图片描述
    txn:tranction,-i:交互式展示
    在这里插入图片描述

(6)租约 lease

用于集群监控以及服务注册发现。

lease grant      # 创建一个租约 
lease keep-alive # 续约 
lease list       # 枚举所有的租约 
lease revoke     # 销毁租约 
lease timetolive # 获取租约信息

在这里插入图片描述
在这里插入图片描述

6、数据存储原理

在这里插入图片描述
etcd 为每个 key 创建一个索引;一个索引对应着一个 B+ 树;B+ 树 key 为 revision,B+ 节点存储的值为value;B+ 树存储着 key 的版本信息从而实现了 etcd 的 mvcc;etcd 不会任由版本信息膨胀,通过定期的 compaction 来清理历史数据。

etcd,在内存中boltdb使用B树存储,复杂度hOlog2n;rocksdb使用跳表存储,复杂度log2n。
rocksdb内存中是跳表,mysql内存中是hash,etcd内存是B树。
etcd B树跟B+树有一个共同点:一个节点存储多个kv对。B+树一个节点是16k连续大小的磁盘空间,假如里面有k1v1、k2v2、k3v3等多个kv对(有序),如果要取k1v1的话,只能一次取16k全部空间。
如果放到红黑树、或者跳表的话,要一个个取k1v1 k2v2等多次运算才能维持红黑树有序,缺点是要一个一个插入,有n个kv,复杂度就是n * log2n。
etcd采用丢到B树的16k里,形成一一映射的关系,复杂度则是O(1),这就是内存使用B树而不使用红黑树、跳表的原因

(1)读写机制

etcd 是串行写、并发读
并发读写时(读写同时进行),读操作是通过 B+ 树 mmap 访问磁盘数据;写操作走日志复制流程;可以得知如果此时读操作走 B 树出现脏读幻读问题;通过 B+ 树访问磁盘数据其实访问的事务开始前的数据,由 mysql 可重复读隔离级别下 MVCC 读取规则可知能避免脏读和幻读问题;
并发读时,可走内存 B 树。

(2)节点在内存

在这里插入图片描述

(3)节点在磁盘

在这里插入图片描述

7、分布式锁

(1)使用背景

  • 单线程锁:
lock(&mtx)
操作临界资源
unlock(&mtx)
  • 多线程锁:
    保证多个线程有序、不冲突访问同一空间内容,使用分布式锁。

(2)技术重点

  • 锁是一种资源
  • 存储在哪个位置
  • 资源所在地的高可用性,即要有备份。
    所以:redis有哨兵、cluster;etcd/zookeeper有半数以上的强一致、高可用性。
  • 互斥语义

保证只有一个线程操作锁

  • 获取锁和释放锁必须为同一对象
  • 锁超时(获取锁和释放锁是通过网络通讯实现的)

获取锁的进程/线程宕机了,无法释放锁,锁自身要有超时机制

  • 锁释放通知问题
  • 主动问询:非公平锁
  • 被动通知:公平锁
  • 是否允许同一对象多次获取锁(可选)
  • 可以多次:重入锁
  • 不可以多次:非重入锁

(3)etcd实现

获取锁

获取锁时,需要在 etcd 做一个标记;根据这些标记进行排序(根据标记的创建版本号排序);
做标记的同时,需要获取当前持有锁的对象;
如果持有锁的对象不是自己,监听版本号刚好小于自己的对象的删除信息;
问题1:如果当前 watch 的对象已经退出但自己仍没有获取锁的权限,怎么修改监听对象?
答案1:继续监听版本号刚好小于自己的对象的删除信息;
问题2:监听动作什么情况下结束?
答案2:没有比自己更小的版本号对象,那么自己就获取了持锁权限。

func (m *Mutex) Lock(ctx context.Context) error { 
	s := m.s 
	client := m.s.Client() 
	m.myKey = fmt.Sprintf("%s%x", m.pfx, s.Lease()) 
	cmp := v3.Compare(v3.CreateRevision(m.myKey), "=", 0) //判断key有没有创建过
	
	// put self in lock waiters via myKey; oldest waiter holds lock 
	put := v3.OpPut(m.myKey, "", v3.WithLease(s.Lease())) //没有创建过则插入,设置lease值

	// reuse key in case this session already holds the lock 
	get := v3.OpGet(m.myKey) //获取当前谁持有锁

	// fetch current holder to complete uncontended path with only one RPC 
	getOwner := v3.OpGet(m.pfx, v3.WithFirstCreate()...) 
	resp, err := client.Txn(ctx).If(cmp).Then(put, getOwner).Else(get, getOwner).Commit() 
	if err != nil { 
		return err 
	}
	m.myRev = resp.Header.Revision
	if !resp.Succeeded { 
		m.myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision 
	}

	// if no key on prefix / the minimum rev is key, already hold the lock 
	ownerKey := resp.Responses[1].GetResponseRange().Kvs 
	if len(ownerKey) == 0 || ownerKey[0].CreateRevision == m.myRev { //自己持有锁,直接返回
		m.hdr = resp.Header 
		return nil 
	}

	// wait for deletion revisions prior to myKey 
	hdr, werr := waitDeletes(ctx, client, m.pfx, m.myRev-1) //不是我持有锁,则监听上一个的删除锁事件

	// release lock key if wait failed 
	if werr != nil { 
		m.Unlock(client.Ctx()) 
	} else { 
		m.hdr = hdr 
	}
	return werr 
}

func waitDeletes(ctx context.Context, client *v3.Client, pfx string, maxCreateRev int64) (*pb.ResponseHeader, error) { 
	getOpts := append(v3.WithLastCreate(), v3.WithMaxCreateRev(maxCreateRev)) //找恰好比我小1的kv版本,即revison-1
	for {
		resp, err := client.Get(ctx, pfx, getOpts...) //获取比我小的那个kv
		if err != nil { 
			return nil, err 
		}
		if len(resp.Kvs) == 0 { 
			return resp.Header, nil 
		}
		lastKey := string(resp.Kvs[0].Key) 
		if err = waitDelete(ctx, client, lastKey, resp.Header.Revision); err != nil { //等待删除事件
			return nil, err //当前自己持有锁
		} 
	} 
}
释放锁
func (m *Mutex) Unlock(ctx context.Context) error { 
	client := m.s.Client() 
	if _, err := client.Delete(ctx, m.myKey); err != nil { 
		return err 
	}
	m.myKey = "\x00" 
	m.myRev = -1 
	return nil 
}

(4)总结

  • 整体行为
    etcd是kv的,如果有lock:leaseid1、lock:leaseid2、lock:leaseid3、lock:leaseid4四个key,那么:
    (1)锁是一种资源:
    锁存在lock,以前缀方式存储;第一个设置成功的获取锁成功
    (2)互斥:
    创建对象是有先后顺序的,先创建先有
    (3)watch:
    lock:leaseid2监听lock:leaseid1的删除,删除后lock:leaseid2获取锁;lock:leaseid3监听lock:leaseid2、lock:leaseid4监听lock:leaseid3。

总的来说,先判断持有锁的是不是自己,是的话直接返回,如果不是则监听上一个kv的删除事件。

[推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值