etcd 中 raft 算法的使用方法

本文详细介绍了如何在etcd中使用raft算法实现一个分布式key-value存储的集群。通过实践,作者发现etcd的raft实现可以独立使用,但官方文档中的示例代码存在一些问题。文中补充了缺失的部分,包括启动节点时的结构体封装、RPC消息处理、节点ID的正确传递以及提案处理的循环应在goroutine中运行。示例代码展示了一个通过HTTP交换raft报文的三节点集群,节点会定期提案并确保集群内数据的一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

raft 协议是一个一致性算法,解决多台机器之间数据一致的问题。raft 声称简洁明了,可以取代非常复杂的 PAXOS 算法。然而翻看 raft 的论文后,会发现即便声称简洁明了,自己完整地实现 raft 还是很麻烦的。

etcd是一个分布式的 key-value 存储组件,它通过 raft 算法保证多台机器数据的一致性。那么 etcd 中的 raft 算法可以提取出来用在自己的项目中吗?

答案是可以的。etcd 不仅实现了 raft,还把 raft 解耦得很完美,完全可以独立使用。代码库点这儿:https://github.com/etcd-io/etcd/tree/master/raft

美中不足的是,etcd raft的使用文档写得很烂,文档中列的代码缺了很多关键部分,是跑不起来的。按照文档中的代码写,不是报错就是 go panic,要不就是跑起来后机器都僵着不选举。经过笔者的实践,补齐了缺失的代码,完成了一个可以跑起来的示例,代码见文章最后。

实践过程中,使用文档中没有提及的几个点:

  1. 文档说 n := raft.StartNode() 就可以启动一个节点,实际这样做会 panic,要自己额外再封装一个 struct ,并且实现 Process() 方法才行(见本文 raft.go里的 rNode

  2. 文档说集群中在收到对方节点的 RPC 消息时,要调用 n.Step() 方法:

func recvRaftRPC(ctx context.Context, m raftpb.Message) {
    n.Step(ctx, m)
}

但这个recvRaftRPC() 又在哪调用呢?回顾第 1 条不是要自己封装一个 struct 吗,n.Step() 应该写在这个 struct 的 Process() 方法里,而不是放在什么 recvRaftRPC() 里(见本文 raft.go 里的 rNode)。raft 算法会在接收到其他节点的RPC请求时调用 Process()

  1. 还是 raft.StartNode() ,文档的这段代码:
n := raft.StartNode(c, []raft.Peer{
  {ID: 0x02}, {ID: 0x03}})

意思是三个节点的集群,如果当前启动节点 ID 是 0x01,那么启动时 peer 列表只传 0x02, 0x03,不传自己,实际这样做启动集群后会僵住不选举。正确做法是把节点自己也传入 peer 列表。

  1. 文档中的 for-select 循环,是要写在一个 go 协程里的。不然启动后集群会僵住不选举。

示例代码介绍

本文的示例代码是一个三节点的集群,节点之前通过 http 交换 raft 报文。

集群启动之后,0x01节点会每隔 1 秒申请提案(也就是业务数据):

for {
    log.Printf("Propose on node %v\n", *id)
    n.node.Propose(context.TODO(), []byte("hello"))
    time.Sleep(time.Second)
}

然后在代码的 这个地方:

for _, entry := range rd.CommittedEntries {
    switch entry.Type {
    case raftpb.EntryNormal:
       log.Printf("Receive committed data on node %v: %v\n", rn.id, string(entry.Data))
    ....
}

集群的每个节点都会收到这个提案,这时后提案在集群里是一致的了,可以放心地持久化了。

完整代码:

main.go

package main

import (
	"context"
	"flag"
	"log"
	"time"
)

func main() {
   
	id := flag.Uint64("id", 1, "node id")
	flag.Parse()
	log.Printf("I'am node %v\n", *id)

	cluster := map[uint64]string{
   
		1: "http://127.0.0.1:22210",
		2: "http://127.0.0.1:22220",
		3: "http://127.0.0.1:22230",
	}
	n := newRaftNode(*id, cluster)

	if *id == 1 {
   
		time.Sleep
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值