成员变更在一致性协议里稍复杂一些,由于不同的成员不可能在同一时刻从旧成员组切换至新成员组,所以可能出现两个不相交的majority,从而导致同一个term出现两个leader,进而导致同一个index的日志不一致,违反一致性协议。下图是个例子:

raft作者提出了一种比较简单的方法,一次只增加或减少一个成员,这样能够保证任何时刻,都不可能出现两个不相交的majority,所以,可以从旧成员组直接切到新成员组。如下图:

切换的时机是把成员变更日志写盘的时候,不管是否commit。这个切换时机带来的问题是如果这条成员变更日志最终没有commit,在发生leader切换的时候,成员组就需要回滚到旧的成员组。

etcd raft为了实现简单,将切换成员组的实机选在apply成员变更日志的时候。

下面看看etcd raft library如何实现的:

应用调用

func (n *node) ProposeConfChange(ctx context.Context, cc pb.ConfChange) error {
	data, err := cc.Marshal()
	if err != nil {
		return err
	}
	return n.Step(ctx, pb.Message{Type: pb.MsgProp, Entries: []pb.Entry{{Type: pb.EntryConfChange, Data: data}}})
}

可以看出,ConfChange是和普通的log entry一样封装在MsgProp消息中,进入propc,

跑raft算法的goroutine从propc中拿到消息后,会做如下判断:

for i, e := range m.Entries {
			if e.Type == pb.EntryConfChange {
				if r.pendingConf {
					r.logger.Infof("propose conf %s ignored since pending unapplied configuration", e.String())
					m.Entries[i] = pb.Entry{Type: pb.EntryNormal}
				}
				r.pendingConf = true
			}
}

检查已经有成员变更正在做,就忽略新的成员变更。然后将pendingConf置为true,意味着目前有成员变更正在做了,从这里可以看出,多个成员变更不能同时进行。follower接收端的处理和普通log entry一样。

如果成员变更日志达成了一致,则会被封装在Ready中,应用拿到后,做如下处理:

if entry.Type == raftpb.EntryConfChange {
          var cc raftpb.ConfChange
          cc.Unmarshal(entry.Data)
          s.Node.ApplyConfChange(cc)
}

ApplyConfChange:

func (n *node) ApplyConfChange(cc pb.ConfChange) *pb.ConfState {
	var cs pb.ConfState
	select {
	case n.confc <- cc:
	case <-n.done:
	}
	select {
	case cs = <-n.confstatec:
	case <-n.done:
	}
	return &cs
}

讲ConfChange放入confc,然后阻塞在confstatec上,跑raft协议的goroutine从confc中拿出ConfChange,做相应的增加/删除节点操作,然后将成员组放入confstatec。

switch cc.Type {
			case pb.ConfChangeAddNode:
				r.addNode(cc.NodeID)
			case pb.ConfChangeRemoveNode:
				// block incoming proposal when local node is
				// removed
				if cc.NodeID == r.id {
					propc = nil
				}
				r.removeNode(cc.NodeID)
			case pb.ConfChangeUpdateNode:
				r.resetPendingConf()
			default:
				panic("unexpected conf type")
			}
			select {
			case n.confstatec <- pb.ConfState{Nodes: r.nodes()}:
			case <-n.done:
}

增加/删除节点操作都只是更新prs,map的每个元素保存一个peer的状态,其中最重要的状态莫过于

Match, Next uint64

看过raft小论文的人一看变量名就很明确意义,Match代表最大的已经落盘的log index,Next代表下一条需要发给这个peer的log index。然后将pendingConf置为false,代表成员变更结束。

重启如何恢复成员组:

hs, cs, err := c.Storage.InitialState()

Storage接口中:

// InitialState returns the saved HardState and ConfState information.
	InitialState() (pb.HardState, pb.ConfState, error)

Storage是个接口,其中InitialState()用于恢复成员组,需要应用自己实现,通常将ConfState记在最后一次Snapshot的Metadata中:

message SnapshotMetadata {
	optional ConfState conf_state = 1 [(gogoproto.nullable) = false];
	optional uint64    index      = 2 [(gogoproto.nullable) = false];
	optional uint64    term       = 3 [(gogoproto.nullable) = false];
}

ConfState:

message ConfState {
	repeated uint64 nodes = 1;
}

