在 ETCD 源码学习过程,不会讲解太多的源码知识,只讲解相关的实现机制,需要关注源码细节的朋友可以自行根据文章中的提示,找到相关源码进行学习。
本文主要介绍 lessor 如果发现过期键,server 如何处理过期键的过程。
过期 lease 发现
lessor goroutine
func (le *lessor) runLoop() {
defer close(le.doneC)
for {
le.revokeExpiredLeases()
le.checkpointScheduledLeases()
...
}
}
runLoop 中,调用 revokeExpiredLeases 找到过期的 lease,并推入 expiredC,等待被处理。
func (le *lessor) revokeExpiredLeases() {
var ls []*Lease
//最大过期元素数量
revokeLimit := leaseRevokeRate / 2
le.mu.RLock()
if le.isPrimary() { //判断是否为主 Lessor
ls = le.findExpiredLeases(revokeLimit) //查找过期键
}
le.mu.RUnlock()
if len(ls) != 0 {
select {
...
case le.expiredC <- ls: //推入管道,等待上传处理
default:
}
}
}
func (le *lessor) findExpiredLeases(limit int) []*Lease {
leases := make([]*Lease, 0, 16)
for {
//在过期 Lease 的优先级队列中,找到过期 Lease
l, ok, next := le.expireExists()
...
if l.expired() {
leases = append(leases, l)
if len(leases) == limit { //如果能达到收集的最大元素个数
break
}
}
}
return leases
}
func (le *lessor) expireExists() (l *Lease, ok bool, next bool) {
...
item := le.leaseExpiredNotifier.Poll() //获取过期优先级队列中的第一个元素
...
now := time.Now()
//第一个元素还没过期(第一个元素是最早过期元素)
if now.UnixNano() < item.time /* expiration time */ {
return l, false, false
}
//这里重新更新时间是为了让这个元素不要在顶部,以便下一次收集过期元素
item.time = now.Add(le.expiredLeaseRetryInterval).UnixNano()
le.leaseExpiredNotifier.RegisterOrUpdate(item)
return l, true, false
}
Server 处理过期 Lease
run(){
var expiredLeaseC <-chan []*lease.Lease
if s.lessor != nil {
expiredLeaseC = s.lessor.ExpiredLeasesC()
}
for {
select {
...
case leases := <-expiredLeaseC: //过期租约
//启动一个 goroutine,处理过期租约
s.goAttach(func() {
c := make(chan struct{}, maxPendingRevokes)
for _, lease := range leases {
...
lid := lease.ID
s.goAttach(func() { //撤销租约
ctx := s.authStore.WithRoot(s.ctx)
_, lerr := s.LeaseRevoke(ctx, &pb.LeaseRevokeRequest{ID:int64(lid)})
...
<-c
})
}
})
return
}
}}
在 s.LeaseRevoke 中,其实走的一个是提案流程,走完整个提案流程之后,才会转换为 apply 消息,被节点应用。
//server 应用提案消息
func (s *EtcdServer) applyEntryNormal(e *raftpb.Entry) {
...
var ar *applyResult
needResult := s.w.IsRegistered(id) //检查请求是否需要响应
if needResult || !noSideEffect(&raftReq) {
...
ar = s.applyV3.Apply(&raftReq) //应用消息
}
...
}
//etcdserver/apply.go
func (a *applierV3backend) Apply(r *pb.InternalRaftRequest) *applyResult {
...
switch {
case r.LeaseGrant != nil:
ar.resp, ar.err = a.s.applyV3.LeaseGrant(r.LeaseGrant)
case r.LeaseRevoke != nil:
ar.resp, ar.err = a.s.applyV3.LeaseRevoke(r.LeaseRevoke)
case r.LeaseCheckpoint != nil:
ar.resp, ar.err = a.s.applyV3.LeaseCheckpoint(r.LeaseCheckpoint)
package 3
...
}
return ar
}
func (a *applierV3backend) LeaseGrant(lc *pb.LeaseGrantRequest) (*pb.LeaseGrantResponse, error) {
l, err := a.s.lessor.Grant(lease.LeaseID(lc.ID), lc.TTL)
resp := &pb.LeaseGrantResponse{}
...
return resp, err
}
func (a *applierV3backend) LeaseRevoke(lc *pb.LeaseRevokeRequest) (*pb.LeaseRevokeResponse, error) {
err := a.s.lessor.Revoke(lease.LeaseID(lc.ID))
return &pb.LeaseRevokeResponse{Header: newHeader(a.s)}, err
}
总结
1.过期键的发现主要通过一个时间戳的优先级队列,这里主要通过最小堆这种数据结构实现,将最小的时间戳,置于顶部,每次只需要判断顶部元素即可。
2.过期键收集要在一定的范围内,不控制每次最大的过期键的处理数量,如果有大量键同时过期,会导致 server 在一定时间内忙于处理过期键,而无法响应其他请求。
3.server 处理过期键的过程是一个完整的提案过程。