learn.lianglianglee.com/专栏/Kafka核心技术与实战/27 关于高水位和Leader Epoch的讨论.md.html
2022-08-14 03:40:33 +08:00

386 lines
31 KiB
HTML
Raw 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>27 关于高水位和Leader Epoch的讨论.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="/专栏/Kafka核心技术与实战/00 开篇词 为什么要学习Kafka.md.html">00 开篇词 为什么要学习Kafka</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/01 消息引擎系统ABC.md.html">01 消息引擎系统ABC</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/02 一篇文章带你快速搞定Kafka术语.md.html">02 一篇文章带你快速搞定Kafka术语</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/03 Kafka只是消息引擎系统吗.md.html">03 Kafka只是消息引擎系统吗</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/04 我应该选择哪种Kafka.md.html">04 我应该选择哪种Kafka</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/05 聊聊Kafka的版本号.md.html">05 聊聊Kafka的版本号</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/06 Kafka线上集群部署方案怎么做.md.html">06 Kafka线上集群部署方案怎么做</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/07 最最最重要的集群参数配置(上).md.html">07 最最最重要的集群参数配置(上)</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/08 最最最重要的集群参数配置(下).md.html">08 最最最重要的集群参数配置(下)</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/09 生产者消息分区机制原理剖析.md.html">09 生产者消息分区机制原理剖析</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/10 生产者压缩算法面面观.md.html">10 生产者压缩算法面面观</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/11 无消息丢失配置怎么实现?.md.html">11 无消息丢失配置怎么实现?</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/12 客户端都有哪些不常见但是很高级的功能?.md.html">12 客户端都有哪些不常见但是很高级的功能?</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/13 Java生产者是如何管理TCP连接的.md.html">13 Java生产者是如何管理TCP连接的</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/14 幂等生产者和事务生产者是一回事吗?.md.html">14 幂等生产者和事务生产者是一回事吗?</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/15 消费者组到底是什么?.md.html">15 消费者组到底是什么?</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/16 揭开神秘的“位移主题”面纱.md.html">16 揭开神秘的“位移主题”面纱</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/17 消费者组重平衡能避免吗?.md.html">17 消费者组重平衡能避免吗?</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/18 Kafka中位移提交那些事儿.md.html">18 Kafka中位移提交那些事儿</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/19 CommitFailedException异常怎么处理.md.html">19 CommitFailedException异常怎么处理</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/20 多线程开发消费者实例.md.html">20 多线程开发消费者实例</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/21 Java 消费者是如何管理TCP连接的.md.html">21 Java 消费者是如何管理TCP连接的</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/22 消费者组消费进度监控都怎么实现?.md.html">22 消费者组消费进度监控都怎么实现?</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/23 Kafka副本机制详解.md.html">23 Kafka副本机制详解</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/24 请求是怎么被处理的?.md.html">24 请求是怎么被处理的?</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/25 消费者组重平衡全流程解析.md.html">25 消费者组重平衡全流程解析</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/26 你一定不能错过的Kafka控制器.md.html">26 你一定不能错过的Kafka控制器</a>
</li>
<li>
<a class="current-tab" href="/专栏/Kafka核心技术与实战/27 关于高水位和Leader Epoch的讨论.md.html">27 关于高水位和Leader Epoch的讨论</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/28 主题管理知多少.md.html">28 主题管理知多少</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/29 Kafka动态配置了解下.md.html">29 Kafka动态配置了解下</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/30 怎么重设消费者组位移?.md.html">30 怎么重设消费者组位移?</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/31 常见工具脚本大汇总.md.html">31 常见工具脚本大汇总</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/32 KafkaAdminClientKafka的运维利器.md.html">32 KafkaAdminClientKafka的运维利器</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/33 Kafka认证机制用哪家.md.html">33 Kafka认证机制用哪家</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/34 云环境下的授权该怎么做?.md.html">34 云环境下的授权该怎么做?</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/35 跨集群备份解决方案MirrorMaker.md.html">35 跨集群备份解决方案MirrorMaker</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/36 你应该怎么监控Kafka.md.html">36 你应该怎么监控Kafka</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/37 主流的Kafka监控框架.md.html">37 主流的Kafka监控框架</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/38 调优Kafka你做到了吗.md.html">38 调优Kafka你做到了吗</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/39 从0搭建基于Kafka的企业级实时日志流处理平台.md.html">39 从0搭建基于Kafka的企业级实时日志流处理平台</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/40 Kafka Streams与其他流处理平台的差异在哪里.md.html">40 Kafka Streams与其他流处理平台的差异在哪里</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/41 Kafka Streams DSL开发实例.md.html">41 Kafka Streams DSL开发实例</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/42 Kafka Streams在金融领域的应用.md.html">42 Kafka Streams在金融领域的应用</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/加餐 搭建开发环境、阅读源码方法、经典学习资料大揭秘.md.html">加餐 搭建开发环境、阅读源码方法、经典学习资料大揭秘</a>
</li>
<li>
<a href="/专栏/Kafka核心技术与实战/结束语 以梦为马,莫负韶华!.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>27 关于高水位和Leader Epoch的讨论</h1>
<p>你好我是胡夕。今天我要和你分享的主题是Kafka 中的高水位和 Leader Epoch 机制。</p>
<p>你可能听说过高水位High Watermark但不一定耳闻过 Leader Epoch。前者是 Kafka 中非常重要的概念,而后者是社区在 0.11 版本中新推出的,主要是为了弥补高水位机制的一些缺陷。鉴于高水位机制在 Kafka 中举足轻重,而且深受各路面试官的喜爱,今天我们就来重点说说高水位。当然,我们也会花一部分时间来讨论 Leader Epoch 以及它的角色定位。</p>
<h2>什么是高水位?</h2>
<p>首先我们要明确一下基本的定义什么是高水位或者说什么是水位水位一词多用于流式处理领域比如Spark Streaming 或 Flink 框架中都有水位的概念。教科书中关于水位的经典定义通常是这样的:</p>
<blockquote>
<p>在时刻 T任意创建时间Event Time为 T且 T≤T 的所有事件都已经到达或被观测到,那么 T 就被定义为水位。</p>
</blockquote>
<p>“Streaming System”一书则是这样表述水位的</p>
<blockquote>
<p>水位是一个单调增加且表征最早未完成工作oldest work not yet completed的时间戳。</p>
</blockquote>
<p>为了帮助你更好地理解水位,我借助这本书里的一张图来说明一下。</p>
<p><img src="assets/fb2c9e883b78c5d10b09b4a9773b8c13.png" alt="img" /></p>
<p>图中标注“Completed”的蓝色部分代表已完成的工作标注“In-Flight”的红色部分代表正在进行中的工作两者的边界就是水位线。</p>
<p>在 Kafka 的世界中水位的概念有一点不同。Kafka 的水位不是时间戳更与时间无关。它是和位置信息绑定的具体来说它是用消息位移来表征的。另外Kafka 源码使用的表述是高水位,因此,今天我也会统一使用“高水位”或它的缩写 HW 来进行讨论。值得注意的是Kafka 中也有低水位Low Watermark它是与 Kafka 删除消息相关联的概念,与今天我们要讨论的内容没有太多联系,我就不展开讲了。</p>
<h2>高水位的作用</h2>
<p>在 Kafka 中,高水位的作用主要有 2 个。</p>
<ol>
<li>定义消息可见性,即用来标识分区下的哪些消息是可以被消费者消费的。</li>
<li>帮助 Kafka 完成副本同步。</li>
</ol>
<p>下面这张图展示了多个与高水位相关的 Kafka 术语。我来详细解释一下图中的内容,同时澄清一些常见的误区。</p>
<p><img src="assets/c2243d5887f0ca7a20a524914b85a8dd.png" alt="img" /></p>
<p>我们假设这是某个分区 Leader 副本的高水位图。首先,请你注意图中的“已提交消息”和“未提交消息”。我们之前在专栏[第 11 讲]谈到 Kafka 持久性保障的时候,特意对两者进行了区分。现在,我借用高水位再次强调一下。在分区高水位以下的消息被认为是已提交消息,反之就是未提交消息。消费者只能消费已提交消息,即图中位移小于 8 的所有消息。注意,这里我们不讨论 Kafka 事务,因为事务机制会影响消费者所能看到的消息的范围,它不只是简单依赖高水位来判断。它依靠一个名为 LSOLog Stable Offset的位移值来判断事务型消费者的可见性。</p>
<p>另外,需要关注的是,<strong>位移值等于高水位的消息也属于未提交消息。也就是说,高水位上的消息是不能被消费者消费的</strong></p>
<p>图中还有一个日志末端位移的概念,即 Log End Offset简写是 LEO。它表示副本写入下一条消息的位移值。注意数字 15 所在的方框是虚线,这就说明,这个副本当前只有 15 条消息,位移值是从 0 到 14下一条新消息的位移是 15。显然介于高水位和 LEO 之间的消息就属于未提交消息。这也从侧面告诉了我们一个重要的事实,那就是:<strong>同一个副本对象,其高水位值不会大于 LEO 值</strong></p>
<p><strong>高水位和 LEO 是副本对象的两个重要属性</strong>。Kafka 所有副本都有对应的高水位和 LEO 值,而不仅仅是 Leader 副本。只不过 Leader 副本比较特殊Kafka 使用 Leader 副本的高水位来定义所在分区的高水位。换句话说,<strong>分区的高水位就是其 Leader 副本的高水位</strong></p>
<h2>高水位更新机制</h2>
<p>现在,我们知道了每个副本对象都保存了一组高水位值和 LEO 值,但实际上,在 Leader 副本所在的 Broker 上,还保存了其他 Follower 副本的 LEO 值。我们一起来看看下面这张图。</p>
<p><img src="assets/be0c738f34e3cd1d95d509f16cbb7f82.png" alt="img" /></p>
<p>在这张图中我们可以看到Broker 0 上保存了某分区的 Leader 副本和所有 Follower 副本的 LEO 值,而 Broker 1 上仅仅保存了该分区的某个 Follower 副本。Kafka 把 Broker 0 上保存的这些 Follower 副本又称为<strong>远程副本</strong>Remote Replica。Kafka 副本机制在运行过程中,会更新 Broker 1 上 Follower 副本的高水位和 LEO 值,同时也会更新 Broker 0 上 Leader 副本的高水位和 LEO 以及所有远程副本的 LEO但它不会更新远程副本的高水位值也就是我在图中标记为灰色的部分。</p>
<p>为什么要在 Broker 0 上保存这些远程副本呢?其实,它们的主要作用是,<strong>帮助 Leader 副本确定其高水位,也就是分区高水位</strong></p>
<p>为了帮助你更好地记忆这些值被更新的时机,我做了一张表格。只有搞清楚了更新机制,我们才能开始讨论 Kafka 副本机制的原理,以及它是如何使用高水位来执行副本消息同步的。</p>
<p><img src="assets/c81e888761b5f04822216845be981649.jpeg" alt="img" /></p>
<p>在这里,我稍微解释一下,什么叫与 Leader 副本保持同步。判断的条件有两个。</p>
<ol>
<li>该远程 Follower 副本在 ISR 中。</li>
<li>该远程 Follower 副本 LEO 值落后于 Leader 副本 LEO 值的时间,不超过 Broker 端参数 replica.lag.time.max.ms 的值。如果使用默认值的话,就是不超过 10 秒。</li>
</ol>
<p>乍一看,这两个条件好像是一回事,因为目前某个副本能否进入 ISR 就是靠第 2 个条件判断的。但有些时候,会发生这样的情况:即 Follower 副本已经“追上”了 Leader 的进度,却不在 ISR 中,比如某个刚刚重启回来的副本。如果 Kafka 只判断第 1 个条件的话,就可能出现某些副本具备了“进入 ISR”的资格但却尚未进入到 ISR 中的情况。此时,分区高水位值就可能超过 ISR 中副本 LEO而高水位 &gt; LEO 的情形是不被允许的。</p>
<p>下面,我们分别从 Leader 副本和 Follower 副本两个维度,来总结一下高水位和 LEO 的更新机制。</p>
<p><strong>Leader 副本</strong></p>
<p>处理生产者请求的逻辑如下:</p>
<ol>
<li>写入消息到本地磁盘。</li>
<li>更新分区高水位值。
i. 获取 Leader 副本所在 Broker 端保存的所有远程副本 LEO 值{LEO-1LEO-2……LEO-n}。
ii. 获取 Leader 副本高水位值currentHW。
iii. 更新 currentHW = min(currentHW, LEO-1LEO-2……LEO-n)。</li>
</ol>
<p>处理 Follower 副本拉取消息的逻辑如下:</p>
<ol>
<li>读取磁盘(或页缓存)中的消息数据。</li>
<li>使用 Follower 副本发送请求中的位移值更新远程副本 LEO 值。</li>
<li>更新分区高水位值(具体步骤与处理生产者请求的步骤相同)。</li>
</ol>
<p><strong>Follower 副本</strong></p>
<p>从 Leader 拉取消息的处理逻辑如下:</p>
<ol>
<li>写入消息到本地磁盘。</li>
<li>更新 LEO 值。</li>
<li>更新高水位值。
i. 获取 Leader 发送的高水位值currentHW。
ii. 获取步骤 2 中更新过的 LEO 值currentLEO。
iii. 更新高水位为 min(currentHW, currentLEO)。</li>
</ol>
<h2>副本同步机制解析</h2>
<p>搞清楚了这些值的更新机制之后,我来举一个实际的例子,说明一下 Kafka 副本同步的全流程。该例子使用一个单分区且有两个副本的主题。</p>
<p>当生产者发送一条消息时Leader 和 Follower 副本对应的高水位是怎么被更新的呢?我给出了一些图片,我们一一来看。</p>
<p>首先是初始状态。下面这张图中的 remote LEO 就是刚才的远程副本的 LEO 值。在初始状态时,所有值都是 0。</p>
<p><img src="assets/2ecec2915d1a52f136517d15192a4c72.png" alt="img" /></p>
<p>当生产者给主题分区发送一条消息后,状态变更为:</p>
<p><img src="assets/42841bfd3d5d4fa8560e176cb9d20b5b.png" alt="img" /></p>
<p>此时Leader 副本成功将消息写入了本地磁盘,故 LEO 值被更新为 1。</p>
<p>Follower 再次尝试从 Leader 拉取消息。和之前不同的是,这次有消息可以拉取了,因此状态进一步变更为:</p>
<p><img src="assets/f65911a5c247ad83826788fd275e1ade.png" alt="img" /></p>
<p>这时Follower 副本也成功地更新 LEO 为 1。此时Leader 和 Follower 副本的 LEO 都是 1但各自的高水位依然是 0还没有被更新。<strong>它们需要在下一轮的拉取中被更新</strong>,如下图所示:</p>
<p><img src="assets/f30a4651605352db542b76b3512df110.png" alt="img" /></p>
<p>在新一轮的拉取请求中,由于位移值是 0 的消息已经拉取成功,因此 Follower 副本这次请求拉取的是位移值 =1 的消息。Leader 副本接收到此请求后,更新远程副本 LEO 为 1然后更新 Leader 高水位为 1。做完这些之后它会将当前已更新过的高水位值 1 发送给 Follower 副本。Follower 副本接收到以后,也将自己的高水位值更新成 1。至此一次完整的消息同步周期就结束了。事实上Kafka 就是利用这样的机制,实现了 Leader 和 Follower 副本之间的同步。</p>
<h2>Leader Epoch 登场</h2>
<p>故事讲到这里似乎很完美依托于高水位Kafka 既界定了消息的对外可见性,又实现了异步的副本同步机制。不过,我们还是要思考一下这里面存在的问题。</p>
<p>从刚才的分析中我们知道Follower 副本的高水位更新需要一轮额外的拉取请求才能实现。如果把上面那个例子扩展到多个 Follower 副本情况可能更糟也许需要多轮拉取请求。也就是说Leader 副本高水位更新和 Follower 副本高水位更新在时间上是存在错配的。这种错配是很多“数据丢失”或“数据不一致”问题的根源。基于此,社区在 0.11 版本正式引入了 Leader Epoch 概念,来规避因高水位更新错配导致的各种不一致问题。</p>
<p>所谓 Leader Epoch我们大致可以认为是 Leader 版本。它由两部分数据组成。</p>
<ol>
<li>Epoch。一个单调增加的版本号。每当副本领导权发生变更时都会增加该版本号。小版本号的 Leader 被认为是过期 Leader不能再行使 Leader 权力。</li>
<li>起始位移Start Offset。Leader 副本在该 Epoch 值上写入的首条消息的位移。</li>
</ol>
<p>我举个例子来说明一下 Leader Epoch。假设现在有两个 Leader Epoch&lt;0, 0&gt;&lt;1, 120&gt;,那么,第一个 Leader Epoch 表示版本号是 0这个版本的 Leader 从位移 0 开始保存消息,一共保存了 120 条消息。之后Leader 发生了变更,版本号增加到 1新版本的起始位移是 120。</p>
<p>Kafka Broker 会在内存中为每个分区都缓存 Leader Epoch 数据,同时它还会定期地将这些信息持久化到一个 checkpoint 文件中。当 Leader 副本写入消息到磁盘时Broker 会尝试更新这部分缓存。如果该 Leader 是首次写入消息,那么 Broker 会向缓存中增加一个 Leader Epoch 条目,否则就不做更新。这样,每次有 Leader 变更时,新的 Leader 副本会查询这部分缓存,取出对应的 Leader Epoch 的起始位移,以避免数据丢失和不一致的情况。</p>
<p>接下来,我们来看一个实际的例子,它展示的是 Leader Epoch 是如何防止数据丢失的。请先看下图。</p>
<p><img src="assets/69f8ccf346b568a7310c69de9863ca42.png" alt="img" /></p>
<p>我稍微解释一下,单纯依赖高水位是怎么造成数据丢失的。开始时,副本 A 和副本 B 都处于正常状态A 是 Leader 副本。某个使用了默认 acks 设置的生产者程序向 A 发送了两条消息A 全部写入成功,此时 Kafka 会通知生产者说两条消息全部发送成功。</p>
<p>现在我们假设 Leader 和 Follower 都写入了这两条消息,而且 Leader 副本的高水位也已经更新了,但 Follower 副本高水位还未更新——这是可能出现的。还记得吧Follower 端高水位的更新与 Leader 端有时间错配。倘若此时副本 B 所在的 Broker 宕机,当它重启回来后,副本 B 会执行日志截断操作,将 LEO 值调整为之前的高水位值,也就是 1。这就是说位移值为 1 的那条消息被副本 B 从磁盘中删除,此时副本 B 的底层磁盘文件中只保存有 1 条消息,即位移值为 0 的那条消息。</p>
<p>当执行完截断操作后,副本 B 开始从 A 拉取消息,执行正常的消息同步。如果就在这个节骨眼上,副本 A 所在的 Broker 宕机了,那么 Kafka 就别无选择,只能让副本 B 成为新的 Leader此时当 A 回来后,需要执行相同的日志截断操作,即将高水位调整为与 B 相同的值,也就是 1。这样操作之后位移值为 1 的那条消息就从这两个副本中被永远地抹掉了。这就是这张图要展示的数据丢失场景。</p>
<p>严格来说,这个场景发生的前提是<strong>Broker 端参数 min.insync.replicas 设置为 1</strong>。此时一旦消息被写入到 Leader 副本的磁盘,就会被认为是“已提交状态”,但现有的时间错配问题导致 Follower 端的高水位更新是有滞后的。如果在这个短暂的滞后时间窗口内,接连发生 Broker 宕机,那么这类数据的丢失就是不可避免的。</p>
<p>现在,我们来看下如何利用 Leader Epoch 机制来规避这种数据丢失。我依然用图的方式来说明。</p>
<p><img src="assets/1078956136267ca958d82bfa16d825e1.png" alt="img" /></p>
<p>场景和之前大致是类似的,只不过引用 Leader Epoch 机制后Follower 副本 B 重启回来后,需要向 A 发送一个特殊的请求去获取 Leader 的 LEO 值。在这个例子中,该值为 2。当获知到 Leader LEO=2 后B 发现该 LEO 值不比它自己的 LEO 值小,而且缓存中也没有保存任何起始位移值 &gt; 2 的 Epoch 条目,因此 B 无需执行任何日志截断操作。这是对高水位机制的一个明显改进,即副本是否执行日志截断不再依赖于高水位进行判断。</p>
<p>现在,副本 A 宕机了B 成为 Leader。同样地当 A 重启回来后,执行与 B 相同的逻辑判断,发现也不用执行日志截断,至此位移值为 1 的那条消息在两个副本中均得到保留。后面当生产者程序向 B 写入新消息时,副本 B 所在的 Broker 缓存中,会生成新的 Leader Epoch 条目:[Epoch=1, Offset=2]。之后,副本 B 会使用这个条目帮助判断后续是否执行日志截断操作。这样,通过 Leader Epoch 机制Kafka 完美地规避了这种数据丢失场景。</p>
<h2>小结</h2>
<p>今天,我向你详细地介绍了 Kafka 的高水位机制以及 Leader Epoch 机制。高水位在界定 Kafka 消息对外可见性以及实现副本机制等方面起到了非常重要的作用,但其设计上的缺陷给 Kafka 留下了很多数据丢失或数据不一致的潜在风险。为此,社区引入了 Leader Epoch 机制,尝试规避掉这类风险。事实证明,它的效果不错,在 0.11 版本之后,关于副本数据不一致性方面的 Bug 的确减少了很多。如果你想深入学习 Kafka 的内部原理,今天的这些内容是非常值得你好好琢磨并熟练掌握的。</p>
<p><img src="assets/42c165479c40770587988cb68a2c5b5c.png" alt="img" /></p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/Kafka核心技术与实战/26 你一定不能错过的Kafka控制器.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/Kafka核心技术与实战/28 主题管理知多少.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":"709972091fa53d60","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>