Linearizable Read通俗来讲,就是读请求需要读到最新的已经commit的数据,不会读到老数据。

对于使用raft协议来保证多副本强一致的系统中,读写请求都可以通过走一次raft协议来满足。然后,现实系统中,读请求通常会占很大比重,如果每次读请求都要走一次raft落盘,性能可想而知。所以优化读性能至关重要。

从raft协议可知,leader拥有最新的状态,如果读请求都走leader,那么leader可以直接返回结果给客户端。然而,在出现网络分区和时钟快慢相差比较大的情况下,这有可能会返回老的数据,即stale read,这违反了Linearizable Read。例如,leader和其他followers之间出现网络分区,其他followers已经选出了新的leader,并且新的leader已经commit了一堆数据,然而由于不同机器的时钟走的快慢不一,原来的leader可能并没有发觉自己的lease过期,仍然认为自己还是合法的leader直接给客户端返回结果,从而导致了stale read。

Raft作者提出了一种叫做ReadIndex的方案:

当leader接收到读请求时,将当前commit index记录下来,记作read index,在返回结果给客户端之前,leader需要先确定自己到底还是不是真的leader,确定的方法就是给其他所有peers发送一次心跳,如果收到了多数派的响应,说明至少这个读请求到达这个节点时,这个节点仍然是leader,这时只需要等到commit index被apply到状态机后,即可返回结果。

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

处理读请求时,应用的goroutine会调用这个函数,其中rctx参数相当于读请求id,全局保证唯一。step会往recvc中塞进一个MsgReadIndex消息,而运行node入口函数

func (n *node) run(r *raft)

的goroutine会从recvc中拿出这个message,并进行处理:

case m := <-n.recvc:
// filter out response message from unknown From.
if _, ok := r.prs[m.From]; ok || !IsResponseMsg(m.Type) {
r.Step(m) // raft never returns an error
}

Step(m)最终会调用到raft结构体的step(m),step是个函数指针,根据node的角色,运行stepLeader()/stepFollower()/stepCandidate()。

  • 如果node是leader,stepLeader()主要代码片段:
	case pb.MsgReadIndex:
if r.raftLog.zeroTermOnErrCompacted(r.raftLog.term(r.raftLog.committed)) != r.Term {
// Reject read only request when this leader has not committed any log entry at its term.
return
} if r.quorum() > 1 {
switch r.readOnly.option {
case ReadOnlySafe:
r.readOnly.addRequest(r.raftLog.committed, m)
r.bcastHeartbeatWithCtx(m.Entries[0].Data)
case ReadOnlyLeaseBased:
var ri uint64
if r.checkQuorum {
ri = r.raftLog.committed
}
if m.From == None || m.From == r.id { // from local member
r.readStates = append(r.readStates, ReadState{Index: r.raftLog.committed, RequestCtx: m.Entries[0].Data})
} else {
r.send(pb.Message{To: m.From, Type: pb.MsgReadIndexResp, Index: ri, Entries: m.Entries})
}
}
}

首先,r.raftLog.zeroTermOnErrCompacted需要检查leader是否在当前term有过commit entry,小论文5.4节关于Safety中给出了解释,以及不这么做会有什么问题,并且给出了反例。

其次,本文讨论的ReadIndex方案对应的是ReadOnlySafe这个option分支,其中addRequest(...)会把这个读请求到达时的commit index保存起来,并且维护一些状态信息,而bcastHeartbeatWithCtx(...)准备好需要发送给peers的心跳消息MsgHeartbeat。当node收到心跳响应消息MsgHeartbeatResp时处理如下:

只保留逻辑相关代码:

case pb.MsgHeartbeatResp:

		if r.readOnly.option != ReadOnlySafe || len(m.Context) == 0 {
return
} ackCount := r.readOnly.recvAck(m)
if ackCount < r.quorum() {
return
} rss := r.readOnly.advance(m)
for _, rs := range rss {
req := rs.req
if req.From == None || req.From == r.id { // from local member
r.readStates = append(r.readStates, ReadState{Index: rs.index, RequestCtx: req.Entries[0].Data})
} else {
r.send(pb.Message{To: req.From, Type: pb.MsgReadIndexResp, Index: rs.index, Entries: req.Entries})
}
}

