一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情。
前面介绍了# etcd 如何实现租约?,本文将会具体介绍 lease 模块架构的解析。
lease 模块架构
lease 模块对外提供了 Lessor 接口,其中定义了包括 Grant、Revoke、Attach 和 Renew 等常用的方法,lessor 结构体实现了 Lessor 接口。lessor 依赖 Lease 对象,而与 BoltDB 的交互也是通过 Lease 实现。lease 模块涉及到的主要对象和接口。
除此之外,lessor 还启动了两个异步 goroutine:RevokeExpiredLease 和 CheckpointScheduledLease,分别用于撤销过期的租约和更新 Lease 的剩余到期时间。
Lessor 接口
Lessor 接口定义了创建、销毁、延长租约的方法:
// 位于 lease/lessor.go:82
type Lessor interface {
//...省略部分接口
// Grant 创建了一个在指定时间过期的 lease 对象
Grant(id LeaseID, ttl int64) (*Lease, error)
// Revoke 撤销指定 Id 的 lease,绑定到其上的键值对将会被移除,如果该 Id 对应的 lease 不存在,则会返回错误
Revoke(id LeaseID) error
// Attach 绑定给定的 leaseItem 到 LeaseID,如果该租约不存在,将会返回错误
Attach(id LeaseID, items []LeaseItem) error
// GetLease 返回 LeaseItem 对应的 LeaseID
GetLease(item LeaseItem) LeaseID
// Detach 将 leaseItem 从给定的 LeaseID 解绑。如果租约不存在,则会返回错误
Detach(id LeaseID, items []LeaseItem) error
// Renew 刷新指定 id 的 lease,结果将会返回刷新后的 TTL
Renew(id LeaseID) (int64, error)
// Lookup 查找指定的 LeaseID,返回对应的 Lease
Lookup(id LeaseID) *Lease
// Leases 列出所有的 leases
Leases() []*Lease
// ExpiredLeasesC 用于返回接收过期 lease 的 channel
ExpiredLeasesC() <-chan []*Lease
}
Lessor 接口定义了很多方法,租约相关的方法都在这里面。常用的方法有:
- Grant 创建一个在指定时间过期的 lease 对象
- Revoke 撤销指定 Id 的 lease,绑定到其上的键值对将会被移除
- Attach 绑定给定的 leaseItem 到 LeaseID
- Renew 刷新指定 id 的 lease,结果将会返回刷新后的 TTL
Lease 与 lessor 结构体
我们接着看下租约相关的 Lease 结构体:
// 位于 lease/lessor.go:800
type Lease struct {
ID LeaseID
ttl int64 // time to live of the lease in seconds
remainingTTL int64 // remaining time to live in seconds, if zero valued it is considered unset and the full ttl should be used
// expiryMu protects concurrent accesses to expiry
expiryMu sync.RWMutex
// expiry is time when lease should expire. no expiration when expiry.IsZero() is true
expiry time.Time
// mu protects concurrent accesses to itemSet
mu sync.RWMutex
itemSet map[LeaseItem]struct{}
revokec chan struct{}
}
租约包含租约 ID、ttl、过期时间等属性。lessor 则是对租约的封装。暴露出一系列操作租约的方法,比如创建、销毁、延长租约的方法。
type lessor struct {
mu sync.RWMutex
// demotec is set when the lessor is the primary.
// demotec will be closed if the lessor is demoted.
demotec chan struct{}
leaseMap map[LeaseID]*Lease
leaseExpiredNotifier *LeaseExpiredNotifier
leaseCheckpointHeap LeaseQueue
itemMap map[LeaseItem]LeaseID
// When a lease expires, the lessor will delete the
// leased range (or key) by the RangeDeleter.
rd RangeDeleter
// When a lease's deadline should be persisted to preserve the remaining TTL across leader
// elections and restarts, the lessor will checkpoint the lease by the Checkpointer.
cp Checkpointer
// backend to persist leases. We only persist lease ID and expiry for now.
// The leased items can be recovered by iterating all the keys in kv.
b backend.Backend
// minLeaseTTL is the minimum lease TTL that can be granted for a lease. Any
// requests for shorter TTLs are extended to the minimum TTL.
minLeaseTTL int64
expiredC chan []*Lease
// stopC is a channel whose closure indicates that the lessor should be stopped.
stopC chan struct{}
// doneC is a channel whose closure indicates that the lessor is stopped.
doneC chan struct{}
lg *zap.Logger
// Wait duration between lease checkpoints.
checkpointInterval time.Duration
// the interval to check if the expired lease is revoked
expiredLeaseRetryInterval time.Duration
}
lessor 实现了 Lessor 接口,lessor 中维护了三个数据结构:
- LeaseMap
map[LeaseID]*Lease 用于根据 LeaseID 快速找到 *Lease - ItemMap
map[LeaseItem]LeaseID 用于根据 LeaseItem 快速找到 LeaseID,从而找到 *Lease - LeaseExpiredNotifier
LeaseExpiredNotifier 是对 LeaseQueue 的一层封装,他实现了快要到期的租约永远在队头。
LeaseQueue 是一个优先级队列,每次插入都会根据过期时间插入到合适的位置。通过这个队列,我们只需要不断检查队头的租约是否到期即可,而避免为每一个租约起一个协程。
关于优先级队列,普遍的做法都是用堆来实现,etcd 中也不例外,他用的是 Go 标准库中的 container/heap 来实现的。可以看到,里边有 leaseMap 和 leaseHeap ,为什么要有两个呢?此处的 leaseHeap 实现是最小堆,比较的关键是 Lease 失效的时间。
怎么保证 lease 失效呢?我们每次从最小堆里判断堆顶元素是否失效,失效就 Pop 就可以了。那为什么又要有 leaseMap 呢?因为 这样可以加速查找,毕竟,哈希表的时间复杂度是 O(1) 。