在使用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-size
和queue-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{}
}
可以看到keepAlive
的chs
字段,正是一个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
函数返回的ch
和recvKeepAlive
函数的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
表示续租的最后期限,它和我们指定的时间间隔是一致的。
下车请走好,欢迎下次乘坐。