⑴ etcdraft vs kafka
在最新版本的fabric中,现在存在三种共识排序方式——solo、kafka、etcdraft,其中kafka和etcdraft是多机部署模式。受此启发,对fabric和kafka做一个对比。
ETCD 是用于共享配置和服务发现的分布式,一致性的KV存储系统。
Kafka 是由[Apache软件基金会]开发的一个开源流处理平台,是一种高吞吐量的[分布式]发布订阅消息系统。
etcd和kafka都是分布式的系统,数据在各个节点之间需要保持一致。kafka是借助zookeeper来实现数据一致性,etcd是使用raft协议来保持数据一致性。下面详细介绍一下kafka和etcd分别通过什么方式来保证数据一致的.
2.1.1 首先介绍两个重要概念
2.1.2 数据同步过程
(1). 主节点接收到数据数据后,会把本地leo+1。
(2). 把数据分发给从节点。
(3). 从节点leo+1。
(4). 从节点执行完成后返回给主节点。
(5). 等ISR列表中的从节点都返回后,主节点执行hw+1。
*对于Leader新写入的msg,Consumer不能立刻消费,Leader会等待该消息被所有ISR中的replica同步后,更新HW,此时该消息才能被Consumer消费,即Consumer最多只能消费到HW位置。这样就保证了如果Leader Broker失效,该消息仍然可以从新选举的Leader中获取。对于来自内部Broker的读取请求,没有HW的限制。
⑵ 基于 etcd 的 watch
一直在思考下面一个问题,在APIServer集群的情况下,状态是如何同步的。比如下面一个场景。
Refector1 连接是ApiServer1, Reflector 2连接的是ApiServer2, 当ApiServer1 收到一个update Pod 的请求时, Refelctor1 可以收到,但是这个请求没有发生在ApiServer2 上,如何让Reflector 2 也能感知到Pod 的event 呢?
这里的关键就是ETCD集群也可以有watch 机制,如果ApiServer1,写入ETCD,ApiServer2能够watch ETCD 的event的话,那就可以实现在ApiServer集群内部的Event 同步了。下面是个简单的例子。
K8S 就是利用这个etcd机制来实现ApiServer 的同步,这对集群功能来说是至关重要的。下面介绍 kubernetes 针对 etcd 的 watch 场景,k8s 在性能优化上面的一些设计, 逐个介绍缓存、定时器、序列化缓存、bookmark 机制、forget 机制、 针对数据的索引与 ringbuffer 等组件的场景以及解决的问题, 希望能帮助到那些对 apiserver 中的 watch 机制实现感兴趣的朋友。
k8s 中并没有将业务的具体处理逻辑耦合在 rest 接口中,rest 接口只负责数据的存储, 通过控制器模式,分离数据存储与业务逻辑的耦合,保证 apiserver 业务逻辑的简洁。
控制器通过 watch 接口来感知对应的资源的数据变更,从而根据资源对象中的期望状态与当前状态之间的差异, 来决策业务逻辑的控制,watch 本质上做的事情其实就是将感知到的事件发生给关注该事件的控制器。
这里我们先介绍基于 etcd 实现的基础的 watch 模块。
一个数据变更本质上无非就是三种类型:新增、更新和删除, 其中新增和删除都比较容易因为都可以通过当前数据获取,而更新则可能需要获取之前的数据, 这里其实就是借助了 etcd 中 revision 和 mvcc 机制来实现,这样就可以获取到之前的状态和更新后的状态, 并且获取后续的通知。
事件管道则是负责事件的传递,在 watch 的实现中通过两级管道来实现消息的分发, 首先通过 watch etcd 中的 key 获取感兴趣的事件,并进行数据的解析, 完成从 bytes 到内部事件的转换并且发送到输入管道 (incomingEventChan) 中, 然后后台会有线程负责输入管道中获取数据,并进行解析发送到输出管道 (resultChan) 中, 后续会从该管道来进行事件的读取发送给对应的客户端。
事件缓冲区是指的如果对应的事件处理程序与当前事件发生的速率不匹配的时候, 则需要一定的 buffer 来暂存因为速率不匹配的事件, 在 go 里面大家通常使用一个有缓冲的 channel 构建。
到这里基本上就实现了一个基本可用的 watch 服务,通过 etcd 的 watch 接口监听数据, 然后启动独立 goroutine 来进行事件的消费,并且发送到事件管道供其他接口调用。
kubernetes 中所有的数据和系统都基于 etcd 来实现,如何减轻访问压力呢, 答案就是缓存,watch 也是这样,本节我们来看看如何实现 watch 缓存机制的实现, 这里的 cacher 是针对 watch 的。
Reflector 是 client-go 中的一个组件,其通过 listwatch 接口获取数据存储在自己内部的 store 中, cacher 中通过该组件对 etcd 进行 watch 操作,避免为每个组件都创建一个 etcd 的 watcher。
watchCache 负责存储 watch 到的事件,并且将 watch 的事件建立对应的本地索引缓存, 同时在构建 watchCache 还负责将事件的传递, 其将 watch 到的事件通过 eventHandler 来传递给上层的 Cacher 组件。
cacheWatcher 顾名思义其是就是针对 cache 的一个 watcher(watch.Interface) 实现, 前端的 watchServer 负责从 ResultChan 里面获取事件进行转发。
Cacher 基于 etcd 的 store 结合上面的 watchCache 和 Reflector 共同构建带缓存的 REST store, 针对普通的增删改功能其直接转发给 etcd 的 store 来进行底层的操作,而对于 watch 操作则进行拦截, 构建并返回 cacheWatcher 组件。
看完基础组件的实现,接着我们看下针对 watch 这个场景 k8s 中还做了那些优化,学习针对类似场景的优化方案。
如果我们有多个 watcher 都 watch 同一个事件,在最终的时候我们都需要进行序列化, cacher 中在分发的时候,如果发现超过指定数量的 watcher, 则会在进行 dispatch 的时候, 为其构建构建一个缓存函数,针对多个 watcher 只会进行一次的序列化。
在上面我们提到过事件缓冲区,但是如果某个 watcher 消费过慢依然会影响事件的分发, 为此 cacher 中通过是否阻塞(是否可以直接将数据写入到管道中)来将 watcher 分为两类, 针对不能立即投递事件的 watcher, 则会在后续进行重试。
针对阻塞的 watcher 在进行重试的时候,会通过 dispatchTimeoutBudget 构建一个定时器来进行超时控制, 那什么叫 Budget 呢,其实如果在这段时间内,如果重试立马就成功,则本次剩余的时间, 在下一次进行定时的时候,则可以使用之前剩余的余额,但是后台也还有个线程,用于周期性重置。
针对上面的 TimeBudget 如果在给定的时间内依旧无法进行重试成功, 则就会通过 forget 来删除对应的 watcher, 由此针对消费特别缓慢的 watcher 则可以通过后续的重试来重新建立 watch, 从而减小对 a piserver 的 watch 压力。
bookmark 机制是大阿里提供的一种优化方案,其核心是为了避免单个某个资源一直没有对应的事件, 此时对应的 informer 的 revision 会落后集群很大, bookmark 通过构建一种 BookMark 类型的事件来进行 revision 的传递, 从而让 informer 在重启后不至于落后特别多。
watchCache 中通过 store 来构建了对应的索引缓存,但是在 listwatch 操作的时候, 则通常需要获取某个 revision 后的所有数据, 针对这类数据 watchCache 中则构建了一个 ringbuffer 来进行历史数据的缓存。
⑶ 京东hotkey源码解析
京东hotkey是一个经过京东大促验证的hotkey防御中间件,大概原理是通过上报key访问数到统计服务器集群,统计服务器集群将hotkey通知到客户端,让hotkey能缓存到本地内存中,做到毫秒级的Scale-Out。处理方式有点像美团cat实时收集数据进行统计,只不过美团cat没有反向通知逻辑而已。非常贴近工作实践,值得一看。
首先看一下缓存入口Cache的get方法,JdHotKeyStore.getValue是获取hotkey的方法,并且会进行访问次数的统计上报,如果获取到hotkey不为空,则直接返回,否则从redis获取并调用JdHotKeyStore.smartSet判断是否有hotkey,有则设置值,最后返回。
JdHotKeyStore.getValue会先调用inRule校验此key是否有对应规则,如果没有对应规则则不处理,然后调用getValueSimple从本地内存中获取hotkey的存储对象ValueModel,如果没有获取到,则调用HotKeyPusher.push开始计数;如果获取到,会调用isNearExpire判断是否快过期了,如果是也计数,然后取出ValueModel里的value是否有设置对应值,有才返回。最后调用KeyHandlerFactory.getCounter().collect进行对应规则的计数。下面来一步步分析此流程。
inRule会去KeyRule缓存中获取对应的规则,经过层层调用会到KeyRuleHolder的findByKey方法,然后继续调用其findRule方法选择对应的KeyRule,如果没有KeyRule就直接返回了,否则会拿到它的ration(hotkey缓存时间),拿到对应ration的本地缓存。实际上这里为了方法的通用性,用了get来代替contain的判断。
findRule的逻辑比较特别,作者已经留下了注释,优先全匹配->prefix匹配-> * 通配,这样做是为了更精确选择对应的规则。比如配置了sku_的前缀规则,但是茅台sku的流量突升,需要针对茅台sku的本地缓存再长一点时间让系统平稳渡过高峰期,那就配置一个sku_moutai_sku_id的全匹配规则,这样不会干扰到其他sku的缓存规则。
那么KEY_RULES的规则是怎么来的呢?这就要说到etcd了,其实可以把etcd当做zookeeper,也有对配置crud,然后通知客户端的功能。这里是做了定时拉取+监听变化的双重保证,这里跟携程apollo的处理非常像:不要把鸡蛋放在一个篮子,兜底功能真的很重要。每5秒定时从etcd拉取规则,开启监听器有变化就去etcd拉取规则。fetchRuleFromEtcd从ectd的rule_path获取rules,然后转化成ruleList继续调用notifyRuleChange进行本地处理。
notifyRuleChange会往EventBus发送KeyRuleInfoChangeEvent的通知,进而进入KeyRuleHolder的putRules方法,这里可以看到维护了KEY_RULES和RULE_CACHE_MAP。
回到原有流程,getValueSimple方法的链路比较长,主要是通过key的规则,获取到对应的ration,然后从对应ration的本地缓存中获取ValueModel。
接下来是HotKeyPusher.push,如果是remove则在etcd创建一个节点然后再删除,达到集群删除的效果。如果是探测并且key在规则内,则调用KeyHandlerFactory.getCollector().collect进行统计。
KeyHandlerFactory.getCollector().collect方法交替使用两个map,对count进行累加,这样清理map的时候就不需要停顿了,交替使用是避免停顿的有效方式。
接回上文,还有一个 KeyHandlerFactory.getCounter().collect收集的是规则的访问次数,也是取到对应的规则,然后对规则的访问总数、热次数进行累加。
两个指标的收集已经分析完毕,那怎么发送到worker呢?来到PushSchelerStarter,这里会启动对两个指标的定时线程池,分别会定时调用NettyKeyPusher的send和sendCount方法。
NettyKeyPusher的send和sendCount方法都是为统计数据选择对应的worker然后进行请求,chooseChannel就是根据key哈希到其中一个worker上,然后发送请求即可。
最后当worker统计到hotkey时,client需要接收worker推送过来的hotkey进行存储,可以看到NettyClientHandler会向EventBus发送ReceiveNewKeyEvent事件,ReceiveNewKeyListener收到此事件后将调用receiveNewKeyListener.newKey,将hotkey放到本地缓存,client端的处理流程就结束了。
由上文可知,client与worker的交互只有推送统计数据到worker,worker接收处理,最后推送hotkey到client。因此worker端只需要分析两个部分:统计数据汇总、推送hotkey。
首先看到HotKey的处理逻辑是在HotKeyFilter中,首先会对totalReceiveKeyCount进行累加,然后调用publishMsg,如果统计信息超时1秒或者在白名单中就不处理,否则继续调用keyProcer.push。
keyProcer.push将未过时的统计信息丢进queue中。
worker端会开启指定数量的KeyConsumer,不断消费queue中的统计数据。根据统计数据的类型调用KeyListener的removeKey和newKey。
KeyListener的removeKey和newKey方法对Cache中的滑动窗口SlidingWindow进行删除或者累加,删除或者达到一定访问数就会推送到根据appname选出所有client进行推送。
京东的hotkey处理是通过计数来动态判断是否为hotkey,然后缓存再本地内存中,做到毫秒级的scale out。那还有没有其他解决方案?下面是我的观点:
1.如果面对一些缓存key很少的场景,比如活动页信息(同时进行的活动页不可能超过1000),完全就可以直接将缓存放在本地内存中,到了刷新时间就从redis拉取最新缓存即可,不需要动态计算hotkey。也就是常见的多级缓存。
2.同样是动态判断hotkey,但会将hotkey迁移到专门的、更多节点、更高性能的hotkey redis集群中,集群中每个节点都有同一个hotkey缓存,这样就可以做到请求的分散,避免流量都流向同一个redis节点,判断是hotkey就去hotkey集群中取,不需要存在本地内存中了,维护起来会比较简单。
⑷ 租约:如何检测你的客户端存活
Leader选举本质是要解决什么问题?
Lease是基于主动型上报模式,提供的一种活性检测机制。
etcd在启动的时候,创建Lessor模块的时候,它会启动两个常驻goroutine,一个是RevokeExpiredLeade任务,定时检查是否有过期Lease,
发起撤销过期的Lease操作。一个是CheckpointScheledLease,定时触发更新Lease的剩余到期时间的操作。
Lease模块提供Grant、Revoke、LeaseTimeToLive、LeaseKeepAlive API 给客户端:
Lease Server收到请求之后,完成Raft模块完成同步,Apply模块通过Lessor模块的Grant接口执行日志内容。
Lessor 的Grant接口会把Lease保存到内存的ItemMap数据结构中,然后持久化Lease保存到boltdb的Lease bucket,
返回一个唯一的LeaseID的client。
etcd重启后,如何知道每个Lease上关联了那些Key?
etcd的MVCC模块在持久化存储key-value的时候,保存到boltdb的value是个结构体(mvccpb.KeyValue),重启的时候,
重建关联各个Lease的key集合列表。
Lease续期是将Lease的过期时间更新为当前系统时间加其TTL。关键问题在于续期的性能能否满足业务诉求。
etcd v3 做了调整:
淘汰过期Lease的工作由Lessor模块的一个异步goroutine模块,定期从最小堆中取出已过期的Lease,执行删除Lease 和
其关联的key列表数据的RevokeExpiredLease任务。
etcd Lessor主循环每隔500ms执行一次撤销Lease检查(RevokeExpiredLease),每次轮询堆顶的元素,若过期则加入到待
淘汰列表,直到堆顶的Lease过期时间大于当前,则结束本轮轮询。
Leader如何通知其他Follower节点他们?
Leasor模块将已经确定过期的LeaseID,保存在一个名为expiredC的channel中,而etcd server的主循环定期channel中获取
LeaseID,发起revoke请求,通过Raft Log传递给Follower节点。各个节点收到Revoke Lease请求后,获取关联到此Lease上的
Key列表,从boltdb删除key,从Leasor的Lease Map内存中删除此Lease对象,最后还需要从boltdb的Lease bucket删除这个Lease。
集群Leader发生切换后,新的Leader基于Lease Map消息,按Lease过期时间构建一个最小堆的,etcd早期在重建的时候会自动给所有
Lease续期。如果Leader切换太频繁,切换时间小于Lease的TTL,会导致Lease永远无法删除,大量key堆积,db大小超过配额等。
etcd启动的时候,Leader节点后台会运行此异步任务,定期批量地将Lease剩余的TTL基于Raft Log同步给Follower节点,Follower节点
收到CheckPoint请求后,更新内存数据结构LeaseMap的剩余TTL信息。
当Leader节点收到KeepAlive请求的时候,它会通过checkpoint机制把此Lease的剩余TTL重置,并同步Follower节点,尽量确保续期后
集群各个节点的Lease剩余TTL一致性。
如果不想使用CheckPoint功能,可以通过experimental-enable-lease-checkpoint参数开关。
⑸ 生产etcd服务器掉电故障修复
客户现场集群异常掉电,我们于中午进行远程恢复集群。启动etcd服务时。出现如下错误
查看资料说是:
One of the member was bootstrapped via discovery service. You must remove the previous data-dir to clean up the member information. Or the member will ignore the new configuration and start with the old configuration. That is why you see the mismatch.
大概意思:
其中一个成员是通过discovery service引导的。必须删除以前的数据目录来清理成员信息。否则成员将忽略新配置,使用旧配置。这就是为什么你看到了不匹配。
看到了这里,问题所在也就很明确了,启动失败的原因在于data-dir (/var/lib/etcd/default.etcd)中记录的信息与 etcd启动的选项所标识的信息不太匹配造成的。
解决方案:将该节点的etcd从集群中移除,并删除相关数据(后面可同步恢复)。再重新加入etcd集群。
1.查看现有etcd节点
2.将报错节点移除
3.修改/usr/lib/systemd/system/etcd.service
4.删除数据
5.重新将etcd节点进行添加
6.启动etcd,重新加入的节点会向前两个节点重新同步数据
⑹ RPC 框架使用 Etcd 作为注册中心
对 etcd 来说,键值对( key-value pair )是最小可操作单元,每个键值对都有许多字段,以 protobuf 格式定义。
除了键和值之外, etcd 还将附加的修订元数据作为键消息的一部分 。该修订信息按创建和修改的时间对键进行排序,这对于管理分布式同步的并发性非常有用。etcd客户端的分布式共享锁使用创建修改来等待锁定所有权。类似地,修改修订用于检测软件事务内存读集冲突并等待领导人选举更新。
申请租约
授权租约
撤销
租约续约
⑺ etcd是什么东西它和ZooKeeper有什么区别
etcd是一个高可用的键值存储系统,主要用于共享配置和服务发现。etcd是由CoreOS开发并维护的,灵感来自于 ZooKeeper 和 Doozer,它使用Go语言编写,并通过Raft一致性算法处理日志复制以保证强一致性。Raft是一个来自Stanford的新的一致性算法,适用于分布式系统的日志复制,Raft通过选举的方式来实现一致性,在Raft中,任何一个节点都可能成为Leader。Google的容器集群管理系统Kubernetes、开源PaaS平台Cloud Foundry和CoreOS的Fleet都广泛使用了etcd。
etcd 集群的工作原理基于 raft 共识算法 (The Raft Consensus Algorithm)。etcd 在 0.5.0 版本中重新实现了 raft 算法,而非像之前那样依赖于第三方库 go-raft 。raft 共识算法的优点在于可以在高效的解决分布式系统中各个节点日志内容一致性问题的同时,也使得集群具备一定的容错能力。即使集群中出现部分节点故障、网络故障等问题,仍可保证其余大多数节点正确的步进。甚至当更多的节点(一般来说超过集群节点总数的一半)出现故障而导致集群不可用时,依然可以保证节点中的数据不会出现错误的结果。
⑻ etcd的应用场景
提到etcd很多人第一反应就是一个键值存储仓库。不过etcd官方文档的定义却是这样的:
etcd作为一个受到ZooKeeper与doozer启发而催生的项目,除了拥有与之类似的功能外,更专注于以下四点。
接下来将针对剖析一些etcd的经典使用场景,来看看etcd这个基于Raft强一致性算法的分布式存储仓库能给我们带来哪些帮助。
在分布式系统中“服务发现”也是比较常见的问题:在同一个集群环境中不同的应用或服务,如何能够找到对方并建立连接,来完成后续的行为。本质上来说,服务发现就是想要知道集群中是否有进程在监听udp或tcp端口,并能通过名字就可以查找和连接。而要解决服务发现的问题,需要满足如下三个方面,缺一不可。
来看服务发现对应的具体场景:
在分布式系统中,最适用的一种组件间通信方式就是消息发布与订阅。即构建一个配置共享中心,数据提供者在这个配置中心发布消息,而消息使用者则订阅他们关心的主题,一旦主题有消息发布,就会实时通知订阅者。通过这种方式可以做到分布式系统配置的集中式管理与动态更新。
在场景一中也提到了负载均衡,本文所指的负载均衡均为软负载均衡。分布式系统中,为了保证服务的高可用以及数据的一致性,通常都会把数据和服务部署多份,以此达到对等服务,即使其中的某一个服务失效了,也不影响使用。由此带来的坏处是数据写入性能下降,而好处则是数据访问时的负载均衡。因为每个对等服务节点上都存有完整的数据,所以用户的访问流量就可以分流到不同的机器上。
这里说到的分布式通知与协调,与消息发布和订阅有些相似。都用到了etcd中的Watcher机制,通过注册与异步通知机制,实现分布式环境下不同系统之间的通知与协调,从而对数据变更做到实时处理。实现方式通常是这样:不同系统都在etcd上对同一个目录进行注册,同时设置Watcher观测该目录的变化(如果对子目录的变化也有需要,可以设置递归模式),当某个系统更新了etcd的目录,那么设置了Watcher的系统就会收到通知,并作出相应处理。
因为etcd使用Raft算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁。锁服务有两种使用方式,一是保持独占,二是控制时序。
分布式队列的常规用法与场景五中所描述的分布式锁的控制时序用法类似,即创建一个先进先出的队列,保证顺序。
另一种比较有意思的实现是在保证队列达到某个条件时再统一按顺序执行。这种方法的实现可以在/queue这个目录中另外建立一个/queue/condition节点。
通过etcd来进行监控实现起来非常简单并且实时性强。
这样就可以第一时间检测到各节点的健康状态,以完成集群的监控要求。
另外,使用分布式锁,可以完成Leader竞选。这种场景通常是一些长时间CPU计算或者使用IO操作的机器,只需要竞选出的Leader计算或处理一次,就可以把结果复制给其他的Follower。从而避免重复劳动,节省计算资源。
这个的经典场景是搜索系统中建立全量索引。如果每个机器都进行一遍索引的建立,不但耗时而且建立索引的一致性不能保证。通过在etcd的CAS机制同时创建一个节点,创建成功的机器作为Leader,进行索引计算,然后把计算结果分发到其它节点。
etcd实现的这些功能,ZooKeeper都能实现。那么为什么要用etcd而非直接使用ZooKeeper呢?
相较之下,ZooKeeper有如下缺点:
而etcd作为一个后起之秀,其优点也很明显。
最后,etcd作为一个年轻的项目,真正告诉迭代和开发中,这既是一个优点,也是一个缺点。优点是它的未来具有无限的可能性,缺点是无法得到大项目长时间使用的检验。然而,目前CoreOS、Kubernetes和CloudFoundry等知名项目均在生产环境中使用了etcd,所以总的来说,etcd值得你去尝试。
从etcd的架构图中我们可以看到,etcd主要分为四个部分。
通常,一个用户的请求发送过来,会经由HTTP Server转发给Store进行具体的事务处理,如果涉及到节点的修改,则交给Raft模块进行状态的变更、日志的记录,然后再同步给别的etcd节点以确认数据提交,最后进行数据的提交,再次同步。
⑼ k8s etcd 与持久化存储
1、是什么
2、etcd架构及工作原理
(1) 数据流程
一个用户的请求发送过来,会经过HTTP Server转发给store进行具体事务处理,如果涉及到节点的修改,则需要交给raft模块进行状态的变更,日志的记录,然后再同步给别的etcd节点确认数据提交,最后进行数据提交,再次同步
(2)工作原理
Etcd使用 Raft协议 来维护集群内各个节点状态的 一致性 。简单说,ETCD集群是一个分布式系统,由多个节点相互通信构成整体对外服务, 每个节点都存储了完整的数据 ,并且通过Raft协议保证每个节点维护的数据是一致的
(3) 主要组成部分
(4)etcd集群中的术语
3、k8s中的etcd
(1)etcd在k8s中的作用: etcd在kubernetes集群是用来存放数据并通知变动的
(2)为什么k8s选择etcd:
PV 目前支持的类型包括:gcePersistentDisk 、AWSElasticBlockStore 、AzureFile 、AzureDisk 、FC ( Fibre Channel ) 、Flocker、NFS 、iSCSI 、RBD (Rados Block Device )、CephFS 、Cinder、GlusterFS 、V sphere Volume 、Quobyte Volumes 、VMware Photon 、Portwonc
Volumes 、ScaleIO Volumes 和HostPath (仅供单机测试)。
如果某个Pod 想申请某种类型的PY ,则首先需要定义一个PersistentVolurneClaim ( PVC )对象,然后,在Pod 的Volume 定义中引用上述PVC 即可:
⑽ ETCD——基础原理
一个ETCD集群一般由3个或者5个节点组成,两个quorum一定存在交集,则
即:3个节点容忍1个节点故障,5个节点容忍2个节点故障,以此类推。
首先,所有的数据都保存在B+树(灰色),当我们指定了版本信息之后,会直接到灰色B+树中去获取相关的数据;同时,还有另外一个B+树,它维护了key和revions的映射关系,查询key的数据时候,会根据key查询到revision,再通过revision查询到相应的key。
etcd 使用 raft 协议来维护集群内各个节点状态的一致性。简单说,etcd 集群是一个分布式系统,由多个节点相互通信构成整体对外服务,每个节点都存储了完整的数据,并且通过 Raft 协议保证每个节点维护的数据是一致的。
每个 etcd 节点都维护了一个状态机,并且,任意时刻至多存在一个有效的主节点。主节点处理所有来自客户端写操作,通过 Raft 协议保证写操作对状态机的改动会可靠的同步到其他节点。
etcd 的设计目标是用来存放非频繁更新的数据,提供可靠的 Watch插件,它暴露了键值对的历史版本,以支持低成本的快照、监控历史事件。这些设计目标要求它使用一个持久化的、多版本的、支持并发的数据数据模型。
当 etcd 键值对的新版本保存后,先前的版本依然存在。从效果上来说,键值对是不可变的,etcd 不会对其进行 in-place 的更新操作,而总是生成一个新的数据结构。为了防止历史版本无限增加,etcd 的存储支持压缩(Compact)以及删除老旧版本。
逻辑视图
从逻辑角度看,etcd 的存储是一个扁平的二进制键空间,键空间有一个针对键(字节字符串)的词典序索引,因此范围查询的成本较低。
键空间维护了多个修订版本(Revisions),每一个原子变动操作(一个事务可由多个子操作组成)都会产生一个新的修订版本。在集群的整个生命周期中,修订版都是单调递增的。修订版同样支持索引,因此基于修订版的范围扫描也是高效的。压缩操作需要指定一个修订版本号,小于它的修订版会被移除。
一个键的一次生命周期(从创建到删除)叫做 “代 (Generation)”,每个键可以有多个代。创建一个键时会增加键的版本(version),如果在当前修订版中键不存在则版本设置为1。删除一个键会创建一个墓碑(Tombstone),将版本设置为0,结束当前代。每次对键的值进行修改都会增加其版本号 — 在同一代中版本号是单调递增的。
当压缩时,任何在压缩修订版之前结束的代,都会被移除。值在修订版之前的修改记录(仅仅保留最后一个)都会被移除。
物理视图
etcd 将数据存放在一个持久化的 B+ 树中,处于效率的考虑,每个修订版仅仅存储相对前一个修订版的数据状态变化(Delta)。单个修订版中可能包含了 B+ 树中的多个键。
键值对的键,是三元组(major,sub,type)
键值对的值,包含从上一个修订版的 Delta。B+ 树 —— 键的词法字节序排列,基于修订版的范围扫描速度快,可以方便的从一个修改版到另外一个的值变更情况查找。
etcd 同时在内存中维护了一个 B 树索引,用于加速针对键的范围扫描。索引的键是物理存储的键面向用户的映射,索引的值则是指向 B+ 树修该点的指针。
元数据存储——Kubernetes
Service Discovery(Name Service)
Distributed Coordination: Leader Election