etcd

news2024/11/16 6:41:37

etcd

etcd 是一个分布式键值对存储,设计用来可靠而快速的保存关键数据并提供访问。通过分布式锁,leader选举和写屏障(write barriers)来实现可靠的分布式协作。etcd集群是为高可用,持久性数据存储和检索而准备。

Etcd 是 CoreOS 基于 Raft 协议开发的分布式 key-value 存储,可用于服务发现、共享配置以及一致性保障(如数据库选主、分布式锁等)。

单机安装

下载地址 https://github.com/etcd-io/etcd/releases,解压后查看版本

etcd --version
etcdctl version

创建配置文件:conf.yaml

name: "hezebin-etcd"
data-dir: /usr/local/etcd/data
listen-client-urls: "http://127.0.0.1:2379"
advertise-client-urls: "http://127.0.0.1:2379"
initial-cluster-token: "hezebin-etcd-token"
initial-cluster: "hezebin-etcd=http://127.0.0.1:2380"
initial-advertise-peer-urls: "http://127.0.0.1:2380"
initial-cluster-state: "new"

启动 etcd 服务:

etcd --config-file=/usr/local/etcd/conf.yaml

打开终端,执行以下命令来检查 etcd 服务的健康状态:

etcdctl endpoint health

如果一切正常,您将看到类似以下的输出:

127.0.0.1:2379 is healthy: successfully committed proposal: took = 11.667µs

如果 etcd 服务正在运行且健康,您会收到健康状态的确认。

您还可以通过执行以下命令来查看 etcd 集群的成员状态:

etcdctl member list

这将列出 etcd 集群中的成员信息,包括成员的 ID、名称和状态。

设置键值对

etcdctl put name "hezebin"
etcdctl get name

# 查询Etcd所有的key
etcdctl get --prefix ""

# 只读取键 name 的值的命令
etcdctl get name --print-value-only

# 以 name 为前缀的所有键的命令,结果数量限制为2:
etcdctl get --prefix --limit=2 name

# 访问修订版本为 4 时的键的版本
etcdctl get --prefix --rev=4 name

# 假设 etcd 集群已经有下列键:
a = 123
b = 456
z = 789
# 读取大于等于键 b 的 byte 值的键的命令:
etcdctl get --from-key b
# 清空数据
etcdctl del name
# 删除所有 n 前缀的节点
etcdctl del n -- prefix
# 删除键 zoo 并返回被删除的键值对的命令:
etcdctl del --prev-kv zoo

watch操作

watch 监测一个键值的变化,一旦键值发生更新,就会输出最新的值并退出

etcdctl watch name

#更新键值
etcdctl put name "korbin"

# watch 阻塞处返回结果
PUT 
name
korbin

读取键过往版本的值

应用可能想读取键的被替代的值。例如,应用可能想通过访问键的过往版本来回滚到旧的配置。或者,应用可能想通过多个请求来得到一个覆盖多个键的统一视图,而这些请求可以通过访问键历史记录而来。因为 etcd 集群上键值存储的每个修改都会增加 etcd 集群的全局修订版本,应用可以通过提供旧有的 etcd 修改版本来读取被替代的键。

假设 etcd 集群已经有下列键:

foo = bar         # revision = 2
foo1 = bar1       # revision = 3
foo = bar_new     # revision = 4
foo1 = bar1_new   # revision = 5

这里是访问键的过往版本的例子:

$ etcdctl get --prefix foo # 访问键的最新版本
foo
bar_new
foo1
bar1_new
$ etcdctl get --prefix --rev=4 foo # 访问修订版本为 4 时的键的版本
foo
bar_new
foo1
bar1
$ etcdctl get --prefix --rev=3 foo # 访问修订版本为 3 时的键的版本
foo
bar
foo1
bar1
$ etcdctl get --prefix --rev=2 foo # 访问修订版本为 2 时的键的版本
foo
bar
$ etcdctl get --prefix --rev=1 foo # 访问修订版本为 1 时的键的版本

压缩修订版本

如我们提到的,etcd 保存修订版本以便应用可以读取键的过往版本。但是,为了避免积累无限数量的历史数据,压缩过往的修订版本就变得很重要。压缩之后,etcd 删除历史修订版本,释放资源来提供未来使用。所有修订版本在压缩修订版本之前的被替代的数据将不可访问。

这是压缩修订版本的命令:

$ etcdctl compact 5
compacted revision 5
# 在压缩修订版本之前的任何修订版本都不可访问
$ etcdctl get --rev=4 foo
Error:  rpc error: code = 11 desc = etcdserver: mvcc: required revision has been compacted

