<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>分布式 on codedump notes</title>
    <link>https://www.codedump.info/zh/tags/%E5%88%86%E5%B8%83%E5%BC%8F/</link>
    <description>Recent content in 分布式 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/%E5%88%86%E5%B8%83%E5%BC%8F/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>周刊（第22期）：图解一致性模型</title>
      <link>https://www.codedump.info/zh/post/20220710-weekly-22/</link>
      <pubDate>Sun, 10 Jul 2022 14:41:24 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220710-weekly-22/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：本文使用大量的图例，同时没有难懂的公式，意图解释清楚一致性模型要解决什么问题，以及三种一致性模型：顺序一致性、线性一致性、因果一致性。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;图解一致性模型&#34;&gt;&#xA;  图解一致性模型&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9b%be%e8%a7%a3%e4%b8%80%e8%87%b4%e6%80%a7%e6%a8%a1%e5%9e%8b&#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;#%e6%a6%82%e8%bf%b0&#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;#%e8%a7%a3%e5%86%b3%e4%bb%80%e4%b9%88%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;分布式系统要保证系统的可用性，就需要对数据提供一定的冗余度：一份数据，要存储在多个服务器上，才能认为保存成功，至于这里要保存的冗余数，有&lt;code&gt;Majority&lt;/code&gt;和&lt;code&gt;Quorum&lt;/code&gt;之说，可以参考之前的文章：&lt;a href=&#34;https://www.codedump.info/post/20220528-weekly-17/&#34;&gt;周刊（第17期）：Read-Write Quorum System及在Raft中的实践&lt;/a&gt;。&lt;/p&gt;&#xA;&lt;p&gt;同一份数据保存在多个机器上提供冗余度，也被称为&lt;code&gt;副本(replica)策略&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;p&gt;很显然，即便在一切都正常工作的条件下，在系统中的一个机器成功写入了数据，因为广播这个修改到系统中的其他机器还需要时间，那么系统的其他机器看到这个修改的结果也还是需要时间的。换言之，中间的这个&lt;code&gt;时间差&lt;/code&gt;可能出现短暂的数据不一致的情况。&lt;/p&gt;&#xA;&lt;p&gt;可以看到，由于这个&lt;code&gt;时间差&lt;/code&gt;的客观存在，并不存在一个&lt;code&gt;绝对&lt;/code&gt;意义上的数据一致性。换言之，&lt;code&gt;数据一致性&lt;/code&gt;有其实现的严格范围，越严格的数据一致，要付出的成本、代价就越大。&lt;/p&gt;&#xA;&lt;p&gt;为了解决一致性问题，需要首先定义一致性模型，在维基的页面上，&lt;code&gt;一致性模型（Consistency model）&lt;/code&gt;的定义如下：&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;In computer science, a consistency model specifies a contract between the programmer and a system, wherein the system guarantees that if the programmer follows the rules for operations on memory, memory will be consistent and the results of reading, writing, or updating memory will be predictable.&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;我们举一个日常生活中常见的问题来解释&lt;code&gt;一致性模型&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;wechat&#34; src=&#34;https://www.codedump.info/media/imgs/20220710-weekly-22/wechat.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; wechat &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;</description>
    </item>
    <item>
      <title>周刊（第21期）：Lamport时钟介绍</title>
      <link>https://www.codedump.info/zh/post/20220703-weekly-21/</link>
      <pubDate>Sun, 03 Jul 2022 10:59:09 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20220703-weekly-21/</guid>
      <description>&lt;hr&gt;&#xA;&lt;p&gt;引言：在分布式系统中，由于有多个机器（进程）在一起协调工作，于是如何定义分布式系统中事件的先后顺序就成了难题，本文介绍论文 &lt;a href=&#34;https://lamport.azurewebsites.net/pubs/time-clocks.pdf&#34;&gt;《Time, Clocks, and the Ordering of Events in a Distributed System》&lt;/a&gt;中提到的Lamport时钟。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;lamport时钟介绍&#34;&gt;&#xA;  Lamport时钟介绍&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#lamport%e6%97%b6%e9%92%9f%e4%bb%8b%e7%bb%8d&#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;#%e6%a6%82%e8%ae%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在分布式系统中，由于有多个机器（进程）在一起协调工作，于是如何定义分布式系统中事件的先后顺序就成了难题，本文介绍论文 &lt;a href=&#34;https://lamport.azurewebsites.net/pubs/time-clocks.pdf&#34;&gt;《Time, Clocks, and the Ordering of Events in a Distributed System》&lt;/a&gt;中提到的Lamport时钟。&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;li&gt;Lamport时钟的原理介绍、&lt;code&gt;happen-before&lt;/code&gt;关系介绍。（原理介绍）&lt;/li&gt;&#xA;&lt;li&gt;分布式一致性的基础。（更远的影响）&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;物理时钟的问题&#34;&gt;&#xA;  物理时钟的问题&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%89%a9%e7%90%86%e6%97%b6%e9%92%9f%e7%9a%84%e9%97%ae%e9%a2%98&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;分布式系统中定义一个事件的先后顺序是一个难点，下意识的第一反应是：给每个事件加上一个物理的时间戳，不就可以比较不同事件的时间戳来决定其顺序了吗？&lt;/p&gt;&#xA;&lt;p&gt;这样做的问题在于：在分布式系统中，由多个机器组合起来协调工作，而每个机器上的物理时间也不尽相同，所以“物理时间戳”本质上是一个机器属性，并不一定系统中所有机器都满足同一个时间度量。&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://sookocheff.com/post/time/lamport-clock/assets/clock-adjustment-error.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;（引用自&lt;a href=&#34;https://sookocheff.com/post/time/lamport-clock/&#34;&gt;Lamport Clocks - Kevin Sookocheff&lt;/a&gt;）&lt;/p&gt;&#xA;&lt;p&gt;上图中：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;server A在发出事件A时，打上了本机的时间戳1点。&lt;/li&gt;&#xA;&lt;li&gt;同理，server B给事件B打上了本机的时间戳12:59。&lt;/li&gt;&#xA;&lt;li&gt;可以看到这两个事件都以本地时间为准，当观察者进程收到这两个事件的时候，先后顺序与事件上所带的时间戳并不一致：先收到了时间戳更大的事件A。&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;#%e5%85%a8%e5%ba%8f%e5%92%8c%e5%81%8f%e5%ba%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;在继续讲解之前，还需要了解两个数学上的概念：全序（total ordering）和偏序（partial ordering）关系。&lt;/p&gt;&#xA;&lt;p&gt;我们首先来定义集合上的几种关系，对一个集合${\displaystyle X}$中的${\displaystyle a,b}$和${\displaystyle c}$ 而言，有以下这些关系：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;自反性：∀a∈S，有a≤a。&lt;/li&gt;&#xA;&lt;li&gt;反对称性：若 $ {\displaystyle a\leq b}$且$ {\displaystyle b\leq a} $ 则 $ {\displaystyle a=b} $。&lt;/li&gt;&#xA;&lt;li&gt;传递性：若${\displaystyle a\leq b} $ 且 $ {\displaystyle b\leq c} $ 则 $ {\displaystyle a\leq c} $。&lt;/li&gt;&#xA;&lt;li&gt;完全性：$ {\displaystyle a\leq b} $ 或 $ {\displaystyle b\leq a} $。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&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>服务调用的演进历史</title>
      <link>https://www.codedump.info/zh/post/20190629-service-history/</link>
      <pubDate>Sat, 29 Jun 2019 12:47:07 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190629-service-history/</guid>
      <description>&lt;blockquote&gt;&#xA;&lt;p&gt;这是2019年给组内分享时整理的一篇服务调用演进历史的科普文。写作本文的时候，我自己最大的感受是：如果能清楚理解演化历史中的一些原则和思路，就会发现现在的变化并不新鲜。它们不是今天才有，也不会止于今天的演化。在技术大发展的今天，更多的关注本质才能让我们不至于在变化中失去方向。&lt;/p&gt;&lt;/blockquote&gt;&#xA;&lt;p&gt;这个题目稍微有点大，纯粹是一篇科普文，将我所了解到的解决“服务调用”相关的技术演进历史简述一下，本文专注于演化过程中每一步的为什么（Why）和是什么（What）上面，尽量不在技术细节（How）上面做太多深入。&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%9c%8d%e5%8a%a1%e7%9a%84%e4%b8%89%e8%a6%81%e7%b4%a0&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;一般而言，一个网络服务包括以下的三个要素：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;地址：调用方根据地址访问到网络接口。地址包括以下要素：IP地址、服务端口、服务协议（TCP、UDP，etc）。&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;ul&gt;&#xA;&lt;li&gt;IP地址提供了在互联网上找到这台机器的凭证。&lt;/li&gt;&#xA;&lt;li&gt;协议以及服务端口提供了在这台机器上找到提供服务的进程的凭证。&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;service-address&#34; src=&#34;https://www.codedump.info/media/imgs/20190629-service-history/service-address.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; service address &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;这都属于TCPIP协议栈的知识点，不在这里深入详述。&lt;/p&gt;&#xA;&lt;p&gt;下图中，以最简单的一个HTTP请求，来拆解请求URL中的服务要素：&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;http-request&#34; src=&#34;https://www.codedump.info/media/imgs/20190629-service-history/http-request.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; http-request &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;http：指明使用的是哪种应用层协议，同类型的还有“https”、“ftp”等。&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.abc.com&#34;&gt;www.abc.com&lt;/a&gt;：域名地址，最终会由DNS域名解析服务器解析成数字的IP地址。&lt;/li&gt;&#xA;&lt;li&gt;8080：前面解析成数字化的IP地址之后，就可以访问到具体提供服务的机器上，但是上面提供服务的进程可能有很多，这时候就需要端口号来告诉协议栈到底是访问哪个进程提供的服务了。&lt;/li&gt;&#xA;&lt;li&gt;hello：该服务进程中，可能提供多个接口供访问，所以需要接口名+协议（即前面的http）告诉进程访问哪个协议的哪个接口。&lt;/li&gt;&#xA;&lt;li&gt;msg=world：不同的接口，需要的参数不同，最后跟上的查询参数（query param）告诉服务请求该接口服务时传入的参数。&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;服务实例：服务对应的IP地址加端口的简称。需要访问服务的时候，需要先寻址知道该服务每个运行实例的地址加端口，然后才能建立连接进行访问。&lt;/li&gt;&#xA;&lt;li&gt;服务注册：某个服务实例宣称自己提供了哪些服务，即某个IP地址+端口都提供了哪些服务接口。&lt;/li&gt;&#xA;&lt;li&gt;服务发现：调用方通过某种方式找到服务提供方，即知道服务运行的IP地址加端口。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;基于ip地址的调用&#34;&gt;&#xA;  基于IP地址的调用&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e4%ba%8eip%e5%9c%b0%e5%9d%80%e7%9a%84%e8%b0%83%e7%94%a8&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;最初的网络服务，通过原始的IP地址暴露给调用者。这种方式有以下的问题：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;IP地址是难于记忆并且无意义的。&lt;/li&gt;&#xA;&lt;li&gt;另外，从上面的服务三要素可以看到，IP地址其实是一个很底层的概念，直接对应了一台机器上的一个网络接口，如果直接使用IP地址进行寻址，更换机器就变的很麻烦。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;“尽量不使用过于底层的概念来提供服务”，是这个演化流程中的重要原则，好比在今天已经很少能够看到直接用汇编语言编写代码的场景了，取而代之的，就是越来越多的抽象，本文中就展现了服务调用这一领域在这个过程中的演进流程。&lt;/p&gt;&#xA;&lt;p&gt;在现在除非是测试阶段，否则已经不能直接以IP地址的形式将服务提供出去了。&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%9f%9f%e5%90%8d%e7%b3%bb%e7%bb%9f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;前面的IP地址是给主机做为路由器寻址的数字型标识，并不好记忆。此时产生了域名系统，与单纯提供IP地址相比，域名系统由于使用有意义的域名来标识服务，所以更容易记忆。另外，还可以更改域名所对应的IP地址，这为变换机器提供了便利。有了域名之后，调用方需要访问某个网络服务时，首先到域名地址服务中，根据DNS协议将域名解析为相应的IP地址，再根据返回的IP地址来访问服务。&lt;/p&gt;&#xA;&lt;p&gt;从这里可以看到，由于多了一步到域名地址服务查询映射IP地址的流程，所以多了一步解析，为了减少这一步带来的影响，调用方会缓存解析之后的结果，在一段时间内不过期，这样就省去了这一步查询的代价。&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%8d%8f%e8%ae%ae%e7%9a%84%e6%8e%a5%e6%94%b6%e4%b8%8e%e8%a7%a3%e6%9e%90&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;以上通过域名系统，已经解决了服务IP地址难以记忆的问题，下面来看协议格式解析方面的演进。&lt;/p&gt;&#xA;&lt;p&gt;一般而言，一个网络协议包括两部分：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;协议包头：这里存储协议的元信息（meta infomation），其中可能会包括协议类型、报体长度、协议格式等。需要说明的是，包头一般为固定大小，或者有明确的边界（如HTTP协议中的\r\n结束符），否则无法知道包头何时结束。&lt;/li&gt;&#xA;&lt;li&gt;协议包体：具体的协议内容。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;无论是HTTP协议，又或者是自定义的二进制网络协议，大体都由这两部分组成。&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;protocol-format&#34; src=&#34;https://www.codedump.info/media/imgs/20190629-service-history/protocol-format.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; protocol format &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;&#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;protocol-statemachine&#34; src=&#34;https://www.codedump.info/media/imgs/20190629-service-history/protocol-statemachine.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; protocol statemachine &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;接收完毕了网络数据，在协议解析方面却长期停滞不前。一个协议，有多个字段（field），而这些不同的字段有不同的类型，简单的raw类型（如整型、字符串）还好说，但是遇到复杂的类型如字典、数组等就比较麻烦。&lt;/p&gt;&#xA;&lt;p&gt;当时常见的手段有以下几种：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;使用json或者xml这样的数据格式。好处是可视性强，表达起上面的复杂类型也方便，缺陷是容易被破解，传输过去的数据较大。&lt;/li&gt;&#xA;&lt;li&gt;自定义二进制协议。每个公司做大了，在这一块难免有几个类似的轮子。笔者见过比较典型的是所谓的TLV格式（Type-Length-Value），自定义二进制格式最大的问题出现在协议联调与协商的时候，由于可视性比较弱，有可能这边少了一个字段那边多了一个字段，给联调流程带来麻烦。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;上面的问题一直到Google的Protocol Buffer（以下简称PB）出现之后才得到很大的改善。PB出现之后，也有很多类似的技术出现，如Thrift、MsgPack等，不在这里阐述，将这一类技术都以PB来描述。&lt;/p&gt;&#xA;&lt;p&gt;与前面的两种手段相比，PB具有以下的优点：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;使用proto格式文件来定义协议格式，proto文件是一个典型的DSL（domain-specific language）文件，文件中描述了协议的具体格式，每个字段都是什么类型，哪些是可选字段哪些是必选字段。有了proto文件之后，C\S两端是通过这个文件来进行协议的沟通交流的，而不是具体的技术细节。&lt;/li&gt;&#xA;&lt;li&gt;PB能通过proto文件生成各种语言对应的序列化反序列化代码，给跨语言调用提供了方便。&lt;/li&gt;&#xA;&lt;li&gt;PB自己能够对特定类型进行数据压缩，减少数据大小。&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;pb&#34; src=&#34;https://www.codedump.info/media/imgs/20190629-service-history/pb.png&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; pb &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&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%9c%8d%e5%8a%a1%e7%bd%91%e5%85%b3&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;有了前面的演化之后，写一个简单的单机服务器已经不难。然而，当随着访问量的增大，一台机器已经不足以支撑所有的请求，此时就需要横向扩展多加一些业务服务器。&lt;/p&gt;</description>
    </item>
    <item>
      <title>《数据密集型应用系统设计》第九章《一致性与共识》笔记</title>
      <link>https://www.codedump.info/zh/post/20190406-ddia-chapter09-consistency-and-consensus/</link>
      <pubDate>Thu, 18 Apr 2019 08:40:34 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190406-ddia-chapter09-consistency-and-consensus/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;一致性保证&#34;&gt;&#xA;  一致性保证&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%80%e8%87%b4%e6%80%a7%e4%bf%9d%e8%af%81&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;最终一致性（eventual consistency）：如果停止更新数据，等待一段时间（时间长度未知），则最终所有读请求将返回相同的内容。&lt;/p&gt;&#xA;&lt;p&gt;然而最终一致性是一种非常弱的一致性保证，因为无法知道何时（when）系统会收敛。而在收敛之前，读请求都可能返回任何值。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;可线性化linearizability&#34;&gt;&#xA;  可线性化（Linearizability）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8f%af%e7%ba%bf%e6%80%a7%e5%8c%96linearizability&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;可线性化（Lineariazability），也被称为原子一致性（atomic consistency）、强一致性（strong consistency），其基本的思想是让一个系统看起来好像只有一个数据副本，且所有的操作都是原子的。有了这个保证，应用程序不需要再关系系统内部有多少个副本。&lt;/p&gt;&#xA;&lt;p&gt;在一个可线性化的系统中，一旦客户端成功提交写请求，所有客户端的读请求一定能看到刚刚写入的值。这一保证让客户端认为只有一个副本，这样任何一次读取都能读到最新的值，而不是过期的数据。&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;9-1&#34; src=&#34;https://www.codedump.info/media/imgs/20190406-ddia-chapter09-consistency-and-consensus/9-1.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 9-1 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，alice和bob同时等待2014年世界杯决赛的结果。在宣布最终比分之后，alice看到了最终的结果，然后将此结果告诉了bob，bob马上在自己的手机上刷新想看最新的结果，但是却返回了过期的数据，显示当前比赛还在进行中。&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%a6%82%e4%bd%95%e5%ae%9e%e7%8e%b0%e5%8f%af%e7%ba%bf%e6%80%a7%e5%8c%96&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&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;9-2&#34; src=&#34;https://www.codedump.info/media/imgs/20190406-ddia-chapter09-consistency-and-consensus/9-2.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 9-2 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中，分为两种操作：针对某个值进行read和write操作。&lt;/p&gt;&#xA;&lt;p&gt;客户端A的第一次和最后一次read操作，分别返回0和1，这没有问题，因为在这两次操作中间有客户端C的write操作将数据x更新为了1。&lt;/p&gt;&#xA;&lt;p&gt;但是，在写操作还在进行的时候，如果读操作返回的值会来回的跳变，即某次读请求返回的是旧值，而某一次又返回的是新值，这对于一个可线性化系统而言是不可接受的。&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;9-3&#34; src=&#34;https://www.codedump.info/media/imgs/20190406-ddia-chapter09-consistency-and-consensus/9-3.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 9-3 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;在上图中，箭头表示时序依赖关系。即先有客户端A的第二次read(x)操作，再有客户端B的第二次read(x)操作。客户端A的第二次读请求返回了x的新值1，而客户端B在这次读请求之后也去读x的值，此时应该返回的也是新值1。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;即：在一个可线性化的系统中，有一个很重要的约束条件，在写操作开始和结束之间必然存在一个时间段，此时读到x的值会在旧值与新值之间跳变。但是，如果某个客户端的读请求返回了新值，那么即使这时写操作还未真正完成，后续的所有读请求也应该返回新值。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;以下的例子进一步解释可线性化的操作，除了读写之外又引入另一种操作：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;cas(x, old, new)：表示一次原子的比较-设置操作（compare-and-set，简称CAS），如果此时x的值为old，则原子设置这个值为new；否则保留原有值不变，这个操作的返回值表示这次x原有的值是否为old，即设置操作是否发生。&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;9-4&#34; src=&#34;https://www.codedump.info/media/imgs/20190406-ddia-chapter09-consistency-and-consensus/9-4.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 9-4 &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;客户端B首先read(x)，接下来客户端D write(x,0)，然后客户端A在write(x,1)，而最终返回给客户端B的值是1（客户端A写入的值）。这个结果是可能的，这意味着数据库执行的顺序是：先处理客户端D的写请求，然后是A的写入操作，最后才是B的读请求。虽然这个顺序并不是上面请求的顺序，但是考虑到请求有网络延迟的情况，比如可能B的请求延迟很大，导致在两次写请求之后才打到数据库，因此只能返回最后A写入的值。&lt;/li&gt;&#xA;&lt;li&gt;客户端A在收到写请求的应答之前，B就收到了新的值1，这表明写入成功。这也是可能的，这并不意味着B的读请求在A的写请求之前发生，只是意味着由于网络延迟等原因导致A稍后才收到响应。&lt;/li&gt;&#xA;&lt;li&gt;客户端的最后一次读取不满足线性化。因为在此之前，A已经读到了由C进行cas(x,2,4)操作设置的新值4，B的最后一次读请求在A读取到4之后，因此B不能读到旧值2了。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;注意可线性化（Lineariazability）和可串行化（Serializability）的区别：&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;数据库可以同时支持可串行化与可线性化，这种组合又被称为严格的可串行化或者强的单副本可串行化（strong one-copy Serializability）。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;线性化的依赖条件&#34;&gt;&#xA;  线性化的依赖条件&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%ba%bf%e6%80%a7%e5%8c%96%e7%9a%84%e4%be%9d%e8%b5%96%e6%9d%a1%e4%bb%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&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%e7%ba%bf%e6%80%a7%e5%8c%96%e7%b3%bb%e7%bb%9f&#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;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;无主复制（可能不可线性化）：对于无主节点复制的系统，依赖于具体的quorum配置，以及如何定义强一致性，可能并不能保证线性化。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;线性化与quorum&#34;&gt;&#xA;  线性化与quorum&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%ba%bf%e6%80%a7%e5%8c%96%e4%b8%8equorum&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&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;9-6&#34; src=&#34;https://www.codedump.info/media/imgs/20190406-ddia-chapter09-consistency-and-consensus/9-6.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 9-6 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，x的初始值为0，写客户端向所有三副本（n=3，w=3）写入更新x为1。而客户端A从两个节点（r=2）读数据，其中一个节点返回1，而客户端B则从两个节点都得到了0。&lt;/p&gt;&#xA;&lt;p&gt;显然这是违反线性化要求的：客户端B在客户端A之后读取数据，但是仍然得到了旧值。&lt;/p&gt;&#xA;&lt;p&gt;总而言之，最安全的假定是类似Dynamo风格的无主复制系统无法保证线性化。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;线性化的代价&#34;&gt;&#xA;  线性化的代价&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e7%ba%bf%e6%80%a7%e5%8c%96%e7%9a%84%e4%bb%a3%e4%bb%b7&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;cap理论&#34;&gt;&#xA;  CAP理论&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#cap%e7%90%86%e8%ae%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;在一个数据中心内部，主要存在不可靠的网络，就可能会违背线性化的风险，需要做出权衡考虑：&lt;/p&gt;</description>
    </item>
    <item>
      <title>《数据密集型应用系统设计》第八章《分布式系统的挑战》笔记</title>
      <link>https://www.codedump.info/zh/post/20190405-ddia-chapter08-the-trouble-with-distributed-system/</link>
      <pubDate>Tue, 16 Apr 2019 21:03:16 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190405-ddia-chapter08-the-trouble-with-distributed-system/</guid>
      <description>&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;#%e6%95%85%e9%9a%9c%e4%b8%8e%e9%83%a8%e5%88%86%e5%a4%b1%e6%95%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;单机上的程序，以一种确定性的方式运行：要么工作，要么出错。&lt;/p&gt;&#xA;&lt;p&gt;然而涉及到多台节点时，会出现系统的一部分正常，一部分异常的情况，称为“部分故障（partial failure）”。&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;#%e4%b8%8d%e5%8f%af%e9%9d%a0%e7%9a%84%e7%bd%91%e7%bb%9c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&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;li&gt;远程节点接收并且处理了请求，但是回复却丢失了。&lt;/li&gt;&#xA;&lt;li&gt;远程节点已经完成了请求，但是回复被延迟了。&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;8-1&#34; src=&#34;https://www.codedump.info/media/imgs/20190405-ddia-chapter08-the-trouble-with-distributed-system/8-1.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 8-1 &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;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;#%e6%a3%80%e6%b5%8b%e7%bd%91%e7%bb%9c%e6%95%85%e9%9a%9c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;如果超时是检测网络故障的唯一可行方法，那么这个超时时间应该如何选择？&lt;/p&gt;&#xA;&lt;p&gt;太小：出现误判的情况。太大：意味着要很长时间才能宣布节点失效了。&lt;/p&gt;&#xA;&lt;p&gt;假设有一个虚拟的系统，网络可以保证数据报在一个最大延迟范围内：要么在时间d内交付完成，要么丢失。此外，非故障节点在时间r内完成请求的处理。此时，就可以确定成功的请求总是在2d+r时间内完成，因此这个时间是一个理想超时时间。&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%90%8c%e6%ad%a5%e7%bd%91%e7%bb%9c%e5%92%8c%e5%bc%82%e6%ad%a5%e7%bd%91%e7%bb%9c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;既然同步网络可以在规定的延迟时间内完成数据的发送，且不会丢失数据包，那么为什么分布式系统没有选择同步网络，在硬件层面就解决网络问题？&lt;/p&gt;&#xA;&lt;p&gt;原因在于，固定电话网络中的电路与TCP连接存在很大的不同：电路方式总是预留固定带宽，在电路建立之后其他人无法使用；而TCP连接的数据包则会尝试使用所有可用的网络带宽。TCP可以传送任意大小可变的数据块，会尽力在最短时间内完成数据传送。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;不可靠的时钟&#34;&gt;&#xA;  不可靠的时钟&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%8d%e5%8f%af%e9%9d%a0%e7%9a%84%e6%97%b6%e9%92%9f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;很多操作依赖时间，但是时间也是靠不住的，本节就是说这部分的内容。&lt;/p&gt;&#xA;&lt;p&gt;计算机的时钟分为两种，墙上时钟（time-of-day clock）和单调时钟（monotonic clock），但是两者在使用上是有区别的。&lt;/p&gt;&#xA;&lt;p&gt;墙上时钟根据某个日历（也称为墙上时间，wall-clock time）返回当前的日期和时间。比如Linux的系统调用clock_gettime(CLOCK_REALTIME)返回自1970年1月1日以来的秒数和毫秒数。&lt;/p&gt;&#xA;&lt;p&gt;单调时钟更适合用于测试持续时间段（时间间隔），Linux的系统调用clock_gettime(CLOCK_MONITONIC)返回的就是单调时钟。单调时钟的名字源于它们总是保证向前走而不会出现回拨现象。&lt;/p&gt;&#xA;&lt;p&gt;可以在一个时间点读取单调时钟的值，完成某项工作然后再次检查时钟，时钟之间的插值就是两次检查的时间间隔。&lt;/p&gt;&#xA;&lt;p&gt;但是，单调时钟的绝对值没有任何意义。&lt;/p&gt;&#xA;&lt;p&gt;单调时钟不需要同步，而墙上时钟需要根据NTP服务器或外部时间源做调整。&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%be%9d%e8%b5%96%e6%97%b6%e9%92%9f%e7%9a%84%e5%90%8c%e6%ad%a5&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;某些操作强依赖时钟的同步，这里往往容易出现问题，这一节就是列举这些问题。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;时间戳与事件顺序&#34;&gt;&#xA;  时间戳与事件顺序&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%97%b6%e9%97%b4%e6%88%b3%e4%b8%8e%e4%ba%8b%e4%bb%b6%e9%a1%ba%e5%ba%8f&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&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;8-3&#34; src=&#34;https://www.codedump.info/media/imgs/20190405-ddia-chapter08-the-trouble-with-distributed-system/8-3.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 8-3 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，客户端A写入x=1的时间是42.004秒，而客户端B写入x+=1即x=2虽然在后面发生但是时间是42.003秒。节点2在收到这两个事件时，会根据时间戳错误的认为x=1是最新的值，丢弃了x=2的值。&lt;/p&gt;&#xA;&lt;p&gt;这种冲突解决方式称为“最后写入获胜（Last Write Win）”，但是这样保持“最新”值并丢弃其他值的做法，由于“最新”的定义强依赖于墙上时钟，则会引入偏差。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;时钟的置信区间&#34;&gt;&#xA;  时钟的置信区间&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%97%b6%e9%92%9f%e7%9a%84%e7%bd%ae%e4%bf%a1%e5%8c%ba%e9%97%b4&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;不应该把墙上时间视为一个精确的时间点，而更应该被视为带有置信区间的时间范围。比如，系统有95%的置信度认为目前时间在[10.3,10.5]秒之间。&lt;/p&gt;&#xA;&lt;p&gt;比如Google Spanner中的TrueTime API，在查询当前时间时，会得到两个值：[不早于，不晚于]分别代表误差的最大偏差范围。&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%bf%9b%e7%a8%8b%e6%9a%82%e5%81%9c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;另外一个分布式系统中危险使用时钟的例子：假设数据库每个分区只有一个主节点，只有主节点可以接收写入，那么其它节点该如何确信该节点没有被宣告失效，可以继续安全写入呢？&lt;/p&gt;</description>
    </item>
    <item>
      <title>《数据密集型应用系统设计》第七章《事务》笔记</title>
      <link>https://www.codedump.info/zh/post/20190403-ddia-chapter07-transaction/</link>
      <pubDate>Wed, 03 Apr 2019 22:33:58 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20190403-ddia-chapter07-transaction/</guid>
      <description>&lt;p&gt;事务提供了一种机制，应用程序可以把一组读和写操作放在一个逻辑单元里，所有在一个事务的读和写操作会被视为一个操作：要么全部失败，要么全部成功，因此应用程序不需要担心部分失败（partial failure）问题，可以安全的重试。&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%b7%b1%e5%85%a5%e7%90%86%e8%a7%a3%e4%ba%8b%e5%8a%a1&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;事务提供的安全性保证即所谓的&lt;code&gt;ACID&lt;/code&gt;，它包括以下四个要求：&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;acid&#34;&gt;&#xA;  ACID&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#acid&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;原子性atomicity&#34;&gt;&#xA;  原子性（Atomicity）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8e%9f%e5%ad%90%e6%80%a7atomicity&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;A（Atomicity，原子性）：在一个事务中的所有操作，要么全部成功，要么全部失败，不存在部分成功或者部分失败的情况。在出错时中断事务，前面成功的操作都会被丢弃。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;一致性consistency&#34;&gt;&#xA;  一致性（consistency）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%80%e8%87%b4%e6%80%a7consistency&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;C（Consistency，一致性）：对数据有特定的预期状态，任何数据修改必须满足这些状态约束，比如针对一个账号，账号上的款项必须保持平衡。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;隔离性isolation&#34;&gt;&#xA;  隔离性（isolation）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%9a%94%e7%a6%bb%e6%80%a7isolation&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;I（Isolation，隔离性）：并发执行的多个事务，不会相互影响。&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;7-1&#34; src=&#34;https://www.codedump.info/media/imgs/20190403-ddia-chapter07-transaction/7-1.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 7-1 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&#xA;如上图中所示，两个客户端同时增加数据库的计时器，由于没有做好隔离，导致最终的结果是43而不是正确的44。&lt;/p&gt;&#xA;&lt;p&gt;ACID语义中的隔离性意味着并发执行的多个事务相互隔离，不能交叉运行。经典的数据库教材将隔离性定义为可串行化（serializability），这就意味着可以假装它是数据库上运行的唯一事务。&lt;/p&gt;&#xA;&lt;p&gt;然而实践中，由于性能问题很少使用串行化隔离。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;持久性durability&#34;&gt;&#xA;  持久性（Durability）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%8c%81%e4%b9%85%e6%80%a7durability&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;D（Durability，持久性）：一旦事务提交，数据将被持久化存储起来。&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%bc%b1%e9%9a%94%e7%a6%bb%e7%ba%a7%e5%88%ab&#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;h2 class=&#34;heading&#34; id=&#34;读提交read-committed&#34;&gt;&#xA;  读提交（read committed）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%af%bb%e6%8f%90%e4%ba%a4read-committed&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&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;/ol&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;防止脏读&#34;&gt;&#xA;  防止脏读&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%98%b2%e6%ad%a2%e8%84%8f%e8%af%bb&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&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;7-4&#34; src=&#34;https://www.codedump.info/media/imgs/20190403-ddia-chapter07-transaction/7-4.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 7-4 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图，用户2仅在用户1的事务提交成功之后，才能读取到这次事务修改的新值x=3。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;防止脏写&#34;&gt;&#xA;  防止脏写&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%98%b2%e6%ad%a2%e8%84%8f%e5%86%99&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&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;7-5&#34; src=&#34;https://www.codedump.info/media/imgs/20190403-ddia-chapter07-transaction/7-5.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 7-5 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;如上图，alice和bob两人试图购买同一辆车。购买时需要两次数据库写入：网站需要更新买主为新买家，而同时发票也需要随之更新。&#xA;但是在上图中，车主被改成了bob，但是发票上面写的却是alice。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;实现读提交&#34;&gt;&#xA;  实现读提交&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%ae%9e%e7%8e%b0%e8%af%bb%e6%8f%90%e4%ba%a4&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;实现防脏写：数据库通常使用行级锁来防止脏写，事务想修改某个对象，必须首先获得该对象的锁，直到事务结束。&lt;/p&gt;&#xA;&lt;p&gt;实现防脏读：也可以使用前面的防脏写来实现防脏读，但是这样代价太大了。一般的方式是保存这个值的两个版本，事务没有提交之前返回旧的值，提交之后才返回新的值。&lt;/p&gt;&#xA;&lt;p&gt;然而，读锁在实际中并不可行，原因在于运行时间较长的事务导致了许多只读事务等待太长的时间。&lt;/p&gt;&#xA;&lt;p&gt;因此，大部分数据库使用7-4中的方式来防止脏读：对于每个待更新的对象，数据库都会维护其旧值和当前持有锁事务将要设置的新值两个版本。在事务提交之前返回的是旧值；仅当事务提交之后，才会切换到新的值。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;快照隔离级别snapshot-isolation和重复读&#34;&gt;&#xA;  快照隔离级别（Snapshot isolation）和重复读&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%bf%ab%e7%85%a7%e9%9a%94%e7%a6%bb%e7%ba%a7%e5%88%absnapshot-isolation%e5%92%8c%e9%87%8d%e5%a4%8d%e8%af%bb&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&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;7-6&#34; src=&#34;https://www.codedump.info/media/imgs/20190403-ddia-chapter07-transaction/7-6.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; 7-6 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，alice有两个账号，但是如果alice在转账过程中去查看账户，会发现少了100美元。&lt;/p&gt;&#xA;&lt;p&gt;原因在于：alice对两个账户的两次读操作是同一个事务，而在这两次读操作之间，还有两次写操作，在这两次写操作完成之后才进行的第二次读操作，这样读出来的数据就不一致了。&lt;/p&gt;&#xA;&lt;p&gt;这种异常现象称为”不可重复读取（nonrepeatable read）“或者”读倾斜（read skew）“问题。&lt;/p&gt;</description>
    </item>
    <item>
      <title>《数据密集型应用系统设计》第六章数据分区笔记</title>
      <link>https://www.codedump.info/zh/post/20181124-ddia-chapter06-partitioning/</link>
      <pubDate>Tue, 02 Apr 2019 22:17:24 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20181124-ddia-chapter06-partitioning/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;键值数据的分区&#34;&gt;&#xA;  键值数据的分区&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e9%94%ae%e5%80%bc%e6%95%b0%e6%8d%ae%e7%9a%84%e5%88%86%e5%8c%ba&#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%9f%ba%e4%ba%8e%e5%85%b3%e9%94%ae%e5%ad%97%e5%8c%ba%e9%97%b4%e7%9a%84%e5%88%86%e5%8c%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;给每个分区分配一段连续的关键字或者关键字区间（以最小值和最大值来指示），从关键字区间的上下限可以确定哪个分区包含这些关键字。&lt;/p&gt;&#xA;&lt;p&gt;关键字的区间段不一定要均匀分布，这是因为数据本身可能就不是均匀的。比如，某些分区包含以A和B开头字母的键，而某些分区包含了T、U、V、X、Y和Z开始的单词。&lt;/p&gt;&#xA;&lt;p&gt;基于关键字的区间分区的缺点是某些访问模式会导致热点（hot spot）。比如关键字是时间戳，分区对应一个时间范围，那么可能会出现所有的写入操作都集中在同一个分区（比如当天的分区），而其他分区始终处于空闲状态。&lt;/p&gt;&#xA;&lt;p&gt;为了避免类似的问题，需要使用时间戳以外的其他内容作为关键字的第一项。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;基于关键字hash值分区&#34;&gt;&#xA;  基于关键字Hash值分区&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e4%ba%8e%e5%85%b3%e9%94%ae%e5%ad%97hash%e5%80%bc%e5%88%86%e5%8c%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;基于关键字Hash值分区，可以解决上面提到的数据倾斜和热点问题，但是丧失了良好的区间查询特性。&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%b4%9f%e8%bd%bd%e5%80%be%e6%96%9c%e5%92%8c%e7%83%ad%e7%82%b9&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;基于关键字Hash值分区的办法，可以减轻数据热点问题，但是不能完全避免这类问题。一种常见的极端场景是，社交网络上某个名人有几百万的粉丝，当其发布一些热点事件时可能会引起访问风暴。此时，Hash起不到任何分流的作用。&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%88%86%e5%8c%ba%e4%b8%8e%e4%ba%8c%e7%ba%a7%e7%b4%a2%e5%bc%95&#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;基于文档分区的二级索引&#34;&gt;&#xA;  基于文档分区的二级索引&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e4%ba%8e%e6%96%87%e6%a1%a3%e5%88%86%e5%8c%ba%e7%9a%84%e4%ba%8c%e7%ba%a7%e7%b4%a2%e5%bc%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&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;figure 6-4&#34; src=&#34;https://www.codedump.info/media/imgs/20181124-ddia-chapter06-partitioning/figure6-4.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; figure 6-4 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，数据根据ID 进行分区，但是实际查询的时候，还可以按照颜色和厂商进行过滤，所以每个分区上面还创建了颜色和厂商的索引。每次往分区中写入新数据时，自动创建这些二级索引。&lt;/p&gt;&#xA;&lt;p&gt;在这种索引方式中，每个分区完全独立。各自维护自己的二级索引。因此文档索引也成为本地索引，而不是全局索引。&lt;/p&gt;&#xA;&lt;p&gt;但是读取的时候，需要查询所有的分区数据然后进行合并才返回给客户端，这种叫分散/聚集（scatter/gather）。&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%9f%ba%e4%ba%8e%e8%af%8d%e6%9d%a1%e7%9a%84%e4%ba%8c%e7%ba%a7%e7%b4%a2%e5%bc%95&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&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;figure 6-5&#34; src=&#34;https://www.codedump.info/media/imgs/20181124-ddia-chapter06-partitioning/figure6-5.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; figure 6-5 &lt;/figcaption&gt;&#xA;    &lt;/div&gt;&#xA;    &#xA;&lt;/figure&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;上图中，所有数据分区中的颜色进行了分区，比如从a到r开始的颜色放在了分区0中，从s到z的颜色放在了分区1中，类似的，厂商索引也被分区。这种索引方式成为词条分区（term-partitioned）。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;优点：读取高效，不需要采用scatter/gather方式对所有分区都进行查询；&lt;/li&gt;&#xA;&lt;li&gt;缺点：写入速度慢并且非常复杂，主要是因为单个文档需要更新的时候，里面可能涉及多个二级索引，而二级索引又放在不同的节点上。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;在实践中，对全局二级索引数据的更新一般都是异步进行的。&lt;/p&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;分区再平衡rebalancing-partitions&#34;&gt;&#xA;  分区再平衡（Rebalancing Partitions）&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%88%86%e5%8c%ba%e5%86%8d%e5%b9%b3%e8%a1%a1rebalancing-partitions&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;实际中，数据会发生某些变化，这时候需要将数据和请求从一个节点转移到另一个节点。这样的一个迁移负载的过程称为再平衡（rebalance）。&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;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%ba%e4%bb%80%e4%b9%88%e4%b8%8d%e8%83%bd%e7%94%a8%e5%8f%96%e6%a8%a1&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&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;#%e5%9b%ba%e5%ae%9a%e6%95%b0%e9%87%8f%e7%9a%84%e5%88%86%e5%8c%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;创建远超实际节点数的分区数，然后给每个节点分配多个分区。比如只有10个节点的集群，划分了1000个逻辑分区。&lt;/p&gt;&#xA;&lt;p&gt;如果集群中添加了一个新节点，该新节点就可以从每个现有节点上匀走几个分区，直到分区再次达到全局平衡。&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;figure 6-6&#34; src=&#34;https://www.codedump.info/media/imgs/20181124-ddia-chapter06-partitioning/figure6-6.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; figure 6-6 &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;动态分区&#34;&gt;&#xA;  动态分区&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%8a%a8%e6%80%81%e5%88%86%e5%8c%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;按节点比例分区&#34;&gt;&#xA;  按节点比例分区&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%8c%89%e8%8a%82%e7%82%b9%e6%af%94%e4%be%8b%e5%88%86%e5%8c%ba&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;自动与手动再平衡操作&#34;&gt;&#xA;  自动与手动再平衡操作&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%87%aa%e5%8a%a8%e4%b8%8e%e6%89%8b%e5%8a%a8%e5%86%8d%e5%b9%b3%e8%a1%a1%e6%93%8d%e4%bd%9c&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;h1 class=&#34;heading&#34; id=&#34;请求路由&#34;&gt;&#xA;  请求路由&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e8%af%b7%e6%b1%82%e8%b7%af%e7%94%b1&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h1&gt;&#xA;&lt;p&gt;当客户端需要发起请求时，如果知道应该连接哪个节点？如果发生了分区再平衡，分区与节点的对应关系发生了变化。&lt;/p&gt;</description>
    </item>
    <item>
      <title>《数据密集型应用系统设计》第五章数据复制笔记</title>
      <link>https://www.codedump.info/zh/post/20181118-ddia-chapter05-replication/</link>
      <pubDate>Mon, 01 Apr 2019 18:19:22 +0800</pubDate>
      <guid>https://www.codedump.info/zh/post/20181118-ddia-chapter05-replication/</guid>
      <description>&lt;h1 class=&#34;heading&#34; id=&#34;主从复制&#34;&gt;&#xA;  主从复制&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%bb%e4%bb%8e%e5%a4%8d%e5%88%b6&#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;figure 5-1&#34; src=&#34;https://www.codedump.info/media/imgs/20181118-ddia-chapter05-replication/figure5-1.jpg&#34; &gt;&#xA;    &lt;/div&gt;&#xA;&#xA;    &#xA;    &lt;div class=&#34;caption-container&#34;&gt;&#xA;        &lt;figcaption&gt; figure 5-1 &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;同步复制和异步复制&#34;&gt;&#xA;  同步复制和异步复制&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%90%8c%e6%ad%a5%e5%a4%8d%e5%88%b6%e5%92%8c%e5%bc%82%e6%ad%a5%e5%a4%8d%e5%88%b6&#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;#%e5%90%8c%e6%ad%a5%e5%a4%8d%e5%88%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&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;h3 class=&#34;heading&#34; id=&#34;异步复制&#34;&gt;&#xA;  异步复制&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%bc%82%e6%ad%a5%e5%a4%8d%e5%88%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&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;不能把集群中所有节点设置为同步节点，因为这样的话任何一个节点的停滞都会导致整个集群的不可用。像Paxos、Raft算法，都要求集群中大多数节点返回就可以了。部分同步、部分异步的集群配置成为半同步（semi-sync）的集群配置。&lt;/p&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;新增新的从节点&#34;&gt;&#xA;  新增新的从节点&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e6%96%b0%e5%a2%9e%e6%96%b0%e7%9a%84%e4%bb%8e%e8%8a%82%e7%82%b9&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&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;li&gt;重复上面三步直到从节点追上主节点的进度。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 class=&#34;heading&#34; id=&#34;处理节点失效&#34;&gt;&#xA;  处理节点失效&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%a4%84%e7%90%86%e8%8a%82%e7%82%b9%e5%a4%b1%e6%95%88&#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;#%e4%bb%8e%e8%8a%82%e7%82%b9%e5%a4%b1%e6%95%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;从节点崩溃恢复之后按照前面新增新的从节点的步骤来追上主节点的数据进度。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;主节点失效&#34;&gt;&#xA;  主节点失效&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e4%b8%bb%e8%8a%82%e7%82%b9%e5%a4%b1%e6%95%88&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&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;除了以上步骤之外，还有以下问题需要考虑：&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;可能会出现集群同时存在两个主节点的情况，也就是所谓的脑裂（split brain）现象，此时两个主节点都认为自己是主节点并且都能接收客户端的写数据请求，会导致数据丢失或者破坏。&lt;/li&gt;&#xA;&lt;li&gt;如何设置合理的超时时间来判断主节点失效？如果太大意味着总体恢复时间长，如果太小意味着某些情况下可能主节点并未失效但是被误判为失效了，比如网络峰值导致延迟高等原因，这样会导致很多不必要的主节点切换。&lt;/li&gt;&#xA;&lt;/ol&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;#%e5%a4%8d%e5%88%b6%e6%97%a5%e5%bf%97%e7%9a%84%e5%ae%9e%e7%8e%b0&#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;#%e5%9f%ba%e4%ba%8e%e8%af%ad%e5%8f%a5%e7%9a%84%e5%a4%8d%e5%88%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;主节点记录所执行的每个写请求并将该语句做为日志发送给从节点。但是有些场景并不适合这么做，比如：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;调用任何非确定函数的语句，比如NOW()获得当前时间，RAND()返回一个随机数。&lt;/li&gt;&#xA;&lt;li&gt;语句中使用了自增列，或者依赖于当前数据库的数据。&lt;/li&gt;&#xA;&lt;li&gt;有副作用的语句，在每个副本上面执行的效果不一样。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;基于预写日志wal&#34;&gt;&#xA;  基于预写日志(WAL)&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e4%ba%8e%e9%a2%84%e5%86%99%e6%97%a5%e5%bf%97wal&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&gt;&#xA;&lt;p&gt;将对数据库的操作写入日志，传送到从节点上然后执行，得到与主节点相同的数据副本。&lt;/p&gt;&#xA;&lt;h3 class=&#34;heading&#34; id=&#34;基于行的逻辑日志复制&#34;&gt;&#xA;  基于行的逻辑日志复制&#xA;  &lt;a class=&#34;anchor&#34; href=&#34;#%e5%9f%ba%e4%ba%8e%e8%a1%8c%e7%9a%84%e9%80%bb%e8%be%91%e6%97%a5%e5%bf%97%e5%a4%8d%e5%88%b6&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h3&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>
