首页 > 其他分享 >记一次etcd全局锁使用不当导致的事故

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

时间:2023-07-01 19:23:17浏览次数:25  
标签:使用不当 err nil ctx v3 client etcd 全局

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 的增长,但实际上并不是这样。而是生产中出现了问题才去看为什么会这样,然后再去解决问题,这是一种不太好的方式,希望以后在编码的时候,尽量多考虑考虑,减少问题出现。

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

标签:使用不当,err,nil,ctx,v3,client,etcd,全局
From: https://www.cnblogs.com/huageyiyangdewo/p/17519746.html

相关文章

  • 众所周知,梯度下降法是一种基本的优化算法,不能保证全局最优,也不能保证效率。为什么它仍
    梯度下降法在深度学习中被广泛应用的原因主要有以下几点:适用性广泛:梯度下降法可以应用于各种深度学习模型,包括神经网络、卷积神经网络、循环神经网络等。而传统的凸优化算法和粒子群算法往往只适用于特定类型的优化问题。原理简单:梯度下降法的原理相对简单,易于理解和实现。......
  • SpringBoot 如何优雅的进行全局异常处理?
    在SpringBoot的开发中,为了提高程序运行的鲁棒性,我们经常需要对各种程序异常进行处理,但是如果在每个出异常的地方进行单独处理的话,这会引入大量业务不相关的异常处理代码,增加了程序的耦合,同时未来想改变异常的处理逻辑,也变得比较困难。这篇文章带大家了解一下如何优雅的进行全局异......
  • vscode 文件关闭后 全局搜索失效
    问题: vscode编译器在文件关闭后,全局搜索失效,无法搜索到文件内容,打开文件后,可以搜索到。原因:电脑安装了绿盾加密软件,对项目文件进行了加密,vscode编译器无法检索关闭的文件。解决方案:对项目文件进行解密操作(申请解密)......
  • laravel8配置全局公共函数步骤详解
    1.首先添加文件,app/Helpers.php,我这里是这个名字因为习惯了,你也可以自己定义<?phpif(!function_exists("getFileName")){/***从路径中获取文件名*@param$fileName*@returnstring*/functiongetFileName($fileName){$s......
  • java中的全局异常处理和局部处理方法
    1.在三层构架项目中,出现了异常,该如何处理?方案一:在所有Controller的所有方法中进行try…catch处理缺点:代码臃肿(不推荐)方案二:全局异常处理器好处:简单、优雅(推荐)2.方法:1.添加类:GlobalExceptionHandler2.添加注解:@RestControllerAdvice3.添加异常处理方法:ex并给方法添加注解......
  • MATLAB代码:分布式最优潮流 本文以全局电压的低成本快速控制为目标,提出基于电气距离和
    MATLAB代码:分布式最优潮流关键词:网络划分;分布式光伏;集群电压控制;分布式优化;有功缩减参考文档:《含分布式光伏的配电网集群划分和集群电压协调控制》仿真平台:MATLAB主要内容:本文以全局电压的低成本快速控制为目标,提出基于电气距离和区域电压调节能力的集群综合性能指标和网络划分......
  • etcd 参数优化
    heartbeat-interval目前heartbeat-interval使用默认值即100, 较小的心跳间隔会导致发送频繁的消息,消耗CPU和网络资源。而较大的心跳间隔,又会导致检测到Leader故障不可用耗时过长,影响业务可用性。我们可以将其优化为300 election-timeout目前 election-timeout使用......
  • springboot 自定义异常 全局异常处理器
    创建自定义异常类,继承 RuntimeException类1publicclassCustomExceptionextendsRuntimeException{2publicCustomException(Stringmessage){3super(message);4}5}在指定情况下抛出该异常,代码如下: @ServicepublicclassCategoryServiceIm......
  • vue组件-使用Vue.component全局注册组件
    通过components注册的时私有子组件例如:在组件A的components节点下,注册了组件F。则组件F只能用在组件A中;不能被用在组件C中。注册全局组件在vue项目的main.js入口文件中,通过Vue.component()方法,可以注册全局组件。importVuefrom'vue'importAppfrom'./App.vue'//导......
  • etcd网络模块解析
    1.RaftHttp模块介绍在etcd里raft模块和网络模块是分开的,raft模块主要负责实现共识算法,保证集群节点的一致性,消息的发送和接收则交给raftHttp网络模块来处理,由上层应用模块来进行协调交互和消息传递。 1.1.整体结构图  (1)当raft模块发生了状态变化时,会把变化的消息封装......