注意: etcd 服务器的当前修订版本可以在任何键(存在或者不存在)以json格式使用get命令来找到。下面展示的例子中 mykey 是在 etcd 服务器中不存在的:

$ etcdctl get mykey -w=json
{"header":{"cluster_id":14841639068965178418,"member_id":10276657743932975437,"revision":15,"raft_term":4}}

lease租约(过期机制)

应用可以为 etcd 集群里面的键授予租约。当键被附加到租约时,它的存活时间被绑定到租约的存活时间,而租约的存活时间相应的被 time-to-live (TTL)管理。在租约授予时每个租约的最小TTL值由应用指定。租约的实际 TTL 值是不低于最小 TTL,由 etcd 集群选择。一旦租约的 TTL 到期,租约就过期并且所有附带的键都将被删除。

授予租约

# 授予租约,TTL为10秒
$ etcdctl lease grant 10
lease 32695410dcc0ca06 granted with TTL(10s)
# 附加键 foo 到租约32695410dcc0ca06
$ etcdctl put --lease=32695410dcc0ca06 foo barOK

应用通过租约 id 可以撤销租约。撤销租约将删除所有它附带的 key。

撤销租约

$ etcdctl lease revoke 32695410dcc0ca06
lease 32695410dcc0ca06 revoked
$ etcdctl get foo
# 空应答,因为租约撤销导致foo被删除

keepAlive续约

应用可以通过刷新键的 TTL 来维持租约,以便租约不过期。维持同一个租约的命令:

$ etcdctl lease keep-alive 32695410dcc0ca06
lease 32695410dcc0ca06 keepalived with TTL(10)
lease 32695410dcc0ca06 keepalived with TTL(10)
lease 32695410dcc0ca06 keepalived with TTL(10)
...

获取租约信息

应用程序可能想知道租约信息,以便可以更新或检查租约是否仍然存在或已过期。应用程序也可能想知道有那些键附加到了特定租约。

获取租约信息的命令:

$ etcdctl lease timetolive 694d5765fc71500b
lease 694d5765fc71500b granted with TTL(500s), remaining(258s)

获取租约信息和租约附带的键的命令:

$ etcdctl lease timetolive --keys 694d5765fc71500b
lease 694d5765fc71500b granted with TTL(500s), remaining(132s), attached keys([zoo2 zoo1])
# 如果租约已经过期或者不存在,它将给出下面的应答:
Error:  etcdserver: requested lease not found

事务

etcd 的事务(Transaction)机制允许你在一个原子操作中执行一系列操作,这些操作要么全部成功,要么全部失败,确保数据的一致性和完整性。

etcdctl txn 并没有对事务提供过多的支持,执行事务最好通过 go 来实现:

go get go.etcd.io/etcd/client/v3
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"go.etcd.io/etcd/clientv3"
)

