Etcd调用KeepAlive时报"lease keepalive response queue is full; dropping response send"警告信息的正确处理方式

在使用Etcd做服务注册时,会用到KeepAlive来维持心跳,官方说法叫续租。但是在调用KeepAlive之后,很有可能会报一个警告:{"level":"warn","ts":"2019-07-19T10:56:49.229+0800","caller":"clientv3/lease.go:524","msg":"lease keepalive response queue is full; dropping response send","queue-size":16,"queue-capacity":16}。其实这个警告并不是什么错误,但是是可以消除的。

来不及解释了,快上车。

这个提示信息是json格式的,我们稍微美化一下。

{
    "level": "warn",
    "ts": "2019-07-19T10:56:49.229+0800",
    "caller": "clientv3/lease.go:524",
    "msg": "lease keepalive response queue is full; dropping response send",
    "queue-size": 16,
    "queue-capacity": 16
}

信息中给出了信息发出的地方是clientv3/lease.go文件的第524行,还有queue-sizequeue-capacity都是16,字面上来讲,一个是队列长度,一个是队列容量,熟悉go语言的应该知道长度和容量的含义。

我们定位到发出消息的函数,如下:

func (l *lessor) recvKeepAlive(resp *pb.LeaseKeepAliveResponse) {
	karesp := &LeaseKeepAliveResponse{
		ResponseHeader: resp.GetHeader(),
		ID:             LeaseID(resp.ID),
		TTL:            resp.TTL,
	}

	l.mu.Lock()
	defer l.mu.Unlock()

	ka, ok := l.keepAlives[karesp.ID]
	if !ok {
		return
	}

	if karesp.TTL <= 0 {
		// lease expired; close all keep alive channels
		delete(l.keepAlives, karesp.ID)
		ka.close()
		return
	}

	// send update to all channels
	nextKeepAlive := time.Now().Add((time.Duration(karesp.TTL) * time.Second) / 3.0)
	ka.deadline = time.Now().Add(time.Duration(karesp.TTL) * time.Second)
	for _, ch := range ka.chs {
		select { //===看这里===
		case ch <- karesp:
		default:
			if l.lg != nil {
				l.lg.Warn("lease keepalive response queue is full; dropping response send",
					zap.Int("queue-size", len(ch)),
					zap.Int("queue-capacity", cap(ch)),
				)
			}
		}
		// still advance in order to rate-limit keep-alive sends
		ka.nextKeepAlive = nextKeepAlive
	}
}

最根本的原因就是往ch这个channel里面发送数据的时候失败了,导致select执行到了default分支。

接着看ch的来历,它是遍历ka.chs得到的结果,很明显,chs应该是一个channel的切片,事实上也是。

再看ka,它是通过l.keepAlives[karesp.ID]得到的,那么keepAlives应该是一个map,这里的karesp.ID就是我们调用clientv3.Put以及clientv3.KeepAlive时带入的租约ID。接下来就是要看看lessor结构体,也就是这个方法的拥有者。

type lessor struct {
	mu sync.Mutex // guards all fields

	// donec is closed and loopErr is set when recvKeepAliveLoop stops
	donec   chan struct{}
	loopErr error

	remote pb.LeaseClient

	stream       pb.Lease_LeaseKeepAliveClient
	streamCancel context.CancelFunc

	stopCtx    context.Context
	stopCancel context.CancelFunc
	//===看这里===
	keepAlives map[LeaseID]*keepAlive

	// firstKeepAliveTimeout is the timeout for the first keepalive request
	// before the actual TTL is known to the lease client
	firstKeepAliveTimeout time.Duration

	// firstKeepAliveOnce ensures stream starts after first KeepAlive call.
	firstKeepAliveOnce sync.Once

	callOpts []grpc.CallOption

	lg *zap.Logger
}

不出所料,keepAlives是一个从租约ID(LeaseID)到keepAlive的映射,keepAlive也是一个结构体:

// keepAlive multiplexes a keepalive for a lease over multiple channels
type keepAlive struct {
    //===看这里===
	chs  []chan<- *LeaseKeepAliveResponse
	ctxs []context.Context
	// deadline is the time the keep alive channels close if no response
	deadline time.Time
	// nextKeepAlive is when to send the next keep alive message
	nextKeepAlive time.Time
	// donec is closed on lease revoke, expiration, or cancel.
	donec chan struct{}
}

可以看到keepAlivechs字段,正是一个channel的切片,channel的类型是*LeaseKeepAliveResponse,这是一个我们非常熟悉的类型,因为我们调用KeepAlive的时候,返回值就是一个该类型的channel。现在我们知道,租约被存储在一个字典中(keepAlives),每个租约对应着一个keepAlive结构,也就是说每个租约对应着多个*LeaseKeepAliveResponse类型的channel。

