【raft】学习八:ReadIndex

13 篇文章 0 订阅
8 篇文章 2 订阅

前言

在正式介绍本文的内容分之前,我们先引用一下大佬的介绍:Raft算法的目标之一是实现 线性一致性(Linearizability) 的语义。线性一致性的实现不仅与Raft算法本身有关,还与整个系统的实现(即状态机)有关。即便Raft算法本身保证了其日志的故障容错有序共识,但是在通过Raft算法实现系统时,仍会存在系统整体线性一致性语义等问题。因此《CONSENSUS: BRIDGING THEORY AND PRACTICE》的“Chapter 6 Client interaction”,专门介绍了实现系统时客户端与系统交互的相关问题。需要实现基于Raft算法的读者应详细阅读该章节中介绍的问题与解决方案。
在还没想好怎么介绍readIndex,先看看一致性吧~

共识和一致性

一致性在程序员的世界里太常见了,最平常的是读写一致,即我写入的数据,在没有被 ‘我’ 改变的情况下,我读到的数据应该和写入的数据保持一致,这种情况时常出现在并发场景下的数据读写问题中。不过这里的一致主要是不同时间下对同一个主体的数据一致。然而在分布式场景下,一致性是一个系统需要达成的目标。因为分布式系统引入了多个节点,节点规模越大,宕机、网络时延、网络分区就会成为常态,任何一个问题都可能导致节点之间的数据不一致,因此Paxos 和 Raft 准确来讲是用来解决一致性问题的共识算法,用于分布式场景,而非”缓存一致性“这种单机场景,所以很多文章也就简称”Paxos是分布式系统中的一致性算法“。

而共识一般是指即多个提议者达成共识的过程,例如Paxos,Raft 就是共识算法,paxos 是一种共识理论,分布式系统是他的场景,一致性是他的目标。即表明使用 Raft或者 paxos 的系统并不代表该系统都是线性一致的(Linearizability 即强一致)。类似于paxos、raft等共识算法只能提供基础,如果要实现线性一致还需要在算法之上做出更多的努力。

本文主要是学习raft的如何读取到一致的数据, 首先看看类似提议方式 LogRead 进行一个读请求,因为raft是日志有序的,因此读取的数据一定是一致的。那为什么没有采用这种方式呢?

优点:实现简单,只需要依赖raft已有的提报机制;
缺点:延迟高、吞吐量低,不仅需要一轮共识所需的开销,又有将这条Raft日志落盘的开销。
那为了优化只读请求的性能,就要想办法绕过Raft算法完整的日志机制。raft的特点是所有的读写请求都会走到leader,因而只要leader是合法的,那么一定是能保证只读操作的线性一致性的。不合法情况我们在前面已经他按逃过,这里就不再详细介绍了。为解决这一问题,《CONSENSUS: BRIDGING THEORY AND PRACTICE》给出了两个方案:Read Index和Lease Read,其中readIndex便是本文着重要介绍机制。etcd/raft对于这两种方式都做了相应的实现,可以通过配置readOnly.option设置。

ReadIndex

由于只读请求并没有需要写入的数据,因此并不需要将其写入Raft日志,而只需要关注收到请求时leader的commit index。只要在该commit index被应用到状态机后执行读操作,就能保证其线性一致性。因此使用了ReadIndex的leader在收到只读请求时,会按如下方式处理:

  1. 记录当前的commit index,作为read index;
  2. 向集群中的所有节点广播一次心跳,如果收到了数量达到quorum的心跳响应,leader可以得知当收到该只读请求时,其一定是集群的合法leader;
  3. 继续执行,直到leader本地的apply index大于等于之前记录的read index。此时可以保证只读操作的线性一致性;
  4. 让状态机执行只读操作,并将结果返回给客户端。

了解了leader的处理方式,接下来以一个只读请求的视角,看看这个请求会经历哪些流程,当然这个实现还是基于etcd/raft。

一个只读请求的一生

  1. 默认所有的只读请求都会调用Node下的ReadIndex方法;
