QCon全球软件开发大会8折优惠倒计时,购票立减¥1760!了解详情 >>> 了解详情
写点什么

深入浅出的 etcd 系列在这里!

2020 年 4 月 02 日

深入浅出的etcd系列在这里!

etcd 作为 FushionStage 的核心组件,负责 FushionStage 绝大多数组件的数据持久化、集群选举、状态同步等功能。作为如此重要的一个组件,我们需要深入地理解其架构设计和内部流程,唯有此,我们才能更好地使用 etcd。本文试图从整体框架到内部细化流程,对 etcd 的代码和设计进行解读,希望能对 etcd 的高可用方案、性能优化、安全加固等指导作用。


etcd 简介


etcd 是一个分布式 key-value 存储,它通过 Raft 协议进行 leader 选举和数据备份,对外提供高可用的数据存储,能有效应对网络问题和机器故障带来的数据丢失问题。同时它还可以提供服务发现、分布式锁、分布式数据队列、分布式通知和协调、集群选举等功能。


Raft 协议

要理解 etcd 分布式协同的工作原理,必须提到 Raft 算法。Raft 算法是斯坦福的 Diego Ongaro、John Ousterhout 两人以易懂(Understandability)为目标设计的一致性共识算法。在此之前,提到共识算法(Consensus Algorithm)必然会提到 Paxos,但是 Paxos 的实现和理解起来都非常复杂,以至于在 Raft 算法提出者的博士论文中,作者提到,他们用了将近一年时间研究这个算法的各种解释,但还是没有完全理解这个算法。Paxos 的算法原理和真正实现也有很大的距离,实现 Paxos 的系统,如 Chubby,对 Paxos 进行了很多的改进有优化,但是细节却是不为人所知的。


Raft 协议采用分治的思想,把分布式协同的问题分为 3 个问题:


  • 选举: 一个新的集群启动时,或者老的 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。


典型内部处理流程:


我们将上面架构图的各个部分进行编号,以便下文的处理流程介绍中,对应找到每个流程处理的组件位置。



消息入口

一个 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)


EtcdServer 消息处理

对于客户端消息,调用到 EtcdServer 处理时,一般都是先注册一个等待队列,调用 node 的 Propose 方法,然后用等待队列阻塞等待消息处理完成。Propose 方法会往 propc 队列中推送一条 MsgProp 消息。


对于节点间的消息,raftNode 的 Process 是直接调用 node 的 step 方法,将消息推送到 node 的 recvc 或者 propc 队列中。


可以看到,外界所有消息这时候都到了 node 结构中的 recvc 队列或者 propc 队列中。


node 处理消息

node 启动时会启动一个协程,处理 node 的各个队列中的消息,当然也包括 recvc 和 propc 队列。从 propc 和 recvc 队列中拿到消息,会调用 raft 对象的 Step 方法,raft 对象封装了 raft 的协议数据和操作,其对外的 Step 方法是真正 raft 协议状态机的步进方法。当接收到消息以后,根据协议类型、Term 字段做相应的状态改变处理,或者对选举请求做相应处理。对于一般的 kv 增删改查数据请求消息,会调用内部的 step 方法。内部的 step 方法是一个可动态改变的方法,将随状态机的状态变化而变化。当状态机处于 leader 状态时,该方法就是 stepLeader;当状态机处于 follower 状态时,该方法就是 stepFollower;当状态机处于 Candidate 状态时,该方法就是 stepCandidate。leader 状态会直接处理 MsgProp 消息。将消息中的日志条目存入本地缓存。follower 则会直接将 MsgProp 消息转发给 leader,转发的过程是将先将消息推送到 raft 的 msgs 数组中。


node 处理完消息以后,要么生成了缓存中的日志条目,要么生成了将要发送出去的消息。缓存中的日志条目需要进一步处理(比如同步和持久化),而消息需要进一步处理发送出去。处理过程还是在 node 的这个协程中,在循环开始会调用 newReady,将需要进一步处理的日志和需要发送出去的消息,以及状态改变信息,都封装在一个 Ready 消息中。Ready 消息会推行到 readyc 队列中。(图中 5)


raftNode 的处理

raftNode 的 start()方法另外启动了一个协程,处理 readyc 队列(图中 6)。取出需要发送的 message,调用 transport 的 Send 方法并将其发送出去(图中 4)。调用 storage 的 Save 方法持久化存储日志条目或者快照(图中 9、10),更新 kv 缓存。


另外需要将已经同步好的日志应用到状态机中,让状态机更新状态和 kv 存储,通知等待请求完成的客户端。因此需要将已经确定同步好的日志、快照等信息封装在一个 apply 消息中推送到 applyc 队列。


EtcdServer 的 apply 处理

EtcdServer 会处理这个 applyc 队列,会将 snapshot 和 entries 都 apply 到 kv 存储中去(图中 8)。最后调用 applyWait 的 Trigger,唤醒客户端请求的等待线程,返回客户端的请求。


重要的数据结构


1. EtcdServer: 是整个 etcd 节点的功能的入口,包含 etcd 节点运行过程中需要的大部分成员。


