Etcd 使用场景:通过分布式锁思路实现自动选主
分布式锁?选主?
分布式锁可以保证当有多台实例同时竞争一把锁时,只有一个人会成功,其他的都是失败。诸如共享资源修改、幂等、频控等场景都可以通过分布式锁来实现。
还有一种场景,也可以通过分布式锁来实现,那就是选主,为了保证服务的可用性,我们都会以一主多从的方式去部署,特别是提供存储能力的服务。Leader服务来接收数据的写入,然后将数据同步给Follower服务。当Leader服务挂掉时,我们需要从Follower服务中重新选举一个服务来当Leader,复杂的方式是通过Raft协议去协商,简单点,可以通过分布式锁的思路来做:
- 所有的Follower服务去竞争同一把锁,并给这个锁设置一个过期时间
- 只会有一个Follower服务取到锁,这把锁的值就为它的标识,他就变成了Leader服务
- 其他Follower服务竞争失败后,去获取锁得到的当前的Leader服务标识,与之通信
- Leader服务需要在锁过期之前不断的续期,证明自己是健康的
- 所有Follower服务监控这把锁是否还被Leader服务持有,如果没有,就跳到了第1步
通过 Redis、Zookeeper 都可以实现,不过这次,我们使用 Etcd 来实现。
Etcd 简单介绍
Etcd:A highly-available key value store for shared configuration and service discovery。
Etcd 是一个K/V存储,和 Redis 功能类似,这是我对它的直观印象,和实现Master选举好像八竿子打不着。随着对 Etcd 了解的加深,我才开始对官网介绍那句话有了一定理解,Redis K/V 存储是用来做纯粹的缓存功能,高并发读写是核心,而 Etcd 这个基于 Raft 的分布式 K/V 存储,强一致性的 K/V 读写是核心,基本这点诞生了很多有想象力的使用场景:服务发现、分布式锁、Master 选举等等。
基于 Etcd 以下特性,我们可以实现自动选主:
- MVCC,key存在版本属性,没被创建时版本号为0
- CAS操作,结合MVCC,可以实现竞选逻辑,if(version == 0) set(key,value),通过原子操作,确保只有一台机器能set成功;
- Lease租约,可以对key绑定一个租约,租约到期时没预约,这个key就会被回收;
- Watch监听,监听key的变化事件,如果key被删除,则重新发起竞选。
准备工作
启动 Etcd
我们使用 Docker 安装,简单方便:
> docker run -d --name Etcd-server \
--publish 2379:2379 \
--publish 2380:2380 \
--env ALLOW_NONE_AUTHENTICATION=yes \
--env ETCD_ADVERTISE_CLIENT_URLS=http://etcd-server:2379 \
bitnami/etcd:latest
最好是使用最新般本
Go 依赖库安装
Etcd 提供开箱即用的选主工作库,我们直接使用就行
> go get go.etcd.io/etcd/client/v3
这一步看似简单,如果放在以前,少不了一顿百度,原因是因为它依赖的 grpc 和 bbolt 库的版本不能是最新的,需要在 go.mod 中去写死版本。所幸赶上了好时代,官方终于出手整改了,现在只要一行命令行。
选主Demo
package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"time"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
)
var (
serverName = flag.String("name", "", "")
)
func main() {
flag.Parse()
// Etcd 服务器地址
endpoints := []string{"127.0.0.1:2379"}
clientConfig := clientv3.Config{
Endpoints: endpoints,
DialTimeout: 2 * time.Second,
}
cli, err := clientv3.New(clientConfig)
if err != nil {
panic(err)
}
s1, err := concurrency.NewSession(cli)
if err != nil {
panic(err)
}
fmt.Println("session lessId is ", s1.Lease())
e1 := concurrency.NewElection(s1, "my-election")
go func() {
// 参与选举,如果选举成功,会定时续期
if err := e1.Campaign(context.Background(), *serverName); err != nil {
fmt.Println(err)
}
}()
masterName := ""
go func() {
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
timer := time.NewTicker(time.Second)
for range timer.C {
timer.Reset(time.Second)
select {
case resp := <-e1.Observe(ctx):
if len(resp.Kvs) > 0 {
// 查看当前谁是 master
masterName = string(resp.Kvs[0].Value)
fmt.Println("get master with:", masterName)
}
}
}
}()
go func() {
timer := time.NewTicker(5 * time.Second)
for range timer.C {
// 判断自己是 master 还是 slave
if masterName == *serverName {
fmt.Println("oh, i'm master")
} else {
fmt.Println("slave!")
}
}
}()
c := make(chan os.Signal, 1)
// 接收 Ctrl C 中断
signal.Notify(c, os.Interrupt, os.Kill)
s := <-c
fmt.Println("Got signal:", s)
e1.Resign(context.TODO())
}
我们在两个终端分别运行下面两个命令,模拟两个服务去竞争:
> go run main.go -name A
session lessId is 7587863771971134868
get master with: A
get master with: A
get master with: A
get master with: A
oh, i'm master
> go run main.go -name B
session lessId is 7587863771971134876
get master with: A
get master with: A
get master with: A
get master with: A
slave!
当我们使用 Ctrl C 中断,此时 B 就成为了 master
> go run main.go -name A
session lessId is 7587863771971134868
get master with: A
get master with: A
get master with: A
get master with: A
oh, i'm master
^CGot signal: interrupt
> go run main.go -name B
session lessId is 7587863771971134876
get master with: A
get master with: A
get master with: A
get master with: A
slave!
get master with: B
get master with: B
get master with: B
get master with: B
oh, i'm master
原理
当我们启动 A 和 B 两个服务时,他们后会在公共前缀 "my-election/" 下创建自己的 key,这个 key 的构成为 "my-election/" + 十六进制(LessId)。这个LessId 是在服务启动时,从 Etcd 服务端取到的客户端唯一标识。比如上面程序运行的两个服务创建的 key 分别是:
- A 服务创建的 key 是 "my-election/694d81e5fc652594",值是 "A"
- B 服务创建的 key 是 "my-election/694d81e5fc65259c",值是 "B"
因为是通过事务的方式去创建 key,可以保证如果这个 key 已经创建了,不去创建了。并且这个 key 是有过期时间,两个服务 A 和 B 会启动一个协程定期去刷新过期时间,通过这个方式证明自己的健康的。
现在两个服务都创建了 key, 那么那个才是 master 呢?我们选取最早创建的那个 key 的拥有者作为 master。
Etcd 服务的查询接口支持根据前缀查询和按照创建时间排序,所以我们可以轻松的拿到第一个创建成功的 key,这个 key 对应的值就是 master 了,也就是 A 服务。
当现在 master 服务挂掉了,因为它的 key 没有在过期之前续期,就会被删除的,此时当初第二个创建的 key 就会变成第一个,那个 master 就变成了 B 服务。
我们是通过e1.Campaign(context.Background(), *serverName)行代码是参加去参加选举的,里面有一个细节:如果竞争失败,这个函数会阻塞,直到它选举成功或者服务中断。也就是说:如果 B 服务创建的 key
不是最早的一个,那它会一直等待,直到服务 A 的 key 被删除后,函数才会有返回。
Etcd 使用场景:通过分布式锁思路实现自动选主的更多相关文章
- zookeeper适用场景:分布式锁实现
问题导读:1.zookeeper如何实现分布式锁?2.什么是羊群效应?3.zookeeper如何释放锁? 在zookeeper应用场景有关于分布式集群配置文件同步问题的描述,设想一下如果有100台机器 ...
- ZooKeeper典型应用场景:分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式.如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要通过一些互斥手段来防止彼此之间的干扰,以保证一致 ...
- 玩转CONSUL(2)–分布式锁
1. 前言 分布式锁的场景,大家应该都有遇到过.比如对可靠性有较高要求的系统中,我们需要做主备切换.这时我们可以利用分布式锁,来做选主动作,抢到锁作为主,执行对应的任务,剩余的实例作为备份 redis ...
- 【分布式锁的演化】“超卖场景”,MySQL分布式锁篇
前言 之前的文章中通过电商场景中秒杀的例子和大家分享了单体架构中锁的使用方式,但是现在很多应用系统都是相当庞大的,很多应用系统都是微服务的架构体系,那么在这种跨jvm的场景下,我们又该如何去解决并发. ...
- Redis、Zookeeper实现分布式锁——原理与实践
Redis与分布式锁的问题已经是老生常谈了,本文尝试总结一些Redis.Zookeeper实现分布式锁的常用方案,并提供一些比较好的实践思路(基于Java).不足之处,欢迎探讨. Redis分布式锁 ...
- 用Redis实现分布式锁 与 实现任务队列(转)
这一次总结和分享用Redis实现分布式锁 与 实现任务队列 这两大强大的功能.先扯点个人观点,之前我看了一篇博文说博客园的文章大部分都是分享代码,博文里强调说分享思路比分享代码更重要(貌似大概是这个意 ...
- Redis实现分布式锁与任务队列
Redis实现分布式锁 与 实现任务队列 这一次总结和分享用Redis实现分布式锁 与 实现任务队列 这两大强大的功能.先扯点个人观点,之前我看了一篇博文说博客园的文章大部分都是分享代码,博文里强调说 ...
- Zookeeper实现分布式锁服务(Chubby)
在分布式系统中,如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰,来保证一致性,在这种情况下,便需要使用到分布式锁例如有N台服务器同时 ...
- 基于Redis的分布式锁真的安全吗?
说明: 我前段时间写了一篇用consul实现分布式锁,感觉理解的也不是很好,直到我看到了这2篇写分布式锁的讨论,真的是很佩服作者严谨的态度, 把这种分布式锁研究的这么透彻,作者这种技术态度真的值得我好 ...
随机推荐
- Bugku练习题---Web---计算器
Bugku练习题---Web---计算器 flag:flag{8b4b2f83db2992d17d770be1db965147} 解题步骤: 1.观察题目,打开场景 2.场景打开后发现是一个验证码界面 ...
- Annotation(注释) _Override _ Deprecated _ SuppressWarnings
Deprecated SuppressWarnings 元注解
- 【ConcurrentHashMap】浅析ConcurrentHashMap的构造方法及put方法(JDK1.7)
目录 引言 代码讲解 构造方法 put方法 ensureSegment Segment.put 引言 ConcurrentHashMap的数据结构如下. 和HashMap的最大区别在于多了一层Segm ...
- [题解][YZOJ50074] 小 C 的岛屿
仅仅是对 \(O(n^4)\) 做法的一个记录. 简要题意 有 \(N\) 座岛屿,初始时没有边.每座岛屿都有一个概率值 \(p_i\) 和一个大小为 \(s_i\) 友好列表 \(A_i\) . 小 ...
- Linux screen命令详解
开源Linux 长按二维码加关注~ 上一篇:局域网IP冲突罪魁祸首是谁? 很多时候,我们都需要执行一些需要很长时间的任务.如果这时候,你的网络连接突然断开了,那么你之前所做的所有工作可能都会丢失,所做 ...
- IIS项目部署和发布
VS2019如何把项目部署和发布 这里演示:通过IIS文件publish的方式部署到Windows本地服务器上 第一步(安装IIS) 1.在自己电脑上搜索Windows功能里的[启用或关闭Window ...
- Docker容器的数据卷
一.数据卷概念 1.数据卷是宿主机中的一个目录或文件 2.当容器目录和数据卷目录绑定后,对方的修改会立即同步 3.一个数据卷可以被多个容器同时挂载 4.一个容器也可以挂载多个数据卷 简单理解:有点类似 ...
- 字符编码,存储引擎,MySQL字段类型,MySQL字段约束条件
字符编码 查看MySQL默认编码命令:\s """ 如果是5.X系列 显示的编码有多种 latin1 gbk 如果是8.X系列 显示的统一是utf8mb4 utf8mb4 ...
- 一、全新安装搭建redis主从集群
前言· 这里分为三篇文章来写我是如何重新搭建redis主从集群和哨兵集群的及原本服务器上有单redis如何通过升级脚本来实现redis集群.(redis结构:主-从(备)-从(备)) 至于为什么要搭建 ...
- JWT 访问令牌
JWT 访问令牌 更为详细的介绍jwt 在学习jwt之前我们首先了解一下用户身份验证 1 单一服务器认证模式 一般过程如下: 用户向服务器发送用户名和密码. 验证服务器后,相关数据(如用户名,用户角色 ...