// ReadIndex request a read state. The read state will be set in the ready.
	// Read state has a read index. Once the application advances further than the read
	// index, any linearizable read requests issued before the read request can be
	// processed safely. The read state will have the same rctx attached.
	// Note that request can be lost without notice, therefore it is user's job
	// to ensure read index retries.
	ReadIndex(ctx context.Context, rctx []byte) error

在这个方法中,传入了一个唯一的ID,以byte数组的形式传入以对应每一个请求。
2. 将这个请求以消息的形式传入raft

func (n *node) ReadIndex(ctx context.Context, rctx []byte) error {
	return n.step(ctx, pb.Message{Type: pb.MsgReadIndex, Entries: []pb.Entry{{Data: rctx}}})
}

与正常消息提报一样,依据不同的节点状态进入不同的处理函数,如果当前节点是follower:

func stepFollower(r *raft, m pb.Message) error {
switch m.Type {
	...
	case pb.MsgReadIndex:
		if r.lead == None {
			r.logger.Infof("%x no leader at term %d; dropping index reading msg", r.id, r.Term)
			return nil
		}
		m.To = r.lead
		// 转发给主节点
		r.send(m)
	case pb.MsgReadIndexResp:
		if len(m.Entries) != 1 {
			r.logger.Errorf("%x invalid format of MsgReadIndexResp from %x, entries count: %d", r.id, m.From, len(m.Entries))
			return nil
		}
		r.readStates = append(r.readStates, ReadState{Index: m.Index, RequestCtx: m.Entries[0].Data})
	}
}

如上所述,如果是follower,就将这个消息转发给主节点。如果是leader,则进入stepLeader

func stepLeader(r *raft, m pb.Message) error {
	// These message types do not require any progress for m.From.
	switch m.Type {
	...
		case pb.MsgReadIndex:
			// 如果是单节点集群则直接返回,报告当前commitIndex
			if r.prs.IsSingleton() {
				if resp := r.responseToReadIndexReq(m, r.raftLog.committed); resp.To != None {
					r.send(resp)
				}
				return nil
			}
	
			// 如果当前主节点还没有提报过日志,即当前主节点的CommitIndex不是最新的,需要等提报一次在进再响应读请求
			if !r.committedEntryInCurrentTerm() {
				r.pendingReadIndexMessages = append(r.pendingReadIndexMessages, m)
				return nil
			}
			// 处理具体读请求
			sendMsgReadIndexResponse(r, m)
	
			return nil
		}
	}
	
func sendMsgReadIndexResponse(r *raft, m pb.Message) {
	// thinking: use an internally defined context instead of the user given context.
	// We can express this in terms of the term and index instead of a user-supplied value.
	// This would allow multiple reads to piggyback on the same message.
	switch r.readOnly.option {
	// If more than the local vote is needed, go through a full broadcast.
	case ReadOnlySafe:
	// 将这个请求加入队列,并保存详细到map中,主要是关联读请求和Index的对应关系
		r.readOnly.addRequest(r.raftLog.committed, m)
		// The local node automatically acks the request.
		r.readOnly.recvAck(r.id, m.Entries[0].Data)
		// 心跳广播,这里继续传入唯一的ID
		r.bcastHeartbeatWithCtx(m.Entries[0].Data)
	case ReadOnlyLeaseBased:
		if resp := r.responseToReadIndexReq(m, r.raftLog.committed); resp.To != None {
			r.send(resp)
		}
	}
}

因此针对Leader的处理有三种情况:
1. 单节点集群
2. 主节点还未提交
3. 正常

接下来, 对于正常情况,会考虑readOnly的option设置,在readIndex中主要做三件事:保存请求(加入队列)、响应以及广播心跳。

// addRequest adds a read only request into readonly struct.
// `index` is the commit index of the raft state machine when it received
// the read only request.
// `m` is the original read only request message from the local or remote node.
func (ro *readOnly) addRequest(index uint64, m pb.Message) {
	s := string(m.Entries[0].Data)
	if _, ok := ro.pendingReadIndex[s]; ok {
		return
	}
	ro.pendingReadIndex[s] = &readIndexStatus{index: index, req: m, acks: make(map[uint64]bool)}
	ro.readIndexQueue = append(ro.readIndexQueue, s)
}