现在,我们再来看看我们调用的KeepAlive函数:

func (l *lessor) KeepAlive(ctx context.Context, id LeaseID) (<-chan *LeaseKeepAliveResponse, error) {
	ch := make(chan *LeaseKeepAliveResponse, LeaseResponseChSize)

	l.mu.Lock()
	// ensure that recvKeepAliveLoop is still running
	select {
	case <-l.donec:
		err := l.loopErr
		l.mu.Unlock()
		close(ch)
		return ch, ErrKeepAliveHalted{Reason: err}
	default:
	}
	ka, ok := l.keepAlives[id]
	if !ok {
		// create fresh keep alive
		ka = &keepAlive{
			chs:           []chan<- *LeaseKeepAliveResponse{ch},//===看这里===
			ctxs:          []context.Context{ctx},
			deadline:      time.Now().Add(l.firstKeepAliveTimeout),
			nextKeepAlive: time.Now(),
			donec:         make(chan struct{}),
		}
		l.keepAlives[id] = ka
	} else {
		// add channel and context to existing keep alive
		ka.ctxs = append(ka.ctxs, ctx)
		ka.chs = append(ka.chs, ch)//===还有这里===
	}
	l.mu.Unlock()

	go l.keepAliveCtxCloser(ctx, id, ka.donec)
	l.firstKeepAliveOnce.Do(func() {
		go l.recvKeepAliveLoop()
		go l.deadlineLoop()
	})
	//===还有这里===
	return ch, nil
}

这就是我们实际调用的KeepAlive函数。注意看到ch这个变量,他也是一个*LeaseKeepAliveResponse类型的channel,并且容量和长度都是LeaseResponseChSize,这个变量的值正是16。

var LeaseResponseChSize = 16

接下来,根据参数带入的租约id,从keepAlives字典中获取对应的keepAlive结构体(ka),如果获取不到就创建一个,并存进去,而ch会被添加到keepAlive结构体的chs字段中,并且最后会返回给你。也就是说,调用KeepAlive函数返回的chrecvKeepAlive函数的select代码段里的ch是同一个channel。

如果我们不从KeepAlive函数返回的channel中及时取出值,那么recvKeepAlive函数最终会将ch这个channel填满。因为没续租一次,recvKeepAlive函数就会往ch里发送一个值,而ch的长度只有16,一旦装满,select就会进入default分支,报告警告信息。而信息中的数字16的含义就是这里的ch通道的长度。

网上有人说续租16次以后开始出现这个警告消息,原因就在这里。续租16次后,通道被装满。所以出现这个问题的现象是一开始好好的,过一会就开始不断出现警告消息。

知道了原因,解决这个问题就非常简单了,只要我们及时从KeepAlive返回的通道中取走值就可以了,取出的值可以直接丢弃。

cli, err := clientv3.New(clientv3.Config{
    Endpoints:   endpoints,
    DialTimeout: dialTimeout,
})
if err != nil {
    log.Fatal(err)
}
defer cli.Close()

resp, err := cli.Grant(context.TODO(), 5)
if err != nil {
    log.Fatal(err)
}

_, err = cli.Put(context.TODO(), "foo", "bar", clientv3.WithLease(resp.ID))
if err != nil {
    log.Fatal(err)
}

// the key 'foo' will be kept forever
ch, kaerr := cli.KeepAlive(context.TODO(), resp.ID)
if kaerr != nil {
    log.Fatal(kaerr)
}
//===看这里===
go func() {
    for {
		ka := <-ch
		fmt.Println("ttl:", ka.TTL)
    }
}

既然说到这里,再说点有意思的事。

KeepAlive函数的末尾,并发调用了一个recvKeepAliveLoop()的函数,它的内部有一个死循环,循环内部调用了recvKeepAlive(resp)函数。这就是recvKeepAlive函数(报出警告信息的函数)被调用的路径,这个函数中还有这样一段代码:

nextKeepAlive := time.Now().Add((time.Duration(karesp.TTL) * time.Second) / 3.0)
ka.deadline = time.Now().Add(time.Duration(karesp.TTL) * time.Second)

nextKeepAlive是下一次续租的时刻,它的计算方式是当前时间加上续租时间间隔,这很好理解。问题是续租的时间间隔并不是我们调用KeepAlive时指定的时间间隔,而是它的三分之一。也就是说如果你带入的是3,那么每隔一秒就会续租一次。deadline表示续租的最后期限,它和我们指定的时间间隔是一致的。

下车请走好,欢迎下次乘坐。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值