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 {
if err = watchLock(client, gMutex, ctx); err != nil {
fmt.Println("get etcd global key failed")
return
}
} // 启动成功,做具体的业务逻辑处理
fmt.Println("todo ..............")
select {} } func watchLock(client *clientv3.Client, gMutex *concurrency.Mutex, ctx context.Context) (err error) { watchCh := client.Watch(ctx, lockName, clientv3.WithPrefix()) for {
select {
case <-ctx.Done():
return ctx.Err()
case <-watchCh:
if err = gMutex.TryLock(ctx); err == nil {
// 获取到锁
return nil
}
}
}
}

将上述代码编译成可执行文件 main.exe、main1.exe 后,先后执行上面两个可执行文件,然后通过下面的命令查看 etcd 中的 RAFT APPLYEND INDEX ,不会出现RAFT APPLYEND INDEX 持续增长的现象,也就是从源头解决了问题。

4、TryLock 源码分析

以下是自己的理解,如果有不对的地方,请不吝赐教,十分感谢

那下面一起看看 TryLock 方法里面做了什么操作,会导致 RAFT APPLYEND INDEX 持续增长呢。

TryLock 方法源码如下:

func (m *Mutex) TryLock(ctx context.Context) error {
resp, err := m.tryAcquire(ctx)
if err != nil {
return err
}
// if no key on prefix / the minimum rev is key, already hold the lock
ownerKey := resp.Responses[1].GetResponseRange().Kvs
if len(ownerKey) == 0 || ownerKey[0].CreateRevision == m.myRev {
m.hdr = resp.Header
return nil
}
client := m.s.Client()
// Cannot lock, so delete the key
// 这里的 client.Delete 会走到 raft 模块,从而使 etcd 的 raft applyed index 增加 1
if _, err := client.Delete(ctx, m.myKey); err != nil {
return err
}
m.myKey = "\x00"
m.myRev = -1
return ErrLocked
}

tryAcquire 方法源码如下:

// 下面主要是使用到了 etcd 中的事务,
func (m *Mutex) tryAcquire(ctx context.Context) (*v3.TxnResponse, error) {
s := m.s
client := m.s.Client() // m.myKey = /TEST/LOCKER/326989110b4e9304
m.myKey = fmt.Sprintf("%s%x", m.pfx, s.Lease())
// 这里就是定义一个判断语句,创建 myKey 时的版本号是否 等于 0
cmp := v3.Compare(v3.CreateRevision(m.myKey), "=", 0)
// put self in lock waiters via myKey; oldest waiter holds lock
// 往 etcd 中写入 myKey
put := v3.OpPut(m.myKey, "", v3.WithLease(s.Lease()))
// reuse key in case this session already holds the lock
// 查询 myKey
get := v3.OpGet(m.myKey)
// fetch current holder to complete uncontended path with only one RPC
getOwner := v3.OpGet(m.pfx, v3.WithFirstCreate()...)
// 这里是重点,判断 cmp 中的条件是否成立,成立则执行 Then 中的语句,否则执行 Else 中的语句
// 这里的语句肯定是成功的,因为我们测试的环境是执行两个不同的 session
// 简单的可以理解为两个不同的程序,实际上是 两个不同的会话就会不同
// 所以我们这里的场景是 会执行 v3.OpPut 操作。所以这里会增加一次 revision
// 即 etcd 的 raft applyed index 会增加 1
resp, err := client.Txn(ctx).If(cmp).Then(put, getOwner).Else(get, getOwner).Commit()
if err != nil {
return nil, err
}
m.myRev = resp.Header.Revision
if !resp.Succeeded {
m.myRev = resp.Responses[0].GetResponseRange().Kvs[0].CreateRevision
}
return resp, nil
}

下面这张图是 debug 时,先启动一个可执行文件,然后使用 debug 方式启动的程序,程序执行完 tryAcquire 方法后,截取的一张图,这也作证了上面的分析。304 这个 key 是之前启动程序就存在的 key,下面 30f 的 key 是 debug 期间生成的 key。

大家如果有不清楚的地方,亲自去调试下,看看代码,就会明白上面说的内容了。

5、思考

其实,这并不是难以考虑到的问题,代码中出现这个问题,主要是自己对 etcd 的了解程度不够,不清楚 TryLock 的原理,以为像简单的查询Get那样,不会导致 revision 的增长,但实际上并不是这样。而是生产中出现了问题才去看为什么会这样,然后再去解决问题,这是一种不太好的方式,希望以后在编码的时候,尽量多考虑考虑,减少问题出现。

还好问题是在同事测试的时候发现的,并没有导致什么损失,幸好幸好。

