- A+
绪论
etcd作为FushionStage的核心组件,负责FushionStage绝大多数组件的数据持久化、集群选举、状态同步等功能。作为如此重要的一个组件,我们需要深入地理解其架构设计和内部流程,唯有此,我们才能更好地使用etcd。本文试图从整体框架到内部细化流程,对etcd的代码和设计进行解读,希望能对etcd的高可用方案、性能优化、安全加固等指导作用。
etcd简介
etcd是一个分布式key-value 存储,它通过Raft协议进行leader选举和数据备份,对外提供高可用的数据存储,能有效应对网络问题和机器故障带来的数据丢失问题。同时它还可以提供服务发现、分布式锁、分布式数据队列、分布式通知和协调、集群选举等功能。
Raft协议
- 选举: 一个新的集群启动时,或者老的leader故障时,会选举出一个新的leader。
- 日志同步: leader必须接受客户端的日志条目并且将他们同步到集群的所有机器。
- 安全: 保证任何节点只要在它的状态机中生效了一条日志条目,就不会在相同的key上生效另一条日志条目。
一个Raft集群一般包含数个节点,典型的是5个,这样可以承受其中2个节点故障。每个节点实际上就是维护一个状态机,节点在任何时候都处于以下三个状态中的一个。
- leader:负责日志的同步管理,处理来自客户端的请求,与Follower保持这heartBeat的联系
- follower:刚启动时所有节点为Follower状态,响应Leader的日志同步请求,响应Candidate的请求,把请求到Follower的事务转发给Leader
- candidate:负责选举投票,Raft刚启动时由一个节点从Follower转为Candidate发起选举,选举出Leader后从Candidate转为Leader状态
状态机的转移图如下所示:
节点启动以后,首先都是follower状态,在follower状态下,会有一个选举超时时间的计时器(这个时间是在配置的超时时间基础上加一个随机的时间得来的)。如果在这个时间内没有收到leader发送的心跳包,则节点状态会变成candidate状态,也就是变成了候选人,候选人会循环广播选举请求,如果超过半数的节点同意选举请求,则节点转化为leader状态。如果在选举过程中,发现已经有了leader或者有更高的任期值的选举信息,则自动变成follower状态。处于leader状态的节点如果发现有更高任期值的leader存在,则也是自动变成follower状态。
Raft把时间划分为任期(Term)(如下图所示),任期是一个递增的整数,一个任期是从开始选举leader到leader失效的这段时间。有点类似于一届总统任期,只是它的时间是不一定的,也就是说只要leader工作状态良好,它可能成为一个独裁者,一直不下台。
etcd的代码整体架构
etcd整体架构如下图所示:
从大体上可以将其划分为以下4个模块
- http:负责对外提供http访问接口和http client
- raft 状态机:根据接受的raft消息进行状态转移,调用各状态下的动作。
- wal 日志存储:持久化存储日志条目。
- kv数据存储:kv数据的存储引擎,v3支持不同的后端存储,当前采用boltdb。通过boltdb支持事务操作。
相对于v2,v3的主要改动点为:
1. 使用grpc进行peer之间和与客户端之间通信
2. v2的store是在内存中的一棵树,v3采用抽象了一个kvstore,支持不同的后端存储数据库。增强了事务能力。
去除单元测试代码,etcd v2的代码行数约40k,v3的代码行数约70k。
典型内部处理流程
我们将上面架构图的各个部分进行编号,以便下文的处理流程介绍中,对应找到每个流程处理的组件位置。
1. 消息入口
一个etcd节点运行以后,有3个通道接收外界消息,以kv数据的增删改查请求处理为例,介绍这3个通道的工作机制。
1. client的http调用:会通过注册到http模块的keysHandler的ServeHTTP方法处理。解析好的消息调用EtcdServer的Do()方法处理。(图中2)
2. client的grpc调用:启动时会向grpc server注册quotaKVServer对象,quotaKVServer是以组合的方式增强了kvServer这个数据结构。grpc消息解析完以后会调用kvServer的Range、Put、DeleteRange、Txn、Compact等方法。kvServer中包含有一个RaftKV的接口,由EtcdServer这个结构实现。所以最后就是调用到EtcdServer的Range、Put、DeleteRange、Txn、Compact等方法。(图中1)
3. 节点之间的grpc消息:每个EtcdServer中包含有Transport结构,Transport中会有一个peers的map,每个peer封装了节点到其他某个节点的通信方式。包括streamReader、streamWriter等,用于消息的发送和接收。streamReader中有recvc和propc队列,streamReader处理完接收到的消息会将消息推到这连个队列中。由peer去处理,peer调用raftNode的Process方法处理消息。(图中3、4)
2. EtcdServer消息处理
对于客户端消息,调用到EtcdServer处理时,一般都是先注册一个等待队列,调用node的Propose方法,然后用等待队列阻塞等待消息处理完成。Propose方法会往propc队列中推送一条MsgProp消息。
对于节点间的消息,raftNode的Process是直接调用node的step方法,将消息推送到node的recvc或者propc队列中。
可以看到,外界所有消息这时候都到了node结构中的recvc队列或者propc队列中。(图中5)
3. node处理消息
4. raftNode的处理
5. EtcdServer的apply处理
EtcdServer会处理这个applyc队列,会将snapshot和entries都apply到kv存储中去(图中8)。最后调用applyWait的Trigger,唤醒客户端请求的等待线程,返回客户端的请求。
重要的数据结构
- EtcdServer: 是整个etcd节点的功能的入口,包含etcd节点运行过程中需要的大部分成员。
typeEtcdServerstruct{ //当前正在发送的snapshot数量 inflightSnapshotsint64 //已经apply到状态机的日志index appliedIndexuint64 //已经提交的日志index,也就是leader确认多数成员已经同步了的日志index committedIndexuint64 //已经持久化到kvstore的index consistIndexconsistentIndex //配置项 Cfg*ServerConfig //启动成功并注册了自己到cluster,关闭这个通道。 readychchanstruct{} //重要的数据结果,存储了raft的状态机信息。 rraftNode //满多少条日志需要进行snapshot snapCountuint64 //为了同步调用情况下让调用者阻塞等待调用结果的。 wwait.Wait //下面3个结果都是为了实现linearizable读使用的 readMusync.RWMutex readwaitcchanstruct{} readNotifier*notifier //停止通道 stopchanstruct{} //停止时关闭这个通道 stoppingchanstruct{} //etcd的start函数中的循环退出,会关闭这个通道 donechanstruct{} //错误通道,用以传入不可恢复的错误,关闭raft状态机。 errorcchanerror //etcd实例id idtypes.ID //etcd实例属性 attributesmembership.Attributes //集群信息 cluster*membership.RaftCluster //v2的kv存储 storestore.Store //用以snapshot snapshotter*snap.Snapshotter //v2的applier,用于将commitedindexapply到raft状态机 applyV2ApplierV2 //v3的applier,用于将commitedindexapply到raft状态机 applyV3applierV3 //剥去了鉴权和配额功能的applyV3 applyV3BaseapplierV3 //apply的等待队列,等待某个index的日志apply完成 applyWaitwait.WaitTime //v3用的kv存储 kvmvcc.ConsistentWatchableKV //v3用,作用是实现过期时间 lessorlease.Lessor //守护后端存储的锁,改变后端存储和获取后端存储是使用 bemusync.Mutex //后端存储 bebackend.Backend //存储鉴权数据 authStoreauth.AuthStore //存储告警数据 alarmStore*alarm.AlarmStore //当前节点状态 stats*stats.ServerStats //leader状态 lstats*stats.LeaderStats //v2用,实现ttl数据过期的 SyncTicker*time.Ticker //压缩数据的周期任务 compactor*compactor.Periodic //用于发送远程请求 peerRthttp.RoundTripper //用于生成请求id reqIDGen*idutil.Generator //forceVersionCisusedtoforcetheversionmonitorloop //todetecttheclusterversionimmediately. forceVersionCchanstruct{} //wgMublocksconcurrentwaitgroupmutationwhileserverstopping wgMusync.RWMutex //wgisusedtowaitforthegoroutinesthatdependsontheserverstate //toexitwhenstoppingtheserver. wgsync.WaitGroup //ctxisusedforetcd-initiatedrequeststhatmayneedtobecanceled //onetcdservershutdown. ctxcontext.Context cancelcontext.CancelFunc leadTimeMusync.RWMutex leadElectedTimetime.Time }
2. raftNode:raft状态机,维护raft状态机的步进和状态迁移。
typeraftNodestruct{ //Cacheofthelatestraftindexandrafttermtheserverhasseen. //Thesethreeunit64fieldsmustbethefirstelementstokeep64-bit //alignmentforatomicaccesstothefields. //状态机当前状态,index代表当前已经apply到状态机的日志index,term是最新日志条目的term,lead是当前的leaderid indexuint64 termuint64 leaduint64 //包含了node、storage等重要数据结构 raftNodeConfig //achantosend/receivesnapshot msgSnapCchanraftpb.Message //achantosendoutapply applycchanapply //achantosendoutreadState readStateCchanraft.ReadState //utility ticker*time.Ticker //contentiondetectorsforraftheartbeatmessage td*contention.TimeoutDetector stoppedchanstruct{} donechanstruct{} }
3. node:包含在raftNode中,是Node接口的实现。里面包含一个协程和多个队列,是状态机消息处理的入口。
typenodestruct{ //Propose队列,调用raftNode的Propose即把Propose消息塞到这个队列里 propcchanpb.Message //Message队列,除Propose消息以外其他消息塞到这个队列里 recvcchanpb.Message //集群配置信息队列,当集群节点改变时,需要将修改信息塞到这个队列里 confcchanpb.ConfChange //外部通过这个队列获取修改后集群配置信息 confstatecchanpb.ConfState //已经准备好apply的信息队列 readycchanReady //每次apply好了以后往这个队列里塞个空对象。通知可以继续准备Ready消息。 advancecchanstruct{} //tick信息队列,用于调用心跳 tickcchanstruct{} donechanstruct{} stopchanstruct{} statuschanchanStatus loggerLogger }
- 安卓客户端下载
- 微信扫一扫
- 微信公众号
- 微信公众号扫一扫