首先只有ReadOnlySafe这个方案时,才会继续往下走。如果接收到了多数派的心跳响应,则会从刚才保存的信息中将对应读请求当时的commit index和请求id拿出来,填充到ReadState中,ReadState结构如下:

type ReadState struct {
Index uint64
RequestCtx []byte
}

可以看出ReadState实际上包含了一个读请求到达node时,当前raft的状态commit index和请求id。

然后将ReadState append到raft结构体中的readStates数组中,readStates数组会被包含在Ready结构体中从readyc中pop出来供应用使用。

看看etcdserver是怎么使用的:

首先,在消费Ready的goroutine中:

if len(rd.ReadStates) != 0 {
select {
case r.readStateC <- rd.ReadStates[len(rd.ReadStates)-1]:
case <-time.After(internalTimeout):
plog.Warningf("timed out sending read state")
case <-r.stopped:
return
}
}

这里重点是把Ready中的ReadState放入readStateC中,readStateC是一个buffer大小为1的channel

然后,在etcdserver跑linearizableReadLoop()的另外一个goroutine中:

// 执行ReadIndex,ctx是request id
if err := s.r.ReadIndex(cctx, ctx); err != nil {
cancel()
if err == raft.ErrStopped {
return
}
plog.Errorf("failed to get read index from raft: %v", err)
nr.notify(err)
continue
} //等待request id对应的ReadState从readStateC中pop出来
for !timeout && !done {
select {
case rs = <-s.r.readStateC:
done = bytes.Equal(rs.RequestCtx, ctx)
if !done {
// a previous request might time out. now we should ignore the response of it and
// continue waiting for the response of the current requests.
plog.Warningf("ignored out-of-date read index response (want %v, got %v)", rs.RequestCtx, ctx)
}
case <-time.After(s.Cfg.ReqTimeout()):
plog.Warningf("timed out waiting for read index response")
nr.notify(ErrTimeout)
timeout = true
case <-s.stopping:
return
}
} if !done {
continue
} // 等待当前apply index大于等于commit index
if ai := s.getAppliedIndex(); ai < rs.Index {
select {
case <-s.applyWait.Wait(rs.Index):
case <-s.stopping:
return
}
}

至此,ReadIndex流程结束,总结一下,就四步:

  1. leader check自己是否在当前term commit过entry
  2. leader记录下当前commit index,然后leader给所有peers发心跳广播
  3. 收到多数派响应代表读请求到达时还是leader,然后等待apply index大于等于commit index
  4. 返回结果

etcd不仅实现了leader上的read only query,同时也实现了follower上的read only query,原理是一样的,只不过读请求到达follower时,commit index是需要向leader去要的,leader返回commit index给follower之前,同样,需要走上面的ReadIndex流程,因为leader同样需要check自己到底还是不是leader,代码不赘述。

