1、背景介绍
前两天,现场的同事使用开发的程序测试时,发现日志中报etcdserver: mvcc: database space exceeded
,导致 etcd 无法连接。很奇怪,我们开发的程序只用到了 etcd 做程序的主备,并没有往 etcd 中写入大量的数据,为什么会造成 etcd 空间不足呢?赶紧叫现场的同事查了下 etcd 存储数据的目录以及 etcd 的状态,看看是什么情况。
查看 etcd 状态:
./etcdctl endpoint status --write-out=table --endpoints=localhost:12380
看到这里就很奇怪了,为什么 RAFT APPLYEND INDEX
会这么大呢?这完全是不正常的。
想到程序中有主备,程序启动时,会去 etcd 中 trylock
相应的锁,获取不到时,则会定期去 trylock
,会不会是这里的备节点 定期去 trylock
导致 RAFT APPLYEND INDEX
持续增长从而导致 etcd 空间不足呢?
后面测试了一下,不启动备节点时,RAFT APPLYEND INDEX
是不会增大的。那么问题的原因找到了,问题也就比较好解决。
虽然 etcd 提供了 compact 的能力,但是对于我们这个现象,是治标不治本的,所以最好还是从源头解决问题比较好。当然也可以使用 compact 来压缩 etcd 的 历史数据,但是需要注意的是 compact 时,etcd 的性能是会收到影响的。
2、场景复现
etcd client 版本
go.etcd.io/etcd/client/v3 v3.5.5
etcd server 版本
etcd-v3.5.8-linux-amd64
模拟代码如下:
package main
import (
"context"
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
"time"
)
var TTL = 5
var lockName = "/TEST/LOCKER"
func main() {
config := clientv3.Config{
Endpoints: []string{"192.168.91.66:12379"},
DialTimeout: 5 * time.Second,
}
// 建立连接
client, err := clientv3.New(config)
if err != nil {
fmt.Println(err)
return
}
session, err := concurrency.NewSession(client, concurrency.WithTTL(TTL))
if err != nil {
fmt.Println("concurrency.NewSession failed, err:", err)
return
}
gMutex := concurrency.NewMutex(session, lockName)
ctx, _ := context.WithCancel(context.Background())
if err = gMutex.TryLock(ctx); err == nil {
fmt.Println("gMutex.TryLock success")
} else {
if err = watchLock(gMutex, ctx); err != nil {
fmt.Println("get etcd global key failed")
return
}
}
// 启动成功,做具体的业务逻辑处理
fmt.Println("todo ..............")
select {}
}
func watchLock(gMutex *concurrency.Mutex, ctx context.Context) (err error) {
ticker := time.NewTicker(time.Second * time.Duration(TTL))
for {
if err = gMutex.TryLock(ctx); err == nil {
// 获取到锁
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
continue
}
}
}
将上述代码编译成可执行文件 main.exe、main1.exe 后,先后执行上面两个可执行文件,然后通过下面的命令查看 etcd 中的 RAFT APPLYEND INDEX
,会发现,RAFT APPLYEND INDEX
每隔五秒钟就会增长,长时间运行就会出现 etcdserver: mvcc: database space exceeded
。
3、如何解决
上面我们已经复现了RAFT APPLYEND INDEX
,其实解决起来也比较简单,主要思路就是不要在 for 循环中 使用 trylock 方法。具体代码如下:
package main
import (
"context"
"fmt"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
"time"
)
var TTL = 5
var lockName = "/TEST/LOCKER"
func main() {
config := clientv3.Config{
Endpoints: []string{"192.168.91.66:12379"},
DialTimeout: 5 * time.Second,
}
// 建立连接
client, err := clientv3.New(config)
if err != nil {
fmt.Println(err)
return
}
session, err := concurrency.NewSession(client, concurrency.WithTTL(TTL))
if err != nil {
fmt.Println("concurrency.NewSession failed, err:", err)
return
}
gMutex := concurrency.NewMutex(session, lockName)
ctx, _ := context.WithCancel(context.Background())
if err = gMutex.TryLock(ctx); err == nil {
fmt.Println("gMutex.TryLock success")
} else