记一次etcd全局锁使用不当导致的事故

这篇具有很好参考价值的文章主要介绍了记一次etcd全局锁使用不当导致的事故。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1、背景介绍

前两天,现场的同事使用开发的程序测试时,发现日志中报etcdserver: mvcc: database space exceeded,导致 etcd 无法连接。很奇怪,我们开发的程序只用到了 etcd 做程序的主备,并没有往 etcd 中写入大量的数据,为什么会造成 etcd 空间不足呢?赶紧叫现场的同事查了下 etcd 存储数据的目录以及 etcd 的状态,看看是什么情况。

记一次etcd全局锁使用不当导致的事故

查看 etcd 状态:

./etcdctl endpoint status --write-out=table --endpoints=localhost:12380

记一次etcd全局锁使用不当导致的事故

看到这里就很奇怪了,为什么 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。

记一次etcd全局锁使用不当导致的事故

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

5、思考

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

想起来前几天看到一篇问题,也是 for 循环中的出现的问题,原文链接,感兴趣的可以去看看 Go坑:time.After可能导致的内存泄露问题分析文章来源地址https://www.toymoban.com/news/detail-514697.html

到了这里,关于记一次etcd全局锁使用不当导致的事故的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【开发经验】之记一次Redis分布式锁造成的事故

    有次,运营和商家做了个限量抢购活动,限量100件,但活动当天却超卖了,最终卖出的数量是160多件。这种超卖是比较严重的事故,出现了的话基本上和分布式锁有关系。 项目中的抢购订单使用了分布式锁,而分布式锁的是基于Redis实现的,下面是订单抢购核心代码(使用伪

    2024年02月08日
    浏览(34)
  • 记一次dlopen使用问题导致Framework重启,tombstones、pmap与反汇编分析(上)

    :Android Framework 动态库 动态链接 Binder Android Studio一次更新后发现install App,设备就重启了,跑了一遍开机动画但不是从开机第一屏开始重启,tombstones内容查看发现是 surfaceflinger 挂在 libbinder.so ,那install app做了什么这个不得而知,理论上有问题应该挂的是PackageManager

    2024年04月08日
    浏览(35)
  • 记一次javaMetaspace导致CPU200%的排查

    insertMotionDataByWxCallBack方法并发多(其实也没多少,可能就3个?)就导致CPU200%了,本地没法复现。 看报错是:java.lang.OutOfMemoryError: Metaspace,刚开始的时候眼挫,忽略了后面的Metaspace,只看到了OutOfMemoryError,就各种找代码问题。 https://arthas.aliyun.com/doc/install-detail.html 然后发现

    2023年04月24日
    浏览(44)
  • 【现网】记一次并发冲突导致流量放大的生产问题

    目录 事故现象 转账 业务背景介绍 背景一:转账流程 转账流程 转账异常处理 转账异常处理流程图 背景二:账户系统合并 实际全流程: 背景三:扣内存数据库逻辑 背景四:调用方重试逻辑 问题定位 总结  资料获取方法 生产环境,转账相关请求失败量暴增。 直接原因 现网

    2024年02月14日
    浏览(35)
  • 记一次swoole连接数太多导致的错误

    原先就有点担心这个项目正式上线会出现各种问题,所以刚上线就赶紧查看日志 果然,频繁出现错误: WARNING Server::accept_connection(): accept() failed, Error: Too many open files[24] 这个错误通常是由于操作系统限制了进程能够打开的文件句柄数量,导致当前进程无法打开更多的文件,从

    2024年02月02日
    浏览(39)
  • 记一次 .NET某工控 宇宙射线 导致程序崩溃分析

    为什么要提 宇宙射线 , 太阳耀斑 导致的程序崩溃呢?主要是昨天在知乎上看了这篇文章:莫非我遇到了传说中的bug? ,由于 rip 中的0x41变成了0x61出现了bit位翻转导致程序崩溃,截图如下: 下面的评论大多是说由于 宇宙射线 ,这个太玄乎了,说实话看到这个 传说bug 的提法

    2024年02月04日
    浏览(33)
  • 记一次 Mockito.mockStatic 泄漏导致的单元测试偶发报错排查过程

    相信用 Java 写过单元测试的读者们对 Mockito 不会陌生。至于 Mockito 是什么,为什么要用 Mockito,本文不再赘述。本文记录了一次在 Apache ShardingSphere 项目中,由 Mockito.mockStatic 使用不当导致的单元测试偶发报错排查过程。 Mockito 自 3.4.0 起新增了一个方法 Mockito.mockStatic ,支持对

    2024年02月10日
    浏览(46)
  • 记一次BootCDN被黑产挂马导致站点跳转博彩网站的问题

      近期发现公司某些站点出现偶尔跳转博彩网站的现象,经过排查发现该现象为供应链投毒攻击,BootCDN上的静态资源无一例外均被污染, 当外站引入BootCDN的静态资源时,如果请求携带的Referer头为指定值(涉及公司隐私不便透露),User-Agent头为手机浏览器UA,触发恶意代码注

    2024年02月08日
    浏览(30)
  • 记一次Git未Commit直接Pull导致本地代码丢失后的挽救过程

    第一次遇到这种问题,有点紧张... 好吧,废话不多说,IDEA或者AndroidStudio进入Git Uncommiteed Changes - Unstash Changes: 在弹出的Unstash Changes对话框点View查看代码,如果代码是本地丢失的代码,那么恭喜你,又可以继续愉快的玩耍了。 不过千万要注意不用随便点到Drop,Clear按钮。 这

    2024年02月06日
    浏览(37)
  • 记一次线上mysql出错:由于docker自动拉取最新mysql镜像导致mysql容器无法启动

    我随便写写,你们随便看看 环境背景:在docker中部署mysql镜像,通过portainer管理docker容器 简单说下过程:docker里mysql的时区没有设置,导致相差8小时,通过增加TZ=Asiz/Shanghai环境变量,然后重启容器来生效。结果重启的时候始终无法启动起来,后来发现是自动升级了mysql镜像版

    2024年02月07日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包