hashicorp/raft模块实现的raft集群存在节点跨集群身份冲突问题
我通过模块github.com/hashicorp/raft使用golang实现了一个raft集群功能,发现如下场景中会遇到一个问题:
测试启动如下2个raft集群,集群名称,和集群node与IP地址如下,raft集群均通过BootstrapCluster方法初始化:
Cluster1 BootstrapCluster servers:
- node1: {raft.ServerID: c1-node1, raft.ServerAddress: 192.168.100.1:7000}
- node2: {raft.ServerID: c1-node2, raft.ServerAddress: 192.168.100.2:7000}
- node3: {raft.ServerID: c1-node3, raft.ServerAddress: 192.168.100.3:7000}
Cluster2 BootstrapCluster servers:
- node3: {raft.ServerID: c2-node3, raft.ServerAddress: 192.168.100.3:7000}
- node4: {raft.ServerID: c2-node4, raft.ServerAddress: 192.168.100.4:7000}
- node5: {raft.ServerID: c2-node5, raft.ServerAddress: 192.168.100.5:7000}
其中,"node3"的地址会存在2个集群中。
- "node1","node2"按照"Cluster1"启动:
sudo ./raft_svr -cluster 'c1-node1,127.0.0.1,800;c1-node2,127.0.0.2,800;c1-node3,127.0.0.3,800' -id c1-node1
sudo ./raft_svr -cluster 'c1-node1,127.0.0.1,800;c1-node2,127.0.0.2,800;c1-node3,127.0.0.3,800' -id c1-node2
- "node3","node4","node5"先按照"Cluster2"启动:
sudo ./raft_svr -cluster 'c2-node3,127.0.0.3,800;c2-node4,127.0.0.4,800;c2-node5,127.0.0.5,800' -id c2-node3
sudo ./raft_svr -cluster 'c2-node3,127.0.0.3,800;c2-node4,127.0.0.4,800;c2-node5,127.0.0.5,800' -id c2-node4
sudo ./raft_svr -cluster 'c2-node3,127.0.0.3,800;c2-node4,127.0.0.4,800;c2-node5,127.0.0.5,800' -id c2-node5
然后就会发现"node3"会在"Cluster1"和"Cluster2"之间来回切换,一会属于"Cluster1",一会属于"Cluster2".
INFO[0170] current state:Follower, leader address:127.0.0.5:800, servers:[{Suffrage:Voter ID:c2-node3 Address:127.0.0.3:800} {Suffrage:Voter ID:c2-node4 Address:127.0.0.4:800} {Suffrage:Voter ID:c2-node5 Address:127.0.0.5:800}], last contact:2025-05-14 15:35:53.330867 +0800 CST m=+169.779019126
INFO[0171] current state:Follower, leader address:127.0.0.1:800, servers:[{Suffrage:Voter ID:c2-node3 Address:127.0.0.3:800} {Suffrage:Voter ID:c2-node4 Address:127.0.0.4:800} {Suffrage:Voter ID:c2-node5 Address:127.0.0.5:800}], last contact:2025-05-14 15:35:54.308388 +0800 CST m=+170.756576126
我的代码如下:
package main
import (
"flag"
"fmt"
"io"
"net"
"os"
"strconv"
"strings"
"time"
"github.com/hashicorp/raft"
log "github.com/sirupsen/logrus"
)
type raftCluster struct {
localRaftID raft.ServerID
servers map[raft.ServerID]raft.ServerAddress // raftID : raftAddressPort
raft *raft.Raft
electionTimeout time.Duration
}
func (r *raftCluster) Start() error {
config := raft.DefaultConfig()
config.HeartbeatTimeout = 2000 * time.Millisecond
config.ElectionTimeout = 5000 * time.Millisecond
config.CommitTimeout = 2000 * time.Millisecond
config.LeaderLeaseTimeout = 2000 * time.Millisecond
config.LocalID = r.localRaftID
config.LogOutput = log.StandardLogger().Out
r.electionTimeout = config.ElectionTimeout * time.Duration(len(r.servers)*2)
localAddressPort := string(r.servers[r.localRaftID])
tcpAddr, err := net.ResolveTCPAddr("tcp", localAddressPort)
if err != nil {
return fmt.Errorf("resolve tcp address %s, %v", localAddressPort, err)
}
transport, err := raft.NewTCPTransport(localAddressPort, tcpAddr, 2, 10*time.Second, log.StandardLogger().Out)
if err != nil {
return fmt.Errorf("fail to create tcp transport, localAddressPort:%s, tcpAddr:%v, %v",
localAddressPort, tcpAddr, err)
}
snapshots := raft.NewInmemSnapshotStore()
logStore := raft.NewInmemStore()
stableStore := raft.NewInmemStore()
fm := NewFsm()
r.raft, err = raft.NewRaft(config, fm, logStore, stableStore, snapshots, transport)
if err != nil {
return fmt.Errorf("create raft error, %v", err)
}
var configuration raft.Configuration
for sID, addr := range r.servers {
server := raft.Server{
ID: sID,
Address: addr,
}
configuration.Servers = append(configuration.Servers, server)
}
err = r.raft.BootstrapCluster(configuration).Error()
if err != nil {
return fmt.Errorf("raft bootstrap faild, conf:%v, %v", configuration, err)
}
log.Infof("bootstrap cluster as config: %v", configuration)
return nil
}
func (r *raftCluster) checkLeaderState() {
ticker := time.NewTicker(time.Second)
for {
select {
case leader := <-r.raft.LeaderCh():
log.Infof("im leader:%v, state:%s, leader address:%s", leader, r.raft.State(), r.raft.Leader())
case <-ticker.C:
verifyErr := r.raft.VerifyLeader().Error()
servers := r.raft.GetConfiguration().Configuration().Servers
switch verifyErr {
case nil:
log.Infof("im leader, servers:%v", servers)
case raft.ErrNotLeader:
// check cluster leader
log.Infof("current state:%v, servers:%+v, leader address:%v, last contact:%v",
r.raft.State(), servers, r.raft.Leader(), r.raft.LastContact())
}
}
}
}
func main() {
var (
clusters = flag.String("cluster", "",
"cluster node address, fmt: ID,IP,Port;ID,IP,Port")
clusterId = flag.String("id", "", "cluster id")
)
flag.Parse()
if *clusterId == "" {
log.Infof("cluster id messing")
os.Exit(1)
}
servers := make(map[raft.ServerID]raft.ServerAddress)
for _, cluster := range strings.Split(*clusters, ";") {
info := strings.Split(cluster, ",")
var (
nid string
nip net.IP
nport int
err error
)
switch {
case len(info) == 3:
nid = info[0]
nip = net.ParseIP(info[1])
if nip == nil {
log.Infof("cluster %s ip %s parse failed", cluster, info[1])
os.Exit(1)
}
nport, err = strconv.Atoi(info[2])
if err != nil {
log.Infof("cluster %s port %s parse failed, %v", cluster, info[2], err)
}
default:
log.Infof("cluster args value is bad format")
os.Exit(1)
}
log.Infof("cluster node id:%s, ip:%v, port:%d", nid, nip, nport)
addr := net.TCPAddr{IP: nip, Port: nport}
servers[raft.ServerID(nid)] = raft.ServerAddress(addr.String())
}
r := raftCluster{
localRaftID: raft.ServerID(*clusterId),
servers: servers,
}
err := r.Start()
if err != nil {
log.Infof("rafter cluster start failed, %v", err)
os.Exit(1)
}
r.checkLeaderState()
}
// SimpleFsm: 实现一个简单的Fsm
type SimpleFsm struct {
db database
}
func NewFsm() *SimpleFsm {
fsm := &SimpleFsm{
db: NewDatabase(),
}
return fsm
}
func (f *SimpleFsm) Apply(l *raft.Log) interface{} {
return nil
}
func (f *SimpleFsm) Snapshot() (raft.FSMSnapshot, error) {
return &f.db, nil
}
func (f *SimpleFsm) Restore(io.ReadCloser) error {
return nil
}
type database struct{}
func NewDatabase() database {
return database{}
}
func (d *database) Get(key string) string {
return "not implemented"
}
func (d *database) Set(key, value string) {}
func (d *database) Persist(sink raft.SnapshotSink) error {
_, _ = sink.Write([]byte{})
_ = sink.Close()
return nil
}
func (d *database) Release() {}
测试了hashicorp/raft多个版本都是相同的情况,以当前最新版本v1.7.3分析了下,应该是如下原因导致的:
- 集群启动后各个节点都通过
BootstrapCluster初始化,并引导集群选举,在node3上可以看见如下日志,说明在选举阶段node3能判断自己不属于Cluster1集群。
[WARN] raft: rejecting appendEntries request since node is not in configuration: from=c1-node1
- 但是当
Cluster1选举出leader后,node3就可能变成Cluster的成员了,这是因为Cluster1的leader会不断通过心跳向集群内node发送日志,而在这个过程中:- fllower节点是不会判断这个请求的leader是否是自己集群中的设备。
- fllower节点只对比请求日志的编号是否比自己本地的大,如果比本地的大,就接收存下来,并将发起请求的leader设置为自己集群的leader。
- 同样的,在
Cluster2选举出leader后,Cluster2的leader也会向node3不断通过心跳发送日志请求。这就导致node3一会属于Cluster1,一会属于Cluster2
这个过程中的漏洞出在raft节点接收日志修改leader的过程,代码位置为hashicop/raft模块中的raft.go:L1440位置的func (r *Raft) appendEntries(rpc RPC, a *AppendEntriesRequest)函数
修改改函数,增加对请求Leader的ID的判断,则可避免这个问题:
// Ignore an older term
if a.Term < r.getCurrentTerm() {
return
}
// yzc add,这里是我们添加的函数,注意,拒绝之后需要返回错误,否则会导致另外一个集群不断重新选举
if len(r.configurations.latest.Servers) > 0 && !inConfiguration(r.configurations.latest, ServerID(a.ID)) {
r.logger.Warn("rejecting appendEntries request since node is not in configuration",
"from", ServerID(a.ID))
rpcErr = fmt.Errorf("node is not in configuration")
return
}
// Increase the term if we see a newer one, also transition to follower
// if we ever get an appendEntries call
if a.Term > r.getCurrentTerm() || (r.getState() != Follower && !r.candidateFromLeadershipTransfer.Load()) {
// Ensure transition to follower
r.setState(Follower)
r.setCurrentTerm(a.Term)
resp.Term = a.Term
}
重新编译运行后,我们看到node3始终保持在Cluster2中,并且可以看到如下日志
[WARN] raft: rejecting appendEntries request since node is not in configuration: from=c1-node1
在Cluster1的leader日志中,我们可以看到该leader向node3发送心跳失败的日志:
[DEBUG] raft: failed to contact: server-id=c1-node3 time=1m29.121143167s
[ERROR] raft: failed to heartbeat to: peer=127.0.0.3:800 backoff time=1s error="node is not in configuration"
hashicorp/raft模块实现的raft集群存在节点跨集群身份冲突问题的更多相关文章
- redis 集群环境搭建-redis集群管理
集群架构 (1)所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽. (2)节点的fail是通过集群中超过半数的节点检测失效时才生效. (3)客户端与redi ...
- Kubernetes使用集群联邦实现多集群管理
Kubernetes在1.3版本之后,增加了“集群联邦”Federation的功能.这个功能使企业能够快速有效的.低成本的跨区跨域.甚至在不同的云平台上运行集群.这个功能可以按照地理位置创建一个复制机 ...
- Elasticsearch 主从同步之跨集群复制
文章转载自:https://mp.weixin.qq.com/s/alHHxXont6XFm_m9PfsGfw 1.什么是跨集群复制? 跨集群复制(Cross-cluster replication, ...
- HUE配置文件hue.ini 的filebrowser模块详解(图文详解)(分HA集群和非HA集群)
不多说,直接上干货! 我的集群机器情况是 bigdatamaster(192.168.80.10).bigdataslave1(192.168.80.11)和bigdataslave2(192.168 ...
- 庐山真面目之七微服务架构Consul集群、Ocelot网关集群和IdentityServer4版本实现
庐山真面目之七微服务架构Consul集群.Ocelot网关集群和IdentityServer4版本实现 一.简介 在上一篇文章<庐山真面目之六微服务架构Consul集群.Ocelot网 ...
- 庐山真面目之十二微服务架构基于Docker搭建Consul集群、Ocelot网关集群和IdentityServer版本实现
庐山真面目之十二微服务架构基于Docker搭建Consul集群.Ocelot网关集群和IdentityServer版本实现 一.简介 在第七篇文章<庐山真面目之七微服务架构Consul ...
- Kubernetes集群搭建之Etcd集群配置篇
介绍 etcd 是一个分布式一致性k-v存储系统,可用于服务注册发现与共享配置,具有以下优点. 简单 : 相比于晦涩难懂的paxos算法,etcd基于相对简单且易实现的raft算法实现一致性,并通过g ...
- docker swarm英文文档学习-6-添加节点到集群
Join nodes to a swarm添加节点到集群 当你第一次创建集群时,你将单个Docker引擎置于集群模式中.为了充分利用群体模式,可以在集群中添加节点: 添加工作节点可以增加容量.当你将服 ...
- Docker swarm集群增加节点和删除节点
Docker swarm集群增加节点 docker swarm初始化 docker swarm init docker swarm 增加节点 在已经初始化的机器上执行:# docker swarm j ...
- 庐山真面目之六微服务架构Consul集群、Ocelot网关集群和Nginx版本实现
庐山真面目之六微服务架构Consul集群.Ocelot网关集群和Nginx版本实现 一.简介 在上一篇文章<庐山真面目之五微服务架构Consul集群.Ocelot网关和Nginx版本实 ...
随机推荐
- 动态代理到AOP
动态代理 代理(proxy)是一种设计模式,通过了目标对象的另外访问方法,即通过代理对象访问目标对象.动态代理是再程序运行时动态地生成一个代理类代替原本的类.该类会拦截对目标对象的方法调用 为什么使用 ...
- CUDA异常捕获
技术背景 在CUDA编程中有可能会遇到一些相对比较隐蔽的报错,但是直接编译运行cu文件是不显现的.那么可以通过添加一个用于检查的宏,来监测CUDA程序运行过程中可能出现的报错. error.cuh 我 ...
- Halcon学习教程(二) 测量量测相关(点到线距离,线到线距离,轮廓线距离,一维测量,圆形测量,矩形测量等)
原文作者:aircraft 原文地址:https://www.cnblogs.com/DOMLX/p/18740576 本篇讲一些测量用到的算子和实例,想了解更多就得去看看halcon实例里一维测量里 ...
- 删除空白行,报错 raise KeyError(list(np.compress(check, subset))) KeyError: ['姓名']
之前这段程序跑完后没报错,经调试发现到这一句报错 > df.dropna(subset=['姓名'],inplace=True) 意思是删除'姓名'列 含有'NAN'的行 思考第一步:应该是 d ...
- Navicat 数据库如何再次打开恢复上次的页面
选项->选项卡->从上次离开的画面继续 2.其他软件有这个需求也可以在设置里找一找!
- .NET Core 中如何实现缓存的预热?
在构建高性能的 .NET Core 应用时,缓存是提升系统响应速度.减轻数据库压力的利器.然而,缓存并非一蹴而就,它也需要"热身"才能发挥最佳性能.这就是缓存预热的意义所在. 一. ...
- docker 配置安装宝塔面板
宝塔镜像 https://hub.docker.com/r/btpanel/baota 配置宝塔 第一步:创建一个名为 bt-data 的目录,此目录将存储宝塔数据. $ mkdir bt-data ...
- Golang入门:协程(goroutine)
goroutine goroutine 是 Go 的并发模型的核心概念.为了理解 goroutine,我们来定义几个术语.第一个是进程.进程是程序的实例,由计算机的操作系统运行.操作系统将一些资源(如 ...
- linux命令:lsof命令
lsof(list open files)是一个列出当前系统打开文件的工具.在linux环境下,任何事物都以文件的形式存在,通过文件不仅仅可以访问常规数据,还可以访问网络连接和硬件.所以如传输控制协议 ...
- 深入理解Hadoop读书笔记-1
背景 公司的物流业务系统目前实现了使用storm集群进行过门事件的实时计算处理,但是还有一个需求,我们需要存储每个标签上传的每条明细数据,然后进行定期的标签报表统计,这个是目前的实时计算框架无法满足的 ...