1

ETCD探索-Lease

梗概

租约,是ETCD的重要特性,用于实现key定时删除功能。与Redis的定时删除功能基本一致。

猜想

我们通常是这么使用Lease的,首先申请一个租约:lease,然后将这个租约赋给一对KeyValue。
image.png
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发送出来,由使用者主动删除。

首先我们看下Grant,申请一个租约的过程
image.png

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的过程
image.png

Attach首先用LeaseID去LeaseMap中查询租约是否存在,如果没有这个租约返回错误。
租约存在则首先将Item保存到对应的租约下(图中没有注明),后将Item和LeaseID保存在ItemMap中。

最后看下Revoke过程
image.png

通常会有一个协程不断消费expiredChan,将过期的租约Revoke。
Revoke首先根据LeaseID从LeaseMap找到对于的Lease并从LeaseMap中删除,后从Lease中找到绑定的Key,从Backend中将KeyValue删除。

以上便是ETCD-Lease的核心逻辑,与猜想中的方案对比,我认为最主要的是优先级队列的使用。

Lessor还有一个概念是Primary,只有ETCD集群中的Leader拥有的Lessor是Primary。也只有是Primary的Lessor可以操作租约。因为与Raft相关,而且与Lease的核心逻辑无关,这里不多介绍。


HammerMax
128 声望17 粉丝

« 上一篇
ETCD探索-Watch
下一篇 »
ETCD探索