learn.lianglianglee.com/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析(下).md.html
2022-05-11 19:04:14 +08:00

652 lines
51 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>阿里云PolarDB及其共享存储PolarFS技术实现分析.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 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 class="current-tab" 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>阿里云PolarDB及其共享存储PolarFS技术实现分析</h1>
<p>上篇介绍了PolarDB数据库及其后端共享存储PolarFS系统的基本架构和组成模块是最基础的部分。本篇重点分析PolarFS的数据IO流程元数据更新流程以及PolarDB数据库节点如何适配PolarFS这样的共享存储系统。</p>
<p>PolarFS的数据IO操作</p>
<p>写操作</p>
<p><img src="assets/2018101218075655117f31-2ab1-4701-8698-958c1774619c.jpg" alt="img" /></p>
<p>一般情况下写操作不会涉及到卷上文件系统的元数据更新因为在写之前就已经通过libpfs的pfs_posix_fallocate()这个API将Block预分配给文件这就避免在读写IO路径上出现代价较高的文件系统元数据同步过程。上图是PolarFS的写操作流程图每步操作解释如下</p>
<ol>
<li>POLARDB通过libpfs发送一个写请求Request1经由ring buffer发送到PolarSwitch</li>
<li>PolarSwitch根据本地缓存的元数据将Request1发送至对应Chunk的Leader节点ChunkServer1</li>
<li>Request1到达ChunkServer1后节点上的RDMA NIC将Request1放到一个预分配好的内存buffer中基于Request1构造一个请求对象并将该对象加到请求队列中。一个IO轮询线程不断轮询这个请求队列一旦发现有新请求则立即开始处理</li>
<li>IO处理线程通过异步调用将Request1通过SPDK写到Chunk对应的WAL日志块上同时将请求通过RDMA异步发向给Chunk的Follower节点ChunkServer2、ChunkServer3。由于都是异步调用所以数据传输是并发进行的</li>
<li>当Request1请求到达ChunkServer2、ChunkServer3后同样通过RDMA NIC将其放到预分配好的内存buffer并加入到复制队列中</li>
<li>Follower节点上的IO轮询线程被触发Request1通过SPDK异步地写入该节点的Chunk副本对应的WAL日志块上</li>
<li>当Follower节点的写请求成功后会在回调函数中通过RDMA向Leader节点发送一个应答响应</li>
<li>Leader节点收到ChunkServer2、ChunkServer3任一节点成功的应答后即形成Raft组的majority。主节点通过SPDK将Request1写到请求中指定的数据块上</li>
<li>随后Leader节点通过RDMA NIC向PolarSwitch返回请求处理结果</li>
<li>PolarSwitch标记请求成功并通知上层的POLARDB。</li>
</ol>
<p>读请求无需这么复杂的步骤lipfs发起的读请求直接通过PolarSwitch路由到数据对应Chunk的Leader节点ChunkServer1从其中读取对应的数据返回即可。需要说明的是在ChunkServer上有个子模块叫IoScheduler用于保证发生并发读写访问时读操作能够读到最新的已提交数据。</p>
<p>基于用户态的网络和IO路径</p>
<p>在本地IO处理上PolarFS基于预分配的内存buffer来处理请求将buffer中的内容直接使用SPDK写入WAL日志和数据块中。PolarFS读写数据基于SPDK套件直接通过DMA操作硬件设备SSD卡而不是操作系统内核IO协议栈解决了内核IO协议栈慢的问题通过轮询的方式监听硬件设备IO完成事件消除了上下文切换和中断的开销。还可以将IO处理线程和CPU进行一一映射每个IO处理线程独占CPU相互之间处理不同的IO请求绑定不同的IO设备硬件队列一个IO请求生命周期从头到尾都在一个线程一颗CPU上处理不需要锁进行互斥。这种技术实现最大化的和高速设备进行性能交互实现一颗CPU达每秒约20万次IO处理的能力并且保持线性的扩展能力也就意味着4颗CPU可以达到每秒80万次IO处理的能力在性能和经济型上远高于内核。</p>
<p>网络也是类似的情况。过去传统的以太网网卡发一个报文到另一台机器中间通过一跳交换机大概需要一百到两百微秒。POLARDB支持ROCE以太网通过RDMA网络直接将本机的内存写入另一台机器的内存地址或者从另一台机器的内存读一块数据到本机中间的通讯协议编解码、重传机制都由RDMA网卡来完成不需要CPU参与使性能获得极大提升传输一个4K大小报文只需要6、7微秒的时间。</p>
<p><img src="assets/201810121807560e0df922-e82b-47dc-a09f-ab579a49cd6b.png" alt="img" /></p>
<p>如同内核的IO协议栈跟不上高速存储设备能力内核的TCP/IP协议栈跟不上高速网络设备能力也被POLARDB的用户态网络协议栈代替。这样就解决了HDFS和Ceph等目前的分布式文件系统存在的性能差、延迟大的问题。</p>
<p>基于ParallelRaft的数据可靠性保证</p>
<p>在PolarFS中位于不同ChunkServer上的3个Chunk数据副本使用改进型Raft协议ParallelRaft来保障可靠性通过快速主从切换和majority机制确保能够容忍少部分Chunk副本离线时仍能够持续提供在线读写服务即数据的高可用。</p>
<p>在标准的Raft协议中raft日志是按序被Follower节点确认按序被Leader节点提交的。这是因为Raft协议不允许出现空洞一条raft日志被提交意味着它之前的所有raft日志都已经被提交。在数据库系统中对不同数据的并发更新是常态也正因为这点才有了事务的组提交技术但如果引入Raft协议意味着组提交技术在PolarFS数据多副本可靠性保障这一层退化为串行提交对于性能会产生很大影响。通过将多个事务batch成一个raft日志通过在一个Raft Group的Leader和Follower间建立多个连接来同时处理多个raft日志这两种方式batching&amp;pipelining能够缓解性能退化。但batch会导致额外延迟batch也不能过大。pipelining由于Raft协议的束缚仍然需要保证按序确认和提交如果出现由于网络等原因导致前后pipeline上的raft日志发送往follow或回复leader时乱序那么就不可避免得出现等待。</p>
<p>为了进一步优化性能PolarFS对Raft协议进行了改进。核心思想就是解除按序确认按序提交的束缚。将其变为乱序确认乱序提交和乱序应用。首先看看这样做的可行性假设每个raft日志代表一个事务多个事务能够并行提交说明其不存在冲突对应到存储层往往意味着没有修改相同的数据比如事务T1修改File1的Block1事务T2修改File1的Block2。显然先修改Block1还是Block2对于存储层还是数据库层都没有影响。这真是能够乱序的基础。下图为优化前后的性能表现</p>
<p><img src="assets/20181012180756884f75b2-4d35-4b10-adf7-d72403a29370.png" alt="img" /></p>
<p>但T1和T2都修改了同一个表的数据导致表的统计信息发生了变化比如T1执行后表中有10条记录T2执行后变为15条举例而已不一定正确。所以他们都需要更新存储层的相同BlockX该更新操作就不能乱序了。</p>
<p>为了解决上述所说的问题ParallelRaft协议引入look behind bufferLBB。每个raft日志都有个LBB缓存了它之前的N个raft日志所修改的LBA信息。LBA即Logical Block Address表示该Block在Chunk中的偏移位置从0到10GB。通过判断不同的raft日志所包含的LBA是否有重合来决定能否进行乱序/并行应用比如上面的例子先后修改了BlockX的raft日志就可以通过LBB发现如果T2对BlockX的更新先完成了确认和提交在应用前通过LBB发现所依赖的T1对BlockX的修改还没有应用。那么就会进入pending队列直到T1对BlockX完成应用。</p>
<p>另外乱序意味着日志会有空洞。因此Leader选举阶段额外引入了一个Merge阶段填补Leader中raft日志的空洞能够有效保障协议的Leader日志的完整性。</p>
<p><img src="assets/2018101218075642d62d6e-8108-44f3-ac3d-6c71fc7eb8ad.png" alt="img" /></p>
<p>PolarFS元数据管理与更新</p>
<p>PolarFS各节点元数据维护</p>
<p>libpfs仅维护文件块块在文件中的偏移位置到卷块块在卷中的偏移位置的映射关系并未涉及到卷中Chunk跟ChunkServer间的关系Chunk的物理位置信息这样libpfs就跟存储层解耦为Chunk分配实际物理空间时无需更新libpfs层的元数据。而Chunk到ChunkServer的映射关系也就是物理存储空间到卷的分配行为由PolarCtrl组件负责PolarCtrl完成分配后会更新PolarSwitch上的缓存确保libpfs到ChunkServer的IO路径是正确的。</p>
<p>Chunk中Block的LBA到Block真实物理地址的映射表以及每块SSD盘的空闲块位图均全部缓存在ChunkServer的内存中使得用户数据IO访问能够全速推进。</p>
<p>PolarFS元数据更新流程</p>
<p>前面我们介绍过PolarDB为每个数据库实例创建了一个volume/卷它是一个文件系统创建时生成了对应的元数据信息。由于PolarFS是个可多点挂载的共享访问分布式文件系统需要确保一个挂载点更新的元数据能够及时同步到其他挂载点上。比如一个节点增加/删除了文件或者文件的大小发生了变化这些都需要持久化到PolarFS的元数据上并让其他节点感知到。下面我们来讨论PolarFS如何更新元数据并进行同步。</p>
<p>PolarFS的每个卷/文件系统实例都有相应的Journal文件和与之对应的Paxos文件。Journal文件记录了文件系统元数据的修改历史是该卷各个挂载点之间元数据同步的中心。Journal文件逻辑上是一个固定大小的循环bufferPolarFS会根据水位来回收Journal。如果一个节点希望在Journal文件中追加项其必需使用DiskPaxos算法来获取Journal文件控制权。</p>
<p>正常情况下为了确保文件系统元数据和数据的一致性PolarFS上的一个卷仅设置一个计算节点进行读写模式挂载其他计算节点以只读形式挂载文件系统读写节点锁会在元数据记录持久化后马上释放锁。但是如果该读写节点crash了该锁就不会被释放为此加在Journal文件上的锁会有过期时间在过期后其他节点可以通过执行DiskPaxos来重新竞争对Journal文件的控制权。当PolarFS的一个挂载节点开始同步其他节点修改的元数据时它从上次扫描的位置扫描到Journal末尾将新entry更新到节点的本地缓存中。PolarFS同时使用push和pull方式来进行节点间的元数据同步。</p>
<p>下图展示了文件系统元数据更新和同步的过程:</p>
<p><img src="assets/2018101218075681f91273-0f69-4647-9e9e-2c07628a85eb.png" alt="img" /></p>
<ol>
<li>Node 1是读写挂载点其在pfs_fallocate()调用中将卷的第201个block分配给FileID为316的文件后通过Paxos文件请求互斥锁并顺利获得锁。</li>
<li>Node 1开始记录事务至journal中。最后写入项标记为pending tail。当所有的项记录之后pending tail变成journal的有效tail。</li>
<li>Node1更新superblock记录修改的元数据。与此同时node2尝试获取访问互斥锁由于此时node1拥有的互斥锁Node2会失败重试。</li>
<li>Node2在Node1释放lock后可能是锁的租约到期所致拿到锁但journal中node1追加的新项决定了node2的本地元数据是过时的。</li>
<li>Node2扫描新项后释放lock。然后node2回滚未记录的事务并更新本地metadata。最后Node2进行事务重试。</li>
<li>Node3开始自动同步元数据它只需要load增量项并在它本地重放即可。</li>
</ol>
<p>PolarFS的元速度更新机制非常适合PolarDB一写多读的典型应用扩展模式。正常情况下一写多读模式没有锁争用开销只读实例可以通过原子IO无锁获取Journal信息从而使得PolarDB可以提供近线性的QPS性能扩展。</p>
<p>数据库如何适配PolarFS</p>
<p>大家可能认为如果读写实例和只读实例共享了底层的数据和日志只要把只读数据库配置文件中的数据目录换成读写实例的目录貌似就可以直接工作了。但是这样会遇到很多问题MySQL适配PolarFS有很多细节问题需要处理有些问题只有在真正做适配的时候还能想到下面介绍已知存在的问题并分析数据库层是如何解决的。</p>
<p>数据缓存和数据一致性</p>
<p>从数据库到硬件,存在很多层缓存,对基于共享存储的数据库方案有影响的缓存层包括数据库缓存,文件系统缓存。</p>
<p>数据库缓存主要是InnoDB的Buffer PoolBP存在2个问题</p>
<ol>
<li>读写节点的数据更改会缓存在bp上只有完成刷脏页操作后polarfs才能感知所以如果在刷脏之前只读节点发起读数据操作读到的数据是旧的</li>
<li>就算PolarFS感知到了只读节点的已经在BP中的数据还是旧的。所以需要解决不同节点间的缓存一致性问题。</li>
</ol>
<p>PolarDB采用的方法是基于redolog复制的节点间数据同步。可能我们会想到Primary节点通过网络将redo日志发送给ReadOnly/Replica节点但其实并不是现在采用的方案是redo采用非ring buffer模式每个文件固定大小大小达到后Rotate到新的文件在写模式上走Direct IO模式确保磁盘上的redo数据是最新的在此基础上Primary节点通过网络通知其他节点可以读取的redo文件及偏移位置让这些节点自主到共享存储上读取所需的redo信息并进行回放。流程如下图所示</p>
<p><img src="assets/201810121807570a821658-a1f6-4974-83d7-728b8b7ba812.png" alt="img" /></p>
<p>由于StandBy节点与读写节点不共享底层存储所以需要走网络发送redo的内容。节点在回放redo时需区分是ReadOnly节点还是StandBy节点对于ReadOnly节点其仅回放对应的Page页已在BP中的redo未在BP中的page不会主动从共享存储上读取且BP中Apply过的Page也不会回刷到共享存储。但对于StandBy节点需要全量回放并回刷到底层存储上。</p>
<p>文件系统缓存主要是元数据缓存问题。文件系统缓存包括Page CacheInode/Dentry Cache等对于Page Cache可以通过Direct IO绕过。但对于VFSVirtual File System层的Inode Cache无法通过Direct IO模式而需采用o_sync的访问模式但这样导致性能严重下降没有实际意义。vfs层cache无法通过direct io模式绕过是个很严重的问题这就意味着读写节点创建的文件只读节点无法感知那么针对这个新文件的后续IO操作只读节点就会报错如果采用内核文件系统不好进行改造。</p>
<p>PolarDB通过元数据同步来解决该问题它是个用户态文件系统数据的IO流程不走内核态的Page Cache也不走VFS的Inode/Dentry Cache完全自己掌控。共享存储上的文件系统元数据通过前述的更新流程实现即可。通过这种方式解决了最基本的节点间数据同步问题。</p>
<p>事务的数据可见性问题</p>
<p>一、MySQL/InnoDB通过Undo日志来实现事务的MVCC由于只读节点跟读写节点属于不同的mysqld进程读写节点在进行Undo日志Purge的时候并不会考虑此时在只读节点上是否还有事务要访问即将被删除的Undo Page这就会导致记录旧版本被删除后只读节点上事务读取到的数据是错误的。</p>
<p>针对该问题PolarDB提供两种解决方式</p>
<ul>
<li>所有ReadOnly定期向Primary汇报自己的最大能删除的Undo数据页Primary节点统筹安排</li>
<li>当Primary节点删除Undo数据页时候ReadOnly接收到日志后判断即将被删除的Page是否还在被使用如果在使用则等待超过一个时间后还未有结束则直接给客户端报错。</li>
</ul>
<p>二、还有个问题由于InnoDB BP刷脏页有多种方式其并不是严格按照oldest modification来的这就会导致有些事务未提交的页已经写入共享存储只读节点读到该页后需要通过Undo Page来重建可见的版本但可能此时Undo Page还未刷盘这就会出现只读上事务读取数据的另一种错误。</p>
<p>针对该问题PolarDB解决方法是</p>
<ol>
<li>限制读写节点刷脏页机制如果脏页的redo还没有被只读节点回放那么该页不能被刷回到存储上。这就确保只读节点读取到的数据它之前的数据链是完整的或者说只读节点已经知道其之前的所有redo日志。这样即使该数据的记录版本当前的事务不可见也可以通过undo构造出来。即使undo对应的page是旧的可以通过redo构造出所需的undo page。</li>
<li>replica需要缓存所有未刷盘的数据变更(即RedoLog)只有primary节点把脏页刷入盘后replica缓存的日志才能被释放。这是因为如果数据未刷盘那么只读读到的数据就可能是旧的需要通过redo来重建出来参考第一点。另外虽然buffer pool中可能已经缓存了未刷盘的page的数据但该page可能会被LRU替换出去当其再次载入所以只读节点必须缓存这些redo。</li>
</ol>
<p>DDL问题</p>
<p>如果读写节点把一个表删了反映到存储上就是把文件删了。对于mysqld进程来说它会确保删除期间和删除后不再有事务访问该表。但是在只读节点上可能此时还有事务在访问PolarFS在完成文件系统元数据同步后就会导致只读节点的事务访问存储出错。</p>
<p>PolarDB目前的解决办法是如果主库对一个表进行了表结构变更操作需要拷表在操作返回成功前必须通知到所有的ReadOnly节点(有一个最大的超时时间),告诉他们,这个表已经被删除了,后续的请求都失败。当然这种强同步操作会给性能带来极大的影响,有进一步的优化的空间。</p>
<p>Change Buffer问题</p>
<p>Change Buffer本质上是为了减少二级索引带来的IO开销而产生的一种特殊缓存机制。当对应的二级索引页没有被读入内存时暂时缓存起来当数据页后续被读进内存时再进行应用这个特性也带来的一些问题该问题仅存在于StandBy中。例如Primary节点可能因为数据页还未读入内存相应的操作还缓存在Change Buffer中但是StandBy节点则因为不同的查询请求导致这个数据页已经读入内存可以直接将二级索引修改合并到数据页上无需经过Change Buffer了。但由于复制的是Primary节点的redo且需要保证StandBy和Primary在存储层的一致性所以StandBy节点还是会有Change Buffer的数据页和其对应的redo日志如果该脏页回刷到存储上就会导致数据不一致。</p>
<p>为了解决这个问题PolarDB引入shadow page的概念把未修改的数据页保存到其中将cChange Buffer记录合并到原来的数据页上同时关闭该Mtr的redo这样修改后的Page就不会放到Flush List上。也就是StandBy实例的存储层数据跟Primary节点保持一致。</p>
<p>性能测试</p>
<p>性能评估不是本文重点,官方的性能结果也不一定是靠谱的,只有真实测试过了才算数。在此仅简单列举阿里云自己的性能测试结果,权当一个参考。</p>
<p>PolarFS性能</p>
<p>不同块大小的IO延迟</p>
<p><img src="assets/20181012180757df8a5f55-170a-48a9-a29a-89684abe0796.png" alt="img" /></p>
<p>4KB大小的不同请求类型</p>
<p><img src="assets/2018101218075894122dfb-1025-423b-96e6-256a23d945ce.png" alt="img" /></p>
<p>PolarDB整体性能</p>
<p><strong>使用不同底层存储时性能表现</strong></p>
<p><img src="assets/2018101218075831d79e75-8a89-4927-a9ff-c8883036b23c.png" alt="img" /></p>
<p><strong>对外展示的性能表现</strong></p>
<p><img src="assets/20181012180758768a9a74-9133-45e5-abb1-d8d65dccb592.png" alt="img" /></p>
<p>与Aurora简单对比</p>
<p>阿里云的PolarDB和AWS Aurora虽然同为基于MySQL和共享存储的Cloud-Native Database云原生数据库方案很多原理是相同的包括基于redo的物理复制和计算节点间状态同步。但在实现上也存在很大的不同Aurora在存储层采用日志即数据的机制计算节点无需再将脏页写入到存储节点大大减少了网络IO量但这样的机制需要对InnoDB存储引擎层做很大的修改难度极大。而PolarDB基本上遵从了原有的MySQL IO路径通过优化网络和IO路径来提高网络和IO能力相对来说在数据库层面并未有框架性的改动相对容易些。个人认为Aurora在数据库技术创新上更胜一筹但PolarDB在数据库系统级架构优化上做得更好以尽可能小的代价获得了足够好的收益。</p>
<p>另附PolarFS的架构师曹伟在知乎上对PolarDB和Aurora所做的对比</p>
<p><em>在设计方法上阿里云的PolarDB和Aurora走了不一样的路归根结底是我们的出发点不同。AWS的RDS一开始就是架设在它的虚拟机产品EC2之上的使用的存储是云盘EBS。EC2和EBS之间通过网络通讯因此AWS的团队认为“网络成为数据库的瓶颈”在Aurora的论文中他们开篇就提出“Instead, the bottleneck moves to the network between the database tier requesting I/Os and the storage tier that performs these I/Os.” Aurora设计于12到13年之际当时网络主流是万兆网络确实容易成为瓶颈。而PolarDB是从15年开始研发的我们见证了IDC从万兆到25Gb RDMA网络的飞跃。因此我们非常大胆的判断未来几年主机通过高速网络互联其传输速率会和本地PCIe总线存储设备带宽打平网络无论在延迟还是带宽上都会接近总线因此不再成为高性能服务器的瓶颈。而恰恰是软件过去基于内核提供的syscall开发的软件代码才是拖慢系统的一环。Bottleneck resides in the software.</em></p>
<p><em>在架构上Aurora和PolarDB各有特色。我认为PolarDB的架构和技术更胜一筹。</em></p>
<p><em>1现代云计算机型的演进和分化计算机型向高主频多CPU大内存的方向演进存储机型向高密度低功耗方向发展。机型的分化可以大大提高机器资源的使用率降低TCO。</em></p>
<p><em>因此PolarStore中大量采用OS-bypass和zero-copy的技术来节约CPU降低处理单位I/O吞吐需要消耗的CPU资源确保存储节点处理I/O请求的效率。而Aurora的存储节点需要大量CPU做redolog到innodb page的转换存储节点的效率远不如PolarStore。</em></p>
<p><em>2Aurora架构的最大亮点是存储节点具有将redolog转换为innodb page的能力这个改进看着很吸引眼球事实上这个优化对关系数据库的性能提升很有限性能瓶颈真的不在这里反而会拖慢关键路径redolog落地的性能。btw在PolarDB架构下redolog离线转换为innodb page的能力不难实现但我们目前不认为这是高优先级要做的。</em></p>
<p><em>3Aurora的存储多副本是通过quorum机制来实现的Aurora是六副本也就是说需要计算节点向六个存储节点分别写六次这里其实计算节点的网络开销又上去了而且是发生在写redolog这种关键路径上。而PolarDB是采用基于RDMA实现的ParallelRaft技术来复制数据计算节点只要写一次I/O请求到PolarStore的Leader节点由Leader节点保证quorum写入其他节点相当于多副本replication被offload到存储节点上。</em></p>
<p><em>此外在最终一致性上Aurora是用gossip协议来兜底的在完备程度上没有PolarDB使用的ParallelRaft算法有保证。</em></p>
<p><em>4Aurora的改动手术切口太大使得它很难后面持续跟进社区的新版本。这也是AWS几个数据库产品线的通病例如Redshift如何吸收PostgrelSQL 10的变更是他们的开发团队很头疼的问题。对新版本做到与时俱进是云数据库的一个朴素需求。怎么设计这个刀口达到effect和cost之间的平衡是对架构师的考验。</em></p>
<p>总得来说PolarDB将数据库拆分为计算节点与存储节点2个独立的部分计算节点在已有的MySQL数据库基础上进行修改而存储节点基于全新的PolarFS共享存储。PolarDB通过计算和存储分离的方式实现提供了即时生效的可扩展能力和运维能力同时采用RDMA和SPDK等最新的硬件来优化传统的过时的网络和IO协议栈极大提升了数据库性能基本上解决了使用MySQL是会遇到的各种问题除此之外本文并未展开介绍PolarDB的ParallelRaft其依托上层数据库逻辑实现IO乱序提交大大提高多个Chunk数据副本达成一致性的性能。以上这些创新和优化都成为了未来数据库的发展方向。</p>
<p>参数资料:</p>
<ul>
<li><a href="http://www.vldb.org/pvldb/vol11/p1849-cao.pdf">PolarFS: An Ultra-low Latency and Failure Resilient. Distributed File System for Shared Storage Cloud Database</a></li>
<li><a href="http://mysql.taobao.org/monthly/2017/09/01/">深入了解阿里云新一代产品 POLARDB</a></li>
<li><a href="http://www.itdks.com/eventlist/detail/1319">阿里云下一代数据库PolarDB架构设计</a></li>
<li><a href="http://ks.netease.com/blog?id=8784">PolarDB技术深入剖析</a></li>
<li><a href="https://www.zhihu.com/question/63987114">如何评价阿里云新一代关系型数据库 PolarDB</a></li>
</ul>
</div>
</div>
<div>
<div style="float: left">
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/文章/面试最常被问的 Java 后端题.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":"7099808b48478b66","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>