etcd 申请租约、绑定和撤销租约的实现解析

1,506 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情

前面介绍了lease 模块架构的解析,本文将会具体介绍 Grant 申请租约,绑定和撤销租约的实现。

Grant 申请租约

Grant 方法用于申请租约,并在指定的 ttl 失效。具体实现如下:

// 位于 lease/lessor.go:258
func (le *lessor) Grant(id LeaseID, ttl int64) (*Lease, error) {
	if id == NoLease {
		return nil, ErrLeaseNotFound
	}

	if ttl > MaxLeaseTTL {
		return nil, ErrLeaseTTLTooLarge
	}

	// TODO: when lessor is under high load, it should give out lease
	// with longer TTL to reduce renew load.
	l := &Lease{
		ID:      id,
		ttl:     ttl,
		itemSet: make(map[LeaseItem]struct{}),
		revokec: make(chan struct{}),
	}

	le.mu.Lock()
	defer le.mu.Unlock()

	if _, ok := le.leaseMap[id]; ok {
		return nil, ErrLeaseExists
	}

	if l.ttl < le.minLeaseTTL {
		l.ttl = le.minLeaseTTL
	}

	if le.isPrimary() {
		l.refresh(0)
	} else {
		l.forever()
	}

	le.leaseMap[id] = l
	item := &LeaseWithTime{id: l.ID, time: l.expiry.UnixNano()}
	le.leaseExpiredNotifier.RegisterOrUpdate(item)
	l.persistTo(le.b)

	leaseTotalTTLs.Observe(float64(l.ttl))
	leaseGranted.Inc()

	if le.isPrimary() {
		le.scheduleCheckpointIfNeeded(l)
	}

	return l, nil
}

绘制的流程图如下所示:

image.png 可以看到,当 Grant 一个租约 l 时,l 被同时放到了 LeaseMap 和 LeaseExpiredNotifier 中。

在队列头,有一个工作协程 revokeExpiredLeases 不断的查看队头的租约是否过期,如果过期就放入 expiredChan 中,不过此时不会 pop (只有 revoke 才会从队头删除)。

Attach 绑定租约

Attach 用于绑定指定的 Lease Id 到一系列的键值对上面。当租约过期,绑定的键值对会被自动移除。绑定时,如果给定的 lease 不存在,将会返回错误。

// 位于 lease/lessor.go:518
func (le *lessor) Attach(id LeaseID, items []LeaseItem) error {
	le.mu.Lock()
	defer le.mu.Unlock()

	l := le.leaseMap[id]
	if l == nil {
		return ErrLeaseNotFound
	}

	l.mu.Lock()
	for _, it := range items {
		l.itemSet[it] = struct{}{}
		le.itemMap[it] = id
	}
	l.mu.Unlock()
	return nil
}

Attach 首先用 LeaseID 去 LeaseMap 中查询租约是否存在,如果没有这个租约返回错误。

租约存在则首先将 Item 保存到对应的租约下(图中没有注明),后将 Item 和 LeaseID 保存在 ItemMap 中。

Revoke 撤销租约

Revoke 方法用于撤销指定 Id 的租约,绑定到该 Lease 上的键值都会被移除。

// 位于
func (le *lessor) Revoke(id LeaseID) error {
	le.mu.Lock()

	l := le.leaseMap[id]
	if l == nil {
		le.mu.Unlock()
		return ErrLeaseNotFound
	}
	defer close(l.revokec)
	// unlock before doing external work
	le.mu.Unlock()

	if le.rd == nil {
		return nil
	}

	txn := le.rd()

	// sort keys so deletes are in same order among all members,
	// otherwise the backend hashes will be different
	keys := l.Keys()
	sort.StringSlice(keys).Sort()
	for _, key := range keys {
		txn.DeleteRange([]byte(key), nil)
	}

	le.mu.Lock()
	defer le.mu.Unlock()
	delete(le.leaseMap, l.ID)
	// lease deletion needs to be in the same backend transaction with the
	// kv deletion. Or we might end up with not executing the revoke or not
	// deleting the keys if etcdserver fails in between.
	le.b.BatchTx().UnsafeDelete(leaseBucketName, int64ToBytes(int64(l.ID)))

	txn.End()

	leaseRevoked.Inc()
	return nil
}

绘制的流程图如下所示。

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

阅读最新文章,关注公众号:aoho求索