raft如何实现Linearizable Read的更多相关文章

  1. etcd raft如何实现Linearizable Read

    Linearizable Read通俗来讲,就是读请求需要读到最新的已经commit的数据,不会读到老数据. 对于使用raft协议来保证多副本强一致的系统中,读写请求都可以通过走一次raft协议来满足 ...

  2. 《In Search of an Understandable Consensus Algorithm》翻译

    Abstract Raft是一种用于管理replicated log的consensus algorithm.它能和Paxos产生同样的结果,有着和Paxos同样的性能,但是结构却不同于Paxos:它 ...

  3. 分布式系统理论进阶 - Raft、Zab

    引言 <分布式系统理论进阶 - Paxos>介绍了一致性协议Paxos,今天我们来学习另外两个常见的一致性协议——Raft和Zab.通过与Paxos对比,了解Raft和Zab的核心思想.加 ...

  4. Raft

    http://thesecretlivesofdata.com/raft/ https://github.com/coreos/etcd   1 Introduction Consensus algo ...

  5. Raft、Zab

    Raft.Zab 引言 <分布式系统理论进阶 - Paxos>介绍了一致性协议Paxos,今天我们来学习另外两个常见的一致性协议--Raft和Zab.通过与Paxos对比,了解Raft和Z ...

  6. etcd raft library设计原理和使用

    早在2013年11月份,在raft论文还只能在网上下载到草稿版时,我曾经写过一篇blog对其进行简要分析.4年过去了,各种raft协议的讲解铺天盖地,raft也确实得到了广泛的应用.其中最知名的应用莫 ...

  7. 基于hashicorp/raft的分布式一致性实战教学

    本文由云+社区发表 作者:Super 导语:hashicorp/raft是raft算法的一种比较流行的golang实现,基于它能够比较方便的构建具有强一致性的分布式系统.本文通过实现一个简单的分布式缓 ...

  8. TiKV 源码解析系列——如何使用 Raft

    本系列文章主要面向 TiKV 社区开发者,重点介绍 TiKV 的系统架构,源码结构,流程解析.目的是使得开发者阅读之后,能对 TiKV 项目有一个初步了解,更好的参与进入 TiKV 的开发中. 需要注 ...

  9. Raft 一致性算法论文译文

    本篇博客为著名的 RAFT 一致性算法论文的中文翻译,论文名为<In search of an Understandable Consensus Algorithm (Extended Vers ...

随机推荐

  1. Sqoop简介及安装

    Hadoop业务的大致开发流程以及Sqoop在业务中的地位: Sqoop概念 Sqoop可以理解为[SQL–to–Hadoop],正如名字所示,Sqoop是一个用来将关系型数据库和Hadoop中的数据 ...

  2. R + ggplot2 Graph Catalog(转)

    Joanna Zhao’s and Jenny Bryan’s R graph catalog is meant to be a complement to the physical book,Cre ...

  3. Seafile安装踩坑

    欢迎大家访问我的博客新地址: 博客:http://monsterlin.com/ 原文地址:http://monsterlin.com/archives/HowToInstallSeafile.htm ...

  4. JSON数据解析:Gson(谷歌)和fastjson(阿里巴巴)的异同点

    Gson和fastjson分别为谷歌和阿里巴巴对JSON数据进行处理封装的jar包 Gson(谷歌)和fastjson(阿里巴巴)两者异同点: 相同点:都是根据JSON数据创建相应的类 不同点: 1. ...

  5. linux升级openssh7.4sp1

    1.准备相关的包 openssh下载地址:http://mirror.internode.on.net/pub/OpenBSD/OpenSSH/portable/ openssl相关包下载:http: ...

  6. textarea placeholder文字换行

    要实现这样的效果 第一反应是直接在placeholder属性值里输入\n换行,如: <textarea rows="5" cols="50" placeh ...

  7. 高性能迷你React框架anu在低版本IE的实践

    理想是丰满的,现实是骨感的,react早期的版本虽然号称支持IE8,但是页面总会不自觉切换到奇异模式下,导致报错.因此必须让react连IE6,7都支持,这才是最安全.但React本身并不支持IE6, ...

  8. [0] (VDP)垂直开发模式

    垂直管理 网格化管理 属地管理 横向管理 面向接口编程 面向对象编程(OOP) 设计模式(GOF.MVC.SOA) 依赖注入(DI/IoC) 面向方面编程(AOP) 领域驱动开发(DDD) 测试驱动开 ...

  9. OFFICE 文档转换为html在线预览

    OFFICE 文档在线预览方案很多: 服务器先转换为PDF,再转换为SWF,最后通过网页加载Flash预览,比如flexpaper Office文档直接转换为SWF,通过网页加载Flash预览 微软的 ...

  10. fpm 制作 rpm 包

    支持的 源类型包 ① dir : 将目录打包成所需要的类型, 可用于源码编译安装软件包 ② rpm : 对 rpm 包进行转换 ③ gem : 对 rubygem 包进行转换 ④ python : 将 ...