容器运行时云原生【免费下载链接】docker-ce:warning: This repository is deprecated and will be archived (Docker CE itself is NOT deprecated) see the https://github.com/docker/docker-ce/blob/master/README.md :warning:项目地址https://gitcode.com/gh_mirrors/do/docker-ce点击查看免费下载Raft 是一种用于在分布式集群中维护**复制状态机Replicated State Machine**的一致性协议。本篇文章以 docker-ce 仓库内随附的etcd/raft库即[raft/README.md](https://link.gitcode.com/i/25aa5013d8ba60788673c153d8fd5077)所述内容为核心对象先讲清楚它的极简设计哲学再给出完整可运行的Node使用范例最后结合仓库中的源码raft.go、node.go、log.go、storage.go、progress.go等逐层剖析其背后的状态机模型、选举与心跳、日志复制与流控、只读线性化查询等实现细节。读完本文你将能够独立完成一个基于 etcd Raft 的最小集群应用并理解这一经典共识算法在工业界的工程化落地方案。本文所涉及代码均位于 components/engine/vendor/github.com/coreos/etcd/raft/ 目录下该库作为第三方依赖随 docker-ce 源码树一同发布是 Docker Swarm 等分布式系统底层一致性的重要支撑组件。一、Raft 协议与库的设计哲学Raft 是一种用于让集群中的多个节点共同维护一份复制状态机的一致性协议每个节点都持有一份状态机的副本通过**复制日志Replicated Log**来保证各副本最终一致。其理论来源是 Diego Ongaro 与 John Ousterhout 的论文In Search of an Understandable Consensus Algorithm本文档亦建议参考 Raft 博士论文Raft thesis理解算法细节。etcd/raft 库最大的特点是只实现 Raft 核心算法本身不做任何存储与网络的假设网络传输由使用者自建库只负责产出需要发给各对等节点的Message实际通过何种 RPC、TCP 或自定义通道发送完全由应用层决定。持久化存储由使用者自建日志条目、HardState、Snapshot 如何落盘由应用实现Storage接口并注入。模型化为确定性状态机为了让库行为可被精确测试整个库被建模为一个状态机。状态机以一条Message作为输入既可以是本地定时器更新也可以是来自远端节点的网络消息输出是一个三元组{[]Messages, []LogEntries, NextState}待发送消息数组、日志条目、以及 Raft 状态变更。对于状态相同的状态机相同的输入永远产生相同的输出——这正是它可被广泛测试与信任的基础。这种极简 确定性的设计换来的是灵活性、确定性与性能网络 IO 与磁盘 IO 都被隔离在库外任何传输层与存储方案都可以接入。二、功能全景完整 Raft 特性与可选增强2.1 协议完整实现该库是一套功能完整的 Raft 协议实现覆盖以下核心能力特性说明Leader 选举Leader election任期Term驱动的选举流程支持随机化选举超时避免多个节点同时竞选日志复制Log replicationLeader 将日志条目复制到全部节点并在多数派确认后提交Commit日志压缩Log compaction通过快照Snapshot机制压缩历史日志避免日志无限增长成员变更Membership changes支持向集群添加、删除节点见文档中的ProposeConfChange/ApplyConfChange流程领导权转移Leadership transferLeader 可以主动将领导权转移给指定节点用于运维场景下的优雅切换高效线性化只读查询见 2.2 节的两种实现方式2.2 线性化只读查询的两种方式库同时提供两种可配置的只读查询方案对应 raft.go 中的ReadOnlyOptionReadOnlySafe默认推荐Leader先与法定多数quorum通信确认自己仍是合法 Leader通过携带上下文的特殊心跳收集确认绕过 Raft 日志即可安全处理只读请求Follower先向 Leader 请求一个安全读索引read index待本地 applied index 追上该索引后再本地处理只读请求。ReadOnlyLeaseBased基于租约更高效Leader完全本地处理只读请求不再与 quorum 通信Follower同样向 Leader 请求安全读索引后本地处理。前提条件该方案依赖集群内所有机器的时钟若存在无界的时钟漂移则可能读到过期数据。源码中强制约束CheckQuorum必须开启见 raft.go 的validate逻辑以降低时钟漂移风险。2.3 可选增强特性在标准协议之外该实现还包含若干工程化增强乐观流水线Optimistic pipelining处于 Replicate 状态的节点无需等待上一条 Append 的确认即可继续发送下一条降低复制延迟日志复制流控Flow control通过inflights滑动窗口限制在途 Append 消息数量避免压垮应用层发送缓冲Raft 消息批量发送Batching减少同步网络 IO 调用次数日志条目批量落盘Batching log entries减少磁盘同步 IO 次数Leader 磁盘并行写按 Raft 论文 10.2.1 节的建议Leader 可与其 followers并行执行磁盘写入摊薄同步 IO 延迟提案转发Internal proposal redirectionFollower 收到的提案自动转发给 Leader失 quorum 自动退位Automatic step-downCheckQuorum开启时Leader 在选举超时内未观测到 quorum 活跃会自动退位为 Follower。三、核心对象与配置参数3.1 Node 接口一切的入口库的主要操作对象是Node接口定义见 node.go。Node 的创建方式有两种raft.StartNode(c *Config, peers []Peer)从零启动新集群节点raft.RestartNode(c *Config)从持久化状态快照 HardState 日志条目恢复一个既有节点。两者底层都会启动一个独立 goroutine 运行事件循环见newNode()与n.run(r)Node通过Go channel与用户代码协作。Node接口的常用方法包括方法作用Tick()推进内部逻辑时钟一步心跳与选举超时均以 tick 为单位Propose(ctx, data)提交应用数据提案被提交后以EntryNormal类型出现在CommittedEntriesProposeConfChange(ctx, cc)提交成员变更提案添加/删除节点Step(ctx, msg)将收到的网络消息喂给状态机Ready() -chan Ready获取当前时刻需要处理的状态批次Advance()通知 Node 上一批Ready已处理完毕ApplyConfChange(cc)应用已提交的成员变更条目ReadIndex(ctx, rctx)发起线性化只读请求ReportSnapshot/ReportUnreachable向库汇报快照发送结果 / 对端不可达Stop()停止节点并释放资源3.2 Config 核心参数Config结构raft.go是启动前的全部配置。下表给出文档示例中出现的参数及源码注释中的取值范围与含义参数含义与建议值ID本节点唯一标识不能为 00 保留为None。ID 必须在集群存续期内唯一且不可复用——文档特别强调不要用 IP 地址作 ID可能被复用可选用随机生成的 64 位整数ElectionTick连续多少个 tick 未收到当前任期 Leader 消息即发起选举。必须大于HeartbeatTick官方建议ElectionTick 10 * HeartbeatTick以避免不必要的 Leader 切换HeartbeatTickLeader 每隔多少个 tick 发送一次心跳。示例取 1即每个 tick 一次心跳Storage实现Storage接口的持久化存储必填。示例使用NewMemoryStorage()MaxSizePerMsg每条 Append 消息的最大字节数。越小则恢复成本越低初次探测与消息丢失但可能影响正常复制吞吐math.MaxUint64表示不限制0表示每条消息至多一条日志条目MaxInflightMsgs乐观复制阶段允许的在途 Append 消息最大数量用于防止打爆应用层传输缓冲TCP/UDP 发送缓冲。必须大于 0Applied仅重启时设置上次已应用到状态机的日志索引避免重复返回已应用条目CheckQuorum是否开启 quorum 活跃检查开启后 Leader 失 quorum 会自动退位ReadOnlyLeaseBased模式下必须开启PreVote是否启用论文 9.6 节的 Pre-Vote 预投票算法防止被网络分区隔离的旧节点回归时干扰集群ReadOnlyOptionReadOnlySafe默认或ReadOnlyLeaseBasedLogger自定义日志器默认为包内raftLoggerDisableProposalForwarding为 true 时 Follower 丢弃而非转发提案适用于 Leader 需为提案数据打混合逻辑时钟时间戳等场景validate()raft.go会在启动时对以上约束做校验包括ID ! 0、HeartbeatTick 0、ElectionTick HeartbeatTick、Storage ! nil、MaxInflightMsgs 0以及 Lease 模式与 CheckQuorum 的绑定关系。四、实战搭建一个三节点集群4.1 启动三节点集群文档给出的三节点集群启动方式如下每个节点持有一个本地Storage示例为MemoryStorage并通过peers参数声明集群中的其他节点storage : raft.NewMemoryStorage() c : Config{ ID: 0x01, ElectionTick: 10, HeartbeatTick: 1, Storage: storage, MaxSizePerMsg: 4096, MaxInflightMsgs: 256, } // 设置 peer 列表为集群中其他节点它们需要被单独启动。 n : raft.StartNode(c, []raft.Peer{{ID: 0x02}, {ID: 0x03}})StartNode内部node.go会为每个 peer 追加一条EntryConfChange类型的引导日志条目把它们标记为已提交并应用——因此调用StartNode后即可立即发起竞选Campaign这也是测试中常见的用法。4.2 启动单节点集群单节点集群让节点 0x01 自己成为 Leader// 创建 storage 与 config如上。 // peer 列表指向自己这样本节点即可成为单节点集群的 Leader。 peers : []raft.Peer{{ID: 0x01}} n : raft.StartNode(c, peers)4.3 新节点加入既有集群新节点不允许通过peers参数加入既有集群正确姿势是两步走先在集群内任意既有节点上调用ProposeConfChange把新节点加入成员配置新节点以空 peer 列表启动// 创建 storage 与 config如上。 n : raft.StartNode(c, nil)4.4 从持久化状态重启节点重启时必须先从持久层恢复快照、HardState 与日志条目到存储再用RestartNode恢复节点成员配置已包含在存储中无需再传 peersstorage : raft.NewMemoryStorage() // 从持久化快照、状态与日志条目恢复内存存储。 storage.ApplySnapshot(snapshot) storage.SetHardState(state) storage.Append(entries) c : Config{ ID: 0x01, ElectionTick: 10, HeartbeatTick: 1, Storage: storage, MaxSizePerMsg: 4096, MaxInflightMsgs: 256, } // 不带 peer 信息重启 raftpeer 信息已包含在存储中。 n : raft.RestartNode(c)4.5 Ready 循环使用者必须承担的职责创建Node之后用户有四大职责顺序至关重要职责一消费Ready()channel 并处理其中更新。其中步骤 1、2 可以并行但步骤 2 依赖步骤 1 完成按顺序持久化Entries、HardState、Snapshot即先写 Entries再写非空的 HardState 与 Snapshot。若持久层支持原子写可一次性写入。注意写入 Index 为 i 的条目时任何 Index i 的旧持久化条目必须被丢弃。将全部Messages发送到To字段指定的节点。关键约束在最新 HardState 落盘、且上一批Ready写入的所有 Entries 完成之前不得发送任何消息同一批次内可以在持久化 Entries 的同时发送消息。若消息类型为MsgSnap发送完成后必须调用Node.ReportSnapshot()快照消息可能很大。注意消息序列化不是线程安全的最稳妥的方式是在主 raft 循环内直接序列化发送。将Snapshot若有与CommittedEntries应用到状态机若已提交条目类型为EntryConfChange须调用Node.ApplyConfChange()应用之成员变更可以在此时通过将NodeID置零来取消但无论如何都必须调用ApplyConfChange且取消决策只能基于状态机自身状态不能依据外部信息如节点健康状况。调用Node.Advance()表示已就绪接收下一批更新可在步骤 1 之后的任意时刻调用但所有更新必须按Ready返回的顺序处理。职责二让所有已持久化日志条目通过Storage接口可访问。可使用库自带的MemoryStorage重启时重新填充或自行实现磁盘存储。职责三收到网络消息后传给Node.Stepfunc recvRaftRPC(ctx context.Context, m raftpb.Message) { n.Step(ctx, m) }职责四周期性调用Node.Tick()通常用time.Ticker。Raft 有两大超时——心跳超时与选举超时而在库内部时间以抽象的 tick 表示。4.6 完整主循环将上述职责组合起来一个完整的处理循环形如for { select { case -s.Ticker: n.Tick() case rd : -s.Node.Ready(): saveToStorage(rd.State, rd.Entries, rd.Snapshot) send(rd.Messages) if !raft.IsEmptySnap(rd.Snapshot) { processSnapshot(rd.Snapshot) } for _, entry : range rd.CommittedEntries { process(entry) if entry.Type raftpb.EntryConfChange { var cc raftpb.ConfChange cc.Unmarshal(entry.Data) s.Node.ApplyConfChange(cc) } } s.Node.Advance() case -s.done: return } }Ready结构node.go中几个字段值得关注MustSync提示应用本次HardState与Entries是否必须同步写盘该标志由MustSync()函数依据是否有新条目、Term/Vote 是否变化计算而来见 node.goReadStates则用于服务线性化只读请求本地 applied index 追过ReadState.Index后即可安全返回。4.7 提交数据提案与成员变更提交应用数据将数据序列化为字节切片后调用Propose提案被提交后数据会以EntryNormal类型出现在已提交条目中。注意提案不保证一定被提交超时后可能需要重新提案。n.Propose(ctx, data)添加/移除节点构建ConfChange结构体后调用ProposeConfChange待成员变更提交后会返回EntryConfChange类型的已提交条目必须通过ApplyConfChange应用到节点n.ProposeConfChange(ctx, cc) // 提交后…… var cc raftpb.ConfChange cc.Unmarshal(data) n.ApplyConfChange(cc)ConfChange的类型在 raft.pb.go 中定义为ConfChangeAddNode/ConfChangeRemoveNode/ConfChangeUpdateNode。⚠️ ID 使用约束文档特别强调——一个 ID 在集群存续期内代表唯一节点即使旧节点被移除该 ID 也绝不能被再次使用。这解释了为何 IP 地址不适合做节点 IDIP 可能被复用且节点 ID 必须非零。五、状态机模型与消息驱动源码剖析5.1 角色状态与状态机输入输出StateType定义了四种角色状态raft.goStateFollower、StateCandidate、StateLeader、StatePreCandidate。状态机由消息驱动Step是所有消息的统一入口raft.go其内部先做任期Term处理再按消息类型分发给对应角色的 step 函数消息 Term 更高跟随高 Term若为MsgApp/MsgHeartbeat/MsgSnap则同时承认消息来源为 LeaderbecomeFollower(m.Term, m.From)MsgPreVote特殊处理——不改变本地 Term消息 Term 更低开启CheckQuorum时对MsgHeartbeat/MsgApp回复MsgAppResp以推动任期前进否则忽略本地消息MsgHup等进入campaign流程发起选举。节点内部通过step、tick两个函数指针随角色切换becomeLeader时tick r.tickHeartbeat其余角色为tickElection这正是时间抽象为 tick的落地方式。5.2 选举机制任期、随机超时与 Pre-Vote随机化选举超时是避免选举风暴的关键resetRandomizedElectionTimeout()将随机超时设置在[electionTimeout, 2*electionTimeout-1)区间内raft.go。tickElection()在超时触发时发送MsgHup开始竞选campaign()中节点自投票后向所有对端发送MsgVote/MsgPreVote携带自己的最后日志索引与任期Index、LogTerm对端通过raftLog.isUpToDatelog.go比较日志新鲜度决定是否投票。投票校验在Step的MsgVote/MsgPreVote分支raft.go仅在尚未投票或任期更高或已投给该请求者且对方日志不落后于自己时才投赞成票learner 节点不能投票。poll()统计票数达到quorum()len(prs)/2 1见 raft.go即当选。Pre-Vote 通过campaignPreElection阶段先以Term1试探、不递增本地 Term避免分区节点回归时引起任期跳变与已提交数据丢失风险。5.3 Leader 心跳、日志复制与提交Leader 心跳与租约检查Leader 每HeartbeatTick通过tickHeartbeatraft.go发送MsgBeat广播心跳若开启CheckQuorum每electionTimeout检查一次checkQuorumActive()——统计各节点RecentActive标记若活跃数不足 quorum 则becomeFollower(r.Term, None)自动退位。日志复制与确认sendAppend()raft.go从pr.Next开始取日志条目发送MsgApp若取条目失败如已被压缩则改发MsgSnap快照。Follower 收到MsgApp后调用handleAppendEntriesraft.go通过raftLog.maybeAppend检查前一条日志的 Index/Term 是否匹配匹配则追加并回复MsgAppResp{Index: mlastIndex}不匹配则回复带RejectHint本节点最后日志索引的拒绝Leader 据此maybeDecrTo回退Next再补发——这就是日志冲突的后退式backoff修复机制。提交推进Leader 收到MsgAppResp后pr.maybeUpdate(m.Index)推进Match然后maybeCommit()raft.go统计多数节点的Match值若某索引在当前任期获得多数确认则提交raftLog.commitTo。Follower 侧则通过心跳消息中的Commit字段sendHeartbeat中commit min(Match, committed)防止越界推进异步追赶提交索引handleHeartbeat中raftLog.commitTo(m.Commit)。5.4 复制状态机Probe / Replicate / Snapshot 三态流控每个对端节点在 Leader 侧对应一个Progressprogress.go状态机三态如下状态行为触发条件ProgressStateProbeLeader 每次只发送一条Append 消息并暂停等待确认确认后才推进Next节点刚加入 / 复制失败后回退ProgressStateReplicate乐观流水线复制发送时直接optimisticUpdate(last)推进Next并记录在inflights窗口内收到确认后freeTo(m.Index)释放窗口首次确认成功ProgressStateSnapshot进入快照发送流程发送MsgSnap后暂停等待ReportSnapshot汇报结果日志已被压缩、需要发送快照inflights是一个滑动窗口progress.go窗口满时full()暂停发送防止在途消息溢出应用层缓冲窗口大小即Config.MaxInflightMsgs。这条暂停-确认-推进的反馈链路正是 2.3 节所述流控与乐观流水线两个增强的底层实现。5.5 成员变更一次一个、提交生效成员变更是工程上最容易出错的部分。本实现遵循 Raft 论文第 4 章**一次只变更一个节点的关键不变量但生效时机有所不同变更在条目被应用applied时生效而非追加到日志时生效——即成员变更条目在旧成员配置**下提交。这在安全性上等价新旧配置必然存在重叠实现上的约束是不安全的并发变更被直接禁止Leader 日志中一旦存在未提交的成员变更pendingConf后续MsgProp中的EntryConfChange会被降级为EntryNormal丢弃见 raft.go成为 Leader 时校验becomeLeader会检查未提交条目中配置变更数量多于 1 个直接 panicraft.go防止不同 quorum 需求的变更同时提交应用侧在Ready.CommittedEntries中看到EntryConfChange后调用ApplyConfChange该调用最终驱动addNode/removeNode更新prs集合raft.go。⚠️ 两节点集群的移除陷阱若两节点集群中某成员在另一成员收到 confchange 提交前死亡集群将无法继续推进quorum 不足成员便无法被移除——因此文档强烈建议任何集群都应使用三个或以上节点。5.6 日志与存储层raftLog 的职责划分raftLoglog.go在稳定存储Storage与不稳定内存缓冲unstable之间划分职责unstable保存尚未持久化的新条目与快照。应用通过Ready.Entries拿到它们并落盘随后Advance()触发node.run中的stableTo/stableSnapTo将其移交稳定存储node.goStorage接口storage.go暴露InitialState、Entries(lo,hi,maxSize)、Term、LastIndex、FirstIndex、Snapshot六个方法。若任一方法返回错误raft 实例将不可操作并拒绝参与选举清理与恢复由应用负责MemoryStorage是库自带的纯内存实现文档示例均使用它它内部以ents[i]对应日志位置isnapshot.Metadata.Index并预留 term 0 的占位条目storage.go。生产环境通常需要自行实现磁盘存储nextEnts()log.go返回(applied, committed]区间内待应用到状态机的条目由newReady填进Ready.CommittedEntriesnode.go。5.7 只读查询的源码级流程结合 read_only.go 与 raft.go两种只读方案的内部流程如下ReadOnlySafeLeader 收到MsgReadIndex后addRequest登记请求并通过bcastHeartbeatWithCtx广播携带请求上下文的特殊心跳对端回复MsgHeartbeatResp时携带该上下文recvAck累计确认数达到 quorum 后advance出队这批请求并返回ReadState本地请求或MsgReadIndexResp远端请求。用户使用Ready.ReadStates中的RequestCtx匹配自己的请求本地 applied index 超过ReadState.Index后即可安全读本地状态机。ReadOnlyLeaseBasedLeader 收到读请求时直接生成ReadState{Index: committed}放回readStates零额外网络往返Follower 则向 Leader 请求读索引并等MsgReadIndexResp。该模式依赖集群时钟同步源码在 raft.go 注释中明确警告时钟漂移无界时读索引不再安全。六、设计权衡与适用边界6.1 极简设计的两面性灵活性存储与传输完全外包可接入任意 RPC 框架、任意磁盘方案甚至可以在单进程内模拟多节点测试友好确定性纯消息驱动、输入输出可复现便于形式化测试与故障注入代价应用层必须正确实现 4.5 节的全部职责尤其是HardState 落盘先于消息发送这一安全不变量一旦违反可能导致数据丢失或脑裂风险。6.2 单 goroutine 事件循环node.run()node.go以单 goroutine 串行处理全部提案、消息、tick 与 Ready/Advance通过 channel 与外部通信propc/recvc/confc/tickc/readyc/advancec等其中tickc为容量 128 的缓冲 channel忙时可缓冲 tick。这一模型天然免除了 raft 内部状态的数据竞争——用户只需保证同一时刻只有一个 goroutine 读写Node。若希望完全掌控事件循环例如嵌入已有单线程框架可使用RawNoderawnode.go——一个线程不安全但更轻量的同步 APIHasReady()配合Ready()/Advance(rd)手动驱动。6.3 使用场景该库已在众多分布式系统中投入使用本文档列举的代表性用户包括CockroachDB可扩展、可存活、强一致的 SQL 数据库、Dgraph分布式图数据库、etcd 本身分布式 KV、TiKV基于 Raft 的分布式事务 KV、Docker SwarmKit任意规模分布式系统编排工具包docker-ce 生态的重要组成部分、Chain Core许可制多资产区块链。这些系统形态迥异却共享同一套由本库提供的共识核心印证了其只做算法、外包 IO设计哲学的普适性。七、学习与实践建议从 MemoryStorage 起步先在内存环境跑通 4.5 节 的完整 Ready 循环再逐步替换为自定义磁盘存储与真实网络传输善用确定性模型相同输入必得相同输出的性质意味着你可以把消息序列录制下来做离线重放调试遵循文档的安全警告三节点起步、ID 不复用、HardState 先落盘、消息不晚发是四道不可逾越的安全红线深入阅读源码以 raft.go、node.go、log.go、progress.go 为顺序通读可对论文中的抽象概念与工程代码形成一一对应这是掌握 Raft 实现的最短路径。赞分享容器运行时云原生【免费下载链接】docker-ce:warning: This repository is deprecated and will be archived (Docker CE itself is NOT deprecated) see the https://github.com/docker/docker-ce/blob/master/README.md :warning:项目地址https://gitcode.com/gh_mirrors/do/docker-ce点击查看免费下载相关推荐深入SubtitleEdit核心库libse架构与API使用深入SubtitleEdit核心库libse架构与API使用 本文详细解析了SubtitleEdit核心库libse的架构设计与API使用。libse采用分层音视频桌面应用PostCSS源码解析深入核心库架构设计与实现原理PostCSS源码解析深入核心库架构设计与实现原理 PostCSS是一个强大的CSS处理工具它通过JavaScript插件系统实现了CSS的现代化转换和处理前端前端构建深入LZ4核心库API设计与实现架构深入LZ4核心库API设计与实现架构 本文深入分析了LZ4压缩算法库的核心架构设计重点解析了其高度模块化的组织结构和API设计理念。LZ4库主要由四个核心模存储系统编程上一篇NomNom存档编辑器No Mans Sky终极修改工具完整指南下一篇终极泰拉瑞亚地图编辑器10分钟从新手变造物主创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考