首页 文章 精选 留言 我的

精选列表

搜索[融合分析],共10000篇文章
优秀的个人博客,低调大师

熔断原理分析与源码解读

熔断机制(Circuit Breaker)指的是在股票市场的交易时间中,当价格的波动幅度达到某一个限定的目标(熔断点)时,对其暂停交易一段时间的机制。此机制如同保险丝在电流过大时候熔断,故而得名。熔断机制推出的目的是为了防范系统性风险,给市场更多的冷静时间,避免恐慌情绪蔓延导致整个市场波动,从而防止大规模股价下跌现象的发生。 同样的,在高并发的分布式系统设计中,也应该有熔断的机制。熔断一般是在客户端(调用端)进行配置,当客户端向服务端发起请求的时候,服务端的错误不断地增多,这时候就可能会触发熔断,触发熔断后客户端的请求不再发往服务端,而是在客户端直接拒绝请求,从而可以保护服务端不会过载。这里说的服务端可能是rpc服务,http服务,也可能是mysql,redis等。注意熔断是一种有损的机制,当熔断后可能需要一些降级的策略进行配合。 熔断原理 现代微服务架构基本都是分布式的,整个分布式系统是由非常多的微服务组成。不同服务之间相互调用,组成复杂的调用链路。在复杂的调用链路中的某一个服务如果不稳定,就可能会层层级联,最终可能导致整个链路全部挂掉。因此我们需要对不稳定的服务依赖进行熔断降级,暂时切断不稳定的服务调用,避免局部不稳定因素导致整个分布式系统的雪崩。 说白了,我觉得熔断就像是那些容易异常服务的一种代理,这个代理能够记录最近调用发生错误的次数,然后决定是继续操作,还是立即返回错误。 熔断器内部维护了一个熔断器状态机,状态机的转换关系如下图所示: 熔断器有三种状态: Closed状态:也是初始状态,我们需要一个调用失败的计数器,如果调用失败,则使失败次数加1。如果最近失败次数超过了在给定时间内允许失败的阈值,则切换到Open状态,此时开启一个超时时钟,当到达超时时钟时间后,则切换到Half Open状态,该超时时间的设定是给了系统一次机会来修正导致调用失败的错误,以回到正常的工作状态。在Closed状态下,错误计数是基于时间的。在特定的时间间隔内会自动重置,这能够防止由于某次的偶然错误导致熔断器进入Open状态,也可以基于连续失败的次数。 Open状态:在该状态下,客户端请求会立即返回错误响应,而不调用服务端。 Half-Open状态:允许客户端一定数量的去调用服务端,如果这些请求对服务的调用成功,那么可以认为之前导致调用失败的错误已经修正,此时熔断器切换到Closed状态,同时将错误计数器重置。如果这一定数量的请求有调用失败的情况,则认为导致之前调用失败的的问题仍然存在,熔断器切回到断开状态,然后重置计时器来给系统一定的时间来修正错误。Half-Open状态能够有效防止正在恢复中的服务被突然而来的大量请求再次打挂。 下图是Netflix的开源项目Hystrix中的熔断器的实现逻辑: 从这个流程图中,可以看到: 有请求来了,首先allowRequest()函数判断是否在熔断中,如果不是则放行,如果是的话,还要看有没有达到一个熔断时间片,如果熔断时间片到了,也放行,否则直接返回错误。 每次调用都有两个函数makeSuccess(duration)和makeFailure(duration)来统计一下在一定的duration内有多少是成功还是失败的。 判断是否熔断的条件isOpen(),是计算failure/(success+failure)当前的错误率,如果高于一个阈值,那么熔断器打开,否则关闭。 Hystrix会在内存中维护一个数据,其中记录着每一个周期的请求结果的统计,超过时长长度的元素会被删除掉。 熔断器实现 了解了熔断的原理后,我们来自己实现一套熔断器。 熟悉go-zero的朋友都知道,在go-zero中熔断没有采用上面介绍的方式,而是参考了《Google Sre》 采用了一种自适应的熔断机制,这种自适应的方式有什么好处呢?下文会基于这两种机制做一个对比。 下面我们基于上面介绍的熔断原理,实现一套自己的熔断器。 代码路径:go-zero/core/breaker/hystrixbreaker.go 熔断器默认的状态为Closed,当熔断器打开后默认的冷却时间是5秒钟,当熔断器处于HalfOpen状态时默认的探测时间为200毫秒,默认使用rateTripFunc方法来判断是否触发熔断,规则是采样大于等于200且错误率大于50%,使用滑动窗口来记录请求总数和错误数。 func newHystrixBreaker() *hystrixBreaker { bucketDuration := time.Duration(int64(window) / int64(buckets)) stat := collection.NewRollingWindow(buckets, bucketDuration) return &hystrixBreaker{ state: Closed, coolingTimeout: defaultCoolingTimeout, detectTimeout: defaultDetectTimeout, tripFunc: rateTripFunc(defaultErrRate, defaultMinSample), stat: stat, now: time.Now, } } func rateTripFunc(rate float64, minSamples int64) TripFunc { return func(rollingWindow *collection.RollingWindow) bool { var total, errs int64 rollingWindow.Reduce(func(b *collection.Bucket) { total += b.Count errs += int64(b.Sum) }) errRate := float64(errs) / float64(total) return total >= minSamples && errRate > rate } } 每次请求都会调用doReq方法,在该方法中,首先通过accept()方法判断是否拒绝本次请求,拒绝则直接返回熔断错误。否则执行req()真正的发起服务端调用,成功和失败分别调用b.markSuccess()和b.markFailure() func (b *hystrixBreaker) doReq(req func() error, fallback func(error) error, acceptable Acceptable) error { if err := b.accept(); err != nil { if fallback != nil { return fallback(err) } return err } defer func() { if e := recover(); e != nil { b.markFailure() panic(e) } }() err := req() if acceptable(err) { b.markSuccess() } else { b.markFailure() } return err } 在accept()方法中,首先获取当前熔断器状态,当熔断器处于Closed状态直接返回,表示正常处理本次请求。 当前状态为Open的时候,判断冷却时间是否过期,如果没有过期的话则直接返回熔断错误拒绝本次请求,如果过期的话则把熔断器状态更改为HalfOpen,冷却时间的主要目的是给服务端一些时间进行故障恢复,避免持续请求把服务端打挂。 当前状态为HalfOpen的时候,首先判断探测时间间隔,避免探测过于频繁,默认使用200毫秒作为探测间隔。 func (b *hystrixBreaker) accept() error { b.mux.Lock() switch b.getState() { case Open: now := b.now() if b.openTime.Add(b.coolingTimeout).After(now) { b.mux.Unlock() return ErrServiceUnavailable } if b.getState() == Open { atomic.StoreInt32((*int32)(&b.state), int32(HalfOpen)) atomic.StoreInt32(&b.halfopenSuccess, 0) b.lastRetryTime = now b.mux.Unlock() } else { b.mux.Unlock() return ErrServiceUnavailable } case HalfOpen: now := b.now() if b.lastRetryTime.Add(b.detectTimeout).After(now) { b.mux.Unlock() return ErrServiceUnavailable } b.lastRetryTime = now b.mux.Unlock() case Closed: b.mux.Unlock() } return nil } 如果本次请求正常返回,则调用markSuccess()方法,如果当前熔断器处于HalfOpen状态,则判断当前探测成功数量是否大于默认的探测成功数量,如果大于则把熔断器的状态更新为Closed。 func (b *hystrixBreaker) markSuccess() { b.mux.Lock() switch b.getState() { case Open: b.mux.Unlock() case HalfOpen: atomic.AddInt32(&b.halfopenSuccess, 1) if atomic.LoadInt32(&b.halfopenSuccess) > defaultHalfOpenSuccesss { atomic.StoreInt32((*int32)(&b.state), int32(Closed)) b.stat.Reduce(func(b *collection.Bucket) { b.Count = 0 b.Sum = 0 }) } b.mux.Unlock() case Closed: b.stat.Add(1) b.mux.Unlock() } } 在markFailure()方法中,如果当前状态是Closed通过执行tripFunc来判断是否满足熔断条件,如果满足则把熔断器状态更改为Open状态。 func (b *hystrixBreaker) markFailure() { b.mux.Lock() b.stat.Add(0) switch b.getState() { case Open: b.mux.Unlock() case HalfOpen: b.openTime = b.now() atomic.StoreInt32((*int32)(&b.state), int32(Open)) b.mux.Unlock() case Closed: if b.tripFunc != nil && b.tripFunc(b.stat) { b.openTime = b.now() atomic.StoreInt32((*int32)(&b.state), int32(Open)) } b.mux.Unlock() } } 熔断器的实现逻辑总体比较简单,阅读代码基本都能理解,这部分代码实现的比较仓促,可能会有bug,如果大家发现bug可以随时联系我进行修正。 hystrixBreaker和googlebreaker对比 接下来对比一下两种熔断器的熔断效果。 这部分示例代码在:go-zero/example下 分别定义了user-api和user-rpc服务,user-api作为客户端对user-rpc进行请求,user-rpc作为服务端响应客户端请求。 在user-rpc的示例方法中,有20%的几率返回错误。 func (l *UserInfoLogic) UserInfo(in *user.UserInfoRequest) (*user.UserInfoResponse, error) { ts := time.Now().UnixMilli() if in.UserId == int64(1) { if ts%5 == 1 { return nil, status.Error(codes.Internal, "internal error") } return &user.UserInfoResponse{ UserId: 1, Name: "jack", }, nil } return &user.UserInfoResponse{}, nil } 在user-api的示例方法中,对user-rpc发起请求,然后使用prometheus指标记录正常请求的数量。 var metricSuccessReqTotal = metric.NewCounterVec(&metric.CounterVecOpts{ Namespace: "circuit_breaker", Subsystem: "requests", Name: "req_total", Help: "test for circuit breaker", Labels: []string{"method"}, }) func (l *UserInfoLogic) UserInfo() (resp *types.UserInfoResponse, err error) { for { _, err := l.svcCtx.UserRPC.UserInfo(l.ctx, &user.UserInfoRequest{UserId: int64(1)}) if err != nil && err == breaker.ErrServiceUnavailable { fmt.Println(err) continue } metricSuccessReqTotal.Inc("UserInfo") } return &types.UserInfoResponse{}, nil } 启动两个服务,然后观察在两种熔断策略下正常请求的数量。 googleBreaker熔断器的正常请求率如下图所示: hystrixBreaker熔断器的正常请求率如下图所示: 从上面的实验结果可以看出,go-zero内置的googleBreaker的正常请求数是高于hystrixBreaker的。这是因为hystrixBreaker维护了三种状态,当进入Open状态后为了避免继续对服务端发起请求造成压力,会使用一个冷却时钟,而在这段时间里是不会放过任何请求的,同时,从HalfOpen状态变为Closed状态后,瞬间又会有大量的请求发往服务端,这时服务端很可能还没恢复,从而导致熔断器又变为Open状态。而googleBreaker采用的是一种自适应的熔断策略,也不需要多种状态,也不会像hystrixBreaker那样一刀切,而是会尽可能多的处理请求,这不也是我们期望的嘛,毕竟熔断对客户来说是有损的。下面我们来一起学习下go-zero内置的熔断器googleBreaker。 源码解读 googleBreaker的代码路径在:go-zero/core/breaker/googlebreaker.go 在doReq()方法中通过accept()方法判断是否触发熔断,如果触发熔断则返回error,这里如果定义了回调函数的话可以执行回调,比如做一些降级数据的处理等。如果请求正常则通过markSuccess()给总请求数和正常请求数都加1,如果请求失败通过markFailure则只给总请求数加1。 func (b *googleBreaker) doReq(req func() error, fallback func(err error) error, acceptable Acceptable) error { if err := b.accept(); err != nil { if fallback != nil { return fallback(err) } return err } defer func() { if e := recover(); e != nil { b.markFailure() panic(e) } }() err := req() if acceptable(err) { b.markSuccess() } else { b.markFailure() } return err } 在accept()方法中通过计算判断是否触发熔断。 在该算法中,需要记录两个请求数,分别是: 请求总量(requests): 调用方发起请求的数量总和 正常处理的请求数量(accepts): 服务端正常处理的请求数量 在正常情况下,这两个值是相等的,随着被调用方服务出现异常开始拒绝请求,请求接受数量(accepts)的值开始逐渐小于请求数量(requests),这个时候调用方可以继续发送请求,直到requests = K * accepts,一旦超过这个限制,熔断器就会打开,新的请求会在本地以一定的概率被抛弃直接返回错误,概率的计算公式如下: max(0, (requests - K * accepts) / (requests + 1)) 通过修改算法中的K(倍值),可以调节熔断器的敏感度,当降低该倍值会使自适应熔断算法更敏感,当增加该倍值会使得自适应熔断算法降低敏感度,举例来说,假设将调用方的请求上限从 requests = 2 acceptst 调整为 requests = 1.1 accepts 那么就意味着调用方每十个请求之中就有一个请求会触发熔断。 func (b *googleBreaker) accept() error { accepts, total := b.history() weightedAccepts := b.k * float64(accepts) // https://landing.google.com/sre/sre-book/chapters/handling-overload/#eq2101 dropRatio := math.Max(0, (float64(total-protection)-weightedAccepts)/float64(total+1)) if dropRatio <= 0 { return nil } if b.proba.TrueOnProba(dropRatio) { return ErrServiceUnavailable } return nil } history从滑动窗口中统计当前的总请求数和正常处理的请求数。 func (b *googleBreaker) history() (accepts, total int64) { b.stat.Reduce(func(b *collection.Bucket) { accepts += int64(b.Sum) total += b.Count }) return } 结束语 本篇文章介绍了服务治理中的一种客户端节流机制 - 熔断。在hystrix熔断策略中需要实现三个状态,分别是Open、HalfOpen和Closed。不同状态的切换时机在上文中也有详细描述,大家可以反复阅读理解,最好是能自己动手实现一下。对于go-zero内置的熔断器是没有状态的,如果非要说它的状态的话,那么也只有打开和关闭两种情况,它是根据当前请求的成功率自适应的丢弃请求,是一种更弹性的熔断策略,丢弃请求概率随着正常处理的请求数不断变化,正常处理的请求越多丢弃请求的概率就越低,反之丢弃请求的概率就越高。 虽然熔断的原理都一样,但实现机制不同导致的效果可能也不同,在实际生产中可以根据实际情况选择符合业务场景的熔断策略。 希望本篇文章对你有所帮助。 本篇文章代码:https://github.com/zhoushuguang/go-zero/tree/circuit-breaker 参考 https://martinfowler.com/bliki/CircuitBreaker.html https://github.com/Netflix/Hystrix/wiki/How-it-Works 项目地址 https://github.com/zeromicro/go-zero https://gitee.com/kevwan/go-zero 欢迎使用 go-zero 并 star 支持我们! 微信交流群 关注『微服务实践』公众号并点击 交流群 获取社区群二维码。

优秀的个人博客,低调大师

WAL的概念与实践分析

Part 1 - 预写式日志 在计算机科学中,预写式日志[1](Write-Ahead logging,缩写WAL)是关系数据库系统中用于提供原子性和持久性(事务ACID属性中的A与D)的一系列技术。在使用WAL的系统中,所有的修改在生效之前都要先写入log文件中。log文件中通常包括redo和undo信息。假设一个程序在执行某些操作的过程中机器掉电了。在重新启动时,程序可能需要知道当时执行的操作是成功了还是部分成功或者是失败了。如果使用了WAL,程序就可以检查log文件,并对突然掉电时计划执行的操作内容跟实际上执行的操作内容进行比较。在这个比较的基础上,程序就可以决定是撤销已做的操作还是继续完成已做的操作,或者是保持原样。 WAL允许用in-place方式更新数据库。另一种用来实现原子更新的方法是shadowpaging。用in-place方式做更新的主要优点是减少索引和块列表的修改。ARIES[2]是WAL系列技术常用的算法,其核心策略为:1)Write ahead logging,对于对象的任何变更都要首先记入日志,同时日志必须要先于对象被写入磁盘;2)Repeating history during Redo,在crash之后重启时,ARIES通过重新执行数据库在crash之前的行为,使数据库恢复到它crash之前那一刻的状态,然后undo掉crash时还在执行的事务;3)Logging changes during Undo,将在undo事务时对数据库所做的变更记录日志,保证在重复重启时不会重做。在文件系统中,WAL通常称为journaling[3]。 RocksDB[4]中的每个更新操作都会写到两个地方:1)写到磁盘上的WAL日志;2)一个名为memtable的内存数据结构,后续会被刷盘到SST文件。WAL会把memtable的操作序列化之后以日志文件形式存储在持久化介质中。在数据库出现崩溃的时候,WAL文件可以用于重新构建memtable,帮助数据库恢复数据库到一个一致的状态。当一个memtable被安全地落盘到持久化介质之后,相关的WAL日志会变成过期的,然后被归档,最终归档的日志会在一定时间后被从硬盘上删除。 1.1WAL管理器 WAL文件使用一个递增的序列号生成到WAL文件夹。为了重新构建数据库的状态,这些文件会被按序列号顺序读取。WAL管理器提供把WAL文件作为一个可独立进行读取的抽象接口。内部使用Writer或者Reader抽象接口打开,并读取文件。 Writer提供一个抽象接口,用于在日志文件末尾增加数据。存储介质相关的内部细节信息通过WriteableFile接口处理。类似的,Reader提供一个抽象接口,用于从一个日志文件中顺序读取日志记录。内部的存储介质相关细节信息有SequentialFile接口处理。 1.2WAL文件格式 WAL文件由一系列的变长记录构成。记录通过kBlockSize(默认为32KB)聚集在一起。如果某个特定记录不能放入剩余的空间,那么剩余空间将会被空数据填充。writer写而reader读数据的时候,是按照一个kBlockSize大小的块来读的。 记录的排列格式如下所示: WAL文件包含一系列的大小为32KB的块,唯一的例外是文件的末尾可能会包含一个分片的块。每个块都由一系列记录构成,一个记录不会在一个块的最后6个Byte开始(毕竟放不下)。任何剩下的数据都构成tailer,tailer由全0构成,读取的时候应该被跳过。如果当前块正好剩下7个Byte,并且一个新的非0长度记录被加入进来,那么write必须加一个FIRST记录(里面不含任何用户数据)来填充剩下的7个byte,然后在下一个块再提交用户数据。 FULL类型的记录保存完整的用户数据。FIRST,MIDDLE,LAST在不得不把用户数据切分成多个分片的时候使用(大多数是因为块边界问题)。FIRST是用户数据的第一个分片用的类型,LAST是最后一个用户数据分片用的记录类型,MIDDLE则是中间那些所有的其他数据的记录类型。 例子:考虑一个用户记录的序列: A会在第一个block里被存储为一个FULL记录。B会被分成三个分片:第一个分片占据第一个块剩下的空间,第二个分片占据第二个块的完整空间,第三个分片占据第三个块的开头部分。第三个块还剩下6个Byte,作为一个tailer,留空。C会在第四个块以FULL记录存储。 1.3 WAL的生命周期 用一个例子来说明一个WAL的生命周期。一个RocksDB的db实例创建了两个列族:"new_cf"和"default"。一旦db被打开,一个新的WAL会在磁盘上被创建,以保证写持久性。 往列族中加入一些数据: 这时WAL需要记录所有的写操作。WAL会保持打开,并不断跟踪后续的写操作,直到大小到达DBOptions::max_total_wal_size设定阈值。 如果用户决定把列族"new_cf"的数据落盘,以下的事情会发生: new_cf的数据(key1和key3)会被落盘到一个新的SST文件; 一个新的WAL会被创建,现在后续的写操作都会写到新的WAL了; 旧的WAL不再接受新的写入,但是删除操作会被延后。 这时,会有两个WAL文件,旧的保存有从key1到key4的内容,新的保存key5和key6。因为旧的还有线上数据,就是"defalut"列族的,还不能被删除。只有当用户最后决定把"default"列族的数据落盘,旧的WAL才能被归档,然后自动从磁盘上删除。 总的来说一个WAL文件会在以下时机被创建: DB打开的时候; 一个列族落盘数据的时候。 一个WAL会在他持有的所有列族的数据的最大请求序列号落盘后被删除(或者归档,如果允许归档),换句话说所有的WAL里的数据都被固定到SST文件。归档WAL会被移到一个独立的位置,然后再从存储设备上清除。实际的删除动作可能会因为拷贝的原因被延后。 1.4 WAL配置 这些配置可以在options.h中找到。 DBOptions::wal_dir:用于设置RocksDB存储WAL文件的目录,这允许用户把WAL和实际数据分开存储(云溪数据库默认两者保存在同一个目录); DBOptions::WAL_ttl_seconds,DBOptions::WAL_size_limit_MB:这两个选项影响WAL文件删除的时间。非0参数表示时间和硬盘空间的阈值,超过这个阀值,会触发删除归档的WAL文件(云溪数据库中默认0); DBOptions::max_total_wal_size:如果希望限制WAL的大小,RocksDB使用其作为列族落盘的触发器。一旦WAL超过这个大小,RocksDB会开始强制列族落盘,以保证删除最老的WAL文件。这个配置在列族以不固定频率更新的时候非常有用。如果没有大小限制,如果这个WAL中有一些非常低频更新的列族的数据没有落盘,用户可能会需要保存非常老的WAL文件(云溪数据库中默认0)。 DBOptions::avoid_flush_during_recovery:选项名已经说明了他的用途(恢复过程中避免落盘)(云溪数据库中默认false); DBOptions::manual_wal_flush:决定WAL是每次写操作之后自动flush还是纯人工flush(用户必须调用FlushWAL来触发一个WALflush)(云溪数据库中默认false); DBOptions::wal_filter:通过改参数用户可以提供一个在恢复过程中处理WAL文件时被调用的filter对象(云溪数据库中默认Disabled); WriteOptions::disableWAL:如果用户依赖于其他写日志方式,或者不担心数据丢失,该参数就非常有用了(云溪数据库中默认false)。 1.5 WAL过滤器12 事务日志迭代器提供一种方法,用来在RocksDB实例间复制数据。一旦一个WAL因为列族被落盘而被归档,WAL不会马上被删掉。这是为了允许事务日志迭代器可以继续读取WAL文件,再发送给从节点。事务日志迭代器提供一种方法,用来在RocksDB实例间复制数据。一旦一个WAL因为列族被落盘而被归档,WAL不会马上被删掉。这是为了允许事务日志迭代器可以继续读取WAL文件,再发送给从节点。 Part 2 - WAL的实现 2.1etcd[5] etcd整体架构如下图所示: etcd的写请求流程大致可以总结为如下过程:1)首先 client 端通过负载均衡算法选择一个 etcd 节点,发起 gRPC 调用;2)然后 etcd 节点收到请求后经过gRPC 拦截器、Quota模块后,进入 KVServer 模块;3)KVServer 模块向 Raft 模块提交一个提案;4)随后此提案通过 RaftHTTP 网络模块转发集群中各个节点;5)经过集群多数节点持久化后,状态会变成已提交;6)etcdserver 从 Raft 模块获取已提交的日志条目,7)传递给 Apply 模块;8)Apply 模块通过MVCC 模块执行提案内容;9)更新状态机。 在流程5中,Raft 模块收到提案后,如果当前节点是Follower,它会转发给Leader,只有Leader 才能处理写请求。Leader收到提案后,通过Raft 模块输出待转发给Follower 节点的消息和待持久化的日志条目,日志条目则封装了提案内容。etcdserver从 Raft 模块获取到以上消息和日志条目后,作为 Leader,它会将 put 提案消息广播给集群各个节点,同时需要把集群 Leader 任期号、投票信息、已提交索引、提案内容持久化到一个 WAL(Write Ahead Log)日志文件中,用于保证集群的一致性、可恢复性。 上图是 WAL 结构,它由多种类型的 WAL 记录顺序追加写入组成,每个记录由类型、数据、循环冗余校验码组成。不同类型的记录通过Type 字段区分,Data为对应记录内容,CRC为循环校验码信息。WAL记录类型目前支持5 种,分别是文件元数据记录、日志条目记录、状态信息记录、CRC 记录、快照记录。文件元数据记录包含节点 ID、集群 ID 信息,它在 WAL 文件创建的时候写入;日志条目记录包含 Raft 日志信息,如 put 提案内容;状态信息记录,包含集群的任期号、节点投票信息等,一个日志文件中会有多条,以最后的记录为准;CRC记录包含上一个 WAL 文件的最后的 CRC(循环冗余校验码)信息,在创建、切割 WAL 文件时,作为第一条记录写入到新的WAL 文件,用于校验数据文件的完整性、准确性等;快照记录包含快照的任期号、日志索引信息,用于检查快照文件的准确性。 WAL模块又是如何持久化一个put 提案的日志条目类型记录呢?首先看看put 写请求如何封装在Raft 日志条目里面。下面是Raft 日志条目的数据结构信息,它由以下字段组成:Term是 Leader 任期号,随着 Leader 选举增加;Index 是日志条目的索引,单调递增增加;Type 是日志类型,比如是普通的命令日志(EntryNormal)还是集群配置变更日志(EntryConfChange);Data 保存述的 put 提案内容。 了解完 Raft 日志条目数据结构后,再看 WAL 模块如何持久化 Raft 日志条目。它首先将 Raft 日志条目内容(含任期号、索引、提案内容)序列化后保存到 WAL 记录的 Data 字段,然后计算 Data 的CRC 值,设置Type 为Entry Type,以上信息就组成了一个完整的 WAL 记录。最后计算 WAL 记录的长度,顺序先写入 WAL 长度(Len Field),然后写入记录内容,调用 fsync 持久化到磁盘,完成将日志条目保存到持久化存储中。当一半以上节点持久化此日志条目后,Raft 模块就会通过channel 告知etcdserver 模块,put提案已经被集群多数节点确认,提案状态为已提交,可以执行此提案内容了。于是进入流程6,etcdserver 模块从 channel 取出提案内容,添加到先进先出(FIFO)调度队列,随后通过 Apply 模块按入队顺序,异步、依次执行提案内容。 [1] https://zh.wikipedia.org/wiki/%E9%A2%84%E5%86%99%E5%BC%8F%E6%97%A5%E5%BF%97 [2] https://en.wikipedia.org/wiki/Algorithms_for_Recovery_and_Isolation_Exploiting_Semantics [3] https://en.wikipedia.org/wiki/Journaling_file_system [4]https://rocksdb.org.cn/doc/Write-Ahead-Log-File-Format.html [5]https://github.com/etcd-io/etcd/blob/release-3.4/wal/doc.go

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册