learn.lianglianglee.com/文章/分布式一致性算法 Raft.md.html
2022-05-11 19:04:14 +08:00

1283 lines
44 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>分布式一致性算法 Raft.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/文章/AQS 万字图文全面解析.md.html">AQS 万字图文全面解析.md.html</a>
</li>
<li>
<a href="/文章/Docker 镜像构建原理及源码分析.md.html">Docker 镜像构建原理及源码分析.md.html</a>
</li>
<li>
<a href="/文章/ElasticSearch 小白从入门到精通.md.html">ElasticSearch 小白从入门到精通.md.html</a>
</li>
<li>
<a href="/文章/JVM CPU Profiler技术原理及源码深度解析.md.html">JVM CPU Profiler技术原理及源码深度解析.md.html</a>
</li>
<li>
<a href="/文章/JVM 垃圾收集器.md.html">JVM 垃圾收集器.md.html</a>
</li>
<li>
<a href="/文章/JVM 面试的 30 个知识点.md.html">JVM 面试的 30 个知识点.md.html</a>
</li>
<li>
<a href="/文章/Java IO 体系、线程模型大总结.md.html">Java IO 体系、线程模型大总结.md.html</a>
</li>
<li>
<a href="/文章/Java NIO浅析.md.html">Java NIO浅析.md.html</a>
</li>
<li>
<a href="/文章/Java 面试题集锦(网络篇).md.html">Java 面试题集锦(网络篇).md.html</a>
</li>
<li>
<a href="/文章/Java-直接内存 DirectMemory 详解.md.html">Java-直接内存 DirectMemory 详解.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中的SPI.md.html">Java中的SPI.md.html</a>
</li>
<li>
<a href="/文章/Java中的ThreadLocal.md.html">Java中的ThreadLocal.md.html</a>
</li>
<li>
<a href="/文章/Java线程池实现原理及其在美团业务中的实践.md.html">Java线程池实现原理及其在美团业务中的实践.md.html</a>
</li>
<li>
<a href="/文章/Java魔法类Unsafe应用解析.md.html">Java魔法类Unsafe应用解析.md.html</a>
</li>
<li>
<a href="/文章/Kafka 源码阅读笔记.md.html">Kafka 源码阅读笔记.md.html</a>
</li>
<li>
<a href="/文章/Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html">Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB Buffer Pool.md.html">MySQL · 引擎特性 · InnoDB Buffer Pool.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB IO子系统.md.html">MySQL · 引擎特性 · InnoDB IO子系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 事务系统.md.html">MySQL · 引擎特性 · InnoDB 事务系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 同步机制.md.html">MySQL · 引擎特性 · InnoDB 同步机制.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 数据页解析.md.html">MySQL · 引擎特性 · InnoDB 数据页解析.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB崩溃恢复.md.html">MySQL · 引擎特性 · InnoDB崩溃恢复.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · 临时表那些事儿.md.html">MySQL · 引擎特性 · 临时表那些事儿.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 半同步复制.md.html">MySQL 主从复制 半同步复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 基于GTID复制.md.html">MySQL 主从复制 基于GTID复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制.md.html">MySQL 主从复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 事务日志(redo log和undo log).md.html">MySQL 事务日志(redo log和undo log).md.html</a>
</li>
<li>
<a href="/文章/MySQL 亿级别数据迁移实战代码分享.md.html">MySQL 亿级别数据迁移实战代码分享.md.html</a>
</li>
<li>
<a href="/文章/MySQL 从一条数据说起-InnoDB行存储数据结构.md.html">MySQL 从一条数据说起-InnoDB行存储数据结构.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:事务和锁的面纱.md.html">MySQL 地基基础:事务和锁的面纱.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据字典.md.html">MySQL 地基基础:数据字典.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据库字符集.md.html">MySQL 地基基础:数据库字符集.md.html</a>
</li>
<li>
<a href="/文章/MySQL 性能优化:碎片整理.md.html">MySQL 性能优化:碎片整理.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html">MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:如何在日志中轻松定位大事务.md.html">MySQL 故障诊断:如何在日志中轻松定位大事务.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:教你快速定位加锁的 SQL.md.html">MySQL 故障诊断:教你快速定位加锁的 SQL.md.html</a>
</li>
<li>
<a href="/文章/MySQL 日志详解.md.html">MySQL 日志详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL 的半同步是什么?.md.html">MySQL 的半同步是什么?.md.html</a>
</li>
<li>
<a href="/文章/MySQL中的事务和MVCC.md.html">MySQL中的事务和MVCC.md.html</a>
</li>
<li>
<a href="/文章/MySQL事务_事务隔离级别详解.md.html">MySQL事务_事务隔离级别详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL优化优化 select count().md.html">MySQL优化优化 select count().md.html</a>
</li>
<li>
<a href="/文章/MySQL共享锁、排他锁、悲观锁、乐观锁.md.html">MySQL共享锁、排他锁、悲观锁、乐观锁.md.html</a>
</li>
<li>
<a href="/文章/MySQL的MVCC多版本并发控制.md.html">MySQL的MVCC多版本并发控制.md.html</a>
</li>
<li>
<a href="/文章/QingStor 对象存储架构设计及最佳实践.md.html">QingStor 对象存储架构设计及最佳实践.md.html</a>
</li>
<li>
<a href="/文章/RocketMQ 面试题集锦.md.html">RocketMQ 面试题集锦.md.html</a>
</li>
<li>
<a href="/文章/SnowFlake 雪花算法生成分布式 ID.md.html">SnowFlake 雪花算法生成分布式 ID.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html">Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 教程:如何开发一个 starter.md.html">Spring Boot 教程:如何开发一个 starter.md.html</a>
</li>
<li>
<a href="/文章/Spring MVC 原理.md.html">Spring MVC 原理.md.html</a>
</li>
<li>
<a href="/文章/Spring MyBatis和Spring整合的奥秘.md.html">Spring MyBatis和Spring整合的奥秘.md.html</a>
</li>
<li>
<a href="/文章/Spring 帮助你更好的理解Spring循环依赖.md.html">Spring 帮助你更好的理解Spring循环依赖.md.html</a>
</li>
<li>
<a href="/文章/Spring 循环依赖及解决方式.md.html">Spring 循环依赖及解决方式.md.html</a>
</li>
<li>
<a href="/文章/Spring中眼花缭乱的BeanDefinition.md.html">Spring中眼花缭乱的BeanDefinition.md.html</a>
</li>
<li>
<a href="/文章/Vert.x 基础入门.md.html">Vert.x 基础入门.md.html</a>
</li>
<li>
<a href="/文章/eBay 的 Elasticsearch 性能调优实践.md.html">eBay 的 Elasticsearch 性能调优实践.md.html</a>
</li>
<li>
<a href="/文章/不可不说的Java“锁”事.md.html">不可不说的Java“锁”事.md.html</a>
</li>
<li>
<a href="/文章/互联网并发限流实战.md.html">互联网并发限流实战.md.html</a>
</li>
<li>
<a href="/文章/从ReentrantLock的实现看AQS的原理及应用.md.html">从ReentrantLock的实现看AQS的原理及应用.md.html</a>
</li>
<li>
<a href="/文章/从SpringCloud开始聊微服务架构.md.html">从SpringCloud开始聊微服务架构.md.html</a>
</li>
<li>
<a href="/文章/全面了解 JDK 线程池实现原理.md.html">全面了解 JDK 线程池实现原理.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性理论与算法.md.html">分布式一致性理论与算法.md.html</a>
</li>
<li>
<a class="current-tab" href="/文章/分布式一致性算法 Raft.md.html">分布式一致性算法 Raft.md.html</a>
</li>
<li>
<a href="/文章/分布式唯一 ID 解析.md.html">分布式唯一 ID 解析.md.html</a>
</li>
<li>
<a href="/文章/分布式链路追踪:集群管理设计.md.html">分布式链路追踪:集群管理设计.md.html</a>
</li>
<li>
<a href="/文章/动态代理种类及原理,你知道多少?.md.html">动态代理种类及原理,你知道多少?.md.html</a>
</li>
<li>
<a href="/文章/响应式架构与 RxJava 在有赞零售的实践.md.html">响应式架构与 RxJava 在有赞零售的实践.md.html</a>
</li>
<li>
<a href="/文章/大数据算法——布隆过滤器.md.html">大数据算法——布隆过滤器.md.html</a>
</li>
<li>
<a href="/文章/如何优雅地记录操作日志?.md.html">如何优雅地记录操作日志?.md.html</a>
</li>
<li>
<a href="/文章/如何设计一个亿级消息量的 IM 系统.md.html">如何设计一个亿级消息量的 IM 系统.md.html</a>
</li>
<li>
<a href="/文章/异步网络模型.md.html">异步网络模型.md.html</a>
</li>
<li>
<a href="/文章/当我们在讨论CQRS时我们在讨论些神马.md.html">当我们在讨论CQRS时我们在讨论些神马.md.html</a>
</li>
<li>
<a href="/文章/彻底理解 MySQL 的索引机制.md.html">彻底理解 MySQL 的索引机制.md.html</a>
</li>
<li>
<a href="/文章/最全的 116 道 Redis 面试题解答.md.html">最全的 116 道 Redis 面试题解答.md.html</a>
</li>
<li>
<a href="/文章/有赞权限系统(SAM).md.html">有赞权限系统(SAM).md.html</a>
</li>
<li>
<a href="/文章/有赞零售中台建设方法的探索与实践.md.html">有赞零售中台建设方法的探索与实践.md.html</a>
</li>
<li>
<a href="/文章/服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html">服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html</a>
</li>
<li>
<a href="/文章/深入浅出Cache.md.html">深入浅出Cache.md.html</a>
</li>
<li>
<a href="/文章/深入理解 MySQL 底层实现.md.html">深入理解 MySQL 底层实现.md.html</a>
</li>
<li>
<a href="/文章/漫画讲解 git rebase VS git merge.md.html">漫画讲解 git rebase VS git merge.md.html</a>
</li>
<li>
<a href="/文章/生成浏览器唯一稳定 ID 的探索.md.html">生成浏览器唯一稳定 ID 的探索.md.html</a>
</li>
<li>
<a href="/文章/缓存 如何保证缓存与数据库的双写一致性?.md.html">缓存 如何保证缓存与数据库的双写一致性?.md.html</a>
</li>
<li>
<a href="/文章/网易严选怎么做全链路监控的?.md.html">网易严选怎么做全链路监控的?.md.html</a>
</li>
<li>
<a href="/文章/美团万亿级 KV 存储架构与实践.md.html">美团万亿级 KV 存储架构与实践.md.html</a>
</li>
<li>
<a href="/文章/美团点评Kubernetes集群管理实践.md.html">美团点评Kubernetes集群管理实践.md.html</a>
</li>
<li>
<a href="/文章/美团百亿规模API网关服务Shepherd的设计与实现.md.html">美团百亿规模API网关服务Shepherd的设计与实现.md.html</a>
</li>
<li>
<a href="/文章/解读《阿里巴巴 Java 开发手册》背后的思考.md.html">解读《阿里巴巴 Java 开发手册》背后的思考.md.html</a>
</li>
<li>
<a href="/文章/认识 MySQL 和 Redis 的数据一致性问题.md.html">认识 MySQL 和 Redis 的数据一致性问题.md.html</a>
</li>
<li>
<a href="/文章/进阶Dockerfile 高阶使用指南及镜像优化.md.html">进阶Dockerfile 高阶使用指南及镜像优化.md.html</a>
</li>
<li>
<a href="/文章/铁总在用的高性能分布式缓存计算框架 Geode.md.html">铁总在用的高性能分布式缓存计算框架 Geode.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/面试最常被问的 Java 后端题.md.html">面试最常被问的 Java 后端题.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计在互联网业务开发中的实践.md.html">领域驱动设计在互联网业务开发中的实践.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计的菱形对称架构.md.html">领域驱动设计的菱形对称架构.md.html</a>
</li>
<li>
<a href="/文章/高效构建 Docker 镜像的最佳实践.md.html">高效构建 Docker 镜像的最佳实践.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>分布式一致性算法 Raft</h1>
<h3><strong>一、Raft 算法背景</strong></h3>
<p>在学术理论界,分布式一致性算法的代表还是 Paxos但是少数理解的人觉得很简单尚未理解的觉得很难大多数人还是一知半解。Paxos 的可理解性 &amp; 工程落地性的门槛很高。斯坦福学者花了很多时间理解 Paxos于是他们研究出来 Raft。本文主要是介绍 Raft 算法的基本原理。</p>
<h3><strong>二、Raft 算法基本原理</strong></h3>
<p>共识算法就是保证一个集群的多台机器协同工作,在遇到请求时,数据能够保持一致。即使遇到机器宕机,整个系<strong>统仍然能够对外保持</strong>服务的可用性。</p>
<p>Raft 将共识问题分解三个子问题:</p>
<ol>
<li><strong>Leader election 领导选举</strong>:有且仅有一个 leader 节点,如果 leader 宕机,通过选举机制选出新的 leader</li>
<li><strong>Log replication 日志复制</strong>leader 从客户端接收数据更新/删除请求,然后日志复制到 follower 节点,从而保证集群数据的一致性;</li>
<li><strong>Safety 安全性</strong>:通过安全性原则来处理一些特殊 case保证 Raft 算法的完备性;</li>
</ol>
<p>所以Raft 算法核心流程可以归纳为:</p>
<ul>
<li>首先选出 leaderleader 节点负责接收外部的数据更新/删除请求;</li>
<li>然后日志复制到其他 follower 节点,同时通过安全性的准则来保证整个日志复制的一致性;</li>
<li>如果遇到 leader 故障followers 会重新发起选举出新的 leader</li>
</ul>
<p>这里先介绍一下日志同步的概念:服务器接收客户的数据更新/删除请求,这些请求会落地为命令日志。只要输入状态机的日志命令相同,状态机的执行结果就相同。所以 Raft 的核心就是 leader 发出日志同步请求follower 接收并同步日志,最终保证整个集群的日志一致性。</p>
<p><img src="assets/v2-02b6004fd2b7886d10ccd611b6d66d6c_1440w.jpg" alt="img" /></p>
<h3><strong>2.1 Leader Election 领导选举</strong></h3>
<p>集群中每个节点只能处于 Leader、Follower 和 Candidate 三种状态的一种:</p>
<ol>
<li><strong>follower 从节点</strong></li>
</ol>
<ul>
<li>节点默认是 follower</li>
<li>如果**刚刚开始 ** 或 <strong>和 leader 通信超时</strong>follower 会发起选举,变成 candidate然后去竞选 leader</li>
<li>如果收到其他 candidate 的竞选投票请求,按照<strong>先来先得</strong> &amp; <strong>每个任期只能投票一次</strong> 的投票原则投票;</li>
</ul>
<ol>
<li><strong>candidate 候选者</strong></li>
</ol>
<ul>
<li>follower 发起选举后就变为 candidate会向其他节点拉选票。candidate 的票会投给自己,所以不会向其他节点投票</li>
<li>如果获得超过<strong>半数</strong>的投票candidate 变成 leader然后马上和其他节点通信表明自己的 leader 的地位;</li>
<li>如果选举超时,重新发起选举;</li>
<li>如果遇到更高任期 Term 的 leader 的通信请求,转化为 follower</li>
</ul>
<ol>
<li><strong>leader 主节点</strong></li>
</ol>
<ul>
<li>成为 leader 节点后,此时可以接受客户端的数据请求,负责日志同步;</li>
<li>如果遇到更高任期 Term 的 candidate 的通信请求,这说明 candidate 正在竞选 leader此时之前任期的 leader 转化为 follower且完成投票</li>
<li>如果遇到更高任期 Term 的 leader 的通信请求,这说明已经选举成功新的 leader此时之前任期的 leader 转化为 follower</li>
</ul>
<p>具体的节点状态转换参考下图:</p>
<p><img src="assets/v2-537082d871c75e59f6b7556b48cee932_1440w.jpg" alt="img" /></p>
<p>Raft 算法把时间轴划分为不同任期 Term。每个任期 Term 都有自己的编号 TermId该编号全局唯一且单调递增。如下图每个任务的开始都** Leader Election 领导选举**。如果选举成功,则进入维持任务 Term 阶段,此时 leader 负责接收客户端请求并负责复制日志。Leader 和所有 follower 都保持通信,如果 follower 发现通信超时TermId 递增并发起新的选举。如果选举成功则进入新的任期。如果选举失败TermId 递增,然后重新发起选举直到成功。</p>
<p>举个例子参考下图Term N 选举成功Term N+1 和 Term N+2 选举失败Term N+3 重新选举成功。</p>
<p><img src="assets/v2-4b0b8661f85d76f662d742ba5b2abcb9_1440w.jpg" alt="img" /></p>
<p>具体的说Leader 在任期内会周期性向其他 follower 节点发送心跳来维持地位。follower 如果发现心跳超时,就认为 leader 节点宕机或不存在。随机等待一定时间后follower 会发起选举,变成 candidate然后去竞选 leader。选举结果有三种情况</p>
<ol>
<li><strong>获取超过半数投票,赢得选举</strong></li>
</ol>
<ul>
<li>
<p>当 Candidate 获得超过半数的投票时,代表自己赢得了选举,且转化为 leader。此时它会马上向其他节点发送请求从而确认自己的 leader 地位,从而阻止新一轮的选举;</p>
</li>
<li>
<p><strong>投票原则</strong>:当多个 Candidate 竞选 Leader 时:</p>
</li>
<li>
<ul>
<li>一个任期内follower 只会<strong>投票一次票</strong>,且投票<strong>先来显得</strong></li>
<li>Candidate 存储的日志至少要和 follower 一样新(<strong>安全性准则</strong>),否则拒绝投票;</li>
</ul>
</li>
</ul>
<ol>
<li><strong>投票未超过半数,选举失败</strong></li>
</ol>
<ul>
<li>当 Candidate 没有获得超过半数的投票时,说明多个 Candidate 竞争投票导致过于分散,或者出现了丢包现象。此时,认为当期任期选举失败,任期 TermId+1然后发起新一轮选举</li>
<li>上述机制可能出现多个 Candidate 竞争投票,导致每个 Candidate 一直得不到超过半数的票,最终导致无限选举投票循环;</li>
<li><strong>投票分散问题解决:</strong> Raft 会给每个 Candidate 在固定时间内随机确认一个超时时间(一般为 150-300ms。这么做可以尽量避免新的一次选举出现多个 Candidate 竞争投票的现象;</li>
</ul>
<ol>
<li><strong>收到其他 Leader 通信请求</strong></li>
</ol>
<ul>
<li>如果 Candidate 收到其他声称自己是 Leader 的请求的时候,通过任期 TermId 来判断是否处理;</li>
<li>如果请求的任期 TermId 不小于 Candidate 当前任期 TermId那么 Candidate 会承认该 Leader 的合法地位并转化为 Follower</li>
<li>否则,拒绝这次请求,并继续保持 Candidate</li>
</ul>
<p>简单的多,<strong>Leader Election 领导选举</strong> 通过若干的投票原则,保证一次选举有且仅可能最多选出一个 leader从而解决了脑裂问题。</p>
<h3><strong>2.2 Log Replication 日志复制</strong></h3>
<p>选举 leader 成功后整个集群就可以正常对外提供服务了。Leader 接收所有客户端请求,然后转化为 log 复制命令,发送通知其他节点完成日志复制请求。每个日志复制请求包括状态机命令 &amp; 任期号,同时还有前一个日志的任期号和日志索引。状态机命令表示客户端请求的数据操作指令,任期号表示 leader 的当前任期。</p>
<p>follower 收到日志复制请求的处理流程:</p>
<ol>
<li>follower 会使用前一个日志的任期号和日志索引来对比自己的数据:</li>
</ol>
<ul>
<li>如果相同,接收复制请求,回复 ok</li>
<li>否则回拒绝复制当前日志,回复 error</li>
</ul>
<ol>
<li>leader 收到拒绝复制的回复后,继续发送节点日志复制请求,不过这次会带上更前面的一个日志任期号和索引;</li>
<li>如此循环往复,直到找到一个共同的任期号&amp;日志索引。此时 follower 从这个索引值开始复制,最终和 leader 节点日志保持一致;</li>
<li>日志复制过程中Leader 会无限重试直到成功。如果超过半数的节点复制日志成功,就可以任务当前数据请求达成了共识,即日志可以 commite 提交了;</li>
</ol>
<p>综上, <strong>Log Replication 日志复制</strong>有两个特点:</p>
<ol>
<li>如果在不同日志中的两个条目有着相同索引和任期号,则所存储的命令是相同的,这点是由 leader 来保证的;</li>
<li>如果在不同日志中的两个条目有着相同索引和任期号,则它们之间所有条目完全一样,这点是由日志复制的规则来保证的;</li>
</ol>
<p>举个例子最上面表示日志索引这个是保证唯一性。每个方块代表指定任期内的数据操作目前来看LogIndex 1-4 的日志已经完成同步LogIndex 5 的正在同步LogIndex6 还未开始同步。Raft 日志提交的过程有点类似两阶段原子提交协议 2PC不过和 2PC 的最大区别是Raft 要求超过一般节点同意即可 commited2PC 要求所有节点同意才能 commited。</p>
<p><img src="assets/v2-2f3ad84935c439bf16b0018351162173_1440w.jpg" alt="img" /></p>
<p><strong>日志不一致问题</strong>在正常情况下leader 和 follower 的日志复制能够保证整个集群的一致性,但是遇到 leader 崩溃的时候leader 和 follower 日志可能出现了不一致的状态,此时 follower 相比 leader 缺少部分日志。</p>
<p>为了解决数据不一致性Raft 算法规定 <strong>follower 强制复制 leader 节日的日志</strong>,即 follower 不一致日志都会被 leader 的日志覆盖,最终 follower 和 leader 保持一致。简单的说,从前向后寻找 follower 和 leader 第一个公共 LogIndex 的位置然后从这个位置开始follower 强制复制 leader 的日志。但是这么多还有其他的安全性问题,所以需要引入<strong>Safety 安全性</strong>规则 。</p>
<h3><strong>2.3 Safety 安全性</strong></h3>
<p>当前的 <strong>Leader election 领导选举</strong><strong>Log replication 日志复制</strong>并不能保证 Raft 算法的安全性,在一些特殊情况下,可能导致数据不一致,所以需要引入下面安全性规则。</p>
<h3><strong>2.3.1 Election Safety 选举安全性:避免脑裂问题</strong></h3>
<p>选举安全性要求一个任期 Term 内只能有一个 leader即不能出现脑裂现象否者 raft 的日志复制原则很可能出现数据覆盖丢失的问题。Raft 算法通过规定若干投票原则来解决这个问题:</p>
<ul>
<li>一个任期内follower 只会<strong>投票一次票</strong>,且先来先得;</li>
<li>Candidate 存储的日志至少要和 follower 一样新;</li>
<li>只有获得超过半数投票才有机会成为 leader</li>
</ul>
<h3><strong>2.3.2 Leader Append-Only 日志只能由 leader 添加修改</strong></h3>
<p>Raft 算法规定,所有的数据请求都要交给 leader 节点处理,要求:</p>
<ol>
<li>leader 只能日志追加日志,<strong>不能覆盖日志</strong></li>
<li>只有 leader 的日志项才能被提交follower 不能接收写请求和提交日志;</li>
<li>只有已经提交的日志项,才能被应用到状态机中;</li>
<li>选举时限制新 leader 日志包含所有已提交日志项;</li>
</ol>
<h3><strong>2.3.3 Log Matching 日志匹配特性</strong></h3>
<p>这点主要是为了保证日志的唯一性,要求:</p>
<ol>
<li>如果在不同日志中的两个条目有着相同索引和任期号,则所存储的命令是相同的;</li>
<li>如果在不同日志中的两个条目有着相同索引和任期号,则它们之间所有条目完全一样;</li>
</ol>
<h3><strong>2.3.4 Leader Completeness 选举完备性leader 必须具备最新提交日志</strong></h3>
<p>Raft 规定:只<strong>有拥有最新提交日志的 follower 节点才有资格成为 leader 节点。</strong> 具体做法candidate 竞选投票时会携带最新提交日志follower 会用自己的日志和 candidate 做比较。</p>
<ul>
<li>如果 follower 的更新,那么拒绝这次投票;</li>
<li>否则根据前面的投票规则处理。这样就可以保证只有最新提交节点成为 leader</li>
</ul>
<p>因为日志提交需要超过半数的节点同意,所以针对日志同步落后的 follower还未同步完全部日志导致落后于其他节点在竞选 leader 的时候,肯定拿不到超过半数的票,也只有那些完成同步的才有可能获取超过半数的票成为 leader。</p>
<p>日志更新判断方式是比较日志项的 term 和 index</p>
<ul>
<li>如果 TermId 不同,选择 TermId 最大的;</li>
<li>如果 TermId 相同,选择 Index 最大的;</li>
</ul>
<p>下面举个例子来解释为什么需要这个原则,如下图,假如集群中 follower4 在 LogIndex3 故障宕机,经过一段时间间,任期 Term3 的 leader 接收并提交了很多日志LogIndex1-5 已经提交LogIndex6 正在复制中)。然后 follower4 恢复正常,在没有和 leader 完成同步日志的情况下,如果 leader 突然宕机,此时开始领导选举。再假设在 Term4 follower4 当选 leader。根据日志复制的规则其他 follower 强制复制 leader 的日志,那么已经提交却没完成同步的日志将会被强制覆盖掉,这回导致已提交日志被覆盖。</p>
<p><img src="assets/v2-b82a1fbff057248ef6f203113b28a41b_1440w.jpg" alt="img" /></p>
<h3><strong>2.3.5 State Machine Safety 状态机安全性:确保当前任期日志提交</strong></h3>
<p>考虑到当前的日志复制规则</p>
<ul>
<li>当前 follower 节点强制复制 leader 节点;</li>
<li>假如以前 Term 日志复制超过半数节点,在面对当前任期日志的节点比较中,很明显当前任期节点更新,有资格成为 leader</li>
</ul>
<p>上述两条就可能出现已有任期日志被覆盖的情况,这意味着已复制超过半数的以前任期日志被强制覆盖了,和前面提到的日志安全性矛盾。</p>
<p>所以Raft 对日志提交有额外安全机制leader 只能提交当前任期 Term 的日志,旧任期 Term以前的数据只能通过当前任期 Term 的数据提交来间接完成提交。简单的说,日志提交有两个条件需要满足:</p>
<ol>
<li><strong>当前任期</strong></li>
<li><strong>复制结点超过半数</strong></li>
</ol>
<p>下面举个例子来解释为什么需要这个原则,如下图:</p>
<p><img src="assets/v2-7d59b070a40226665fdfec7aad4135c7_1440w.jpg" alt="img" /></p>
<ol>
<li>任期 Term2</li>
</ol>
<ul>
<li>follower1 是 leader此时 LogIndex3 已经复制到 follower2且正在给 follower3 复制,此时 follower 突然宕机;</li>
</ul>
<ol>
<li>任期 Term3</li>
</ol>
<ul>
<li>leader 选举。follower5 发起投票可以得到自己、follower3、follower4 的票3/5最终成为 leader</li>
<li>在任期 Term3 内,提交接收客户请求并提交 LogIndex3-5但是暂时未复制到其他节点然后宕机</li>
</ul>
<ol>
<li>任期 Term4</li>
</ol>
<ul>
<li>leader 选举follower1 发起选举可以得到自己、follower2、follower3、follower4 的票4/5最终成为 leader</li>
<li>此时 follower1 将 LogIndex3 复制到 follower3此时 LogIndex3 复制超过半数,接着在本地提交了 LogIndex4然后宕机</li>
</ul>
<ol>
<li>任期 Term4</li>
</ol>
<ul>
<li>leader 选举follower5 发起选举可以得到自己、follower2、follower3、follower4 的票4/5最终成为 leader</li>
<li>此时其他节点需要强制复制 follower5 的日志,那么 follower1、follower2、follower3 的日志被强制覆盖掉。即虽然 LogIndex3 被复制到了超过半数节点,但也有可能被覆盖掉;</li>
</ul>
<p>如何解决这个问题呢Raft 在日志项提交上增加了限制:只有 <strong>当前任期</strong><strong>复制超过半数</strong> 的日志才可以提交。即只有 LogIndex4 提交后LogIndex3 才会被提交。</p>
<h3><strong>三、Paxos VS Raft这个世界上只有一种一致性算法那就是 Paxos</strong></h3>
<p>Basic Paxos 算法没有 leader proposer 角色,是一个纯粹的去中心化的分布式算法,但是它存在若干不足(只能单值共识 &amp; 活锁 &amp; 网络开销大)。所以才有了以 leader proposer 为核心的 Multi Paxos 算法(由一个去中心化的算法变为 leader-based 的算法。Raft 算法相当于 Multi Paxos 的进一步优化,主要通过增加两个限制:</p>
<ol>
<li><strong>日志添加次序性</strong></li>
</ol>
<ul>
<li>Raft 要求日志必须要串行连续添加的;</li>
<li>Multi Paxos 可以并发添加日志,没有顺序性要求,所以日志可能存在空洞现象;</li>
</ul>
<ol>
<li><strong>选主限制</strong></li>
</ol>
<ul>
<li>Raft 要求只有拥有最新日志的节点才有资格当选 leader因为日志是串行连续添加的所以 Raft 能够根据日志确认最新节点;</li>
<li>在 Multi Paxos 算法中由于日志是并发添加的,所以无法确认最新日志的节点,所以 Multi Paxos 可以选择任意节点作为了 leader proposer 节点,成为 leader 节点后需要把其他日志补全;</li>
</ul>
<p>下面是我个人的理解:</p>
<ul>
<li>作为分布式算法Raft 的规则限制很多,但是每个规则都简单易懂,最重要的是 leader-based 的,整个程序是一个串行的流程,这使得更加容易理解和实现;</li>
<li>作为对比Multi Paxos 的限制就很少了,每个节点都可以成为 leader并发添加日志这使得理解和落地就没那么简单</li>
<li>不同业务场景下有着不同的述求,所以一致性算法选择 Multi Paxos 还是 Raft 看各自需求。</li>
</ul>
<h3><strong>四、总结</strong></h3>
<p>Raft 协议就是一种 leader-based 的共识算法,算法设计出发点就是可理解性以及工程的落地性。学习总结分布式一致性算法 Paxos 和 Raft 对我们理解、设计、实现、部署、测试分布式系统都大有益处,如果文章有啥不合适的地方,希望提出宝贵意见。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/文章/分布式一致性理论与算法.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/文章/分布式唯一 ID 解析.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70998042cf9b8b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>