<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Raft on codedump notes</title>
    <link>https://www.codedump.info/zh/tags/raft/</link>
    <description>Recent content in Raft on codedump notes</description>
    <generator>Hugo</generator>
    <language>zh</language>
    <lastBuildDate>Sun, 12 Apr 2026 09:59:41 +0800</lastBuildDate>
    <atom:link href="https://www.codedump.info/zh/tags/raft/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Rockraft：基于 OpenRaft 与 RocksDB 的强一致 KV 存储框架</title>
      <link>https://www.codedump.info/zh/post/20260412-rockraft/</link>
      <pubDate>Sun, 12 Apr 2026 09:59:41 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20260412-rockraft/</guid>
      <description>&lt;h2 class=&#34;heading&#34; id=&#34;动机&#34;&gt;&#xA;  动机&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8a%a8%e6%9c%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Redis协议已经成为事实意义上的key-value存储协议标准。除了官方的Redis实现以外，我们看到还有各种兼容Redis协议的实现：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/valkey-io/valkey&#34;&gt;Valkey&lt;/a&gt;：Linux基金会官方fork的Redis 7.2分支，采用BSD许可证，是Redis更换SSPL许可证后社区推出的真正开源替代方案，完全兼容Redis协议和持久化机制。&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/dragonflydb/dragonfly&#34;&gt;Dragonfly&lt;/a&gt;：追求极致性能的现代多线程内存数据库，可提供相比Redis高达25倍的吞吐量和更低的尾延迟，但采用BSL许可证（4年后转Apache 2.0）。&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/snapchat/keydb&#34;&gt;KeyDB&lt;/a&gt;：Snapchat收购维护的Redis多线程分支，在100%兼容Redis API的基础上增加了主动复制（Active Replication）和Flash存储扩展能力，但更新已放缓。&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/apache/kvrocks&#34;&gt;Kvrocks&lt;/a&gt;：Apache顶级项目，基于RocksDB实现数据持久化到磁盘的分布式KV存储，支持数十TB级数据且成本仅为内存方案的1/5-1/10，适合大容量低成本场景。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;但是，上面的任何一个实现，都没有在分布式系统的强一致性上走得更远，在这个维度，它们依然采用了Redis原生实现的最终一致性。&lt;/p&gt;&#xA;&lt;p&gt;最开始，我创建&lt;a href=&#34;https://github.com/lichuang/coredb&#34;&gt;coredb&lt;/a&gt;项目，是为了利用raft共识算法，实现一个满足强一致性且兼容redis协议的服务。我知道一定有人会有疑问：大家使用Redis类的系统，是为了缓存数据，在这类型的项目里，一般都会选择&lt;a href=&#34;https://en.wikipedia.org/wiki/CAP_theorem&#34;&gt;CAP&lt;/a&gt;中的AP，把可用性放在第一位，而非一致性。&lt;/p&gt;&#xA;&lt;p&gt;但是，回到文章最开始的结论：Redis协议已经成为事实意义上的key-value存储协议标准。如果在这个大前提下，它的生态价值不应仅仅局限于传统的内存缓存。通过为其引入强一致性的持久化存储，我们可以赋予它全新的生命力和应用场景——正如 HTTP 协议从早期的网页传输协议，最终演变为无处不在的通信基石一样。&lt;/p&gt;&#xA;&lt;p&gt;最开始，我构建的项目只有coredb，这是一个采用Raft算法+rocksdb的强一致且兼容Redis协议的服务，也就是说，可以继续使用redis客户端访问这个服务，但是它满足强一致性：只要数据写入时返回成功，意味着至少在集群中的半数以上节点写入成功。&lt;/p&gt;&#xA;&lt;p&gt;在开发过程中我意识到，“Raft + RocksDB” 的架构组合具有极高的通用价值。考虑到许多开发者可能也需要这样一套可靠的底层基座，去构建他们自己专属的强一致性存储系统，我便将这部分核心逻辑进行了解耦，单独抽离出了 &lt;a href=&#34;https://github.com/lichuang/rockraft&#34;&gt;Rockraft&lt;/a&gt; 这个基础框架项目。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;设计与实现&#34;&gt;&#xA;  设计与实现&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%ae%be%e8%ae%a1%e4%b8%8e%e5%ae%9e%e7%8e%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Rockraft采用Rust开发，这是我目前最喜欢的系统编程语言：类型安全且内存安全，这两个特性是我最喜欢的Rust语言特性，有了这两个特性在编程时会更加放心。目前Rust语言的常见Raft实现有以下两个：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://github.com/tikv/raft-rs&#34;&gt;raft-rs&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Rust生态中最成熟、生产验证最充分的Raft实现，被近千个生产环境采用。它源自etcd的Go实现移植，但完全用Rust重写，保证了线程安全和内存安全。&lt;/p&gt;&#xA;&lt;p&gt;架构特点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;核心共识模块：仅提供纯共识算法核心（Raft状态机），不包含日志存储、网络传输或状态机实现&lt;/li&gt;&#xA;&lt;li&gt;高度可定制：需自行实现Storage Trait（日志存储）和RaftMessage传输层，灵活性极高&lt;/li&gt;&#xA;&lt;li&gt;多Raft支持：TiKV基于此实现了Multi-Raft架构，支持海量Region分片&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;功能完整性：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;✅ Leader选举、日志复制、成员变更（Joint Consensus）&lt;/li&gt;&#xA;&lt;li&gt;✅ PreVote机制避免网络分区干扰&lt;/li&gt;&#xA;&lt;li&gt;✅ Leader Lease读取优化&lt;/li&gt;&#xA;&lt;li&gt;✅ Snapshot快照传输&lt;/li&gt;&#xA;&lt;li&gt;✅ CheckQuorum检查机制&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;raft-rs的生产用户包括：&lt;a href=&#34;https://github.com/tikv/tikv&#34;&gt;TiKV&lt;/a&gt;（分布式事务KV数据库）&lt;/p&gt;&#xA;&lt;p&gt;注意：该库已进入维护模式，新功能开发放缓，建议新项目考虑OpenRaft&lt;/p&gt;&#xA;&lt;p&gt;2、&lt;a href=&#34;https://github.com/databendlabs/openraft&#34;&gt;OpenRaft&lt;/a&gt; 🚀 现代化异步架构&lt;/p&gt;&#xA;&lt;p&gt;设计理念：完全异步事件驱动，不依赖定时tick，消息批处理优化高吞吐。&lt;/p&gt;&#xA;&lt;p&gt;核心亮点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;事件驱动架构：基于Raft事件而非轮询，避免空转，大幅提升资源利用率&lt;/li&gt;&#xA;&lt;li&gt;统一API：单一Raft类型，通过RaftLogStorage、RaftStateMachine、RaftNetwork三个Trait扩展存储和网络层&lt;/li&gt;&#xA;&lt;li&gt;完善的成员变更：采用更通用的Joint Consensus，支持任意成员变更（单次可增删多节点），而非单步变更&lt;/li&gt;&#xA;&lt;li&gt;内置可观测性：集成tracing日志和分布式追踪，支持编译时调整日志级别&lt;/li&gt;&#xA;&lt;li&gt;手动控制：支持手动触发选举(trigger_elect)、快照(trigger_snapshot)、日志清理(purge_log)，便于运维&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;功能特性：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;✅ 线性一致性读取(ensure_linearizable)&lt;/li&gt;&#xA;&lt;li&gt;✅ Learner(Non-voter)角色支持&lt;/li&gt;&#xA;&lt;li&gt;✅ 动态心跳/选举开关控制&lt;/li&gt;&#xA;&lt;li&gt;⛔️ 不支持单步成员变更（设计取舍，倾向更安全的Joint Consensus）&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;它的生产用户：&lt;a href=&#34;https://github.com/databendlabs/databend&#34;&gt;Databend&lt;/a&gt;（云原生数仓）、&lt;a href=&#34;https://github.com/cnosdb/cnosdb&#34;&gt;CnosDB&lt;/a&gt;（时序数据库）、&lt;a href=&#34;https://github.com/robustmq/robustmq&#34;&gt;RobustMQ&lt;/a&gt;（云原生消息队列）。&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第17期）：Read-Write Quorum System及在Raft中的实践</title>
      <link>https://www.codedump.info/zh/post/20220528-weekly-17/</link>
      <pubDate>Sat, 28 May 2022 16:16:57 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220528-weekly-17/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：在Paxos、Raft这类一致性算法的描述里，经常会看到&lt;code&gt;Majority&lt;/code&gt;、&lt;code&gt;Quorum&lt;/code&gt;这两个词，在以前我以为都是表达“半数以上”的含义，最近才发现两者有不小的区别。本文介绍这两者的区别，以及在Raft中实践中的问题。有了&lt;code&gt;Quorum&lt;/code&gt;的视角，能更好得理解一致性算法。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;read-write-quorum-system&#34;&gt;&#xA;  Read-Write Quorum System&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#read-write-quorum-system&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;首先来在数学上给出&lt;code&gt;Read-Write Quorum System&lt;/code&gt;的定义。&lt;/p&gt;&#xA;&lt;p&gt;一个&lt;code&gt;Read-Write Quorum System（读写法定系统）&lt;/code&gt;是两个集合组成的元组，即&lt;code&gt;Q=(R,W)&lt;/code&gt;，其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;集合&lt;code&gt;R&lt;/code&gt;被称为&lt;code&gt;Read Quorum（读法定集合）&lt;/code&gt;，即可以认为读操作都是读的集合&lt;code&gt;R&lt;/code&gt;中的元素；&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;集合&lt;code&gt;W&lt;/code&gt;被称为&lt;code&gt;Write Quorum（写法定集合）&lt;/code&gt;，即可以认为写操作都是写入到集合&lt;code&gt;W&lt;/code&gt;中的元素。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;$r∈R,  w∈W,r∩w≠0 $，即任从读集合中取一个成员r，以及任从写集合中取一个成员w，这两个集合一定有交集。&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;都知道在分布式系统中，一个写入操作要达成一致，读写操作一定要有一定的冗余度，即：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;写入多份数据成功才能认为写入成功，&lt;/li&gt;&#xA;&lt;li&gt;从多个节点读到同一份数据才认为读取成功。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;在&lt;code&gt;Majority&lt;/code&gt;系统中，这个冗余度就是系统内半数以上节点。因为根据&lt;a href=&#34;https://baike.baidu.com/item/%E6%8A%BD%E5%B1%89%E5%8E%9F%E7%90%86/233776&#34;&gt;抽屉原理&lt;/a&gt;，当写入到至少半数以上节点时，读操作与写操作一定有重合的节点。&lt;/p&gt;&#xA;&lt;p&gt;但是在一个&lt;code&gt;Read-Write Quorum System&lt;/code&gt;中，这个条件变的更宽泛了，在这类系统中，只需要满足以下条件即可认为读写成功：&lt;/p&gt;&#xA;&lt;p&gt;$r∈R,  w∈W,r∩w≠0 $&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;用直观的大白话来说：在&lt;code&gt;Read-Write Quorum System&lt;/code&gt;中，只要读、写集合中的任意元素有重合即可。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;我们来详细看看&lt;code&gt;Majority&lt;/code&gt;和&lt;code&gt;Read-Write Quorum System&lt;/code&gt;这两个系统的区别在哪里。&lt;/p&gt;&#xA;&lt;p&gt;首先，&lt;code&gt;Majority&lt;/code&gt;系统并没有区分读、写两类不同的集合，因为在它的视角里，读和写操作都要到半数以上节点才能达到一致。但是在&lt;code&gt;Read-Write Quorum System&lt;/code&gt;系统里，是严格区分了读、写集合的，尽管可能在很多时候，这两类集合是一样的。&lt;/p&gt;&#xA;&lt;p&gt;再次，有了前面严格区分的读、写集合之后，以这个视角来看分布式系统中，一个数据达成一致的大前提是“读、写操作一定有重合的节点”，这样就能保证：写入一个数据到写集合中，最终会被读集合读到。在&lt;code&gt;Majority&lt;/code&gt;系统里，读、写集合都必须是半数以上节点的要求当然能够满足这个条件，但是这个条件太&lt;code&gt;强&lt;/code&gt;了。如果只考虑&lt;code&gt;读、写集合有重合&lt;/code&gt;这个条件，是可以适当放宽而且还不影响系统的一致性的。&lt;/p&gt;&#xA;&lt;p&gt;从以上的讨论，可以得到下面的结论：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;分布式系统中，只要读、写集合有重合，就能保证数据的一致性了。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;Majority&lt;/code&gt;系统是对上述条件的一个强实现，但是存在比这个实现更弱一些的实现，同样能保证数据的一致性。&lt;/li&gt;&#xA;&lt;li&gt;以&lt;code&gt;Read-Write Quorum System&lt;/code&gt;的定义和视角来看，&lt;code&gt;Majority&lt;/code&gt;系统相当于在这两方面强化了&lt;code&gt;Read-Write Quorum System&lt;/code&gt;系统的要求：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;读、写集合完全一样，&lt;/li&gt;&#xA;&lt;li&gt;且都是半数以上节点集合的&lt;code&gt;Read-Write Quorum System&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;strong&gt;即可以认为&lt;code&gt;Majority&lt;/code&gt;系统，只是&lt;code&gt;Read-Write Quorum System&lt;/code&gt;的一个子集。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;quorum&#34; src=&#34;https://www.codedump.info/media/imgs/20220528-weekly-17/quorum.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; quorum &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;讲了这么多，来看一个非&lt;code&gt;Majoiry&lt;/code&gt;的 &lt;code&gt;Read-Write Quorum System&lt;/code&gt;，下面的集合&lt;code&gt;{a,b,c,d,e,f}&lt;/code&gt;组成的网格（grid）被划分成了横竖两个读、写集合：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;grid&#34; src=&#34;https://www.codedump.info/media/imgs/20220528-weekly-17/grid.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; grid &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中，定义了一个&lt;code&gt;Read-Write Quorum System&lt;/code&gt;，&lt;code&gt;Q={{abc}∪{def},{ab}∪{bc}∪{ac}}&lt;/code&gt;，其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;读集合为&lt;code&gt;{abc}∪{def}&lt;/code&gt;，即横着的两个集合&lt;code&gt;{abc}&lt;/code&gt;和&lt;code&gt;{def}&lt;/code&gt;组成了读集合。&lt;/li&gt;&#xA;&lt;li&gt;写集合为&lt;code&gt;{ad}∪{be}∪{cf}&lt;/code&gt;，即竖着的三个集合&lt;code&gt;{ad}&lt;/code&gt;、&lt;code&gt;{be}&lt;/code&gt;、&lt;code&gt;{cf}&lt;/code&gt;组成了写集合。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;显然这个划分是能够满足前面的条件：$r∈R,  w∈W,r∩w≠0 $ 的，因为任选一个读集合中的集合如&lt;code&gt;{abc}&lt;/code&gt;，写集合中任选的一个集合如&lt;code&gt;{ad}&lt;/code&gt;，这两个集合中的元素都会有重合。&lt;/p&gt;&#xA;&lt;p&gt;假设是这样构成的一个分布式系统，那么写操作只需要写入写集合中的任意一个集合即可认为成功，可以看到一个写集合最小可以只有两个节点构成，这个数量是小于&lt;code&gt;Majority&lt;/code&gt;的。&lt;/p&gt;&#xA;&lt;p&gt;有了对&lt;code&gt;Read-Write Quorum System&lt;/code&gt;系统及与&lt;code&gt;Majority&lt;/code&gt;的区分和联系，以这个视角来看看raft的成员变更算法。&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第14期）：重读Raft论文中的集群成员变更算法（二）：实践篇</title>
      <link>https://www.codedump.info/zh/post/20220507-weekly-14/</link>
      <pubDate>Sat, 07 May 2022 17:57:08 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220507-weekly-14/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：以前阅读Raft大论文的时候，对“集群变更”这部分内容似懂非懂。于是最近又重读了大论文这部分的内容，以下是重读时做的一些记录。这部分内容打算分为两篇文章，上篇讲解成员变更流程的理论基础，下篇讲解实践中存在的问题。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;重读raft论文中的集群成员变更算法二实践篇&#34;&gt;&#xA;  重读Raft论文中的集群成员变更算法（二）：实践篇&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%87%8d%e8%af%bbraft%e8%ae%ba%e6%96%87%e4%b8%ad%e7%9a%84%e9%9b%86%e7%be%a4%e6%88%90%e5%91%98%e5%8f%98%e6%9b%b4%e7%ae%97%e6%b3%95%e4%ba%8c%e5%ae%9e%e8%b7%b5%e7%af%87&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;单步成员变更存在的问题&#34;&gt;&#xA;  单步成员变更存在的问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8d%95%e6%ad%a5%e6%88%90%e5%91%98%e5%8f%98%e6%9b%b4%e5%ad%98%e5%9c%a8%e7%9a%84%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;正确性问题&#34;&gt;&#xA;  正确性问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%ad%a3%e7%a1%ae%e6%80%a7%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;单步变更成员时，可能出现正确性问题。如下面的例子所示，最开始时，系统的成员是&lt;code&gt;{a,b,c,d}&lt;/code&gt;这四个节点的集合，要将节点&lt;code&gt;u&lt;/code&gt;和&lt;code&gt;v&lt;/code&gt;加入集群，按照单步变更成员的做法，依次会经历：&lt;code&gt;{a,b,c,d}&lt;/code&gt;-&amp;gt;&lt;code&gt;{a,b,c,d,u}&lt;/code&gt;-&amp;gt;&lt;code&gt;{a,b,c,d,u,v}&lt;/code&gt;的变化，每次将一个节点加入到集群里。&lt;/p&gt;&#xA;&lt;p&gt;上面的步骤看起来很美好，但是考虑下面的例子，在变更过程中leader节点发生了变化的情况：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;C₀ = {a, b, c, d}&#xA;Cᵤ = C₁ ∪ {u}&#xA;Cᵥ = C₁ ∪ {v}&#xA;&#xA;Lᵢ: Leader in term `i`&#xA;Fᵢ: Follower in term `i`&#xA;☒ : crash&#xA;&#xA;    |&#xA; u  |         Cᵤ                  F₂  Cᵤ&#xA;--- | ----------------------------------&#xA; a  | C₀  L₀  Cᵤ  ☒               L₂  Cᵤ&#xA; b  | C₀  F₀          F₁          F₂  Cᵤ&#xA; c  | C₀  F₀          F₁  Cᵥ          Cᵤ&#xA; d  | C₀              L₁  Cᵥ  ☒       Cᵤ&#xA;--- | ----------------------------------&#xA; v  |                     Cᵥ                  time&#xA;    +--------------------------------------------&amp;gt;&#xA;          t₁  t₂  t₃  t₄  t₅  t₆  t₇  t₈&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;（引用自&lt;a href=&#34;https://blog.openacid.com/distributed/raft-bug/&#34;&gt;TiDB 在 Raft 成员变更上踩的坑 - OpenACID Blog&lt;/a&gt;）&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第13期）：重读Raft论文中的集群成员变更算法（一）：理论篇</title>
      <link>https://www.codedump.info/zh/post/20220417-weekly-13/</link>
      <pubDate>Sun, 17 Apr 2022 15:16:30 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220417-weekly-13/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：以前阅读Raft大论文的时候，对“集群变更”这部分内容似懂非懂。于是最近又重读了大论文这部分的内容，以下是重读时做的一些记录。这部分内容打算分为两篇文章，上篇讲解成员变更流程的理论基础，下篇讲解实践中存在的问题。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;重读raft论文中的集群成员变更算法一理论篇&#34;&gt;&#xA;  重读Raft论文中的集群成员变更算法（一）：理论篇&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%87%8d%e8%af%bbraft%e8%ae%ba%e6%96%87%e4%b8%ad%e7%9a%84%e9%9b%86%e7%be%a4%e6%88%90%e5%91%98%e5%8f%98%e6%9b%b4%e7%ae%97%e6%b3%95%e4%b8%80%e7%90%86%e8%ae%ba%e7%af%87&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;“集群成员变更（cluster membership change）”意指一个集群内节点的增、删操作，这在一个分布式系统中是必不可少的操作，因为并不能保证一个集群的所有节点都一直能工作的很好。Raft大论文《&lt;a href=&#34;https://web.stanford.edu/~ouster/cgi-bin/papers/OngaroPhD.pdf&#34;&gt;Consensus: Bridging Theory and Practice&lt;/a&gt;》中有专门的一节来讲解这部分内容。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;安全性&#34;&gt;&#xA;  安全性&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%89%e5%85%a8%e6%80%a7&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;首先，Raft算法中要求所有操作都需要保证安全性（safety），即：任何时候都不能在集群中同时存在两个leader节点。“集群成员变更”算法也必须保证安全性这个大前提不能被破坏，于是论文中阐述了为什么直接变更多个节点是不被允许的：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;4.2&#34; src=&#34;https://www.codedump.info/media/imgs/20220417-weekly-13/4.2.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 4.2 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图的图示中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;旧集群有1、2、3这三个节点，而需要将这个三节点的集群新增节点4、5变更到5节点集群去。&lt;/li&gt;&#xA;&lt;li&gt;如果直接如图中这样变更，由于每个节点的时间窗口并不一致，可能就会出现这种情况：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;在某一时刻，节点1、2还使用的是旧集群（只含有{1,2,3}）的成员配置，而3、4、5已经是新集群（含有{1,2,3,4,5}）的成员配置了。&lt;/li&gt;&#xA;&lt;li&gt;这样就可能出现还使用旧集群节点配置的1、2选出了一个leader，以及已经使用了新集群配置的节点3、4、5选出了另一个leader的情况，于是违反了上面阐述的“安全性”要求。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;需要说明的是，在上面这个错误的示例中，是由于有两类行为同时出现才导致的错误：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一次性变更多个节点。在例子中，就是一次性把4、5两个节点加入到集群中。&lt;/li&gt;&#xA;&lt;li&gt;直接（directly）变更。直接变更由于集群中不同节点的步子不一样，而不一样的节点如果出现了两个不同的集群，那么就可能导致选出两个不同的leader。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;cluster-membership-change&#34; src=&#34;https://www.codedump.info/media/imgs/20220417-weekly-13/cluster-membership-change.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; cluster-membership-change &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;于是，由于这两个错误操作是一起发生才会导致错误，论文中给出了两种方案：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;要么一次性严格限制只变更一个节点。&lt;/li&gt;&#xA;&lt;li&gt;如果实在想一次变更多个节点，那就不能直接变更，需要经过一个中间状态的过渡之后才能完成同时变更多个节点的操作。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下分别来阐述这两种不同的实现。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;一次变更单个节点&#34;&gt;&#xA;  一次变更单个节点&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%80%e6%ac%a1%e5%8f%98%e6%9b%b4%e5%8d%95%e4%b8%aa%e8%8a%82%e7%82%b9&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;如果限制每次只变更一个节点，那么就能保证“新、旧集合的quorum集合是有重合的”，由于有重合，这样就能保证新旧两个集群的集合不会选出不同的leader，就能间接保证安全性。&lt;/p&gt;&#xA;&lt;p&gt;论文中以下面几个例子来说明这样操作的正确性：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;4.3&#34; src=&#34;https://www.codedump.info/media/imgs/20220417-weekly-13/4.3.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 4.3 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;这几个图，是在两个维度上做示范的：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;增、删操作。&lt;/li&gt;&#xA;&lt;li&gt;原集群节点数量是奇数还是偶数。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;两个维度的组合一共就是上面的4中情况，但是无论哪一种情况，由于都保证了“新、旧集合的quorum集合是有重合的”这个条件，于是不会选出不一样的leader来。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;一次变更多个节点&#34;&gt;&#xA;  一次变更多个节点&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%80%e6%ac%a1%e5%8f%98%e6%9b%b4%e5%a4%9a%e4%b8%aa%e8%8a%82%e7%82%b9&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;从上面的例子中可以看到：只要能保证一次只变更一个节点，是可以直接（directly）变更的。即：无需中间状态，直接从A集合变更到A+1集合，因为这两个集合的quorum肯定有重合。&lt;/p&gt;&#xA;&lt;p&gt;但是，在一次需要变更多个节点的情况下，就不能这样直接变更，因为会出现最开始示例的那样同时选出两个leader的情况。于是，为了解决这个问题，需要引入一个中间状态：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;假设原先的集群节点集合为C_Old，新的集群节点集合为C_New，那么首先变更配置到{C_Old,C_New}，也就是新旧集群节点集合的并集。&lt;/li&gt;&#xA;&lt;li&gt;上面这次变更提交之后，再向集群变更配置到C_New。在这次变更提交之后，那些不在C_New节点集合中的节点，收到这个变更时，自动下线退出集群。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以证明：上面两个步骤中，都不会出现“同时存在两个leader”的情况。&lt;/p&gt;&#xA;&lt;p&gt;从本质上来说，这种变更算法，属于一种两阶段的成员变更算法，Raft大论文中称之为“Joint Consensus（联合共识）”算法。下图中演示了Joint Consensus算法这两个阶段的流程：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;4.8&#34; src=&#34;https://www.codedump.info/media/imgs/20220417-weekly-13/4.8.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 4.8 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;failover&#34;&gt;&#xA;  Failover&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#failover&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;我们来看看Joint Consensus算法，在变更过程中如果出错，是如何failover选出新leader的。&lt;/p&gt;&#xA;&lt;p&gt;第一阶段，这时候选出来的leader只有可能有两种情况，还是旧的C_Old节点集合，或者已经收到了{C_Old,C_New}节点集合：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;只有C_Old节点集合的节点：由于这时候这个leader并没有第一阶段提交的{C_Old,C_New}节点集合变更，因此那些已有{C_Old,C_New}节点集合的follower这部分的日志将被截断，成员变更失败，回退回C_Old集合。&lt;/li&gt;&#xA;&lt;li&gt;有{C_Old,C_New}节点集合的节点：这意味这个leader已经有第一阶段提交的{C_Old,C_New}节点集合变更，可以继续将未完成的成员变更流程走完。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;类似的，也可以去推导一下在第二阶段出现leader宕机时，选出来的leader只可能具备两种情况，但是这两种情况都不可能选出多个leader。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;集群变更何时生效&#34;&gt;&#xA;  集群变更何时生效？&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%9b%86%e7%be%a4%e5%8f%98%e6%9b%b4%e4%bd%95%e6%97%b6%e7%94%9f%e6%95%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;以上讲解完毕两种不同的集群变更方式，下面来聊一聊集群变更何时生效。&lt;/p&gt;&#xA;&lt;p&gt;在Raft、Paxos这类状态机模型的一致性算法中，将任何变更操作都认为是一个命令（Command），命令的处理流程是这样的：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;状态机收到命令，首先在自己本地将命令持久化。&lt;/li&gt;&#xA;&lt;li&gt;然后广播给集群中的其他节点。&lt;/li&gt;&#xA;&lt;li&gt;当收到集群半数以上节点的应答时，认为命令是可以被提交（commit）的，于是可以生效将这些已经被提交的日志传给应用层的状态机使用了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以上流程可以看到：一条命令，只有在“提交（commit）”之后才能“生效（apply）”。&lt;/p&gt;&#xA;&lt;p&gt;在Raft中，“成员变更”这个操作，也是一类命令，即：&lt;/p&gt;</description>
    </item>
    <item>
      <title>etcd 3.5版本的joint consensus实现解析</title>
      <link>https://www.codedump.info/zh/post/20220101-etcd3.5-joint-consensus/</link>
      <pubDate>Sat, 01 Jan 2022 15:02:50 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220101-etcd3.5-joint-consensus/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在以前的etcd实现中，“集群节点变更”这一功能，仅支持每次变更一个节点，最新的etcd已经能支持一次变更多个节点配置的功能了。本文将就这部分的实现进行解析。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;原理&#34;&gt;&#xA;  原理&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8e%9f%e7%90%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;Raft论文《CONSENSUS: BRIDGING THEORY AND PRACTICE》的第四章”集群成员变更“中，支持两种集群变更方式：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;每次变更单节点，即“One Server Config Change”。&lt;/li&gt;&#xA;&lt;li&gt;多节点联合共识，即“Joint Consensus”。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;本文先就这两种实现方式进行原理上的讲解。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;集群节点变更的问题&#34;&gt;&#xA;  集群节点变更的问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%9b%86%e7%be%a4%e8%8a%82%e7%82%b9%e5%8f%98%e6%9b%b4%e7%9a%84%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;要保证Raft协议的安全性，就是要保证任意时刻，集群中只有唯一的&lt;code&gt;leader&lt;/code&gt;节点。如果不加限制条件，那么动态向当前运行集群增删节点的操作，有可能会导致存在多个&lt;code&gt;leader&lt;/code&gt;的情况。如下图所示：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;集群节点变更问题&#34; src=&#34;https://www.codedump.info/media/imgs/20220101-etcd3.5-joint-Consensus/multi-server.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 集群节点变更问题 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;图中有两种颜色的配置，绿色表示旧的集群配置（&lt;code&gt;C_old&lt;/code&gt;），蓝色表示新的集群配置（&lt;code&gt;C_new&lt;/code&gt;），如果不加任何限制，直接将配置启用，由于不同的集群节点之间，存在时间差，那么可能出现这样的情况：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Server{1,2}：当前都使用旧的集群配置，所以可能选出server1为集群的leader。&lt;/li&gt;&#xA;&lt;li&gt;Server{3,4,5}：当前都使用新的集群配置，可能选出server3为集群的leader。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;由上图可以看到：如果不加任何限制，直接应用新的集群配置，由于时间差的原因，可能导致集群中出现两个不同leader的情况。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;单节点成员变更one-server-confchange&#34;&gt;&#xA;  单节点成员变更（One Server ConfChange）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8d%95%e8%8a%82%e7%82%b9%e6%88%90%e5%91%98%e5%8f%98%e6%9b%b4one-server-confchange&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;“单节点成员变更”，意指每次只添加或删除一个节点，这样就能保证集群的安全性，不会在同一时间出现多个&lt;code&gt;leader&lt;/code&gt;的情况。之所以能有这个保证，是因为每次变更一个节点，那么新旧两种配置的半数节点（majorrity）肯定存在交集。以下图来说明：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;单节点成员变更&#34; src=&#34;https://www.codedump.info/media/imgs/20220101-etcd3.5-joint-Consensus/one-server-confchange.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 单节点成员变更 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图演示了向偶数或奇数的集群增删一个节点的所有可能情况。不论哪种情况，新旧配置都有交集，在每个任期只能投出一张票的情况下，是不会出现多&lt;code&gt;leader&lt;/code&gt;的情况的。&lt;/p&gt;&#xA;&lt;p&gt;有了上面的理论基础，下面来看&lt;code&gt;单节点集群变更&lt;/code&gt;的全流程，当下发集群节点变更配置时，新的配置会以一种特殊的日志方式进行提交，即：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;普通日志：半数通过，提交成功时，会传给应用层的状态机。&lt;/li&gt;&#xA;&lt;li&gt;配置变更类日志：半数通过，提交成功时，集群节点将以新的集群配置生效。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;其流程如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;将集群配置变更数据，序列化为日志数据，需要将日志类型标记为&lt;code&gt;集群配置变更&lt;/code&gt;类的日志，提交给&lt;code&gt;leader&lt;/code&gt;节点。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;leader&lt;/code&gt;节点收到日志后，需要存储该日志的索引为&lt;code&gt;未完成的集群配置变更索引&lt;/code&gt;，像其它正常日志一样处理：先写本地的日志，再广播给集群的其他节点，半数应答则认为日志达成一致可以提交了。如果提交了这类日志，可以将前面保存的&lt;code&gt;未完成的集群配置变更索引&lt;/code&gt;置为空了。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;集群配置变更&lt;/code&gt;日志提交之后，对照新旧的集群变更数据，该添加到集群的添加到集群，该删除的节点停机。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;需要注意的是，同一时间只能有唯一一个&lt;code&gt;集群变更类日志&lt;/code&gt;存在，怎么保证这一点？就算是在&lt;code&gt;leader&lt;/code&gt;收到该类型日志时，判断&lt;code&gt;未完成的集群配置变更索引&lt;/code&gt;是否为空。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;多节点联合共识joint-consensus&#34;&gt;&#xA;  多节点联合共识（Joint Consensus）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a4%9a%e8%8a%82%e7%82%b9%e8%81%94%e5%90%88%e5%85%b1%e8%af%86joint-consensus&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;除了上面的单节点变更，有时候还需要一次提交多个节点的变更。但是按照前面的描述，如果一次提交多个节点，很可能会导致集群的安全性被破坏，即同时出现多个&lt;code&gt;leader&lt;/code&gt;的情况。因此，一次提交多节点时，就需要走&lt;code&gt;联合共识&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;p&gt;所谓的&lt;code&gt;联合共识&lt;/code&gt;，就是将新旧配置的节点一起做为一个节点集合，只有该节点集合达成半数一致，才能认为日志可以提交，由于新旧两个集合做了合并，那么就不会出现多&lt;code&gt;leader&lt;/code&gt;的情况了。具体流程如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;leader&lt;/code&gt;收到成员变更请求，新集群节点集合为&lt;code&gt;C_new&lt;/code&gt;，当前集群节点集合为&lt;code&gt;C_old&lt;/code&gt;，此时首先会以新旧节点集合的交集&lt;code&gt;C_{old,new}&lt;/code&gt;做为一个&lt;code&gt;集群配置变更&lt;/code&gt;类的日志，走正常的日志提交流程。注意，这时候的日志，需要提交到&lt;code&gt;C_{old,new}&lt;/code&gt;中的所有节点。&lt;/li&gt;&#xA;&lt;li&gt;当&lt;code&gt;C_{old,new}&lt;/code&gt;集群变更日志提交之后，&lt;code&gt;leader&lt;/code&gt;节点再马上创建一个只有&lt;code&gt;C_new&lt;/code&gt;节点集合的&lt;code&gt;集群配置变更&lt;/code&gt;类日志，再次走正常的日志提交流程。这时候的日志，只需要提交到&lt;code&gt;C_new&lt;/code&gt;中的所有节点。&lt;/li&gt;&#xA;&lt;li&gt;当&lt;code&gt;C_new&lt;/code&gt;日志被提交之后，集群的配置就能切换到&lt;code&gt;C_new&lt;/code&gt;对应的新集群配置下了。而不在&lt;code&gt;C_new&lt;/code&gt;配置内的节点，将被移除。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;可以看到，&lt;code&gt;多节点联合共识&lt;/code&gt;的提交流程分为了两次提交：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;先提交新旧集合的交集&lt;code&gt;C_{old,new}&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;再提交新节点集合&lt;code&gt;C_new&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下图来说明，这几个阶段中，集群的安全性都得到了保证：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;多节点联合共识&#34; src=&#34;https://www.codedump.info/media/imgs/20220101-etcd3.5-joint-Consensus/Joint-Consensus.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 多节点联合共识 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;code&gt;C_{old,new}&lt;/code&gt;配置提交之前：在做个阶段，集群中的节点，要么处于&lt;code&gt;C_old&lt;/code&gt;配置下，要么处于&lt;code&gt;C_new,old&lt;/code&gt;配置之下。此时，如果集群的&lt;code&gt;leader&lt;/code&gt;节点宕机，那么将会基于&lt;code&gt;C_old&lt;/code&gt;或者&lt;code&gt;C_new,old&lt;/code&gt;配置来选出新的&lt;code&gt;leader&lt;/code&gt;，而不会仅仅基于&lt;code&gt;C_new&lt;/code&gt;，因此不会选出不同的&lt;code&gt;leader&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;C_{old,new}&lt;/code&gt;配置提交之后，&lt;code&gt;C_new&lt;/code&gt;下发之前：如果这时候&lt;code&gt;leader&lt;/code&gt;宕机，只会基于&lt;code&gt;C_{old,new}&lt;/code&gt;的配置选出&lt;code&gt;leader&lt;/code&gt;，因此也不会选出不同的&lt;code&gt;leader&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;C_new&lt;/code&gt;下发但还未提交时：如果这时候&lt;code&gt;leader&lt;/code&gt;宕机，只会基于&lt;code&gt;C_{old,new}&lt;/code&gt;或者&lt;code&gt;C_new&lt;/code&gt;的配置选出&lt;code&gt;leader&lt;/code&gt;，同时也不再会发给仅仅在&lt;code&gt;C_old&lt;/code&gt;中的节点了，所以无论是哪个配置，都需要得到&lt;code&gt;C_new&lt;/code&gt;的半数同意，因此不会选出不同的&lt;code&gt;leader&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;C_new&lt;/code&gt;提交之后：此时集群中只有一种配置了，安全性得到了保证。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;实现&#34;&gt;&#xA;  实现&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%9e%e7%8e%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;了解了原理之后，可以来具体看etcd 3.5中这部分的实现了。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;learner&#34;&gt;&#xA;  learner&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#learner&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;首先需要了解&lt;code&gt;learner&lt;/code&gt;这个概念，在Raft中，这类型节点有以下特点：&lt;/p&gt;</description>
    </item>
    <item>
      <title>为什么Raft协议不能提交之前任期的日志？</title>
      <link>https://www.codedump.info/zh/post/20211011-raft-propose-prev-term/</link>
      <pubDate>Mon, 11 Oct 2021 23:14:01 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20211011-raft-propose-prev-term/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在Raft大论文中3.6.2中，有一个细节“不允许提交之前任期的日志”，之前看了几次都理解的不够准确，把这部分内容展开阐述一下。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;问题&#34;&gt;&#xA;  问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;还是先从论文的图例开始解释，如下图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;论文截图&#34; src=&#34;https://www.codedump.info/media/imgs/20211011-raft-propose-prev-term/propose-prev-term.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 论文截图 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;需要特别说明的是，图例中演示的是**“如果允许提交之前任期的日志，将导致什么问题”**，这是大前提，这个前提条件后面会反复强调。&lt;/p&gt;&#xA;&lt;p&gt;有了这个前提，下面展开图中的步骤讨论：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;(a) ：S1 是leader，将黄色的日志2同步到了S2，然后S1崩溃。&lt;/li&gt;&#xA;&lt;li&gt;(b) ：S5 在任期 3 里通过 S3、S4 和自己的选票赢得选举，将蓝色日志3存储到本地，然后崩溃了。&lt;/li&gt;&#xA;&lt;li&gt;\(c\)：S1重新启动，选举成功。注意在这时，&lt;strong&gt;如果允许“提交之前任期的日志”&lt;/strong&gt;，将首先开始同步过往任期的日志，即将S1上的本地黄色的日志2同步到了S3。这时黄色的节点2已经同步到了集群多数节点，然后S1写了一条新日志4，然后S1又崩溃了。&lt;/li&gt;&#xA;&lt;li&gt;接下来，就可能出现两种不同的情况：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;（d1）：S5重新当选，&lt;strong&gt;如果允许“提交之前任期的日志”&lt;/strong&gt;，就开始同步往期日志，将本地的蓝色日志3同步到所有的节点。结果已经被同步到半数以上节点的黄色日志2被覆盖了。这说明，如果允许“提交之前任期的日志”，会可能出现即便已经同步到半数以上节点的日志被覆盖，这是不允许的。&lt;/li&gt;&#xA;&lt;li&gt;（d2）：反之，如果在崩溃之前，S1不去同步往期的日志，而是首先同步自己任期内的日志4到所有节点，就不会导致黄色日志2被覆盖。因为leader同步日志的流程中，会通过不断的向后重试的方式，将日志同步到其他所有follower，只要日志4被复制成功，在它之前的日志2就会被复制成功。（d2）是想说明：不能直接提交过往任期的日志，即便已经被多数通过，但是可以先同步一条自己任内的日志，如果这条日志通过，就能带着前面的日志一起通过，这是（c）和（d2）两个图的区别。图（c）中，S1先去提交过往任期的日志2，图（d2）中，S1先去提交自己任内的日志4。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;再次强调，这里图示想演示的是**“如果允许提交之前任期的日志，将导致什么问题”**。&lt;/p&gt;&#xA;&lt;p&gt;我们可以看到的是，如果允许这么做，那么：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;\(c\)中，S1恢复之后，又再次提交在任期2中的黄色日志2。但是，从后面可以看到，即便这个之前任期中的黄色日志2，提交到大部分节点，如果允许“提交之前任期的日志”，仍然存在被覆盖的可能性，因为：&lt;/li&gt;&#xA;&lt;li&gt;(d1)中，S5恢复之后，也会提交在自己本地上保存的之前任期3的蓝色日志，这会导致覆盖了前面已经到半数以上节点的黄色日志2。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;所以，“如果允许提交之前任期的日志”，即如同\(c\)和(d1)演示的那样：重新当选之后，马上提交自己本地保存的、之前任期的日志，就会&lt;strong&gt;可能导致即便已经同步到半数以上节点的日志，被覆盖的情况&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;而“已同步到半数以上节点的日志”，一定在新当选leader上（否则这个节点不可能成为新leader）且达成了一致可提交，即不允许被覆盖。&lt;/p&gt;&#xA;&lt;p&gt;这就是矛盾的地方，即允许“提交之前任期的日志”，最终导致了违反协议规则的情况。&lt;/p&gt;&#xA;&lt;p&gt;那么，如何确保新当选的leader节点，其本地的未提交日志被正确提交呢？图(d2)展示了正常的情况：即当选之后，不要首先提交本地已有的黄色日志2，而是首先提交一条新日志4，如果这条新日志被提交成功，那么按照Raft日志的匹配规则（log matching property）：日志4如果能提交，它前面的日志也提交了。&lt;/p&gt;&#xA;&lt;p&gt;可是，新的问题又出现了，如果在(d2)中，S1重新当选之后，客户端写入没有这条新的日志4，那么前面的日志2是不是永远无法提交了？为了解决这个问题，raft要求每个leader新当选之后，马上写入一条只有任期号和索引、而没有内容的所谓“no-op”日志，以这条日志来驱动在它之前的日志达成一致。&lt;/p&gt;&#xA;&lt;p&gt;这就是论文中这部分内容想要表达的。这部分内容之所以比较难理解，是因为经常忽略了这个图示展示的是错误的情况，允许“提交之前任期的日志”可能导致的问题。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;其他疑问&#34;&gt;&#xA;  其他疑问&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%85%b6%e4%bb%96%e7%96%91%e9%97%ae&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;和-有什么区别&#34;&gt;&#xA;  \(c\)和\(d2\) 有什么区别？&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%92%8c-%e6%9c%89%e4%bb%80%e4%b9%88%e5%8c%ba%e5%88%ab&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;看起来，\(c\)和\(d2\)一样，S1当选后都提交了日志1、2、4，那么两者的区别在哪里？&lt;/p&gt;&#xA;&lt;p&gt;虽然两个场景中，提交的日志都是一样的，但是日志达成一致的顺序并不一致：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;\(c\)：S1成为leader之后，先提交过往任期、本地的日志2，再提交日志4。这就是“提交之前任期日志”的情况。&lt;/li&gt;&#xA;&lt;li&gt;\(d2\)：S1成为leader之后，先提交本次任期的日志4，如果日志4能提交成功，那么它前面的日志2就能提交成功了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;关于\(d2\)的这个场景，有可能又存在着下一个疑问：&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;如何理解中本任期的日志4提交成功那么它前面的日志2也能提交成功了&#34;&gt;&#xA;  如何理解\(d2\)中，“本任期的日志4提交成功，那么它前面的日志2也能提交成功了”？&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a6%82%e4%bd%95%e7%90%86%e8%a7%a3%e4%b8%ad%e6%9c%ac%e4%bb%bb%e6%9c%9f%e7%9a%84%e6%97%a5%e5%bf%974%e6%8f%90%e4%ba%a4%e6%88%90%e5%8a%9f%e9%82%a3%e4%b9%88%e5%ae%83%e5%89%8d%e9%9d%a2%e7%9a%84%e6%97%a5%e5%bf%972%e4%b9%9f%e8%83%bd%e6%8f%90%e4%ba%a4%e6%88%90%e5%8a%9f%e4%ba%86&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;这是由raft日志的&lt;code&gt;Log Matching Property&lt;/code&gt;决定的:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;If two entries in different logs have the same index and term, then they store the same command.&#xA;If two entries in different logs have the same index and term, then the logs are identical in all preceding entries.&lt;/li&gt;&#xA;&lt;li&gt;If two entries in different logs have the same index and term, then the logs are identical in all preceding entries.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;第一条性质，说明的是在不同节点上的已提交的日志，如果任期号、索引一样，那么它们的内容肯定一样。这是由leader节点的安全性和leader上的日志只能添加不能覆盖来保证的，这样leader就永远不会在同一个任期，创建两个相同索引的日志。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Etcd Raft库的日志存储</title>
      <link>https://www.codedump.info/zh/post/20210628-etcd-wal/</link>
      <pubDate>Mon, 28 Jun 2021 17:01:53 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20210628-etcd-wal/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;之前看etcd raft实现的时候，由于wal以及日志的落盘存储部分，没有放在raft模块中，对这部分没有扣的特别细致。而且，以前我的观点认为etcd raft把WAL这部分留给了上层的应用去实现，自身通过&lt;code&gt;Ready&lt;/code&gt;结构体来通知应用层落盘的数据，这个观点也有失偏颇，etcd只是没有把这部分代码放在raft模块中，属于代码组织的范畴问题，并不是需要应用层自己来实现。&lt;/p&gt;&#xA;&lt;p&gt;于是，决定专门写一篇文章把这部分内容给讲解一下，主要涉及以下内容：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;日志（包括快照）文件的格式。&lt;/li&gt;&#xA;&lt;li&gt;日志（包括快照）内容的落盘、恢复。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以前的系列文章可以在下面的链接中找到，本文不打算过多重复原理性的内容：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20210515-raft/&#34;&gt;Etcd Raft库的工程化实现 - codedump的网络日志&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;wal及快照文件格式&#34;&gt;&#xA;  WAL及快照文件格式&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#wal%e5%8f%8a%e5%bf%ab%e7%85%a7%e6%96%87%e4%bb%b6%e6%a0%bc%e5%bc%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;首先来讲解这两种文件的格式，了解了格式才能继续展开下面的讲述。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;wal文件格式&#34;&gt;&#xA;  WAL文件格式&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#wal%e6%96%87%e4%bb%b6%e6%a0%bc%e5%bc%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;wal文件的文件名格式为：seq-index.wal（见函数&lt;code&gt;walName&lt;/code&gt;）。其中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;seq：序列号，从0开始递增。&lt;/li&gt;&#xA;&lt;li&gt;index：该wal文件存储的第一条日志数据的索引。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;因此，如果将一个目录下的所有wal文件按照名称排序之后，给定一个日志索引，很快就能知道该索引的日志落在哪个wal文件之中的。&lt;/p&gt;&#xA;&lt;p&gt;WAL文件中每条记录的格式如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-proto&#34; data-lang=&#34;proto&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;message&lt;/span&gt; &lt;span style=&#34;color:#666;font-weight:bold;font-style:italic&#34;&gt;Record&lt;/span&gt; {&lt;span style=&#34;&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;&#34;&gt;&lt;/span&gt;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;optional&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;int64&lt;/span&gt; type  = 1 [(gogoproto.nullable) = &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;false&lt;/span&gt;];&lt;span style=&#34;&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;&#34;&gt;&lt;/span&gt;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;optional&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;uint32&lt;/span&gt; crc  = 2 [(gogoproto.nullable) = &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;false&lt;/span&gt;];&lt;span style=&#34;&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;&#34;&gt;&lt;/span&gt;&#x9;&lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;optional&lt;/span&gt; &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;bytes&lt;/span&gt; data  = 3;&lt;span style=&#34;&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;&#34;&gt;&lt;/span&gt;}&lt;span style=&#34;&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;&#xA;&lt;li&gt;type：记录的类型，下面解释。&lt;/li&gt;&#xA;&lt;li&gt;crc：后面data部分数据的crc32校验值。&lt;/li&gt;&#xA;&lt;li&gt;data：数据部分，根据类型的不同有不同格式的数据。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;记录数据的类型如下：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;font-weight:bold;font-style:italic;text-decoration:underline&#34;&gt;const&lt;/span&gt; (&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 以下是WAL存放的数据类型&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 元数据&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;metadataType &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;int64&lt;/span&gt; = &lt;span style=&#34;font-weight:bold;text-decoration:underline&#34;&gt;iota&lt;/span&gt; + 1&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 日志数据&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;entryType&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 状态数据&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;stateType&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 校验初始值&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;crcType&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;&lt;span style=&#34;color:#888;font-style:italic&#34;&gt;// 快照数据&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#x9;snapshotType&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;下面展开解释。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Etcd Raft库的工程化实现</title>
      <link>https://www.codedump.info/zh/post/20210515-raft/</link>
      <pubDate>Sat, 15 May 2021 13:52:08 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20210515-raft/</guid>
      <description>&lt;p&gt;最近回顾前几年写的Raft、etcd raft的实现文章，以及重新阅读Raft论文、etcd raft代码，发现之前有些理解不够准确、深刻，但是不打算在原文上做修正，于是写这篇补充的文章做一些另外角度的解释，以前的系列文章可以在下面的链接中找到，本文不打算过多重复原理性的内容：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在开始展开讨论前，先介绍这个Raft论文中的示意图，我认为能理解这幅图才能对一致性算法有个全貌的了解：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;Etcd Raft与应用层的交互&#34; src=&#34;https://www.codedump.info/media/imgs/20210515-raft/statemachine.jpeg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; Etcd Raft与应用层的交互 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;图中分为两种进程：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;server进程：server进程中运行着一致性算法模块、持久化保存的日志、以及按照日志提交的顺序来进行顺序操作的状态机。&lt;/li&gt;&#xA;&lt;li&gt;client进程：用于向server提交日志的进程。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;需要说明的是，两种进程都用叠加的矩形来表示，意指系统中这两类进程不止一个。&lt;/p&gt;&#xA;&lt;p&gt;一个日志要被正确的提交，图中划分了几步：&lt;/p&gt;&#xA;&lt;p&gt;1、client进程提交数据到server进程，server进程将收到的日志数据灌入一致性模块。&lt;/p&gt;&#xA;&lt;p&gt;2、一致性模块将日志写入本地WAL，然后同步给集群中其他server进程。&lt;/p&gt;&#xA;&lt;p&gt;3、多个节点对某条日志达成一致之后，将修改本地的提交日志索引（commit index）；落盘后的日志按照顺序灌入状态机，只要保证所有server进程上的日志顺序，那么最后状态机的状态肯定就是一致的了。&lt;/p&gt;&#xA;&lt;p&gt;4、灌入状态机之后，server进程可以应答客户端。&lt;/p&gt;&#xA;&lt;p&gt;所以，本质上，一个使用了一致性算法的库，划分了成了两个不同的模块：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;一致性算法库，这里泛指Raft、Paxos、Zab等一致性协议。这类一致性算法库主要做如下的事情：&#xA;&lt;ul&gt;&#xA;&lt;li&gt;用户输入库中日志（log），由库根据各自的算法来检测日志的正确性，并且通知上层的应用层。&#xA;&lt;ul&gt;&#xA;&lt;li&gt;输入到库中的日志维护和管理，算法库中需要知道哪些日志提交、提交成功、以及上层的应用层已经applied过的。当发生错误的时候，某些日志还会进行回滚（rollback）操作。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;日志的网络收发，这部分属于可选功能。有一些库，比如braft把这个事情也揽过来自己做了，优点是使用者不需要关注这部分功能，缺点是braft和它自带的网络库brpc耦合的很紧密，不可能拆开来使用；另一些raft实现，比如这里重点提到etcd raft实现，并不自己完成网络数据收发的工作，而是通知应用层，由应用层自己实现。&lt;/li&gt;&#xA;&lt;li&gt;日志的持久化存储：这部分也属于可选功能。前面说过，一致性算法库中维护了未达成一致的日志缓冲区，达成一致的日志才通知应用层，因此在这里不同的算法库又有了分歧，braft也是自己完成了日志持久化的工作，etcd raft则是将这部分工作交给了应用层。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;应用层：即工作在一致性算法之上的库使用者，这个就比上图中的“状态机”：只有达成一致并且落盘的数据才灌入应用层，只要保证灌入应用层的日志顺序一致那么最后的状态就是一致的。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;总体来看，一个一致性算法库有以下必选和可选功能：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;输入日志进行处理的算法（必选）。&lt;/li&gt;&#xA;&lt;li&gt;日志的维护和管理（必选）。&lt;/li&gt;&#xA;&lt;li&gt;日志（包括快照）数据的网络收发（可选）。&lt;/li&gt;&#xA;&lt;li&gt;日志（包括快照）的持久化存储（可选）。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;需要特别说明的是，即便是后面两个工作是可选的，但是可选还是必选的区别在于，这部分工作是一致性算法库自己完成，还是由算法库通知给上面的应用层去完成，并不代表这部分工作可以完全不做。&lt;/p&gt;&#xA;&lt;p&gt;在下表中列列举了etcd raft和braft在这几个特性之间的区别：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;功能&lt;/th&gt;&#xA;          &lt;th&gt;etcd raft&lt;/th&gt;&#xA;          &lt;th&gt;braft&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;raft一致性算法&lt;/td&gt;&#xA;          &lt;td&gt;实现&lt;/td&gt;&#xA;          &lt;td&gt;实现&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;日志的维护和管理&lt;/td&gt;&#xA;          &lt;td&gt;实现&lt;/td&gt;&#xA;          &lt;td&gt;实现&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;日志数据的网络收发&lt;/td&gt;&#xA;          &lt;td&gt;交由应用层&lt;/td&gt;&#xA;          &lt;td&gt;自己实现&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;日志数据的持久化存储&lt;/td&gt;&#xA;          &lt;td&gt;交由应用层&lt;/td&gt;&#xA;          &lt;td&gt;自己实现&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;优缺点&lt;/td&gt;&#xA;          &lt;td&gt;松耦合，易于验证、测试；需要应用者做更多的事情&lt;/td&gt;&#xA;          &lt;td&gt;与其rpc库紧耦合，难拆分；应用层做的事情不多，易于用来做服务&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;两种实现各有自己的优缺点，braft类实现更适合提供一个需要集成raft的服务时，可以直接用来实现服务；etcd raft类的实现，由于与网络、存储层耦合不紧密，易于进行测试，更适合拿来做为库使用。&lt;/p&gt;&#xA;&lt;p&gt;如果把前面的一致性算法的几个特性做一个抽象，我认为一致性算法库本质上就是一个“维护操作日志的算法库，只要大家都按照相同的顺序将日志灌入应用层”就好，其工作原理大体如下图：&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;一致性算法的本质&#34; src=&#34;https://www.codedump.info/media/imgs/20210515-raft/co-algo.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 一致性算法的本质 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如果把问题抽象成这样的话，那么本质上，所谓的“一致性算法库”跟一个经常看到的tcp、kcp甚至是一个应用层的协议栈也就没有什么区别了：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;大家都要维护一个数据区：只有确认过正确的，才会抛给上一层。以TCP协议算法来说，比如发送但未确认的数据由协议栈的缓冲区维护，如果超时还未等到对端的确认，将发起超时重传等，这些都是每种协议算法的具体细节，但是本质上这些协议都要维护一个未确认数据的缓冲区。一致性算法在数据的维护上会更复杂一些，一是参与确认的节点不止通信的C/S两端，需要集群中半数以上节点的确认；同时，在未确认之前日志需要首先落盘，在提交成功之后再抛给应用层。&lt;/li&gt;&#xA;&lt;li&gt;只要保证所有参与的节点，都以相同的数据灌入日志给应用层，那么得到的结果将最终一致。&lt;/li&gt;&#xA;&lt;li&gt;确认的流程是可以pipeline异步化的，提交日志的进程并不需要一直等待日志被提交成功，而是提交之后等待。不妨以下面的流程来做解释：&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;流水线异步化的日志提交流程&#34; src=&#34;https://www.codedump.info/media/imgs/20210515-raft/pipeline.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 流水线异步化的日志提交流程 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;其中：&lt;/p&gt;</description>
    </item>
    <item>
      <title>Etcd存储的实现</title>
      <link>https://www.codedump.info/zh/post/20181125-etcd-server/</link>
      <pubDate>Sun, 25 Nov 2018 15:13:28 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20181125-etcd-server/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;本文是博客解析raft算法及etcd raft库实现的系列三篇文章之一，之所以详细结合etcd实现解析raft算法原理及实现，因为etcd的raft实现是最接近论文本身的，结合论文原理一起阅读十分酸爽。这个系列文章的索引如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20210515-raft/&#34;&gt;Etcd Raft库的工程化实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;另外，我个人还针对etcd 3.1.10版本的raft相关代码实现做了一些代码的注释笔记，地址在此：&lt;a href=&#34;https://github.com/lichuang/etcd-3.1.10-codedump&#34;&gt;etcd-3.1.10-codedump&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;概览&#34;&gt;&#xA;  概览&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%a7%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;在前面已经分析了Raft算法原理、etcd raft库的实现，接着就可以看etcd如何使用raft实现存储服务的了。&lt;/p&gt;&#xA;&lt;p&gt;以下的分析主要针对etcd V3版本的实现。&lt;/p&gt;&#xA;&lt;p&gt;下图中展示了etcd如何处理一个客户端请求的涉及到的模块和流程。图中淡紫色的矩形表示etcd，它包括如下几个模块：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;etcd server：对外接收客户端的请求，对应etcd代码中的etcdserver目录，其中还有一个raft.go的模块与etcd-raft库进行通信。etcdserver中与存储相关的模块是applierV3，这里封装了V3版本的数据存储，WAL（write ahead log），用于写数据日志，etcd启动时会根据这部分内容进行恢复。&lt;/li&gt;&#xA;&lt;li&gt;etcd raft：etcd的raft库，前面的文章已经具体分析过这部分代码。除了与本节点的etcd server通信之外，还与集群中的其他etcd server进行交互做一致性数据同步的工作（在图中集群中其他etcd服务用橙色的椭圆表示）。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;etcd server&#34; src=&#34;https://www.codedump.info/media/imgs/20181125-etcd-server/etcd-server.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; etcd server &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中，一个请求与一个etcd集群交互的主要流程分为两大部分：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;写数据到某个etcd server中。&lt;/li&gt;&#xA;&lt;li&gt;该etcd server与集群中的其他etcd节点进行交互，当确保数据已经被存储之后应答客户端。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;请求流程划分为了以下的子步骤：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;1.1：etcd server收到客户端请求。&lt;/li&gt;&#xA;&lt;li&gt;1.2：etcd server将请求发送给本模块中的raft.go，这里负责与etcd raft模块进行通信。&lt;/li&gt;&#xA;&lt;li&gt;1.3：raft.go将数据封装成raft日志的形式提交给raft模块。&lt;/li&gt;&#xA;&lt;li&gt;1.4：raft模块会首先保存到raftLog的unstable存储部分。&lt;/li&gt;&#xA;&lt;li&gt;1.5：raft模块通过raft协议与集群中其他etcd节点进行交互。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;注意在以上流程中，假设这里写入数据的etcd是leader节点，因为在raft协议中，如果提交数据到非leader节点的话需要路由到etcd leader节点去。&lt;/p&gt;&#xA;&lt;p&gt;而应答步骤如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;2.1：集群中其他节点向leader节点应答接收这条日志数据。&lt;/li&gt;&#xA;&lt;li&gt;2.2：当超过集群半数以上节点应答接收这条日志数据时，etcd raft通过Ready结构体通知etcd server中的raft该日志数据已经commit。&lt;/li&gt;&#xA;&lt;li&gt;2.3：raft.go收到Ready数据将首先将这条日志写入到WAL模块中。&lt;/li&gt;&#xA;&lt;li&gt;2.4：通知最上层的etcd server该日志已经commit。&lt;/li&gt;&#xA;&lt;li&gt;2.5：etcd server调用applierV3模块将日志写入持久化存储中。&lt;/li&gt;&#xA;&lt;li&gt;2.6：etcd server应答客户端该数据写入成功。&lt;/li&gt;&#xA;&lt;li&gt;2.7：最后etcd server调用etcd raft，修改其raftLog模块的数据，将这条日志写入到raftLog的storage中。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;从上面的流程可以看到&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;etcd raft模块在应答某条日志数据已经commit之后，是首先写入到WAL模块中的，因为这个模块只是添加一条日志，所以速度会很快，即使在后面applierV3写入失败，重启的时候也可以根据WAL模块中的日志数据进行恢复。&lt;/li&gt;&#xA;&lt;li&gt;etcd raft中的raftLog，按照前面文章的分析，其中的数据是保存到内存中的，重启即失效，上层应用真实的数据是持久化保存到WAL和applierV3中的。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;以下就来分析etcd server与这部分相关的几个模块。&lt;/p&gt;</description>
    </item>
    <item>
      <title>etcd Raft库解析</title>
      <link>https://www.codedump.info/zh/post/20180922-etcd-raft/</link>
      <pubDate>Sat, 22 Sep 2018 11:01:02 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20180922-etcd-raft/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;本文是博客解析raft算法及etcd raft库实现的系列三篇文章之一，之所以详细结合etcd实现解析raft算法原理及实现，因为etcd的raft实现是最接近论文本身的，结合论文原理一起阅读十分酸爽。这个系列文章的索引如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20210515-raft/&#34;&gt;Etcd Raft库的工程化实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;另外，我个人还针对etcd 3.1.10版本的raft相关代码实现做了一些代码的注释笔记，地址在此：&lt;a href=&#34;https://github.com/lichuang/etcd-3.1.10-codedump&#34;&gt;etcd-3.1.10-codedump&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;序言&#34;&gt;&#xA;  序言&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ba%8f%e8%a8%80&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;今年初开始学习了解Raft协议，论文读下来之后还是决定结合一个成熟的代码进行更深的理解。etcd做为一个非常成熟的作品，其Raft库实现也非常精妙，屏蔽了网络、存储等模块，提供接口由上层应用者来实现。&lt;/p&gt;&#xA;&lt;p&gt;本篇文章解析etcd的Raft库实现，基于etcd 3.1.10版本。etcd的Raft库，位于其代码目录的Raft中。&lt;/p&gt;&#xA;&lt;p&gt;我自己也单独将3.1.10的代码拉出了一个专门添加了我阅读代码注释的版本，目前Raft这部分基本都做了注释，见：&#xA;&lt;a href=&#34;https://github.com/lichuang/etcd-3.1.10-codedump&#34;&gt;https://github.com/lichuang/etcd-3.1.10-codedump&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;以下在介绍的时候，可能会混用中文和英文术语，这里先列举出来：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;英文&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: right&#34;&gt;中文&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Term&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;选举任期，每次选举之后递增1&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Vote&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;选举投票(的ID)&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Entry&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;Raft算法的日志数据条目&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;candidate&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;候选人&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;leader&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;领导者&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;follower&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;跟随者&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;commit&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;提交&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;propose&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;提议&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;输入及输出&#34;&gt;&#xA;  输入及输出&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%be%93%e5%85%a5%e5%8f%8a%e8%be%93%e5%87%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;既然做为一个库使用，就有其确定的输入和输出接口，先来了解这部分再进行后续的展开讨论。&lt;/p&gt;&#xA;&lt;p&gt;作为一个一致性算法的库，不难想象使用的一般场景是这样的：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;应用层接收到新的写入数据请求，向该算法库写入一个数据。&lt;/li&gt;&#xA;&lt;li&gt;算法库返回是否写入成功。&lt;/li&gt;&#xA;&lt;li&gt;应用层根据写入结果进行下一步的操作。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;然而，Raft库却相对而言更复杂一些，因为还有以下的问题存在：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;写入的数据，可能是集群状态变更的数据，Raft库在执行写入这类数据之后，需要返回新的状态给应用层。&lt;/li&gt;&#xA;&lt;li&gt;Raft库中的数据不可能一直以日志的形式存在，这样会导致数据越来越大，所以有可能被压缩成快照（snapshot）的数据形式，这种情况下也需要返回这部分快照数据。&lt;/li&gt;&#xA;&lt;li&gt;由于etcd的Raft库不包括持久化数据存储相关的模块，而是由应用层自己来做实现，所以也需要返回在某次写入成功之后，哪些数据可以进行持久化保存了。&lt;/li&gt;&#xA;&lt;li&gt;同样的，etcd的Raft库也不自己实现网络传输，所以同样需要返回哪些数据需要进行网络传输给集群中的其他节点。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;以上的这些，集中在raft/node.go的Ready结构体中，其包括以下成员：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: left&#34;&gt;成员名称&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: right&#34;&gt;类型&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;作用&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;SoftState&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;SoftState&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;软状态，软状态易变且不需要保存在WAL日志中的状态数据，包括：集群leader、节点的当前状态&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;HardState&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;HardState&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;硬状态，与软状态相反，需要写入持久化存储中，包括：节点当前Term、Vote、Commit&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;ReadStates&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;[]ReadStates&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;用于读一致性的数据，后续会详细介绍&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Entries&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;[]pb.Entry&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;在向其他集群发送消息之前需要先写入持久化存储的日志数据&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Snapshot&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;pb.Snapshot&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;需要写入持久化存储中的快照数据&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;CommittedEntries&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;[]pb.Entry&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;需要输入到状态机中的数据，这些数据之前已经被保存到持久化存储中了&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: left&#34;&gt;Messages&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: right&#34;&gt;[]pb.Message&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;在entries被写入持久化存储中以后，需要发送出去的数据&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;以上的成员说明，最开始看不一定能理解其含义和用法，不过在后续会慢慢展开讨论。&lt;/p&gt;</description>
    </item>
    <item>
      <title>Raft算法原理</title>
      <link>https://www.codedump.info/zh/post/20180921-raft/</link>
      <pubDate>Fri, 21 Sep 2018 20:15:32 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20180921-raft/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;概述&#34;&gt;&#xA;  概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;本文是博客解析raft算法及etcd raft库实现的系列三篇文章之一，之所以详细结合etcd实现解析raft算法原理及实现，因为etcd的raft实现是最接近论文本身的，结合论文原理一起阅读十分酸爽。这个系列文章的索引如下：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180921-raft/&#34;&gt;Raft算法原理&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20180922-etcd-raft/&#34;&gt;etcd Raft库解析&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20181125-etcd-server/&#34;&gt;Etcd存储的实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.codedump.info/post/20210515-raft/&#34;&gt;Etcd Raft库的工程化实现&lt;/a&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;另外，我个人还针对etcd 3.1.10版本的raft相关代码实现做了一些代码的注释笔记，地址在此：&lt;a href=&#34;https://github.com/lichuang/etcd-3.1.10-codedump&#34;&gt;etcd-3.1.10-codedump&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;简介&#34;&gt;&#xA;  简介&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%ae%80%e4%bb%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;关于Raft算法，有两篇经典的论文，一篇是《In search of an Understandable Consensus Algorithm》，这是作者最开始讲述Raft算法原理的论文，但是这篇论文太简单了，很多算法的细节没有涉及到。更详细的论文是《Consensus: Bridging Theory and Practice》，除了包括第一篇论文的内容以外，还加上了很多细节的描述。在我阅读完etcd raft算法库的实现之后，发现这个库的代码基本就是按照后一篇论文来写的，甚至有部分测试用例的注释里也写明了是针对这篇论文的某一个小节的情况做验证。&lt;/p&gt;&#xA;&lt;p&gt;这篇文章做为我后续分析etcd raft算法的前导文章，将结合后一篇论文加上一些自己的演绎和理解来讲解Raft算法的原理。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;算法的基本流程&#34;&gt;&#xA;  算法的基本流程&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%ae%97%e6%b3%95%e7%9a%84%e5%9f%ba%e6%9c%ac%e6%b5%81%e7%a8%8b&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;raft算法概述&#34;&gt;&#xA;  Raft算法概述&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#raft%e7%ae%97%e6%b3%95%e6%a6%82%e8%bf%b0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;Raft算法由leader节点来处理一致性问题。leader节点接收来自客户端的请求日志数据，然后同步到集群中其它节点进行复制，当日志已经同步到超过半数以上节点的时候，leader节点再通知集群中其它节点哪些日志已经被复制成功，可以提交到raft状态机中执行。&lt;/p&gt;&#xA;&lt;p&gt;通过以上方式，Raft算法将要解决的一致性问题分为了以下几个子问题。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;leader选举：集群中必须存在一个leader节点。&lt;/li&gt;&#xA;&lt;li&gt;日志复制：leader节点接收来自客户端的请求然后将这些请求序列化成日志数据再同步到集群中其它节点。&lt;/li&gt;&#xA;&lt;li&gt;安全性：如果某个节点已经将一条提交过的数据输入raft状态机执行了，那么其它节点不可能再将相同索引&#xA;的另一条日志数据输入到raft状态机中执行。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Raft算法需要一直保持的几个属性。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;选举安全性（Election Safety）：在一个任期内只能存在最多一个leader节点。&lt;/li&gt;&#xA;&lt;li&gt;Leader节点上的日志为只添加（Leader Append-Only）：leader节点永远不会删除或者覆盖本节点上面的日志数据，leader节点上写日志的操作只可能是添加操作。&lt;/li&gt;&#xA;&lt;li&gt;日志匹配性（Log Matching）：如果两个节点上的日志，在日志的某个索引上的日志数据其对应的任期号相同，那么在两个节点在这条日志之前的日志数据完全匹配。&lt;/li&gt;&#xA;&lt;li&gt;leader完备性（Leader Completeness）：如果一条日志在某个任期被提交，那么这条日志数据在leader节点上更高任期号的日志数据中都存在。&lt;/li&gt;&#xA;&lt;li&gt;状态机安全性（State Machine Safety）：如果某个节点已经将一条提交过的数据输入raft状态机执行了，那么其它节点不可能再将相同索引的另一条日志数据输入到raft状态机中执行。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;raft-propertities&#34; src=&#34;https://www.codedump.info/media/imgs/20180921-raft/raft-propertities.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; raft-propertities &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;raft算法基础&#34;&gt;&#xA;  Raft算法基础&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#raft%e7%ae%97%e6%b3%95%e5%9f%ba%e7%a1%80&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在Raft算法中，一个集群里面的所有节点有以下三种状态：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Leader：领导者，一个集群里只能存在一个Leader。&lt;/li&gt;&#xA;&lt;li&gt;Follower：跟随者，follower是被动的，一个客户端的修改数据请求如果发送到Follower上面时，会首先由Follower重定向到Leader上，&lt;/li&gt;&#xA;&lt;li&gt;Candidate：参与者，一个节点切换到这个状态时，将开始进行一次新的选举。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;每一次开始一次新的选举时，称为一个“任期”。每个任期都有一个对应的整数与之关联，称为“任期号”，任期号用单词“Term”表示，这个值是一个严格递增的整数值。&lt;/p&gt;&#xA;&lt;p&gt;节点的状态切换状态机如下图所示。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&#xA;&lt;figure class=&#34;&#34;&gt;&#xA;&#xA;    &lt;div&gt;&#xA;        &lt;img loading=&#34;lazy&#34; alt=&#34;raft states&#34; src=&#34;https://www.codedump.info/media/imgs/20180921-raft/raft-states.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; raft states &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中标记了状态切换的6种路径，下面做一个简单介绍，后续都会展开来详细讨论。&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;start up：起始状态，节点刚启动的时候自动进入的是follower状态。&lt;/li&gt;&#xA;&lt;li&gt;times out, starts election：follower在启动之后，将开启一个选举超时的定时器，当这个定时器到期时，将切换到candidate状态发起选举。&lt;/li&gt;&#xA;&lt;li&gt;times out, new election：进入candidate 状态之后就开始进行选举，但是如果在下一次选举超时到来之前，都还没有选出一个新的leade，那么还会保持在candidate状态重新开始一次新的选举。&lt;/li&gt;&#xA;&lt;li&gt;receives votes from majority of servers：当candidate状态的节点，收到了超过半数的节点选票，那么将切换状态成为新的leader。&lt;/li&gt;&#xA;&lt;li&gt;discovers current leader or new term：candidate状态的节点，如果收到了来自leader的消息，或者更高任期号的消息，都表示已经有leader了，将切换回到follower状态。&lt;/li&gt;&#xA;&lt;li&gt;discovers server with higher term：leader状态下如果收到来自更高任期号的消息，将切换到follower状态。这种情况大多数发生在有网络分区的状态下。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;如果一个candidate在一次选举中赢得leader，那么这个节点将在这个任期中担任leader的角色。但并不是每个任期号都一定对应有一个leader的，比如上面的情况3中，可能在选举超时到来之前都没有产生一个新的leader，那么此时将递增任期号开始一次新的选举。&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
