云原⽣etcd系列-6|租约机制
云原⽣ etcd 系列-6|⽤“租约”给 key 加⼀个期限!
什么是租约?
在 redis 中有⼀个 ttl 的功能。ttl 是 time to live 的缩写。在 redis ⾥我们可以设置 key 的 ttl ,从⽽指定这个 key 存活的时间,过期就会⾃动销毁。
在 etcd 也有⼀个类似的机制:租约( Lea )机制。从效果上来讲,租约机制也能做到类似的过期⾃动删除 key 的功能。但是两者细节⼤有不同。
租约( Lea )是什么?
简单讲就是⼀个具有⼀个时间期限的“对象”。
划重点:时间期限。
举个不准确的栗⼦:
有⼀个⼤公司(代表⼀个中⼼权威组织,⽐如 etcd )有个粗活,并且⼯作特殊明确只需要⼀个程序猿( worker )。月光传说
那⼀般怎么操作呢?
有个程序猿 A 想做这个事,来找公司申请,于是公司给了他⼀个 3 天期限的租约( Lea ),并承诺该权限 3 天内不会再给这个权限给别⼈,但是 3 天之后,公司就可以另寻他猿了( 注意:如果猿 A 在 3 天内续约了,A 就可以延续他的权限了,那就是另外⼀回事了 )。
这样的话,就能保证始终只有⼀个猿有合法权限做这件事。 如果猿 A 三天后不抗压、失联了,那 3 天之后公司也能安全的(没有违反承诺)再找⼀只猿。
划重点:租约就是⼀个带时间期限的承诺。
怎么使⽤租约?
和 redis 不同,etcd 中把这个时间相关的概念抽离出来,命名为 Lea 对象。所以,要使⽤租约则先要创建这么⼀个 Lea 对象。然后把 key 绑定到这个 Lea 上,就相当于设置了这个 key 的⽣命周期。
细节来了,key 和 Lea 是怎么对应的?
划重点:key 和 Lea 是多对⼀的关系。⼀个 key 最多只能挂绑定⼀个 Lea ,但是⼀个 Lea 上能挂多个 key 。 这种设计提⾼ etcd 整体的性能。Lea 刷新⼀次就对应了⼀批的 key ,否则每⼀个 key 都独⽴刷新 ttl 的话,开销可不⼩呢。
举个 etcd 实际的栗⼦,怎么设置⼀个 60 秒有效的 key ?如下:
先创建⼀个 Lea 对象:
root@ubuntu:~/# etcdctl lea grant 60
lea 694d7d17eaab280f granted with TTL(60s)
再把⼀个 key 绑定到这个 Lea :
root@ubuntu:~/# etcdctl put hello world --lea=694d7d17eaab280f
OK
这样 hello/world 这对 key/value 就创建好了,并且 60 秒后将被⾃动删除。
租约机制的适⽤场景?
有些童鞋可能会好奇,租约⼀般⽤来做什么呢?
就本质上来讲,租约就是⼀个具有⽣命周期的对象。怎么使⽤它?这依赖于⽤户的想象⼒。
曾经我在分布式的分享章节⾥提到过,lea 是分布式的基⽯技术之⼀,lea 就是⼀个有时间限定的权限(承诺)。分布式的冗余节点都可以来申请⼀段时间的权限(有了这个权限就可以做某件事情),租约过期之后就可以回收,租约没过期之前就维持承诺。这个租约的管理⼀般放在⼀个中⼼化的节点(或者集群中),⽐如 etcd 集群。
为什么申请的权限⼀定要附上时间期限呢?
因为在分布式的恶劣环境下,谁都有可能挂。挂了的话,冗余节点要能顶上来,这个权限要能安全移交。租约没过期之前,权限的移交都是不安全的。租约过期之后,权限就能安全移交。所以,租约常常⽤在恶劣的分布式系统中做可靠的授权管理。
还⼀个 etcd 最常见的场景是当作注册中⼼来⽤,worker 节点注册到 etcd 集群。每个都申请了租约,并且定期的会续约( keep-alive 保活),⼀旦长久失效,那么就可以剔除。这样起到⼀个节点的管理之⽤。
etcd 的租约原理
下⾯从 etcd 内部的实现原理出发,来看租约机制的核⼼知识点。在 etcd 中,由⼀个叫做 lessor 的对象来管理租约,并且关于续租等等操作都必须要是 leader 才能操作。
1 租约的创建
租约的创建必须⾛ raft 状态机,把 Lea 创建这个消息在集群中达到⼀致,达到⼀致之后,每个节点就可以构建 Lea 结构体,并且持久化这个结构体到 boltdb 中,存储在⼀个叫做 “lea” 的 bucket 中。
func (le *lessor) Grant(id LeaID, ttl int64) (*Lea, error) {
refu什么意思// 构造⼀个 Lea 结构体
venyl := &Lea{
ID: id,
ttl: ttl,
// ..
}
// 设置 expire time( primary 可做)
交替传译// Lea 在内存的 map ⾥也放⼀份,好索引呀
le.leaMap[id] = l
怎样学习英语// 持久化到 boltdb ⾥去
l.persistTo(le.b)
// 投递到⼀个带⼩堆的队列中,这个关联超时机制(primary 可做)
le.leaExpiredNotifier.RegisterOrUpdate(item)
// 投递到 checkpoint 的队列中,这个关联 checkpoint 机制(primary 可做)
le.scheduleCheckpointIfNeeded(l)
}
租约创建很简单,最关键的是先要⾛ raft 机制,然后⾛上⾯的 grant 流程,传⼊⼀个 LeaID,⼀个 ttl ,持久化到 boltdb 并修改内存结构,主要步骤:
1. 构建⼀个 Lea 结构体;
2. 修改 leaMap,id => lea ;learned
3. 持久化,把 Lea 这个结构体写到磁盘( boltdb );
4.
1. 对应写到“lea” 这个桶⾥;
5. 设置 piry ,这⾥是设置为 now + ttl 的时间,是未来超时的那个时刻;
6. 构建⼀个 LeaWithTime 的结构体,加⼊到 heap ⾥⾯去管理;
欧美7.
1. 加到 leaExpiredNotifier ⾥⾯,关联超时机制;
2. 加到 leaCheckpointHeap ⾥⾯,关联 checkpoint 机制;
inhuman划重点:Lea 的创建是要持久化的,并且是先⾛ raft 的状态机在 etcd 集群达到⼀致后,才持久化到 boltdb 中。
2 租约的绑定
key 是怎么绑定到 Lea 的呢?这是⼀个⾮常关键的问题。
时机肯定在 key/value 上传的时候,也就是 put 的时候,位于 storeTxnWrite.put ⽅法之中:
// etcd/mvcc/
func (tw *storeTxnWrite) put(key, value []byte, leaID lea.LeaID) {
// ...
// 存储到 bolt db ⽂件
// ...
// 如果 leaID 有效,那么说明要绑定 Lea 了
if leaID != lea.NoLea {
// LeaID 和 key 关联起来
err = tw.s.le.Attach(leaID, []lea.LeaItem{{Key: string(key)}})
}
}
划重点:数据持久化到 boltdb 之后,再去关联对应的 Lea 结构体。 那关联是什么操作呢?很简单,就是把这个 key 加到 Lea 内部的 map 中:
// etcd/
func (le *lessor) Attach(id LeaID, items []LeaItem) error {
for _, it := range items {
// 把这个 key 放到 Lea 结构体⾥
l.itemSet[it] = struct{}{}
// 把这个 key 放到 lessor 的结构体⾥,这⾥作为⼀个平坦的 map
le.itemMap[it] = id
}
}
跟这个 Lea 关联的所有 key 都在 Lea.itemSet 这个 map 中。
3 租约的过期
租约的过期和销毁是 etcd 内部的流程触发。租约的过期在创建的时候就关联上了,还记得创建的时候有⼀个加队列的代码吗?
// 投递到⼀个带⼩堆的队列中,这个关联超时机制(primary 可做)
le.leaExpiredNotifier.RegisterOrUpdate(item)
meto这⾏代码把 Lea 加到⼀个内含最⼩堆的结构中。每次都看⼩堆顶即可(因为它⽣命剩余最⼩,最有可能超时),⼩堆顶的 Lea 超时了,那么就取出来,直到取到没超时的 Lea ,那么本轮结束。
// 取出⼀个超时的 Lea 结构,它上⾯可能有⼀批的 key
func (le *lessor) expireExists() (l *Lea, ok bool, next bool) {
// 取⼩堆顶
item := le.leaExpiredNotifier.Poll()
now := time.Now()
// 看是否超时
if now.UnixNano() < item.time /* expiration time */ {
return l, fal, fal
}
}
这样每次都处理⼀批超时的 Lea 结构,⾛销毁流程,过期销毁主要做两件事:
1. 销毁 Lea 本⾝;labeled
2. 销毁 Lea 关联的 key/value 键值对 ;
key 被销毁之后就相当于被⾃动删除了,⽤户就下载不到了。 销毁的流程在 lessor.Revoke :
func (le *lessor) Revoke(id LeaID) error {
// 遍历删除这个 Lea 关联的所有 key (从 boltdb ⾥删除)
for _, key := range keys {
txn.DeleteRange([]byte(key), nil)
}
// 销毁内存结构
delete(le.leaMap, l.ID)
/
/ 把这个 Lea 从 boltdb 的 lea 桶⾥删除
le.b.BatchTx().UnsafeDelete(leaBucketName, int64ToBytes(int64(l.ID)))
}
划重点:Lea 的销毁不仅是内存的,还有 boltdb 的 lea 桶⾥的都要清理。是设计到持久化的。
4 租约的续租