etcd-raftexample-源码简析

ps: 常用博客地址为 https://qtozeng.top
最近集中了解了ZABRaftPaxos协议的基本理论,因此想进一步深入到源代码仔细体验一致性协议如何在分布式系统中发挥作用。虽然在 MIT 6.824 课程中有简单实现Raft协议,并基于Raft构建了一个粗糙的 kv 存储系统。但还是想了解下工业生产级别的Raft协议的实现内幕,故选择etcd进行解读。etcd是 CoreOS 基于Raft协议使用 go 开发的分布式 kv 存储系统,可用于服务发现、共享配置及其它利用一致性保障的功能(如leader选举及分布式锁、队列等)。这些功能ZooKeeper不也有提供?没错。它们都可以作为其它分布式应用的独立协调服务,这通过通用的一致性元信息存储来实现。但在易用性上,etcd可谓略胜一筹。因此,后续的一系列博客会简单对etcd各重要组成部分的源码进行简要分析(重点在Raft实现)。本文主要是分析etcdraftexample的代码。它是etcd官方提供的如何使用etcd内部的Raft协议组件来构建分布式应用的一个简单示例。

(阐述etcd-raft的系列文章对应的etcd-raft的版本为 3.3.11,但遗憾实际上看的master unstable版本)etcd内部使用Raft协议对集群各节点的状态(数据、日志及快照等)进行同步。类似于ZooKeeper利用ZAB协议作为底层的可靠的事务广播协议。但etcdRaft的实现有点特殊,它底层的Raft组件库只实现了Raft协议最核心的部分,这主要包括选主逻辑、一致性具体实现以及成员关系变化。而将诸如WALsnapshot以及网络传输等模块让用户来实现,这明显增加了使用的难度,但对于应用本质上也更灵活。

本文会简单分析etcd提供的如何其核心的Raft协议组件来构建一个简单的高可用内存 kv 存储(其本质是一个状态机),用户可以通过 http 协议来访问应用(kv 存储系统),以对数据进行读写操作,在对日志进行读写过程中,Raft组件库能够保证各节点数据的一致性。其对应的源码目录为/etcd-io/etcd/tree/master/contrib/raftexample。另外,需要强调的是,本文的主题是利用Raft协议库来构建一个简单的 kv 存储,关于Raft协议库实现的细节不会过多阐述。若读者想继续了解此文,个人建议clone源代码,在阅读源代码的过程中,参考本文效果可能会更好,如果有理解错误的地方,欢迎指正!

数据结构

在按raftexample/main的示例完整解读整个流程之前,先熟悉几个重要的数据结构会有好处。此示例构建的应用为 kv 存储系统,因此,先来了解 kvstore定义的相关字段:

// a key-value store backed by raft
type kvstore struct {
   
	proposeC    chan<- string // channel for proposing updates
	mu          sync.RWMutex
	kvStore     map[string]string // current committed key-value pairs
	snapshotter *snap.Snapshotter
} // kvstore.go

关键结构成员解释如下:

  • proposeC: 应用与底层Raft核心库之间的通信channel,当用户向应用通过 http 发送更新请求时,应用会将此请求通过channel传递给底层的Raft库。
  • kvStore: kv 结构的内存存储,即对应应用的状态机。
  • snapshotter: 由应用管理的快照snapshot接口。

接下来分析一下应用封装底层Raft核心库的结构raftNode,应用通过与raftNode结构进行交互来使用底层的Raft核心协议,它封装完整的Raft协议相关的逻辑(如WALsnapshot等)。我们先列举它的相关处理逻辑,然后展示其结构内容。具体地逻辑如下:

  • 将应用的更新请求传递给Raft核心来执行。
  • 同时,将Raft协议已提交的日志传回给应用,以指示应用来将日志请求应用到状态机。
  • 另外,它也处理由Raft协议相关的指令,包括选举、成员变化等。
  • 处理WAL日志相关逻辑。
  • 处理快照相关的逻辑。
  • 将底层Raft协议的指令消息传输到集群其它节点。