记一次etcd全局锁使用不当导致的事故的更多相关文章

  1. ETCD分布式锁实现选主机制(Golang实现)

    ETCD分布式锁实现选主机制(Golang) 为什么要写这篇文章 做架构的时候,涉及到系统的一个功能,有一个服务必须在指定的节点执行,并且需要有个节点来做任务分发,想了半天,那就搞个主节点做这事呗,所 ...

  2. MySQL 全局锁、表级锁、行级锁,你搞清楚了吗?

    大家好,我是小林. 最近重新补充了<MySQL 有哪些锁>文章内容: 增加记录锁.间隙锁.net-key 锁 增加插入意向锁 增加自增锁为 innodb_autoinc_lock_mode ...

  3. 【MySQL 读书笔记】全局锁 | 表锁 | 行锁

    全局锁 全局锁是针对数据库实例的直接加锁,MySQL 提供了一个加全局锁的方法, Flush tables with read lock 可以使用锁将整个表的增删改操作都锁上其中包括 ddl 语句,只 ...

  4. MySQL 笔记整理(6) --全局锁和表锁:给表加个字段怎么有这么多阻碍

    笔记记录自林晓斌(丁奇)老师的<MySQL实战45讲> 6) --全局锁和表锁:给表加个字段怎么有这么多阻碍 数据库锁设计的初衷是处理并发问题.作为多用户共享的资源,当出现并发访问的时候, ...

  5. python GIL 全局锁,多核cpu下的多线程性能究竟如何?

    python GIL 全局锁,多核cpu下的多线程性能究竟如何?GIL全称Global Interpreter Lock GIL是什么? 首先需要明确的一点是GIL并不是Python的特性,它是在实现 ...

  6. MySQL 全局锁、表锁以及行锁

    1. 系统版本 MySQL 5.7.25 ubuntu 16.04 2. 全局锁 全局锁即对整个数据库实例加锁,使得整个库处于只读状态,会阻塞DML和DDL语句.使用如下命令(简称FTWRL)可为数据 ...

  7. GIL线程全局锁 协程

    GIL线程全局锁 线程全局锁(Global Interpreter Lock),即Python为了保证线程安全而采取的独立线程运行的限制,说白了就是一个核只能在同一时间运行一个线程.对于io密集型任务 ...

  8. **解释器全局锁(Global Interpreter Lock)

    解释器全局锁(Global Interpreter Lock),即Python为了保证线程安全而采取的独立线程运行的限制,说白了就是一个核只能在同一时间运行一个线程. [解决办法就是多进程和协程(协程 ...

  9. [ Python - 11 ] 多线程及GIL全局锁

    1. GIL是什么? 首先需要明确的一点是GIL并不是python的特性, 它是在实现python解析器(Cpython)时所引入的一个概念. 而Cpython是大部分环境下默认的python执行环境 ...

  10. Mysql实战45讲 06讲全局锁和表锁:给表加个字段怎么有这么多阻碍 极客时间 读书笔记

    Mysql实战45讲 极客时间 读书笔记 Mysql实战45讲 极客时间 读书笔记 笔记体会: 根据加锁范围:MySQL里面的锁可以分为:全局锁.表级锁.行级锁 一.全局锁:对整个数据库实例加锁.My ...

随机推荐

  1. [操作系统]记一次未尽的三星 Galaxy A6s(SM-G6200)刷机过程

    给女王大人刷机,第一次刷机,很遗憾,遇到了三星的"锁三键"问题,没有搞成.记录一下这个过程所涉猎的一些刷机基本知识,不妨当作一次学习过程. 1 刷机过程 Step1 查看手机基本信 ...

  2. Vue2异步更新及nextTick原理

    vue 官网中是这样描述 nextTick 的 在下次 DOM 更新循环结束之后执行延迟回调.在修改数据之后立即使用这个方法,可以获取更新后的 DOM. 在学习 nextTick 是如何实现之前,我们 ...

  3. Python爬取pexels图片

    研究Python爬虫,网上很多爬取pexels图片的案例,我下载下来运行没有成功,总量有各种各样的问题. 作为菜鸟初学者,网上的各个案例代码对我还是有不少启发作用,我用搜索引擎+chatGPT逐步对代 ...

  4. RFS[3]: No standby redo logfiles available for thread 1

    问题描述:备库恢复DG之后,mrp进程一直是wait_for_log,主库创建数据没有正常同步,只有在切换归档的时候备库才能同步主库数据 查看主库日志,主库RFS进程提示没有可用的standby re ...

  5. OpenAI-GPT

    操作系统:CentOS 7.6 安装依赖软件 进入 root 账号: sudo -i 安装部署 ChatGPT 必备的软件,并且启动 nginx : yum install git nginx -y ...

  6. 2020-11-24:n个物品每个物品都有一定价值,分给2个人,怎么分两个人的价值差最小?

    福哥答案2020-11-24: 背包问题:背包容量是SUM/2. 每个物体的体积是数的大小,然后尽可能的装满背包. golang代码如下: package main import ( "fm ...

  7. 2021-05-07:给定一个数组arr,你可以在每个数字之前决定+或者-,但是必须所有数字都参与 ,再给定一个数target,请问最后算出target的方法数是多少?

    2021-05-07:给定一个数组arr,你可以在每个数字之前决定+或者-,但是必须所有数字都参与 ,再给定一个数target,请问最后算出target的方法数是多少? 福大大 答案2021-05-0 ...

  8. Django4全栈进阶之路12 render 函数和 redirect 函数

    在 Django 中,你可以使用 render 函数来渲染模板并将其返回给客户端,也可以使用 redirect 函数来重定向到其他 URL. 在 Django 中,render 函数和 redirec ...

  9. uni-app 运行项目

    运行-运行到浏览器-选择浏览器 运行新项目:真机测试需要打开真机的开发者选项usb安装调试功能

  10. GPT大语言模型Vicuna本地化部署实践(效果秒杀Alpaca)

    背景 上一篇文章<GPT大语言模型Alpaca-lora本地化部署实践>介绍了斯坦福大学的Alpaca-lora模型的本地化部署,并验证了实际的推理效果. 总体感觉其实并不是特别理想,原始 ...