typeEtcdServerstruct{//当前正在发送的snapshot数量inflightSnapshotsint64//已经apply到状态机的日志indexappliedIndexuint64//已经提交的日志index,也就是leader确认多数成员已经同步了的日志indexcommittedIndexuint64//已经持久化到kvstore的indexconsistIndexconsistentIndex//配置项Cfg*ServerConfig//启动成功并注册了自己到cluster,关闭这个通道。readychchanstruct{}//重要的数据结果,存储了raft的状态机信息。rraftNode//满多少条日志需要进行snapshotsnapCountuint64//为了同步调用情况下让调用者阻塞等待调用结果的。wwait.Wait//下面3个结果都是为了实现linearizable读使用的readMusync.RWMutexreadwaitcchanstruct{}readNotifier*notifier//停止通道stopchanstruct{}//停止时关闭这个通道stoppingchanstruct{}//etcd的start函数中的循环退出,会关闭这个通道donechanstruct{}//错误通道,用以传入不可恢复的错误,关闭raft状态机。errorcchanerror//etcd实例ididtypes.ID//etcd实例属性attributesmembership.Attributes//集群信息cluster*membership.RaftCluster//v2的kv存储storestore.Store//用以snapshotsnapshotter*snap.Snapshotter//v2的applier,用于将commitedindexapply到raft状态机applyV2ApplierV2//v3的applier,用于将commitedindexapply到raft状态机applyV3applierV3//剥去了鉴权和配额功能的applyV3applyV3BaseapplierV3//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//用于生成请求idreqIDGen*idutil.Generator//forceVersionCisusedtoforcetheversionmonitorloop//todetecttheclusterversionimmediately.forceVersionCchanstruct{}//wgMublocksconcurrentwaitgroupmutationwhileserverstoppingwgMusync.RWMutex//wgisusedtowaitforthegoroutinesthatdependsontheserverstate//toexitwhenstoppingtheserver.wgsync.WaitGroup//ctxisusedforetcd-initiatedrequeststhatmayneedtobecanceled//onetcdservershutdown.ctxcontext.Contextcancelcontext.CancelFuncleadTimeMusync.RWMutexleadElectedTimetime.Time}
复制代码


2. raftNode:raft 状态机,维护 raft 状态机的步进和状态迁移。


typeraftNodestruct{//Cacheofthelatestraftindexandrafttermtheserverhasseen.//Thesethreeunit64fieldsmustbethefirstelementstokeep64-bit//alignmentforatomicaccesstothefields.//状态机当前状态,index代表当前已经apply到状态机的日志index,term是最新日志条目的term,lead是当前的leaderidindexuint64termuint64leaduint64//包含了node、storage等重要数据结构raftNodeConfig//achantosend/receivesnapshotmsgSnapCchanraftpb.Message//achantosendoutapplyapplycchanapply//achantosendoutreadStatereadStateCchanraft.ReadState//utilityticker*time.Ticker//contentiondetectorsforraftheartbeatmessagetd*contention.TimeoutDetectorstoppedchanstruct{}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{}statuschanchanStatusloggerLogger}
复制代码


本文简要介绍了 raft 协议和 etcd 的框架,介绍了 etcd 内部的和消息流的处理。


后续将分心跳和选举、数据同步、数据持久化等不同课题详细讲述 etcd 的内部处理流程。


本文转载自 华为云产品与解决方案 公众号。


原文链接:https://mp.weixin.qq.com/s/7bbKGHm4wYJ1dDl0qscwPQ


2020 年 4 月 02 日 14:39871

评论

发布
暂无评论
发现更多内容

计算机操作系统基础(十二)---线程同步之自旋锁

书旅

php laravel 线程 操作系统 进程

架构师训练营第五周总结

一剑

PHP实现一致性Hash算法

Arthur.Li

php 极客大学架构师训练营 一致性hash

iOS sonar实践

余志斐

ios sonar

架构师训练营第五章作业

饶军

Week3:作业二

车小勺的男神

Cypress与TestCafe WebUI端到端测试框架简介

软测小生

自动化测试 Cypress TestCafe Web UI 测试框架

架构师训练营 - 第 4 周命题作业

红了哟

为什么C++可以返回Vector局部变量

韩小非

c++ 内存泄露 函数调用 堆内存管理

【第五周】学习总结——缓存、消息队列、负载均衡

三尾鱼

极客大学架构师训练营

第五周-作业2-学习总结

seng man

架构师训练营第五周作业

CATTY

一致性Hash算法

疫情防控加速数字化,亚洲普惠金融迎来大发展

CECBC区块链专委会

数字化 普惠金融 合作共赢

分布式缓存框架

Arvin

消息队列与异步架构||负载均衡架构

独孤魂

Week3:作业一

车小勺的男神

week5.课后作业

个人练习生niki

操作系统概览

引花眠

计算机基础

架构师是怎样炼成的 05-1 分布式缓存,异步与集群

闷骚程序员

实现一致性 hash 算法

不在调上

区块链产业正开启“赛马”模式

CECBC区块链专委会

产业落地 政策扶持 赛马模式 技术革命

架构师训练营第 0 期第 5 周作业

Arthur

极客大学架构师训练营

第五周总结

胡江涛

极客大学架构师训练营

架构师训练营 - 学习总结 第 5 周

水边

极客大学架构师训练营

练习 05-1

闷骚程序员

第五周总结

不在调上

读《看见》

YoungZY

区块链各行业应用案例

CECBC区块链专委会

产业落地 政策扶持 去中心化信任 防篡改不可逆 低廉高效

架构师训练营作业-Week5

wyzwlj

极客大学架构师训练营

架构师训练营--第五周作业

花花大脸猫

极客大学架构师训练营

ARTS打卡-05

Geek_yansheng25

移动应用开发的下一站

移动应用开发的下一站

深入浅出的etcd系列在这里!-InfoQ