mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-25 20:56:42 +08:00
1081 lines
33 KiB
HTML
1081 lines
33 KiB
HTML
<!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">00 开篇词 为什么要学习Kafka?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/01 消息引擎系统ABC.md">01 消息引擎系统ABC.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/02 一篇文章带你快速搞定Kafka术语.md">02 一篇文章带你快速搞定Kafka术语.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/03 Kafka只是消息引擎系统吗?.md">03 Kafka只是消息引擎系统吗?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/04 我应该选择哪种Kafka?.md">04 我应该选择哪种Kafka?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/05 聊聊Kafka的版本号.md">05 聊聊Kafka的版本号.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/06 Kafka线上集群部署方案怎么做?.md">06 Kafka线上集群部署方案怎么做?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/07 最最最重要的集群参数配置(上).md">07 最最最重要的集群参数配置(上).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/08 最最最重要的集群参数配置(下).md">08 最最最重要的集群参数配置(下).md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/09 生产者消息分区机制原理剖析.md">09 生产者消息分区机制原理剖析.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/10 生产者压缩算法面面观.md">10 生产者压缩算法面面观.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/11 无消息丢失配置怎么实现?.md">11 无消息丢失配置怎么实现?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/12 客户端都有哪些不常见但是很高级的功能?.md">12 客户端都有哪些不常见但是很高级的功能?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/13 Java生产者是如何管理TCP连接的?.md">13 Java生产者是如何管理TCP连接的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/14 幂等生产者和事务生产者是一回事吗?.md">14 幂等生产者和事务生产者是一回事吗?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/15 消费者组到底是什么?.md">15 消费者组到底是什么?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/16 揭开神秘的“位移主题”面纱.md">16 揭开神秘的“位移主题”面纱.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/17 消费者组重平衡能避免吗?.md">17 消费者组重平衡能避免吗?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/18 Kafka中位移提交那些事儿.md">18 Kafka中位移提交那些事儿.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/19 CommitFailedException异常怎么处理?.md">19 CommitFailedException异常怎么处理?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/20 多线程开发消费者实例.md">20 多线程开发消费者实例.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/21 Java 消费者是如何管理TCP连接的.md">21 Java 消费者是如何管理TCP连接的.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/22 消费者组消费进度监控都怎么实现?.md">22 消费者组消费进度监控都怎么实现?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/23 Kafka副本机制详解.md">23 Kafka副本机制详解.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/24 请求是怎么被处理的?.md">24 请求是怎么被处理的?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/25 消费者组重平衡全流程解析.md">25 消费者组重平衡全流程解析.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/26 你一定不能错过的Kafka控制器.md">26 你一定不能错过的Kafka控制器.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
<a class="current-tab" href="/专栏/Kafka核心技术与实战/27 关于高水位和Leader Epoch的讨论.md">27 关于高水位和Leader Epoch的讨论.md.html</a>
|
||
|
||
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/28 主题管理知多少.md">28 主题管理知多少.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/29 Kafka动态配置了解下?.md">29 Kafka动态配置了解下?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/30 怎么重设消费者组位移?.md">30 怎么重设消费者组位移?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/31 常见工具脚本大汇总.md">31 常见工具脚本大汇总.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/32 KafkaAdminClient:Kafka的运维利器.md">32 KafkaAdminClient:Kafka的运维利器.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/33 Kafka认证机制用哪家?.md">33 Kafka认证机制用哪家?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/34 云环境下的授权该怎么做?.md">34 云环境下的授权该怎么做?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/35 跨集群备份解决方案MirrorMaker.md">35 跨集群备份解决方案MirrorMaker.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/36 你应该怎么监控Kafka?.md">36 你应该怎么监控Kafka?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/37 主流的Kafka监控框架.md">37 主流的Kafka监控框架.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/38 调优Kafka,你做到了吗?.md">38 调优Kafka,你做到了吗?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/39 从0搭建基于Kafka的企业级实时日志流处理平台.md">39 从0搭建基于Kafka的企业级实时日志流处理平台.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/40 Kafka Streams与其他流处理平台的差异在哪里?.md">40 Kafka Streams与其他流处理平台的差异在哪里?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/41 Kafka Streams DSL开发实例.md">41 Kafka Streams DSL开发实例.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/42 Kafka Streams在金融领域的应用.md">42 Kafka Streams在金融领域的应用.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/加餐 搭建开发环境、阅读源码方法、经典学习资料大揭秘.md">加餐 搭建开发环境、阅读源码方法、经典学习资料大揭秘.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/结束语 以梦为马,莫负韶华!.md">结束语 以梦为马,莫负韶华!.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 事务,因为事务机制会影响消费者所能看到的消息的范围,它不只是简单依赖高水位来判断。它依靠一个名为 LSO(Log 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,而高水位 > LEO 的情形是不被允许的。</p>
|
||
|
||
<p>下面,我们分别从 Leader 副本和 Follower 副本两个维度,来总结一下高水位和 LEO 的更新机制。</p>
|
||
|
||
<p><strong>Leader 副本</strong></p>
|
||
|
||
<p>处理生产者请求的逻辑如下:</p>
|
||
|
||
<ol>
|
||
|
||
<li>写入消息到本地磁盘。</li>
|
||
|
||
<li>更新分区高水位值。
|
||
|
||
i. 获取 Leader 副本所在 Broker 端保存的所有远程副本 LEO 值{LEO-1,LEO-2,……,LEO-n}。
|
||
|
||
ii. 获取 Leader 副本高水位值:currentHW。
|
||
|
||
iii. 更新 currentHW = min(currentHW, LEO-1,LEO-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<0, 0> 和 <1, 120>,那么,第一个 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 值小,而且缓存中也没有保存任何起始位移值 > 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">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/Kafka核心技术与实战/28 主题管理知多少.md">下一页</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>
|
||
|