// recvAck notifies the readonly struct that the raft state machine received
// an acknowledgment of the heartbeat that attached with the read only request
// context.
func (ro *readOnly) recvAck(id uint64, context []byte) map[uint64]bool {
	rs, ok := ro.pendingReadIndex[string(context)]
	if !ok {
		return nil
	}

	rs.acks[id] = true
	return rs.acks
}

心跳消息会包含leader的commit index以及这个请求的ID(rctx),follower收到心跳消息后,首先将commit Index与主节点同步,然后回复心跳消息,也包含这个ID的数据,方便主节点在处理心跳响应时,能够顺便处理readIndex请求。

func (r *raft) handleHeartbeat(m pb.Message) {
	r.raftLog.commitTo(m.Commit)
	r.send(pb.Message{To: m.From, Type: pb.MsgHeartbeatResp, Context: m.Context})
}
// 主节点的响应
func stepLeader(r *raft, m pb.Message) error {
	// These message types do not require any progress for m.From.
	switch m.Type {
		case pb.MsgHeartbeatResp:
				...
				// 如果不是ReadOnlySafe 或者ID内容为空,就不用处理readIndex请求,直接返回
				if r.readOnly.option != ReadOnlySafe || len(m.Context) == 0 {
					return nil
				}
				// 如果响应超过大多数,则表明当前的主节点是合法的,且可以返回在这个ID之前的所有读请求,因为所有的读请求都是用队列有序的保存
				if r.prs.Voters.VoteResult(r.readOnly.recvAck(m.From, m.Context)) != quorum.VoteWon {
					return nil
				}
			// 处理之前保存的读请求,拿到当前ID的之前的所有请求消息,并移除队列和Map中的数据;
				rss := r.readOnly.advance(m)
				for _, rs := range rss {
				// 将这个读请求返回给应用层,表明当前readIndex已经同步,可以读取数据
					if resp := r.responseToReadIndexReq(rs.req, rs.index); resp.To != None {
						r.send(resp)
					}
				}
			...
		}
	}

应用层需要做几件事:
1. 将请求传入raft
2. 监听raft的返回,如果请求ID返回,等待当前节点应用到Index
3. 告知具体的服务层,可以读取数据了
在这里插入图片描述

Lease Read

本来之前没有关注Lease Read,主要是项目中没有使用。借这个机会一起来看看。前面说道,readIndex需要主节点向其他从节点发送心跳消息,主要是查看当前主节点是否合法。在【raft】学习六中了解了raft选举优化的几种措施,其中谈到Check Quorum 和 Lease leader,保在一定时间内不发生选举即不产生新的leader,也就保证主节点在一定时间内是合法的,这样就可以省去了广播心跳的开销,其commit index可以直接作为read index。
但是只要有时间间隔,便会存在失效的时间片段,即有一个时间片段可能会存在多个leader,导致读取数据不一致/读到旧的数据,这个就可以根据项目具体情况来取舍了。

注意: 这里的主节点合法和选取时主节点有效,针对的象是不一样的,有一定的区别。在选举时,如果设置了lease Leader,则当follower节点收到选取请求,如果主节点还在合法的时间内,那么这个选举请求将会被拒绝;但lease read 是针对主节点自己来看,如果设置了这个时间区间,主节点认为自己在这个时间范围内,自己是合法的,超过这个时间内会进行一次check quorum。所以这两个还是存在一定区别,学习raft的朋友注意区分。

总结

本文主要了解了一下一致性的一些基本知识,以及跟随etcd/raft的代码逻辑,探讨了一个只读请求需要经历哪些操作,主要探讨了两种方式:readIndex和lease read。

参考

  1. https://mrcroxx.github.io/posts/code-reading/etcdraft-made-simple/6-readonly/#13-lease-read
  2. https://segmentfault.com/a/1190000022248118
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值