大数据技术的演进:从 Hive、GFS 到 Raft
目录
大数据这个领域发展了二十多年,技术名词换了很多轮,但底层的设计模式其实一直比较收敛。本文按发展史的顺序,依次介绍 Hive 的原理、GFS 与 CDN 各自的定位、GFS 与 Elasticsearch 在架构上的相似之处、分布式系统中 master 节点的管理方式、几种主流的共识算法,最后落到 Raft 上,并且介绍一下把 Raft 当作类库嵌入到应用内部这种较新的用法。
大数据发展简史
2003–2006,谷歌发论文奠定基础。 谷歌当时要做全网索引,数据规模远超一般企业,选择的方案是用大量廉价 PC 组成集群,通过软件层面来处理容错。这一时期谷歌陆续发了三篇论文:GFS(2003,分布式文件系统)、MapReduce(2004,分布式计算模型)、Bigtable(2006,分布式存储)。核心思路可以概括为“移动计算比移动数据便宜”,也就是把计算调度到数据所在的节点执行,避免大规模的数据搬运。
2006–2008,Hadoop 出现。 Doug Cutting 参考谷歌的论文实现了 HDFS 和 MapReduce,用他儿子玩具大象的名字命名为 Hadoop。2006 年 Yahoo 开始大规模投入,2008 年 Hadoop 进入 Apache 顶级项目。
2008–2013,生态趋于丰富。 Facebook 开源了 Hive,让开发者可以直接用 SQL 写 MapReduce 作业;HBase 借鉴 Bigtable 的思路实现;LinkedIn 开源了 Kafka。同一时期 NoSQL 类的数据库也被广泛讨论。之后 YARN 把资源调度从 MapReduce 里剥离出来,Hadoop 也从一个纯粹的计算框架,逐渐承担起类似操作系统的角色。
2014–2018,内存计算和流处理。 MapReduce 的每一个阶段都要落盘,对迭代式计算不太友好。Spark 引入了内存计算和 DAG 执行模型,性能相比 MapReduce 有一到两个数量级的提升,逐渐取代了 MapReduce 在离线计算中的位置。同一时期流处理领域从 Storm 发展到 Flink,“批流一体”也成为常见的讨论话题。
2019 至今,云原生和湖仓一体。 对象存储(如 S3)逐步取代 HDFS 成为主要的数据底座,存算分离带来了更好的弹性伸缩能力。Databricks 提出了 Lakehouse 的概念,Iceberg、Hudi、Delta Lake 等项目让数据湖也具备了 ACID 事务能力。一个比较标志性的事件是 Cloudera 和 Hortonworks 在 2019 年合并,传统 Hadoop 发行版的商业模式基本走到终点。
整体看下来,这条主线大致是:先解决“数据怎么存下来”(GFS/HDFS),再解决“怎么算得动”(MapReduce/Spark),然后是“怎么算得快”(Flink 一类的实时化方向),最后是“怎么管得好、用得省”(湖仓和云原生)。每一代技术基本都是在解决上一代留下的最主要的问题。
Hive 是什么
Hive 有时候会被误解成一种数据库,实际上它更像一个把 SQL 翻译成分布式计算作业的编译器。理解了这一点,后面的很多设计就都比较好理解了。
这条编译流水线里有两个关键设计。
Metastore 让文件“看起来像表”。 HDFS 上存放的其实只是一堆文本或者 ORC/Parquet 文件,本身没有结构信息。Hive 通过挂一个关系型数据库(通常是 MySQL)作为 Metastore,用来记录表名到 HDFS 路径的映射、列的名称和类型、分隔符等元数据。这种模式称为 schema-on-read:传统数据库是在写入时就强制施加结构(schema-on-write),而 Hive 是在读取时根据元数据把文件解释为表。文件本身不变,但有了这层元数据之后,SQL 里引用的表名和列名就有了对应关系。
SQL 里的关系代数可以自然地映射到 Map-Shuffle-Reduce。 SQL 的底层是关系代数中的几种算子,比如投影、过滤、聚合、连接,正好可以和 MapReduce 的三个阶段一一对应。以 SELECT city, COUNT(*) FROM users GROUP BY city 这个例子来说:
WHERE 条件和列裁剪都是在 Map 端就地完成的,属于纯粹的本地操作。GROUP BY 的分组语义由 Shuffle 完成,框架本身就保证了相同 key 会被路由到同一个 reducer,这一步 Hive 层面不需要额外处理。COUNT、SUM、AVG 这类聚合函数则在 Reduce 端执行。JOIN 的处理方式类似:把 join key 作为 shuffle key,两张表中相同 key 的行会汇聚到同一个 reducer 再做拼接,这就是常见的 common join。如果其中一张表小到可以放进内存,还可以直接广播到每个 mapper 做哈希查找,这样连 shuffle 都可以省掉,也就是所谓的 map join。
SQL 之所以能被拿来做分布式计算,一个关键原因是 SQL 是声明式的,只描述“要什么结果”,而不规定“怎么执行”。这就给编译器留出了很大的自由度,可以把同一份 SQL 翻译成不同的物理执行方式。在单机数据库里可能翻译成 B+ 树扫描,在 Hive 里就翻译成一个分布式作业。这也是为什么 Hive 后来能把底层的执行引擎从 MapReduce 换成 Tez、再换成 Spark,而用户写的 SQL 完全不需要改动。Presto、Trino、Spark SQL 走的都是类似的思路。
这套设计也决定了 Hive 的适用边界:每个作业要经过调度、JVM 启动、中间结果落盘,整体延迟在分钟级,所以 Hive 只适合离线批处理,做不了在线点查和事务型的场景。目前 MapReduce 基本已经退役,Hive 在很多公司里也只保留了 Metastore 作为元数据中心(catalog)在使用。
GFS 与 CDN:各自是什么,又是什么关系
GFS 和 CDN 表面上看都是“把文件分布到很多机器上”,但两者的目标完全不同,容易被放到一起混淆,所以在这里一并说明。
CDN 要解决的是“用户离数据太远”。 CDN 把热门内容缓存到离用户最近的边缘节点,关注的是访问延迟,服务对象是终端用户。它的一个关键前提假设是访问存在明显的热点,即少数内容占据大部分流量,缓存才有意义。请求路径大致是:先经过智能调度(DNS 或 Anycast,这里的“最近”指的是网络意义上的 RTT 和链路质量,不是地理距离),到达边缘节点后再按 cache key 查缓存,热数据放在内存、温数据放在 SSD。中间层(L2)缓存是其中比较关键的一环:如果 100 个边缘节点各自回源,源站要承担 100 次 miss;加入一层区域汇聚节点后,同一份内容全网只回源一次,其余的 99 次都会在 L2 层被拦截下来。
GFS 要解决的是“一份数据太大、也不能只存一份”。 它是数据中心内部使用的分布式文件系统,关注的是吞吐和持久性,服务对象是内部的计算任务。它的一个关键前提假设是“机器会坏”,因此在设计上做了大量容错考虑:文件切成 64MB 的大 chunk(远大于传统文件系统的 4KB 块,主要为了适配大文件顺序读写的负载,大块可以摊薄元数据的开销),每个 chunk 默认三副本,并且刻意放在不同机架上,避免整个机架断电或者网络故障时数据全部不可用。
一个是仓库,一个是前置的货架,这两者本身没有直接的关系。但在现代架构中它们经常成为上下游:CDN 的源站通常是对象存储,而对象存储的内部实现在思路上是 GFS 的后代(GCS 的底层就是 GFS 的继任者 Colossus)。也就是说,GFS 的后代其实就藏在 CDN 的源站里面,只不过被对象存储的 HTTP API 包装了起来,使用方通常不会直接感知到。
再看一下 GFS 内部的设计。以一次写入为主线来看,比较能体现它的设计思路:
这里有三个值得关注的细节。
数据流和控制流是解耦的(图中 ③ 和 ④ 是两条独立的路径)。数据先沿“链”推出去:客户端把数据推送给网络拓扑上最近的一个副本,这个副本一边接收一边继续转发给下一个副本,类似接力。之所以采用链式而不是星型(客户端同时向三个副本发送),是为了让每台机器都能同时全速接收和发送,总耗时接近于“数据量 ÷ 单机带宽”。数据到位之后,客户端再向 primary 发送一个较小的写命令,由 primary 给这批变更确定一个全局的顺序,然后下发给 secondary 各自按同样的顺序落盘。顺序是保证一致性的关键,而这个顺序由持有租约的 primary 决定。Master 只负责发放租约(默认 60 秒,通过心跳续期),本身完全不参与写路径。
Master 只负责元数据,不经手实际数据。 元数据全部保存在内存里,通过 operation log(写磁盘并做远程复制)加定期 checkpoint 来保证可恢复性,此外还有只读的 shadow master 做灾备。有一点值得注意:chunk 具体存在哪几台机器上这条信息,Master 并不做持久化,重启之后是通过 Chunkserver 心跳汇报重新构建的。原因是磁盘一旦损坏,副本位置本来就会发生变化,持久化反而会成为负担。
租约是有到期时间的。 出现网络分区的时候,被隔离一侧的 primary 手里的租约会过期,过期之后它自己就不会再接受写入;Master 也会等到旧租约的最长可能存活期结束之后,才发出新的租约。整个机制通过时间做了自然的隔离,新旧的权威在时间轴上不会重叠。
关于后续的演化也可以简单提一下。单 Master 的元数据容量最终成了瓶颈,谷歌在 2010 年前后用 Colossus 替换了 GFS:元数据本身也做了分布式化(存在 Bigtable 里,而 Bigtable 又存在 Colossus 上,形成一个自举结构),三副本换成了 Reed-Solomon 纠删码,存储开销从 3 倍降到 1.5 倍左右。HDFS 后来走的路径基本一致。
从 GFS 看 Elasticsearch
用过 Elasticsearch 的人再回头看上面的 GFS 架构,可能会觉得眼熟。对照一下:
| GFS | Elasticsearch |
|---|---|
| Master(元数据、租约、副本调度) | Master 节点(cluster state、分片分配) |
| Chunkserver | Data 节点 |
| chunk(64MB 定长块) | shard(一个完整的 Lucene 索引) |
| 三副本,primary 持租约定序 | primary shard 定序,同步给 replica |
| 数据流不经过 Master | 读写不经过 master,协调节点直达 data 节点 |
| 心跳汇报 chunk 位置 | 节点加入/离开触发 shard 重分配 |
| operation log 恢复元数据 | translog 恢复未刷盘数据 |
这种相似并不是巧合,两者其实是同一种模式的不同实现:集中式管理小规模元数据,用分区加复制承载大规模数据,实际的读写路径绕开元数据节点。HDFS(NameNode/DataNode)、Kafka(controller 和 partition leader-follower)、HBase(Master/RegionServer)、MongoDB(config server 和 shard)都可以看成这个模式的变体。看懂其中一个,再看其他系统时基本就是在填空:分区叫什么(chunk、shard、partition、region)、由谁来决定写入顺序(lease primary、primary shard、partition leader)、元数据存在哪里。
差异之处也值得关注,主要有三点。
第一,分区的“内容物”不同。GFS 里的 chunk 就是哑的字节块,本身不理解内容;ES 里的 shard 则是一个活跃的 Lucene 索引,自带倒排索引、分词、segment merge 等功能。因此 ES 的 data 节点比较吃 CPU 和内存,而 Chunkserver 主要吃磁盘和网卡。
第二,Master 的定位不同。GFS 的 Master 在读写路径上(客户端需要先向它询问位置,通过客户端缓存来缓解压力);ES 的 master 平时完全不在请求路径上,任何节点都可以充当协调节点,根据本地缓存的 cluster state 直接路由,master 只在拓扑变化的时候需要工作。
第三,一致性目标不同。GFS 有意选择了较松的一致性模型(record append 可能重复,需要应用层去重);早年的 ES 一致性问题反而更严重,脑裂丢数据是它比较有名的问题,这个话题接下来再展开。
Master 的管理方式
分布式系统里,“唯一的 master”通常是所有秩序的来源。但 master 本身由谁来管理?如果这一层没有设计好,就会出现常说的脑裂:
选主的本质是投票,而投票需要回答“多少票算赢”这个问题。从数学上讲,唯一安全的答案是过半数(quorum):任何一次网络分区最多只有一侧能够凑够过半数,天然就不会出现两个赢家。但在 7.x 之前,Elasticsearch(Zen Discovery)把这个数字做成了一个需要人工配置的参数 minimum_master_nodes,默认值是 1。这就带来了一些常见的运维问题:3 台集群应该填 2,扩容到 5 台时忘了改成 3,或者干脆没配置。网络抖动时(GC 停顿过长也算,节点没死但 ping 超时),两侧可能各自选出 master,各自让自己这边的 primary shard 接受写入并且返回成功。分区恢复之后集群只能承认一个 cluster state,另一侧那些已经 ack 给客户端的写入就会丢失。把安全性建立在“运维记得改一个数字”上,是这个设计比较根本的问题。ES 一直到 7.x 重写了整个协调层(引入递增的 term、过半数投票、投票配置自动维护,同时删掉了 minimum_master_nodes)才解决这个历史问题。
从行业实践来看,管理 master 大致有三种方式。
1. 指定 + 外部仲裁(GFS、Bigtable/HBase + Chubby/ZooKeeper、Kafka 的 ZK 时代)。系统自身不做选举,“谁是 master”交给一个强一致的第三方锁服务。GFS 的 Master 就是指定的一个进程,如果挂掉了由集群外部的监控设施在别的机器上用复制的 operation log 重启。Bigtable 的 Master 通过抢 Chubby 锁来保证唯一性。整体上比较简单可靠,代价是多了一个外部依赖。
2. 内置共识(ES 7+、MongoDB 副本集、Kafka 的 KRaft、etcd 本身)。master-eligible 节点内部跑 Raft 或 Paxos 类协议,通过过半数选举加任期号来保证。这种方式自包含,缺点是协议实现的正确性非常难保证。ES 用了七个大版本才把这块补齐,Kafka 也用了十几年才完全摆脱 ZooKeeper。
3. 单点 + 快速恢复(GFS 原始形态、HDFS 早期 NameNode)。承认单点存在,把工程精力放在秒级重启和日志复制上。工程上最简单,但可用性上限也最低。
不管选择哪种方式,都有一个配套机制不能少:fencing(围栏)。设想一个场景,老 master 拿着锁的时候发生了 Full GC,停顿了 40 秒,锁服务判定它超时并且把锁给了新 master。老 master 恢复之后并不知道自己已经下岗,仍然会继续发号施令。正确的做法是发放锁时附带一个单调递增的 token(在 Chubby 里叫 sequencer,在 ES 里就是 term,在 GFS 里对应 chunk version),下游收到指令时校验 token,旧任期的指令统一拒绝。这也是“用 Redis SETNX 做分布式锁”在强正确性场景下不太够用的原因。之前比较有名的 Redlock 之争(Kleppmann 和 antirez 之间)核心讨论的就是缺乏可靠的 fencing 这一点。业务上用 Redis 锁做幂等防重,能够容忍极小概率的双执行是可以的;但如果用它来裁决“谁是唯一 writer”,就需要更慎重一些。总结一下,分布式系统里所有涉及“唯一性”的问题,最后基本都归结到几件事上:要么走过半数投票,要么用带过期时间的租约,要么两者一起用。
分布式共识:算法与实例
前面提到的方式 1 会引出一个很自然的问题:那个第三方(ZooKeeper、etcd)自己也是集群,它也需要选主,那它靠谁来仲裁?答案是没有第四方了,递归到这里就终止了。ZooKeeper 内部跑 ZAB,etcd 跑 Raft,Chubby 跑 Paxos,过半数共识协议在数学上是自洽的:任意两个过半数集合必然存在交集,交集里的节点不会在同一任期内投两次票,所以两个 leader 数学上不可能同时当选;出现分区时少数派直接拒绝服务,不依赖任何仲裁方,只依赖简单的算术。共识可以看作分布式系统的最底层基础,再往下就没有别的了。
既然共识是自洽的,也可以内置到系统内部,为什么大量系统还是选择外包给 ZK 或 etcd?主要有两个原因。
共识协议实现的正确性非常难保证,所以只做一次就好。 论文本身不长,但工程上有很多细节:磁盘写一半断电、时钟漂移、成员变更期间的双 quorum、快照边界等等。这里可以顺便介绍一下 Jepsen。它是 Kyle Kingsbury 开发的分布式系统测试框架,做法是启动一个真实集群,一边高并发读写,一边人为制造各种故障:切断网络造分区、随机杀进程、时钟跳变、暂停进程模拟长 GC。然后收集所有客户端观察到的结果,验证系统承诺的一致性保证是不是真的守住了。十几年下来 Jepsen 几乎测试过所有主流的分布式系统,也发现了不少问题:早期的 ES 就是在 Jepsen 报告(2014、2015 年)中被指出在分区下会丢失已确认写入,这直接推动了后来 7.x 的重写;MongoDB 早期版本、Redis 的 Redlock、Kafka、Cassandra 等系统也都被查出过一致性缺陷。ZooKeeper 是少数在这些测试里基本没有翻车的系统之一。所以这些协调系统的“正确性”,很大程度上是靠十几年对抗性测试和大量公开的 bug 修复累积出来的。新手写的 Raft 实现和 etcd 之间的差距,通常不是在算法理解上,而是在这几千个已经被别人踩过、修好过的边角 case 上。
共识协议本身比较昂贵,所以只用来处理最小的数据集。 过半数协议每次写入都需要多数节点同步落盘并确认,吞吐比较低、延迟比较高,不适合承载大规模数据。但它可以承载少量元数据,比如“当前 master 是谁”、“租约到期时间”这种几 KB 的信息。因此常见的架构会做成一个杠杆结构:由 3 到 5 台机器组成的微型共识内核(控制面)承担强一致仲裁“谁是老大”这类小规模决策;成百上千台机器的数据集群(数据面)拿到裁决结果之后,用比较普通的 primary-replica 机制搬 TB 级的数据。昂贵的协议管最小的状态,便宜的协议管最大的数据。这和 GFS 那种“控制流和数据流分离”的思路,其实是同一种思想在更高层的重演。
算法与实现的对应关系,可以用一张表说明:
| 算法 | 代表实现 | 主要使用者 |
|---|---|---|
| Paxos(1989,理论鼻祖) | Chubby、Spanner | 基本只在谷歌系内部 |
| ZAB(Paxos 变体) | ZooKeeper | Kafka(旧)、HBase、Flink HA |
| Raft(2014,为可理解而设计) | etcd、Consul、以及很多内嵌实现 | K8s、TiKV、CockroachDB、Kafka KRaft |
有几个点值得单独说明。Chubby 一般用不到,因为它从未开源,只存在于论文里。它的历史意义在于定义了“共识锁服务”这个品类,ZooKeeper 就是雅虎参照 Chubby 论文做的开源实现。实际能选择的独立协调服务基本就是 ZooKeeper、etcd、Consul 三种:JVM 生态一般用 ZK,云原生一代主要用 etcd(受益于 K8s 的普及),Consul 更多用在服务发现场景。在算法层面,Raft 已经成为事实上的选择。 Paxos 一直有难懂难实现的说法(Chubby 团队自己也发过论文讨论工程化的难度),Raft 的论文标题就叫 “In Search of an Understandable Consensus Algorithm”,通过强 leader、日志连续性等约束把算法讲得更容易理解、也更容易实现,2014 年之后新出现的系统几乎都选择了 Raft。可以简单归纳成一句话:Paxos 是理论上的正统,ZAB 属于历史遗产,Raft 是当前的主流选择。三者的数学内核基本一致,都是过半数、任期号加日志复制,差异更多在工程表述上。
Raft 算法简介,以及作为类库的新用法
算法核心
Raft 主要解决两个问题:如何选出唯一的 leader,以及 leader 如何把日志安全地复制出去。它的基础是复制状态机(Replicated State Machine):只要多台机器以相同顺序执行相同的命令日志,它们的状态最终一定一致。所以算法的核心目标就是维护一份在多数节点上保持一致的、只增不减的日志。
每个节点在任意时刻处于三种角色中的一种:
平时 leader 会定期广播心跳,follower 每收到一次心跳就重置自己的选举计时器。这里一个关键的设计是计时器时长是随机的(论文建议 150~300ms)。一旦 leader 挂掉,总会有一个 follower 先超时,它会自增任期号、转为候选人、发起投票,拿到过半选票后当选。随机化的作用主要是打散“多个候选人同时拉票、选票被瓜分”的情况,用一个比较朴素的概率手段解决协议层面的问题,是 Raft 相对 Paxos 的一个典型风格。任期号(term)就是前面提到的 fencing token:任何节点看到比自己更大的任期都会立即降级,看到更小的任期则直接拒绝,整个系统靠这一个单调递增的整数来对齐“当前处于谁的时代”。
日志复制是一个严格的单向管道:
所有写入都必须走 leader:先追加到自己的日志(此时尚未提交),然后并行推送给 follower,等到过半节点落盘确认之后才推进 commitIndex、把命令应用到状态机、返回客户端。图中第 7、8 条就属于“已追加但未提交”的悬空状态。如果 leader 此时挂掉,这两条日志可能丢失(客户端还没收到 ack,不算违约),而已经提交的 1~6 条算法上保证不会丢。每一个 AppendEntries 都会带上“前一条日志的 index 和 term”,follower 会校验,一旦不匹配就直接拒绝,leader 会回退重试直到找到匹配点,再覆盖式补齐后面的日志。这个机制保证了 Log Matching 性质:只要两份日志在某个位置的 index 和 term 相同,那么这个位置之前的所有内容也一定完全相同。日志相当于一条节节咬合的链,落后的副本靠常规的复制逻辑就能自愈。
安全性方面主要有两块基石。第一是选举限制:节点只会投票给“日志至少和自己一样新”的候选人。已提交意味着过半节点已经持有,当选也需要过半选票,两个过半必然有交集,交集里的节点会否决日志更旧的候选人,所以任何能当选的 leader 都一定拥有全部已提交的日志。第二是 leader 不能仅凭“复制过半”就提交前任任期的条目(论文里的 Figure 8 反例)。工程上的做法是每个 leader 上任后先提交一条自己任期的空日志(no-op),把前任的遗留条目一次性 commit 掉。
工程上还有三个必备机制:日志压缩(定期做快照并截断日志,落后太多的 follower 直接接收快照)、成员变更(一次只变更一个节点,保证新旧配置的多数派必然有交集)、Pre-Vote(被隔离的节点会不停超时把任期刷得很高,回归时会导致 leader 无谓下台;Pre-Vote 让它先做一次试探性拉票,确认能赢再正式自增任期,etcd 默认开启)。
作为类库的新用法:内嵌 Raft
传统使用共识的方式是部署一个实例:单独跑一套 ZK 或 etcd 集群,应用作为客户端连过去。近年比较明显的一个趋势是把 Raft 做成类库,像引入普通依赖一样 import 到自己的进程里,Kafka 的 KRaft、TiKV、CockroachDB 都属于这种形态。
分工上比较清晰。库负责算法本身:选举计时、投票规则、日志复制、成员变更这些实现难度较大的部分。使用者负责三件粘合层的事情:一是网络传输(库产出“把这条消息发给节点 2”的指令,具体怎么发由使用者决定);二是持久化(库要求“这几条日志落盘之后才能继续”,落到哪里由使用者决定);三是最核心的状态机,库把已提交的日志按顺序回调出来,使用者在回调里执行真正的业务动作:
1 | val node = RaftNode(config, transport, storage) |
只要 apply 是确定性的,多个节点各自回放同一份日志就一定会得到相同的状态。主流的库包括 etcd/raft(Go,CockroachDB 在用)、raft-rs(Rust,TiKV 在用)、hashicorp/raft(Consul、Vault 的内核)、Apache Ratis(Java)。所以“Kafka 甩掉 ZooKeeper”更准确的说法是:Kafka 没有取消共识,而是把原来跑在 ZK 集群里的共识,以库的形式内嵌到了自己的 controller 节点里。
内嵌方式还带来了外挂模式做不到的一种用法:Multi-Raft。既然 Raft 是一个库,一个进程里就可以同时跑成百上千个相互独立的 Raft 组。TiKV 就是把数据切成 Region,每个 Region 就是一个自己的 Raft 组,有自己的 leader 和日志。共识不再只是那个管元数据的旁观者,而是下沉到了每一个数据分片内部,数据本身的复制也由 Raft 保证强一致。这种做法在外挂 ZK 的架构里基本做不到(总不能为一万个 shard 建一万个 ZK 集群)。GFS 里那种“Master 发租约 + primary 定序”的松一致复制,在新一代系统里正是被 per-shard 的 Raft 取代的。
代价是使用者需要处理全部复杂性:库只保证算法本身正确,如果存储层把日志写坏了、apply 里出现非确定性(比如用了本地时间戳)、快照恢复有 bug,一样会导致数据丢失。所以实践中的选择大致是:如果是做基础设施类的产品(数据库、消息队列),才有必要内嵌,因为共识本身就是产品价值的一部分;做业务系统一般不需要涉及这个层面,需要强一致协调直接使用 etcd、ZK 或者云服务提供的能力就可以了。
尾声
把整条线索收拢起来看:Raft 里的 term 是 fencing token 的一种实现,“过半提交”对应的是 quorum 数学,leader lease 是 GFS 租约思想的延续,“所有写走 leader、日志定序、follower 回放”这个模式也可以直接对应到其他系统上。把 leader 换成 primary shard、日志换成 translog 就是 ES;换成 partition leader 就是 Kafka;换成持租约的 primary chunkserver 就是 GFS。Raft 严格来说并不是全新的东西,而是把这二十多年反复出现的几个基本构件,用一种比较容易证明的方式组合成了一个相对标准的答案。这也是它论文标题里 “Understandable” 的用意所在,重点并不是它变得更简单了,而是每个组件读者其实都已经似曾相识。