func main() {
	// 创建 etcd 客户端
	cli, err := clientv3.New(clientv3.Config{
		Endpoints:   []string{"http://localhost:2379"}, // 替换为你的 etcd 地址
		DialTimeout: 5 * time.Second,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer cli.Close()

	// 创建一个 etcd 事务
	etcdTxn := clientv3.NewKV(cli).Txn(context.Background())

	// 定义事务的比较和操作
	etcdTxn.If(
		clientv3.Compare(clientv3.Value("my_key"), "=", "10"),
	).
		Then(
			clientv3.OpPut("my_key", "20"),
		).
		Else(
			clientv3.OpPut("my_key", "30"),
		)

	// 提交事务
	txnResp, err := etcdTxn.Commit()
	if err != nil {
		log.Fatal(err)
	}

	// 检查事务是否成功
	if !txnResp.Succeeded {
		fmt.Println("Transaction failed")
	} else {
		fmt.Println("Transaction succeeded")
	}
}

注意:etcdTxn.If 中指定的条件是 clientv3.Compare(clientv3.Value("my_key"), "=", "10"),也就是检查键 “my_key” 的值是否等于 “10”。如果这个条件不满足(例如,键 “my_key” 的值为空),那么事务的 Else 分支会执行,而且整个事务会被标记为失败。

基于etcd实现分布式锁

原生实现

要在 etcd 中实现分布式锁,你可以利用 etcd 的事务特性和租约(Lease)机制。以下是一个使用 etcd 实现分布式锁的示例:

# 伪代码,etcdctl 并不支持下述命令,需要以 go 方式实现,api 更丰富
etcdctl txn -- \
  put my_lock some_value  --lease=0d7689bc575d6611  \
  if_not_exists

# if_not_exists: 这是一个条件,表示只有在键不存在时才执行上述的 put 操作。这个条件确保只有第一次创建节点的时候才会成功,从而实现锁的获取。

这个命令的目的是在一个 etcd 事务中,尝试创建一个指定键名的键值对,如果该键名在 etcd 中尚不存在(即尝试获取锁),则创建成功,否则操作失败。这个操作模式可以帮助实现分布式锁的基本机制。

Go 代码实现:

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"go.etcd.io/etcd/clientv3"
)

func main() {
	// 创建 etcd 客户端
	cli, err := clientv3.New(clientv3.Config{
		Endpoints:   []string{"http://localhost:2379"}, // 替换为你的 etcd 地址
		DialTimeout: 5 * time.Second,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer cli.Close()

	// 创建租约
	leaseResp, err := cli.Grant(context.Background(), 10) // 10 秒的租约时间
	if err != nil {
		log.Fatal(err)
	}

	// 锁的键名
	lockKey := "my_lock"

	// 尝试获取锁
	txnResp, err := cli.Txn(context.Background()).
		If(clientv3.Compare(clientv3.CreateRevision(lockKey), "=", 0)).
		Then(clientv3.OpPut(lockKey, "lock_holder", clientv3.WithLease(leaseResp.ID))).
		Commit()
	if err != nil {
		log.Fatal(err)
	}

	// 检查是否成功获取锁
	if !txnResp.Succeeded {
		log.Println("Failed to acquire lock")
		return
	}

	log.Println("Lock acquired")

	// 续约循环(保持锁)
	keepAliveCh, err := cli.KeepAlive(context.Background(), leaseResp.ID)
	if err != nil {
		log.Fatal(err)
	}

	go func() {
		for range keepAliveCh {
			// 续约成功,执行你的逻辑,或者也可以不做任何处理
		}
	}()

	// 在这里执行需要保护的临界区代码

	// 释放锁(取消续约)
	_, err = cli.Revoke(context.Background(), leaseResp.ID)
	if err != nil {
		log.Fatal(err)
	}

	log.Println("Lock released")
}

官方 concurrency 包实现

cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
if err != nil {
    log.Fatal(err)
}
defer cli.Close()

// 创建两个单独的会话用来演示锁竞争
s1, err := concurrency.NewSession(cli)
if err != nil {
    log.Fatal(err)
}
defer s1.Close()
m1 := concurrency.NewMutex(s1, "/my-lock/")

s2, err := concurrency.NewSession(cli)
if err != nil {
    log.Fatal(err)
}
defer s2.Close()
m2 := concurrency.NewMutex(s2, "/my-lock/")

// 会话s1获取锁
if err := m1.Lock(context.TODO()); err != nil {
    log.Fatal(err)
}
fmt.Println("acquired lock for s1")

m2Locked := make(chan struct{})
go func() {
    defer close(m2Locked)
    // 等待直到会话s1释放了/my-lock/的锁
    if err := m2.Lock(context.TODO()); err != nil {
        log.Fatal(err)
    }
}()

if err := m1.Unlock(context.TODO()); err != nil {
    log.Fatal(err)
}
fmt.Println("released lock for s1")

<-m2Locked
fmt.Println("acquired lock for s2")

通过源码可以发现concurrency包的实现原理为执行一个 key 的前缀,并在最终设置到 etcd key 的尾部拼接上 /lease_id,租约为 60 秒,且每隔 20 秒续租一次。

在 tryAcquire 函数中通过 put 上述的键值,判断版本号,若设置成功则拿到锁,否则阻塞等待锁:
在这里插入图片描述

阻塞期间先通过查询一次 key 是否存在判断是否阻塞,若不存在表示拿到锁,结束阻塞;存在则通过 watch 监听 key 的变更,仅允许DELETE类型,其他变更操作会报错,并做强制解锁。
在这里插入图片描述
在这里插入图片描述

服务注册与发现

当使用 etcd 实现服务注册与发现时,通常需要以下步骤:

  1. 引入 etcd 的 Go 客户端库
  2. 连接到 etcd 服务器
  3. 注册服务:将服务的信息写入 etcd 中
  4. 发现服务:从 etcd 中获取已注册的服务信息

以下是一个简单示例,演示如何使用 Go 语言操作 etcd 实现基本的服务注册与发现功能。请确保已经安装了 etcd 并启动了 etcd 服务器。

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"go.etcd.io/etcd/clientv3"
)

func main() {
	// 创建 etcd 客户端
	cli, err := clientv3.New(clientv3.Config{
		Endpoints:   []string{"localhost:2379"}, // etcd 服务器地址
		DialTimeout: 5 * time.Second,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer cli.Close()

	// 注册服务
	serviceName := "my-service"
	serviceIP := "192.168.1.100"
	servicePort := "8080"

	serviceKey := fmt.Sprintf("/services/%s/%s:%s", serviceName, serviceIP, servicePort)
	serviceValue := "some-metadata-about-the-service"

	ctx := context.Background()
	_, err = cli.Put(ctx, serviceKey, serviceValue)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Service registered: %s\n", serviceKey)

	// 发现服务
	discoveryKey := fmt.Sprintf("/services/%s", serviceName)
	resp, err := cli.Get(ctx, discoveryKey, clientv3.WithPrefix())
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println("Discovered services:")
	for _, kv := range resp.Kvs {
		fmt.Printf("Key: %s, Value: %s\n", kv.Key, kv.Value)
	}
}

可以结合 watch 机制优化更新服务变更。

Go 操作 Etcd 参考

go get go.etcd.io/etcd/client/v3
  • 民间文档:http://www.topgoer.com/%E6%95%B0%E6%8D%AE%E5%BA%93%E6%93%8D%E4%BD%9C/go%E6%93%8D%E4%BD%9Cetcd/%E6%93%8D%E4%BD%9Cetcd.html

  • 官方文档:https://github.com/etcd-io/etcd/blob/main/client/v3/README.md

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/838597.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java】批量生成条码

批量生成PDF条码 效果图&#xff1a; //调用下方接口注意编码格式if(CollectionUtil.isNotEmpty(productExList)){String exportFileName URLEncoder.encode("商品条码", "UTF-8") DateUtil.format(new Date(), "yyyyMMddHHmmss");response.…

git 公钥密钥 生成与查看

1.什么是公钥 很多服务器都是需要认证的&#xff0c;ssh认证是其中的一种。在客户端生成公钥&#xff0c;把生成的公钥添加到服务器&#xff0c;你以后连接服务器就不用每次都输入用户名和密码了。 很多git服务器都是用ssh认证方式&#xff0c;你需要把你生成的公钥发送给代码仓…

【每日一题】21. 合并两个有序链表

【每日一题】21. 合并两个有序链表 21. 合并两个有序链表题目描述解题思路 21. 合并两个有序链表 题目描述 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4…

中间人攻击

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、启动 Kali Linux准备 Apache 服务器使用Ettercap插件进行欺骗使用 Wireshark 捕获 Https 流量总结 前言 您可能已经熟悉中间人攻击&#xff1a;攻击者通过…

测试平台——项目模块模型类设计

这里写目录标题 一、项目应用1、项目包含接口&#xff1a;2、创建子应用3、项目模块设计a、模型类设计b、序列化器类设计c、视图类设计 4、接口模块设计a、模型类设计b、序列化器类设计c、视图类设计 5、环境模块设计6、DRF中的通用过滤6.1、设置过滤器后端 一、项目应用 1、项…

js省市区下拉框联动——前端笔记

问题&#xff1a; 我们常常要用到下拉框联动的功能&#xff0c;比如最常用的是选择地址的 省 市 区 的联动。思路&#xff1a; 先填充第一个下拉框&#xff0c;然后写一个第一个下拉框的change事件来加载第二个下拉框&#xff0c;再写第二个下拉框的change事件来加载第三个下…

面试总结-c++

1该吹牛逼吹牛逼。在自己能说出个所以然的情况下&#xff0c;该吹就吹&#xff0c;不吹没工作&#xff0c;吹了有希望。 比如 c组长&#xff0c;确有其事&#xff0c;但是挺唬人。说自己在北京定居也是侧面吹牛逼&#xff0c;证明自己的能力。还有媳妇在研究所。 2.对自己做过…

uniapp:图片验证码检验问题处理

图形验证码功能实现 uniapp&#xff1a;解决图形验证码问题及利用arraybuffer二进制转base64格式图片&#xff08;后端传的图片数据形式&#xff1a;x00\x10JFIF\x00\x01\x02\x00…&#xff09;_❆VE❆的博客-CSDN博客 UI稿&#xff1a; 需求&#xff1a;向后端请求验证码图片&…

Flask 是什么?Flask框架详解及实践指南

Flask 是一个轻量级的 Python Web 框架&#xff0c;它被广泛用于构建 Web 应用程序和 API。Flask 简单易用&#xff0c;具有灵活性和可扩展性&#xff0c;是许多开发者喜欢用其构建项目的原因。本文将介绍 Flask 是什么以及如何使用它来构建 Web 应用程序&#xff0c;同时提供一…

《Java-SE-第二十八章》之CAS

前言 在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!” 博客主页&#xff1a;KC老衲爱尼姑的博客主页 博主的github&#xff0c;平常所写代码皆在于此 共勉&#xff1a;talk is cheap, show me the code 作者是爪哇岛的新手&#xff0c;水平很有限&…

Cocos Creator的 Cannot read property ‘applyForce‘ of undefined报错

序&#xff1a; 1、博主是看了这个教程操作的时候出的bug>游戏开发 | 17节课学会如何用Cocos Creator制作3D跑酷游戏 | P9 代码控制对象移动_哔哩哔哩_bilibili 2、其实问题不是出在代码上&#xff0c;但是发现物体就是不平移 3、node全栈的资料》node全栈框架 正文…

【文件操作:解锁高效读写与管理技巧】

本章重点 为什么使用文件 什么是文件 文件的打开和关闭 文件的顺序读写 文件的随机读写 文本文件和二进制文件 文件读取结束的判定 文件缓冲区 1. 为什么使用文件 C语言中的变量和数据通常只在程序运行时存在于内存中&#xff0c;一旦程序结束&#xff0c;这些数据就…

LeetCode933. 最近的请求次数

题干 写一个 RecentCounter 类来计算特定时间范围内最近的请求。 请你实现 RecentCounter 类&#xff1a; RecentCounter() 初始化计数器&#xff0c;请求数为 0 。int ping(int t) 在时间 t 添加一个新请求&#xff0c;其中 t 表示以毫秒为单位的某个时间&#xff0c;并返回…

三、PWM呼吸灯

1. 什么是呼吸灯 如下图中的蓝色LED灯,不再是亮灭交替,而是慢慢亮慢慢灭,这就是呼吸灯 生活中常见 2. 怎样实现? 答:用PWM

Android中级——RemoteView

RemoteView RemoteView的应用NotificationWidgetPendingIntent RemoteViews内部机制模拟RemoteViews RemoteView的应用 Notification 如下开启一个系统的通知栏&#xff0c;点击后跳转到某网页 public class MainActivity extends AppCompatActivity {private static final …

项目一:基于stm32的阿里云智慧消防监控系统

若该文为原创文章&#xff0c;转载请注明原文出处。 Hi&#xff0c;大家好&#xff0c;我是忆枫&#xff0c;今天向大家介绍一个单片机项目。 一、简介 智慧消防监控系统&#xff0c;是用于检测火灾&#xff0c;温度&#xff0c;烟雾的监控系统。以 stm32单片机为核心外加 MQ…

一次redis缓存不均衡优化经验

背景 高并发接口&#xff0c;引入redis作为缓存之后&#xff0c;运行一段时间发现redis各个节点在高峰时段的访问量严重不均衡&#xff0c;有的节点访问量7000次/s&#xff0c;有的节点访问量500次/s 此种现象虽然暂时不影响系统使用&#xff0c;但是始终是个安全隐患&#x…

ARM进阶:内存屏障(DMB/DSB/ISB)的20个使用例子详解

在上一节内存屏障指令之DMB、DSB和ISB详解中&#xff0c;介绍了一下内存屏障的三个指令的作用并举了一些例子&#xff0c;对于内存屏障指令的使用时机&#xff0c;与处理器架构(比如Cortex-M和Cortex-A)和处理器的系统实现(同样的架构&#xff0c;有不同的实现&#xff0c;如ST…

大厂必面:你们系统qps多少,怎么部署的?假设每天有几千万请求,该如何部署?

前言 在40岁老架构师 尼恩的读者社区(50)中&#xff0c;很多小伙伴要拿高薪&#xff0c;这就要面大厂、面架构&#xff0c;拿高薪。 在高级开发面试、大厂面试、架构师的面试过程中&#xff0c;常常会遇到下面的问题&#xff1a; 你们系统qps多少&#xff1f;怎么部署的&…

第三章 图论 No.4最小生成树的简单应用

文章目录 裸题&#xff1a;1140. 最短网络裸题&#xff1a;1141. 局域网裸题&#xff1a;1142. 繁忙的都市裸题&#xff1a;1143. 联络员有些麻烦的裸题&#xff1a;1144. 连接格点 存在边权为负的情况下&#xff0c;无法求最小生成树 裸题&#xff1a;1140. 最短网络 1140. 最…