拿到ConfState后就可以初始化上面提到的prs,snapshot后续的已经commit的log entry一样,通过Ready封装,应用进行apply,如果其中有ConfChange,则调用

s.Node.ApplyConfChange(cc)
posted @ 2017-07-16 15:23 吴镝 阅读(1911) 评论(0) 推荐(0) 编辑
摘要: leadership transfer可以把raft group中的leader身份转给其中一个follower。这个功能可以用来做负载均衡,比如可以把leader放在性能更好的机器或者离客户端更近的机器上。 对于一个大规模分布式系统来说,负载均衡非常重要。然而raft本身在选主方面必须要求新主包含 阅读全文
posted @ 2017-07-14 23:32 吴镝 阅读(1038) 评论(0) 推荐(0) 编辑
摘要: Linearizable Read通俗来讲,就是读请求需要读到最新的已经commit的数据,不会读到老数据。 对于使用raft协议来保证多副本强一致的系统中,读写请求都可以通过走一次raft协议来满足。然后,现实系统中,读请求通常会占很大比重,如果每次读请求都要走一次raft落盘,性能可想而知。所以 阅读全文
posted @ 2017-07-13 17:56 吴镝 阅读(4204) 评论(0) 推荐(0) 编辑
摘要: 早在2013年11月份,在raft论文还只能在网上下载到草稿版时,我曾经写过一篇 "blog" 对其进行简要分析。4年过去了,各种raft协议的讲解铺天盖地,raft也确实得到了广泛的应用。其中最知名的应用莫过于etcd。etcd将raft协议本身实现为一个library,位于https://git 阅读全文
posted @ 2017-07-08 17:33 吴镝 阅读(2020) 评论(0) 推荐(0) 编辑
摘要: 本文提到的一些术语,比如Serializability和Linearizability,解释看Linearizability, Serializability and Strict Serializability。 本文中观点大部分都是参考了CockroachDB多篇官方blog,设计文档,代码以及 阅读全文
posted @ 2017-05-21 17:18 吴镝 阅读(4255) 评论(0) 推荐(0) 编辑
摘要: paxos 说multi paxos之前先简要说一下paxos paxos是在多个成员之间对某个值(提议)达成一致的一致性协议。这个值可以是任何东西。比如多个成员之间进行选主,那么这个值就是主的身份。在把multi paxos协议应用在日志同步中时,这个值就是一条日志。网上讲paxos的文章已经很多 阅读全文
posted @ 2016-05-12 21:59 吴镝 阅读(7186) 评论(2) 推荐(2) 编辑
摘要: 这篇文章是对知乎上 "如何自己实现一个关系型数据库" 的一个尝试性回答,后续会不断更新。 对外数据模型为关系型数据库,内部的实现主要分成两大类,一类是disk based,比如mysql,postgres,一类是memory based,后者包括MemSQL,SAP HAHA,OceanBase。这 阅读全文
posted @ 2016-05-06 00:19 吴镝 阅读(4089) 评论(1) 推荐(0) 编辑
摘要: physical clock 机器上的物理时钟,不同的机器在同一个时间点取到的physical clock不一样,之间会存在一定的误差,NTP可以用来控制这个误差,机器之间的时钟误差可以控制在几十ms以内。两个事件a和b,a在机器M1上physical clock为12点5分0秒6ms发生,b在机器 阅读全文
posted @ 2015-11-22 14:51 吴镝 阅读(5471) 评论(0) 推荐(1) 编辑
摘要: HBase对外暴露出来的是一个表格数据模型,如下图所示![](http://pic002.cnblogs.com/images/2012/176446/2012010621260722.jpg)rowkey应用程序可以自己设计。每一个Cell可以保存多个版本的数据,由timestamp标示版本。应用... 阅读全文
posted @ 2015-08-04 21:38 吴镝 阅读(1415) 评论(0) 推荐(0) 编辑
摘要: 今天谈谈分布式事务的时序问题。在说这个问题之前首先说说这为什么是个问题。####单机场景对于数据库来说,读到已经commit的数据是最基本的要求。一般来说,为了性能,读写不互相阻塞,现在的数据库系统(Oracle,MySQL,OceanBase,Spanner,CockRoachDB,HBase)几... 阅读全文
posted @ 2015-03-30 19:24 吴镝 阅读(5483) 评论(0) 推荐(1) 编辑
点击右上角即可分享
微信分享提示