// A key-value stream backed by raft
type raftNode struct {
   
	proposeC    <-chan string            // proposed messages (k,v)
	confChangeC <-chan raftpb.ConfChange // proposed cluster config changes
	commitC     chan<- *string           // entries committed to log (k,v)
	errorC      chan<- error             // errors from raft session

	id          int      // client ID for raft session
	peers       []string // raft peer URLs
	join        bool     // node is joining an existing cluster
	waldir      string   // path to WAL directory
	snapdir     string   // path to snapshot directory
	getSnapshot func() ([]byte, error)
	lastIndex   uint64 // index of log at start

	confState     raftpb.ConfState
	snapshotIndex uint64
	appliedIndex  uint64

	// raft backing for the commit/error channel
	node        raft.Node
	raftStorage *raft.MemoryStorage
	wal         *wal.WAL

	snapshotter      *snap.Snapshotter
	snapshotterReady chan *snap.Snapshotter // signals when snapshotter is ready

	snapCount uint64
	transport *rafthttp.Transport
	stopc     chan struct{
   } // signals proposal channel closed
	httpstopc chan struct{
   } // signals http server to shutdown
	httpdonec chan struct{
   } // signals http server shutdown complete
} // raft.go

关键结构成员解释如下:

  • proposeC: 同kvStore.proposeC通道类似,事实上,kvStore会将用户的更新请求传递给raftNode以使得其最终能传递给底层的Raft协议库。
  • confChangeC: Raft协议通过此channel来传递集群配置变更的请求给应用。
  • commitC: 底层Raft协议通过此channel可以向应用传递准备提交或应用的channel,最终kvStore会反复从此通道中读取可以提交的日志entry,然后正式应用到状态机。
  • node: 即底层Raft协议组件,raftNode可以通过node提供的接口来与Raft组件进行交互。
  • raftStorage: Raft协议的状态存储组件,应用在更新kvStore状态机时,也会更新此组件,并且通过raft.Config传给Raft协议。
  • wal: 管理WAL日志,前文提过etcd将日志的相关逻辑交由应用来管理。
  • snapshotter: 管理 snapshot文件,快照文件也是由应用来管理。
  • transport: 应用通过此接口与集群中其它的节点(peer)通信,比如传输日志同步消息、快照同步消息等。网络传输也是由应用来处理。

其它的相关的数据结构不再展开,具体可以查看源代码,辅助注释理解。

关键流程

我们从main.go中开始通过梳理一个典型的由客户端发起的状态更新请求的完整流程来理解如何利用Raft协议库来构建应用状态机。main.go的主要逻辑如下:

func main() {
   
	// 解析客户端请求参数信息
    ...
	proposeC := make(chan string)
	defer close(proposeC)
	confChangeC := make(chan raftpb.ConfChange)
	defer close(confChangeC)

	// raft provides a commit stream for the proposals from the http api
	var kvs *kvstore
	getSnapshot := func() ([]byte, error) {
    return kvs.getSnapshot() }
	commitC, errorC, snapshotterReady := newRaftNode(*id, strings.Split(*cluster, ","), *join, getSnapshot, proposeC, confChangeC)

	kvs = newKVStore(<-snapshotterReady, proposeC, commitC, errorC)

	// the key-value http handler will propose updates to raft
	serveHttpKVAPI(kvs, *kvport, confChangeC, errorC)
} // main.go

显然,此示例的步骤较为清晰。主要包括三方面逻辑:其一,初始化raftNode,并通过 go routine 来启动相关的逻辑,实际上,这也是初始化并启动Raft协议组件,后面会详细相关流程。其二,初始化应用状态机,它会反复从commitC通道中读取raftNode/Raft传递给它的准备提交应用的日志。最后,启动 http 服务以接收客户端读写请求,并设置监听。下面会围绕这三个功能相关的逻辑进行阐述。

Raft 初始化

首先我们来理顺Raft初始化的逻辑,这部分相对简单。

func newRaftNode(id int, peers []string, join bool, getSnapshot func() ([]byte, error), proposeC <-chan string,
	confChangeC <-chan raftpb.ConfChange) (<-chan *string, <-chan error, <-chan *snap.Snapshotter) {
   

	commitC := make(chan *string)
	errorC := make(chan error)

	rc := &raftNode{
   
		proposeC:    proposeC,
		confChangeC: confChangeC,
		commitC:     commitC,
		errorC:      errorC,
		id:          id,
		peers:       peers,
		join:        join,
		waldir:      fmt.Sprintf("raftexample-%d", id),
		snapdir:     fmt.Sprintf("raftexample-%d-snap", id),
		getSnapshot: getSnapshot,
		snapCount:   defaultSnapshotCount, // 只有当日志数量达到此阈值时才执行快照
		stopc:       make(chan struct{
   
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值