ETCD探索-Lease
梗概
租约,是ETCD的重要特性,用于实现key定时删除功能。与Redis的定时删除功能基本一致。
猜想
我们通常是这么使用Lease的,首先申请一个租约:lease,然后将这个租约赋给一对KeyValue。
ETCD-Lease的实现不难,在讨论怎么实现之前,可以先猜测下。
我的直观想法:
func putWithLease(key string, value string, ttl int) {
go func() {
time.Sleep(ttl * time.Second)
delete(key)
}()
put(key, value)
}
简单说明,当put一对kv时,开启一个协程用于计时。当过了ttl后,将该key删除。
这么做可以实现key的定时删除功能,但有一些问题:
- 不容易续租(续租:延长ttl)
- 不容易提前删除租约
之所以说不容易
,是说你可以通过添加复杂的逻辑实现这些功能,但这样做有一个无法避免的问题:
- 当租约很多时,协程就会很多
虽然起一个协程成本很低,但过多的协程对资源浪费严重,还有可能被操纵系统强行kill。
那么我们来看下ETCD是如何实现Lease的
实现
结构体介绍
- backend
在我们对MVCC的介绍中,我们知道ETCD的数据最终都是存在backend结构体中,所以backend掌握了对数据的增、删、改、查。租约使用了backend的删除能力。
- Lease
租约,包含租约ID、ttl、过期时间等属性。
- LeaseItem
只有一个属性:key。即保存了租约依附的key。说白了就是Key
- LeaseQueue
租约队列,多个租约是以队列的形式保存在LeaseQueue中。
- Lessor
对租约的封装。暴露出一系列操作租约的方法,比如创建、销毁、延长租约的方法。
如何使用租约
我如果想给key=foo绑定一个租约,并且时间过期后将key删除
func testLease() {
le := newLessor() // 创建一个lessor
le.Promote(0) // 将lessor设置为Primary,这个与raft会出现网络分区有关,不了解可以忽略
go func() { // 开启一个协程,接收过期的key,主动删除
for {
expireLease := <-le.ExpiredLeasesC()
for _, v := range expireLease {
le.Revoke(v.ID) // 通过租约ID删除租约,删除租约时会从backend中删除绑定的key
}
}
}()
ttl = 5 // 过期时间设置5s
lease := le.Grant(id, ttl) // 申请一个租约
le.Attach(lease, "foo") // 将租约绑定在"foo"上
time.Sleep(10 * time.Second) // 阻塞10s,方便看到结果
}
以上展示了是如何使用lessor这个结构体的。不难看出,lessor提供了Grant、Revoke、Attach等一系列对租约的操作。同时有一点需要注意,lessor不会主动删除过期的租约,而是将过期的lease通过一个chan发送出来,由使用者主动删除。
lessor中维护了三个数据结构
- LeaseMap
map[LeaseID]*Lease
用于根据LeaseID快速找到*Lease - ItemMap
map[LeaseItem]LeaseID
用于根据LeaseItem快速找到LeaseID,从而找到*Lease - LeaseExpiredNotifier
LeaseExpiredNotifier是对LeaseQueue的一层封装,他实现了快要到期的租约永远在队头
。
正如图中所述,LeaseQueue是一个优先级队列,每次插入都会根据过期时间插入到合适的位置。通过这个队列,我们只需要不断检查队头的租约是否到期即可,而避免了猜想
中的方法,为每一个租约起一个协程。
关于优先级队列,普遍的做法都是用堆来实现,ETCD中也不例外,他用的是GO标准库中的container/heap
来实现的。这里不具体说了。
从图中可以看出,当Grant一个租约l时,l被同时放到了LeaseMap和LeaseExpiredNotifier中。
在队列头,有一个工作协程revokeExpiredLeases不断的查看队头的租约是否过期,如果过期就放入expiredChan中,不过此时不会pop。(只有revoke才会从队头删除)
Attach首先用LeaseID去LeaseMap中查询租约是否存在,如果没有这个租约返回错误。
租约存在则首先将Item保存到对应的租约下(图中没有注明),后将Item和LeaseID保存在ItemMap中。
通常会有一个协程不断消费expiredChan,将过期的租约Revoke。
Revoke首先根据LeaseID从LeaseMap找到对于的Lease并从LeaseMap中删除,后从Lease中找到绑定的Key,从Backend中将KeyValue删除。
以上便是ETCD-Lease的核心逻辑,与猜想
中的方案对比,我认为最主要的是优先级队列的使用。
Lessor还有一个概念是Primary,只有ETCD集群中的Leader拥有的Lessor是Primary。也只有是Primary的Lessor可以操作租约。因为与Raft相关,而且与Lease的核心逻辑无关,这里不多介绍。