first commit
This commit is contained in:
@ -0,0 +1,86 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
00 开篇词 吃透分布式数据库,提升职场竞争力
|
||||
你好,我是高洪涛,前华为云技术专家、前当当网系统架构师和 Oracle DBA,也是 Apache ShardingSphere PMC 成员。作为创始团队核心成员,我深度参与的 Apache ShardingShpere 目前已经服务于国内外上百家企业,并得到了业界广泛的认可。
|
||||
|
||||
我在分布式数据库设计与研发领域工作近 5 年,也经常参与和组织一些行业会议,比如中国数据库大会、Oracle 嘉年华等,与业界人士交流分布式数据库领域的最新动向和发展趋势。
|
||||
|
||||
近十年来,整个行业都在争先恐后地进入这个领域,从而大大加速了技术进步。特别是近五年,云厂商相继发布重量级分布式数据库产品,普通用户接触这门技术的门槛降低了,越来越多人正在参与其中,整个领域生态呈现出“百花齐放”的态势。
|
||||
|
||||
|
||||
|
||||
2021 年数据大会上,阿里云发布了分布式数据库使用率统计图
|
||||
|
||||
学好分布式数据库将给你带来哪些机会?
|
||||
|
||||
但在生产实践过程中我们会发现,许多技术人员对分布式数据库还停留在一知半解的状态,比如下面这些疑问:
|
||||
|
||||
|
||||
听说 MongoDB 比 MySQL 好用,但它适合我的业务吗?
|
||||
TiDB 与阿里云 PolarDB 看起来都支持 MySQL 语法,它们之间有什么区别呢?应该如何选择?
|
||||
|
||||
|
||||
这本质上就是由于缺乏对分布式数据库基本原理的了解,容易导致使用该种数据库时问题频发。好比 Apache Cassandra 或 Azure CosmosDB 都支持多种一致性,但如果不了解分布式一致性模型,你很有可能会选错,从而造成业务数据不一致等问题。
|
||||
|
||||
也因此长久以来,业界一直存在一个典型的误解:分布式数据库只能遵循 CAP 原则,无法实现传统数据库的 ACID 级别的一致性,我的业务无法迁移到分布式数据库上。
|
||||
|
||||
而事实上,现代分布式数据库(特别是 NewSQL 类数据库),已经可以在一定程度上解决这一问题了。(我会分别在第 5 讲和第 15 讲中和你讨论一致性模型,你会获得想要的答案。)
|
||||
|
||||
虽然传统数据库中,大多数会使用复制同步技术来提高查询性能和可用性,但这些技术像一堆“补丁”,对已经不堪重负的传统数据库进行修修补补,解决问题有限的同时,反而可能带来更多问题(比如,复制延迟会长期困扰 MySQL 的复制高可用方案)。
|
||||
|
||||
而分布式数据库,基本上是从底层开始,针对分布式场景设计出来的,因此从基础层面就可以解决传统数据库的一些棘手问题。虽然初期投入相对大一些,却可以保证后续技术体系的健康发展,在长期成本上具有显著优势。
|
||||
|
||||
此外,分布式数据库好比一个“百宝箱”,其中蕴含了独具特色的设计理念、千锤百炼的架构模式,以及取之不尽的算法细节。随着分布式数据库迅猛发展,越来越多的研发、产品和运维人员或多或少都会接触分布式数据库,因此学好分布式数据库,也会为你提升职场竞争优势带来帮助,成为你技术履历上的闪光点。
|
||||
|
||||
|
||||
对于数据库工程师,除了日常使用,相关面试中常常会涉及设计数据库集群架构、保障数据库的横纵向扩展等内容,因此理解主流分布式数据库原理和相关案例,会帮助你完美应对。
|
||||
对于云产品经理,掌握目前商用与开源领域中主流的分布式数据库原理同样非常重要,这是规划和设计相关云产品的前置条件。
|
||||
甚至在一般概念里,不与后端数据库直接打交道的移动 App 研发,想要解决多终端共享数据的同步问题,都可以从分布式数据库原理中获取灵感。
|
||||
当进行系统运维支撑时,如果清楚分布式数据库内部到底发生了什么,将有助于设计合理的支撑策略。在处理具体问题时,也会更加得心应手。
|
||||
|
||||
|
||||
学习过程中有哪些难点?
|
||||
|
||||
不过,分布式数据库的学习曲线非常陡峭,你会发现与其他知识类型相比,它有一个显著的区别,就是:学习资料过于丰富,且难度普遍不低。
|
||||
|
||||
|
||||
由于数据库技术已经发展多年,其演化的分支过于庞杂,每个研究人员都会结合自身的专业背景与技术领域来解释分布式数据库。因此,将这些复杂的背景知识了解透彻,就成了大多数人深入这一领域的难题。
|
||||
同时,该领域学术化气氛浓厚,因此大量核心技术是以论文的形式进行表述的,不仅内容晦涩,且大部分为英文,这也为探索核心理论提高了门槛。
|
||||
还有一些课程往往注重 DBA 方向的培养,且一般限定在某个特定的数据库中(如云厂商数据库认证或 Oracle DBA 认证培训等),并没有抽象出一些共有的特性,方便大家掌握分布式数据库的核心理念。
|
||||
|
||||
|
||||
这也在一定程度上导致人们对分布式数据库这一概念“误解”不断。不过,这也坚定了我想要帮助你了解通用分布式数据库的设计原理,借此带你重新审视业务实践的决心。
|
||||
|
||||
学习本课程后,你将对技术选型、系统架构设计,以及如何解决关键的技术难题有更为清晰的方案;在晋升评审&面试求职中,也能更加从容地应对相关技术问题。
|
||||
|
||||
我是如何设计这个课程的?
|
||||
|
||||
由于分布式数据库内涵丰富,知识结构繁杂,为使你能高效了解和掌握其中的关键信息,我采用了三种思路来设计这个课程。
|
||||
|
||||
|
||||
化繁为简。去掉过时、不重要的技术细节,直接讲解与分布式数据库有关的内容,但同时我也会引导你去发现技术背后的细节,希望可以授人以渔。
|
||||
知识全面。内容不仅仅介绍了分布式理论相关内容,同时介绍了一般资料少有提及的存储引擎,两者共同配合,才造就了分布式数据库高性能和高扩展性的特点。
|
||||
注重实际。本着将技术理念与实际案例结合的精神,在介绍技术细节时,我会联系相关的分布式数据库,从多方位打通你的知识体系。
|
||||
|
||||
|
||||
基于以上设计思路,我把课程分为 4 个模块,合计 24 讲。
|
||||
|
||||
|
||||
模块一,分布式数据历史演变及其核心原理。从历史背景出发,讲解了分布式数据库要解决的问题、应用场景,以及核心技术特点。
|
||||
模块二,分布式数据库的高性能保证——存储引擎。这是专栏的亮点内容,简要展示了现代数据库的存储引擎,比如典型存储引擎、分布式索引、数据文件与日志结构存储、事务处理。其中,我会特别介绍分布式数据库与传统数据库在存储层面上的差异。学完之后,你会对分布式数据库中的重要特性(如一致性和分布式事务)有一个完整的理解,明白为什么一些特定存储引擎(如日志结构存储)更适合去构建分布式数据库。
|
||||
模块三,分布式数据库的高扩展性保证——分布式系统。详细介绍分布式数据库中所蕴含的系统设计原理、算法等,包含但不限于错误侦测、领导选举、数据可靠传播、分布式事务、共识算法等内容。虽然分布式内容很多,但我不会面面俱到,而是帮你提炼精华,基于实例为你建立知识体系。
|
||||
模块四,知识拓展。我会和你探讨当代最成功的分布式数据库(传统&新型),探讨它们成功的关键,同时将它们与之前模块中所介绍的技术原理进行相应的映射,让你的知识体系更加丰富。
|
||||
|
||||
|
||||
|
||||
|
||||
讲师寄语
|
||||
|
||||
本课程的设计目标是,尽最大程度解决你的实际问题,让你在不同的工程实践中,对分布式场景下的数据库存储有更加专业的认知,并对技术趋势建立深入的洞察。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,166 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
01 导论:什么是分布式数据库?聊聊它的前世今生
|
||||
你好,欢迎学习分布式数据库,我们的课程就正式开始了。
|
||||
|
||||
在开设这门课程之前,我简短地与身边同僚、朋友交流了课程的大纲。当时,大家都表示出了浓厚的兴趣,并且不约而同地问了我这样一个问题:啥是分布式数据库?更有“爱好学习”的朋友希望借此展现出“勤学好问”的品德,进而补充道:“这是哪个大厂出的产品?”
|
||||
|
||||
好吧,我的朋友,你们真的戳中了我的笑点。但笑一笑后,我不禁陷入了思考:为什么分布式数据库在大众,甚至专业领域内认知如此之低呢?
|
||||
|
||||
原因我大概可以总结为两点:数据库产品特点与商业氛围。
|
||||
|
||||
首先,数据库产品的特点是抽象度高。用户一般仅仅从使用层面接触数据库,知道数据库能实现哪些功能,而不关心或者很难关心其内部原理。而一些类型的分布式数据库的卖点正是这种抽象能力,从而使用户觉得应用这种分布式化的数据库与传统单机数据库没有明显的差别,甚至更加简单。
|
||||
|
||||
其次,数据库的商业氛围一直很浓厚。数据库产品高度抽象且位置关键,这就天然成为资本追逐的领地。而商业化产品和服务的卖点就是其包含支撑服务,而且许多商业数据库最赚钱的部分就是提供该服务。因此这些产品有意无意地对终端用户掩盖了数据库的技术细节,而用户有了这层商业保障,也很难有动力去主动了解内部原理。
|
||||
|
||||
这就造成即使你工作中接触了分布式数据库,也没有意识到它与过去的数据库有什么不同。但“福报迟到,但不会缺席”——当由于对其原理缺乏必要认识,导致技术问题频发时,用户才会真正意识到它们好像类似,但本质却截然不同。
|
||||
|
||||
而随着分布式数据库逐步渗透到各个领域,用户再也不能“傻瓜式”地根据特性选择数据库产品了。新架构催生出来的新特性,促使使用者需要深入参与其中,并需要他们认真评估数据库技术特点,甚至要重新设计自己的产品来与之更好地结合。
|
||||
|
||||
因此,我将本专栏课程设计为一把钥匙,帮助你打开分布式数据库的大门。你也可以将本门课程当作一个网游的新手村任务,完成后会获取初始装备(原理与方法论),继而掌握深入该领域所必要的知识。
|
||||
|
||||
我是“历史决定论”的忠实簇拥者,在这一讲中,我会沿着分布式数据库的发展脉络来介绍它。相信你在读完后,会对一开始的那个问题有自己的答案。那么现在我们从基本概念开始说起。
|
||||
|
||||
基本概念
|
||||
|
||||
分布式数据库,从名字上可以拆解为:分布式+数据库。用一句话总结为:由多个独立实体组成,并且彼此通过网络进行互联的数据库。
|
||||
|
||||
理解新概念最好的方式就是通过已经掌握的知识来学习,下表对比了大家熟悉的分布式数据库与集中式数据库之间主要的 5 个差异点。
|
||||
|
||||
|
||||
|
||||
从表中,我们可以总结出分布式数据库的核心——数据分片、数据同步。
|
||||
|
||||
1. 数据分片
|
||||
|
||||
该特性是分布式数据库的技术创新。它可以突破中心化数据库单机的容量限制,从而将数据分散到多节点,以更灵活、高效的方式来处理数据。这是分布式理论带给数据库的一份礼物。
|
||||
|
||||
分片方式包括两种。
|
||||
|
||||
|
||||
水平分片:按行进行数据分割,数据被切割为一个个数据组,分散到不同节点上。
|
||||
垂直分片:按列进行数据切割,一个数据表的模式(Schema)被切割为多个小的模式。
|
||||
|
||||
|
||||
2. 数据同步
|
||||
|
||||
它是分布式数据库的底线。由于数据库理论传统上是建立在单机数据库基础上,而引入分布式理论后,一致性原则被打破。因此需要引入数据库同步技术来帮助数据库恢复一致性。
|
||||
|
||||
简而言之,就是使分布式数据库用起来像“正常的数据库”。所以数据同步背后的推动力,就是人们对数据“一致性”的追求。这两个概念相辅相成,互相作用。
|
||||
|
||||
当然分布式数据库还有其他特点,但把握住以上两点,已经足够我们理解它了。下面我将从这两个特性出发,探求技术史上分布式数据库的发展脉络。我会以互联网、云计算等较新的时间节点来进行断代划分,毕竟我们的核心还是着眼现在、面向未来。
|
||||
|
||||
商业数据库
|
||||
|
||||
互联网浪潮之前的数据库,特别是前大数据时代。谈到分布式数据库绕不开的就是 Oracle RAC。
|
||||
|
||||
|
||||
|
||||
Oracle RAC 是典型的大型商业解决方案,且为软硬件一体化解决方案。我在早年入职国内顶级电信行业解决方案公司的时候,就被其强大的性能所震撼,又为它高昂的价格所深深折服。它是那个时代数据库性能的标杆和极限,是完美方案与商业成就的体现。
|
||||
|
||||
我们试着用上面谈到的两个特性来简单分析一下 RAC:它确实是做到了数据分片与同步。每一层都是离散化的,特别在底层存储使用了 ASM 镜像存储技术,使其看起来像一块完整的大磁盘。
|
||||
|
||||
这样做的好处是实现了极致的使用体验,即使用单例数据库与 RAC 集群数据库,在使用上没有明显的区别。它的分布式存储层提供了完整的磁盘功能,使其对应用透明,从而达到扩展性与其他性能之间的平衡。甚至在应对特定规模的数据下,其经济性又有不错的表现。
|
||||
|
||||
这种分布式数据库设计被称为“共享存储架构”(share disk architecture)。它既是 RAC 强大的关键,又是其“阿喀琉斯之踵”,DBA 坊间流传的 8 节点的最大集群限制可以被认为是 RAC 的极限规模。
|
||||
|
||||
该规模在当时的环境下是完全够用的,但是随着互联网的崛起,一场轰轰烈烈的“运动”将会打破 Oracle RAC 的不败金身。
|
||||
|
||||
大数据
|
||||
|
||||
我们知道 Oracle、DB2 等商业数据库均为 OLTP 与 OLAP 融合数据库。而首先在分布式道路上寻求突破的是 OLAP 领域。在 2000 年伊始,以 Hadoop 为代表的大数据库技术凭借其“无共享”(share nothing)的技术体系,开始向以 Oracle 为代表的关系型数据库发起冲击。
|
||||
|
||||
|
||||
|
||||
这是一次水平扩展与垂直扩展,通用经济设备与专用昂贵服务,开源与商业这几组概念的首次大规模碰撞。拉开了真正意义上分布式数据库的帷幕。
|
||||
|
||||
当然从一般的观点出发,Hadoop 一类的大数据处理平台不应称为数据库。但是从前面我们归纳的两点特性看,它们又确实非常满足。因此我们可以将它们归纳为早期面向商业分析场景的分布式数据库。从此 OLAP 型数据库开始了自己独立演化的道路。
|
||||
|
||||
除了 Hadoop,另一种被称为 MPP(大规模并行处理)类型的数据库在此段时间也经历了高速的发展。MPP 数据库的架构图如下:
|
||||
|
||||
|
||||
|
||||
我们可以看到这种数据库与大数据常用的 Hadoop 在架构层面上非常类似,但理念不同。简而言之,它是对 SMP(对称多处理器结构)、NUMA(非一致性存储访问结构)这类硬件体系的创新,采用 shared-nothing 架构,通过网络将多个 SMP 节点互联,使它们协同工作。
|
||||
|
||||
MPP 数据库的特点是首先支持 PB 级的数据处理,同时支持比较丰富的 SQL 分析查询语句。同时,该领域是商业产品的战场,其中不仅仅包含独立厂商,如 Teradata,还包含一些巨头玩家,如 HP 的 Vertica、EMC 的 Greenplum 等。
|
||||
|
||||
大数据技术的发展使 OLAP 分析型数据库,从原来的关系型数据库之中独立出来,形成了完整的发展分支路径。而随着互联网浪潮的发展,OLTP 领域迎来了发展的机遇。
|
||||
|
||||
互联网化
|
||||
|
||||
国内数据库领域进入互联网时代第一个重大事件就是“去 IOE”。
|
||||
|
||||
|
||||
|
||||
其中尤以“去 Oracle 数据库”产生的影响深远。十年前,阿里巴巴喊出的这个口号深深影响了国内数据库领域,这里我们不去探讨其中细节,也不去评价它正面或负面的影响。但从对于分布式数据库的影响来说,它至少带来两种观念的转变。
|
||||
|
||||
|
||||
应用成为核心:去 O 后,开源数据库需要配合数据库中间件(proxy)去使用,但这种组合无法实现传统商业库提供的一些关键功能,如丰富的 SQL 支持和 ACID 级别的事务。因此应用软件需要进行精心设计,从而保障与新数据库平台的配合。应用架构设计变得非常关键,整个技术架构开始脱离那种具有调侃意味的“面向数据库” 编程,转而变为以应用系统为核心。
|
||||
弱一致性理念普及:虽然强一致性仍然需求旺盛,但人们慢慢接受了特定场景下可以尝试弱一致性来解决系统的吞吐量问题。而这带来了另外一个益处,一线研发与设计人员开始认真考虑业务需要什么样的一致性,而不是简单依靠数据库提供的特性。
|
||||
|
||||
|
||||
以上两个观念都是在破除了对于 Oracle 的迷信后产生的,它们本身是正面的,但是如果没有这场运动,其想要在普通用户之中普及确实有很大困难。而这两种观念也为日后分布式数据库,特别是国产分布式数据的发展带来了积极的影响。
|
||||
|
||||
而与此同期,全球范围内又上演着 NoSQL 化浪潮,它与国内去 IOE 运动一起推动着数据库朝着横向分布的方向一路狂奔。关于 NoSQL 的内容,将会在下一讲详细介绍。
|
||||
|
||||
与上一部分中提到的大数据技术类似,随着互联网的发展,去 IOE 运动将 OLTP 型数据库从原来的关系型数据库之中分离出来,但这里需要注意的是,这种分离并不是从基础上构建一个完整的数据库,而是融合了旧有的开源型数据库,同时结合先进的分布式技术,共同构造了一种融合性的“准”数据库。它是面向具体的应用场景的,所以阉割掉了传统的 OLTP 数据库的一些特性,甚至是一些关键的特性,如子查询与 ACID 事务等。
|
||||
|
||||
而 NoSQL 数据库的重点是支持非结构化数据,如互联网索引,GIS 地理数据和时空数据等。这种数据在传统上会使用关系型数据库存储,但需要将此种数据强行转换为关系型结构,不仅设计烦琐,而且使用效率也比较低下。故NoSQL 数据库被认为是对整个数据库领域的补充,从而人们意识到数据库不应该仅仅支持一种数据模式。
|
||||
|
||||
随着分布式数据库的发展,一种从基础上全新设计的分布式 OLTP 数据库变得越来越重要,而云计算更是为这种数据库注入新的灵魂,两者的结合将会给分布式数据库带来美妙的化学反应。
|
||||
|
||||
云原生是未来
|
||||
|
||||
从上文可以看到人们真正具有广泛认知的分布式数据库,即 OLTP 型交易式分布式数据库,依然是分布式数据库领域一个缺失的片段,且是一个重要的片段。一个真正的 OLTP 数据库应该具备什么特点呢?
|
||||
|
||||
实际上人们需要的是它既具有一个单机的关系型数据库的特性,又有分布式的分片与同步特性。 DistributedSQL 和 NewSQL 正是为了这个目的而生的 。它们至少具有如下两点引人注目的特性:
|
||||
|
||||
|
||||
SQL 的完整支持
|
||||
可靠的分布式事务。
|
||||
|
||||
|
||||
典型的代表有 Spanner、NuoDB、TiDB 和 Oceanbase 等。并且本课程会重点围绕 DistributedSQL 的关键特性展开研究,这些特性是现代分布式数据库的基石。这里我就不占用过多篇幅介绍了,在 02 | SQL vs NoSQL:一次搞清楚五花八门的各种“SQL”中我们再一起详细学习。
|
||||
|
||||
与此同时,随着云计算的纵向深入发展,分布式数据库又迎来新的革命浪潮——云原生数据库。
|
||||
|
||||
首先,由于云服务天生的“超卖”特性,造成其采购成本较低,从而使终端用户尝试分布式数据库的门槛大大降低。
|
||||
|
||||
其次,来自云服务厂商的支撑人员可以与用户可以进行深度的合作,形成了高效的反馈机制。这种反馈机制促使云原生的分布式数据库有机会进行快速的迭代,从而可以积极响应客户的需求。
|
||||
|
||||
这就是云原生带给分布式数据库的变化,它是通过生态系统的优化完成了对传统商业数据库的超越。以下来自 DB-Engines 的分析数据说明了未来的数据库市场属于分布式数据库,属于云原生数据库。
|
||||
|
||||
|
||||
|
||||
随着分布式数据库的发展,我们又迎来了新的一次融合:那就是 OLTP 与 OLAP 将再一次合并为 HTAP(融合交易分析处理)数据库。
|
||||
|
||||
该趋势的产生主要来源于云原生 OLTP 型分布式数据库的日趋成熟。同时由于整个行业的发展,客户与厂商对于实时分析型数据库的需求越来越旺盛,但传统上大数据技术包括开源与 MPP 类数据库,强调的是离线分析。
|
||||
|
||||
如果要进行秒级的数据处理,那么必须将交易数据与分析数据尽可能地贴近,并减少非实时 ELT 的引入,这就促使了 OLTP 与 OLAP 融合为 HTAP。下图就是阿里云 PolarDB 的 HTAP 架构。
|
||||
|
||||
|
||||
|
||||
总结
|
||||
|
||||
用《三国演义》的第一句话来说:“天下大势,分久必合,合久必分。”而我们观察到的分布式数据库,乃至数据库本身的发展正暗合了这句话。
|
||||
|
||||
分布式数据库发展就是一个由合到分,再到合的过程:
|
||||
|
||||
|
||||
早期的关系型商业数据库的分布式能力可以满足大部分用户的场景,因此产生了如 Oracle 等几种巨无霸数据库产品;
|
||||
OLAP 领域首先寻求突破,演化出了大数据技术与 MPP 类型数据库,提供功能更强的数据分析能力;
|
||||
去 IOE 引入数据库中间件,并结合应用平台与开源单机数据库形成新一代解决方案,让商业关系型数据库走下神坛,NoSQL 数据库更进一步打破了关系型数据库唯我独尊的江湖地位;
|
||||
新一代分布式 OLTP 数据库正式完成了分布式领域对数据库核心特性的完整支持,它代表了分布式数据库从此走向了成熟,也表明了 OLAP 与 OLTP 分布式场景下,分别在各自领域内取得了胜利;
|
||||
HTAP 和多模式数据处理的引入,再一次将 OLAP 与 OLTP 融合,从而将分布式数据库推向如传统商业关系型数据库数十年前那般的盛况,而其产生的影响要比后者更为深远。
|
||||
|
||||
|
||||
我们回顾历史,目的是更好地掌握未来。在本课程中,我们将详细分析现代分布式数据库、OLTP 型数据库的关键技术、使用场景和应用案例。使你在未来可以更好地评估和使用分布式数据库。
|
||||
|
||||
而分布式数据库的历史同时体现了实用主义的特色,其演化是需求与技术博弈的结果,而不是精心设计出来的。我们的课程也会体现出实用主义的特点,让你学以致用,学有所获。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,127 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
06 实践:设计一个最简单的分布式数据库
|
||||
本讲是一节知识回顾与拓展实践课。经过前几讲的学习,相信你已经对分布式数据库有了直观的认识,今天我们来总结一下模块一的学习成果,并通过一个实际案例来加深印象,我也会就前几讲中同学们提出的典型问题进行答疑。
|
||||
|
||||
分布式数据库核心总结
|
||||
|
||||
现在让我们来总结一下第一模块的核心知识。
|
||||
|
||||
这个模块介绍了什么是分布式数据库。主要从历史发展的角度,介绍了传统数据库的分布式模式、大数据背景下的分析型分布式数据库,而后以去 IOE 为背景聊到了数据库中间件,以及开源数据库模式,接着说到了 DistributedSQL 与 NewSQL,最后介绍了 HTAP 融合型数据库,它被看作是分布式数据库未来发展的趋势。
|
||||
|
||||
通过第 1 讲的学习,我想你不仅了解了分布式数据库由合到分、再到合的发展历史,更重要的收获是知道了到底什么是分布式数据库,这个最根本的问题。
|
||||
|
||||
从广义上讲,在不同主机或容器上运行的数据库就是分布式数据库,故我们能看到其丰富的产品列表。但是,正是由于其产品线过于丰富,我不可能面面俱到地去讲解所有知识点。同时由于数据库在狭义上可以被理解为 OLTP 型交易类数据库,因此本课程更加聚焦于 DistributedSQL 与 NewSQL 的技术体系,也就是 OLTP 类分布式数据库。在后续的模块中我会着重介绍它们涉及的相关知识,这里给你一个预告。
|
||||
|
||||
同时,这一模块也点出了分片与同步两种特性是分布式数据库的重要特性。
|
||||
|
||||
我们还一起学习了关于 SQL 的历史沿革,了解了什么是 NoSQL。这部分主要是对一些历史性的概念进行的“拨乱反正”,说明了NoSQL 本身是一个营销概念。而后我们介绍了 NewSQL、DistributedSQL 的特点。如前所述,这其实才是本课程所要学习的重点。
|
||||
|
||||
SQL 的重要性如我介绍的那样,这使得它的受众非常广泛。如果数据库想要吸引更多的用户,想要在影响力上或在商业领域寻求突破,那 SQL 可以说是一个必然的特性。反之,如果是专业领域的分布式数据库,那么 SQL 就不如分片与同步这两个特性重要了。
|
||||
|
||||
在分片那一讲中,我们首先学习了分片的意义,它是分布式数据库提高数据容量的关键特性。我们学习了主要的分片算法,包括范围分片与哈希分片;也介绍了一些优化方法;最后用 Apache ShardingShpere 的例子来直观介绍了分片算法的应用,包含了分布式唯一 ID 的生成算法等相关内容。
|
||||
|
||||
数据分片是分布式数据库两个核心内容之一,但其概念是比较直观的。学习难度相比数据同步来讲不是很大。
|
||||
|
||||
我们会经常遇到一个问题:设计一套分库分片的结构,保证尽可能少地迁移数据库。其实这个需求本质上在分布式数据库语境下是毫无意义的,自动弹性的扩缩数据库节点应该是这种数据库必要特性。过分地使用分片算法来规避数据库迁移固然可以提高性能,但总归是一种不完整的技术方案,具有天然的缺陷。
|
||||
|
||||
模块一的最后我们学习了同步数据的概念。同步其实是复制+一致性两个概念的综合。这两个概念互相配合造就了分布式数据库数据同步多样的表现形式。其中,复制是它的前提与必要条件,也就是说,如果一份数据不需要复制,也就没有所谓一致性的概念,那么同步技术也就不存在了。
|
||||
|
||||
在同步那一讲中,最先进入我们视野的是异步复制,这类似于没有一致性的参与,是一种单纯的、最简单的复制方式。后面说的其他的同步、半同步等复合技术,多少都有一致性概念的参与。而除了复制模式以外,我们还需要关注诸如复制协议、复制方式等技术细节。最后我们用 MySQL 复制技术的发展历程,总结了多种复制技术的特点,并点明了以一致性算法为核心的强一致性复制技术是未来的发展方式。
|
||||
|
||||
接着我们介绍了一致性相关知识,这是模块一中最抽象的部分。因为 CAP 理论与一致性模型都是抽象化评估分布式数据库的工具。它们的好处之一就是可以是帮助我们快速评估数据库的一致性,比如一个数据库号称自己是线性一致的 CP 数据库,那么对于其特性,甚至大概的实现方式,我们就会心中有数了;另一个益处就是设计数据库时,你可以根据需要解决的问题,设计数据库一致性方面的特点。
|
||||
|
||||
CAP 理论首先要明确,其中的C 指的是一致性模型中最强的线性一致。正因为是线性一致这样的强一致,才不会同时满足 CAP 三个特性。同时要注意可用性和高可用性的区别,可用性是抽象评估概念,网络分区后,每个分区只有一个副本,只要它提供服务,我们就可以说它其实是可用的,而不能说它是高可用。最后我提到了世界上只有 CP 和 AP 两种数据库,因为 P,即网络分区是客观规律,无法排除,不会存在 CA 类数据库。
|
||||
|
||||
说完了 CAP 理论后,我介绍了一致性模型。它来源于共享内存设计,但其理论可以被分布式数据库乃至一般的分布式系统所借鉴。你需要知道,这部分介绍的三种一致性都是强一致性,其特点解决了复制部分提到的复制延迟,使用户不管从哪个节点写入或查询数据,看起来都是一致的。另外,这三种一致性又是数据一致,与其相对的还有客户端一致,这个我会在之后的分布式模块中具体介绍。
|
||||
|
||||
最后,作为数据库,一个重要的概念就是事务。它与一致性是什么关系呢?其实事务的 ACID 特性中,AID 是数据库提供的对于 C 的保证。其中 I,即隔离性才是事务的关键特性。而隔离性其实解决的是并行事务的问题,一致性模型研究是单对象、单操作的问题,解决的是非并行的事务之间的问题。故隔离性加上一致性模型才是分布式数据库事务特点的总和。
|
||||
|
||||
至此,我们总结了模块一主要的内容。那么学习了这些知识后,除了可以帮助你评估分布式数据库外,还有什么用呢?现在让我们来试着设计一个分布式数据库吧。
|
||||
|
||||
为什么要自己实现分布式数据库?
|
||||
|
||||
分布式数据库,特别是 NoSQL 和 NewSQL 数据库,是目前主要的发展方向。同时,这两种数据库的品种也极为丰富。其中很多都是针对特定场景服务的,比如 NoSQL 中 Elasticsearch 针对的是搜索场景,Redis 针对缓存场景。而 NewSQL 更是百花齐放,如国内的滴滴、字节跳动等企业,都针对自己的业务特点实现了 NewSQL 数据库。更不要说如 BAT、Google 这样的大厂,他们都有自己的 NewSQL 类数据库。
|
||||
|
||||
这背后的动力来源于内驱需求与外部环境,这两者共同叠加而产生了目前这种局面。
|
||||
|
||||
内驱需求是,随着某种特定业务的产生并伴随其使用规模的扩大,从数据库这种底层解决该问题的需求逐步强烈。因为从数据库层面可以保证写入和查询满足某种一致性特性,而分布式数据库天然的服务化特性,又给使用者带来极大便利,从而可以加速这类业务快速发展。
|
||||
|
||||
外部环境是,分布式数据库使用的技术逐步成熟化,且可选开源产品众多。早先构造数据库的一个难点是,几乎所有涉及的技术类别都需要从基础开始构建,比如 SQL 解析、分布式协议和存储引擎等。而目前,有众多的开源项目、丰富的技术路线可供挑选,这样就大大降低了构造分布式数据库的门槛。
|
||||
|
||||
以上两点互相作用,从而使现在很多组织和技术团队都开始去构建属于自己的分布式数据库。
|
||||
|
||||
设计分布式数据库案例
|
||||
|
||||
熟悉我的朋友可能知道,我另外一个身份是 Apache SkyWalking 的创始成员,它是一个开源的 APM 系统。其架构图可以在官网找到,如下所示。
|
||||
|
||||
|
||||
|
||||
可以看到其中的 Storage Option,也就是数据库层面可以有多种选择。除了单机内存版本的 H2 以外,其余生产级别的数据库均为分布式数据库。
|
||||
|
||||
选择多一方面证明了 SkyWalking 有很强的适应能力,但更重要的是目前业界没有一款数据库可以很好地满足其使用场景。
|
||||
|
||||
那么现在我们来尝试给它设计一个数据库。这里我简化了设计流程,只给出了需求分析与概念设计,目的是展示设计方式,帮助你更好地体会分布式数据库的关键点。
|
||||
|
||||
需求分析
|
||||
|
||||
我们先来介绍一下 SkyWalking 处理数据的特点。
|
||||
|
||||
由于 SkyWalking 的 APM 特性,其对写入有很高的诉求。不管是最早使用的 HBase,还是现在的主力存储 Elasticsearch,都对写入很友好。为了保证数据写入高速且一致,OAP 节点层已经将计算指标进行了分片,也就是同一个指标是在相同的节点计算出来的。另外,该应用还采用了批量写入的模式,即每 10 秒进行一些批量写入。
|
||||
|
||||
SkyWalking 在使用场景下可以被看成一个查询少写入多的系统,查询很少发生,可以容忍一定的查询延迟。可用性方面是允许牺牲一定的可用性来换取性能的,比如目前对 Elasticsearch 的副本数量建议为 0,也就是说不进行数据复制。
|
||||
|
||||
如果开启复制,一致性方面要求也比较低。因为对于最大的工作负载写入来说,几乎不在写入的时候进行数据查询。但是一些低负载操作需要保证一致性,比如写入监控结果,写入后需要马上能查询出来。
|
||||
|
||||
由于查询协议的数据结构是非关系型的,且查询种类不多,故不需要一定支持 SQL 语句。
|
||||
|
||||
以上围绕着第一模块的核心内容,分析了 SkyWalking 的数据库应该具备的特点。现在让我们来针对需求分析中提到的要点,来设计针对 SkyWalking 的分布式数据库。
|
||||
|
||||
概要设计
|
||||
|
||||
首先 OAP 节点实际上已经做过哈希分片,这样我们可以将数据库节点与 OAP 节点组成一对一,甚至多对一(二次哈希)的结构,保障一个指标只写入一个数据库节点,这样就避免了数据迁移的麻烦。甚至我们可以将数据库节点与 OAP 节点部署在一起,从而最大限度降低网络延迟,同时提高资源的利用率。
|
||||
|
||||
对于弹性扩缩容,由于 SkyWalking 可以容忍部分数据不可用,可以直接增加分片节点,而无须迁移数据。如果想要保证老数据可以查询,可以将扩容时间点做记录;而后老数据查询老节点,新数据查询新节点。由于 SkyWalking 所有数据都有生命周期,一旦节点上旧的数据被删除,缩容场景下,该节点也可以被安全移除。
|
||||
|
||||
虽然 SkyWalking 不强制要求可用性,但一些数据如果一旦遭遇故障,也会给使用者带来不好的体验。特别是对于类似一天内的平均响应时间,一旦某个节点故障,在没有副本的情况下,该指标的数据将会有非常大的偏差。
|
||||
|
||||
一旦开启数据复制,应该使用什么一致性呢?这个问题需要区分来看。对于大量写入的指标数据来说,弱一致是满足条件的。因为写入和读取是由不同的端点发起的,且写入可以认为是单对象单操作,故弱一致就满足条件。
|
||||
|
||||
但告警场景却不是这样,告警产生后会通知相关人员,他们希望能马上查询到数据。如果采用弱一致,很可能无法查询。这里我们不需要使用特别强的一致性,采用因果一致就可以满足需求。实现方式是,将写入告警产生的数据时间戳页传递给用户。用户查询的时候将时间戳发送给一个数据库节点,如果该节点没有该时间戳的数据,它会尝试请求其他节点去同步。
|
||||
|
||||
最后关于查询接口,由于不一定需要 SQL,故我们可以使用简单的 RESTful 风格的 API 去实现查询和写入。但为了写入高效,可以独立设计写入协议,采用高效的二进制长连接的协议风格。
|
||||
|
||||
案例总结
|
||||
|
||||
以上就是根据第一模块学习的知识并结合 SkyWalking 的需求特点,设计的针对该系统的分布式数据库。设计层面我只强调了关键设计要点,并未进行详细说明。而关于底层的存储引擎,相信你在学习完模块二之后,会有自己的答案。
|
||||
|
||||
通过这个案例,我们可以看到设计分布式数据库只要结合分片和同步两个特点,就可以大概勾画出一个分布式数据库的外貌。你可以自己在工作和学习中,尝试设计分布式数据库来解决具有一定共性的数据问题。
|
||||
|
||||
留言答疑
|
||||
|
||||
开课以来,我收到了大家积极的反馈,其中有些问题非常专业,让我很惊喜。这里首先非常感谢你对课程的喜爱,你的积极反馈就是我写下去的动力。
|
||||
|
||||
这里我总结了一些共性问题,为你解答。
|
||||
|
||||
第一,有人提出了名词概念第一次出现应该给出全称的问题。
|
||||
|
||||
这里先向你道歉,出于个人习惯,我脑海中会将自己比较熟悉的概念直接以缩写或别名输出。这确实对第一次接触该知识的同学不太友好。在以后的写作中,我会尽量避免该问题。
|
||||
|
||||
第二个比较集中的问题是关于 MySQL InnoDB Cluster 是不是分布式数据库。
|
||||
|
||||
我在文章中提到,分布式的基础定义非常宽泛。如果从它出发,那么 InnoDB Cluster 是分布式数据库。但是从我们说的两个特性来看,它并不具有分片的特点,严格来说它不是分布式数据库,更不要说它是 NewSQL。但是我们可以为其引入分片的功能,比如利用分库分表中间件,以 InnoDB Cluster 为基础去构建分布式数据库,即 NewSQL 数据库。
|
||||
|
||||
这里我要强调一下,你不需要陷入概念区分的陷阱里,这不是考试,但现实生活比考试要复杂。把握住关键特点,才可以以不变应万变。
|
||||
|
||||
好了,答疑就先到这里。最后再次感谢你的积极反馈,希望在下一个模块结束后也能看到你精彩的留言。
|
||||
|
||||
总结
|
||||
|
||||
本讲首先回顾了模块一的主要内容,帮助你将各个部分串联起来,形成完整的知识拼图。而后通过一个案例介绍了如何使用这些知识设计一个分布式数据库,将所学知识应用到实际工作和学习中。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,129 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
07 概要:什么是存储引擎,为什么需要了解它?
|
||||
经过第一个模块的学习,相信你已经知道了什么是分布式数据库,对分布式数据库的核心知识有了比较全面和深入的了解了。
|
||||
|
||||
这一讲是第二模块存储引擎的概要,主要目的是为你解释什么是存储引擎,以及它在分布式数据库中起到什么样的作用。
|
||||
|
||||
数据库的一个首要目标是可靠并高效地管理数据,以供人们使用。进而不同的应用可以使用相同的数据库来共享它们的数据。数据库的出现使人们放弃了为每个独立的应用开发数据存储的想法,同时,随着数据库广泛的使用,其处理能力飞速发展,演进出如现代的分布式数据库这般惊人的能力。
|
||||
|
||||
那么,为了支撑抽象的多种场景。一般的数据库都会采用多模块或多子系统的架构来构建数据库,从而方便数据库项目团队依据现实的场景来组合不同的子模块,进而构造出一众丰富的数据库产品。
|
||||
|
||||
而存储引擎就是这一众模块中极为重要的一环,下面我们开始解释它在整个数据库架构中的定位和意义。
|
||||
|
||||
存储引擎的定位
|
||||
|
||||
这个世界上,没有针对数据库设计的一定之规。每个数据库都是根据它所要解决的问题,并结合其他因素慢慢发展成如今的模样的。所以数据库子模块的分化也没有一个广泛接受的标准,且有些模块之间的边界也是很模糊的。特别是需要优化数据库性能时,原有被设计为独立存在的模块很可能会融合以提高数据库整体性能。
|
||||
|
||||
这里,我总结出了一个比较典型的分布式数据库的架构和模块组合标准。虽然不能完全代表所有分布式数据库,但是可以帮助你理解模块的组成方式。这里需要注意,我给出的模型是基于客户端/服务器,也就是 C/S 模式的,因为这是大部分分布式数据库的架构模式。
|
||||
|
||||
|
||||
传输层:它是接受客户端请求的一层。用来处理网络协议。同时,在分布式数据库中,它还承担着节点间互相通信的职责。
|
||||
查询层:请求从传输层被发送到查询层。在查询层,协议被进行解析,如 SQL 解析;后进行验证与分析;最后结合访问控制来决定该请求是否要被执行。解析完成后,请求被发送到查询优化器,在这里根据预制的规则,数据分布并结合数据库内部的统计,会生成该请求的执行计划。执行计划一般是树状的,包含一系列相关的操作,用于从数据库中查询到请求希望获取的数据。
|
||||
执行层:执行计划被发送到执行层去运行。执行层一般包含本地运行单元与远程运行单元。根据执行计划,调用不同的单元,而后将结果合并返回到传输层。
|
||||
|
||||
|
||||
细心的你可能会注意到,这里只有查询层,那么数据是怎么写入的?这对于不同的数据库,答案会非常不同。有的数据库会放在传输层,由于协议简单,就不需要额外处理,直接发送到执行层;而有些写入很复杂,会交给查询层进行处理。
|
||||
|
||||
以上就是数据库领域中比较常见的模块划分方式。你可能有这样的疑问:那么存储引擎在哪里呢?
|
||||
|
||||
执行层本地运行单元其实就是存储引擎。它一般包含如下一些功能:
|
||||
|
||||
|
||||
事务管理器:用来调度事务并保证数据库的内部一致性(这与模块一中讨论的分布式一致性是不同的);
|
||||
锁管理:保证操作共享对象时候的一致性,包括事务、修改数据库参数都会使用到它;
|
||||
存储结构:包含各种物理存储层,描述了数据与索引是如何组织在磁盘上的;
|
||||
内存结构:主要包含缓存与缓冲管理,数据一般是批量输入磁盘的,写入之前会使用内存去缓存数据;
|
||||
提交日志:当数据库崩溃后,可以使用提交日志恢复系统的一致性状态。
|
||||
|
||||
|
||||
以上就是存储引擎比较重要的几个功能,其核心就是提供数据读写功能,故一般设计存储引擎时,会提供对其写入路径与读取路径的描述。
|
||||
|
||||
好了,现在你清楚了存储引擎的定位和主要结构,那么存储引擎的种类也是很多的,下面我通过一些关键特性,来介绍几种典型的存储引擎。
|
||||
|
||||
内存与磁盘
|
||||
|
||||
存储引擎中最重要的部分就是磁盘与内存两个结构。而根据数据在它们之中挑选一种作为主要的存储,数据库可以被分为内存型数据库与磁盘型数据库。由此可见存储引擎的一个功能,就是可以被作为数据库类型划分的依据,可见引擎的重要性。
|
||||
|
||||
内存型存储是把数据主要存储在内存里,其目的很明显,就是加快数据读写性能。分布式数据库一个重要的门类就是内存型数据库,包括 Redis、NuoDB 和 MySQL Cluster 等。当然其缺点也很明显,那就是内存的成本较高,且容量有限。而分布式的架构能有效地扩充该类数据库的容量,这也是内存数据库主要是分布式数据库的原因。
|
||||
|
||||
磁盘存储相对传统,它存储主要数据,而内存主要作为缓冲来使写入批量化。磁盘存储的好处是,存储性价比较高,这主要得益于磁盘甚至是磁带的单位存储价格相比内存非常低廉。但是与内存型数据库相比,磁盘型数据库的性能比较低。不过,随着近年 SSD 磁盘的普及,这种趋势得到了有效的改善。
|
||||
|
||||
这两种存储引擎的差别还体现在功能实现的难度上。内存型数据库相对简单,因为写入和释放随机的内存空间是相对比较容易的;而磁盘型数据库需要处理诸如数据引用、文件序列化、碎片整理等复杂的操作,实现难度很高。
|
||||
|
||||
从目前的分布式数据库发展来看,磁盘型存储引擎还是占据绝对统治地位的。除了性价比因素外,内存型数据库要保证不丢失数据的代价是很高昂的,因为掉电往往就意味着数据的丢失。虽然可以使用不间断电源来保证,但是需要复杂的运维管理来保证数据库稳定运行。
|
||||
|
||||
然而近年来,随着 NVM(Non-Volatile Memory,非易失性内存)等技术的引入。这种情况开始出现了一些变化,此种存储具有 DRAM 内存的性能,同时能保证掉电后数据不丢失。且最重要的是读写模式类似于内存,方便应用去实现功能。有了它的加持,未来内存型数据库还将有比较大的发展。
|
||||
|
||||
除了硬件加持,内存型数据库也可以通过结构设计来保证数据不丢失。最常用的手段就是使用数据备份+提交日志的模式。数据库为了不影响写入读取性能,可以异步地备份数据。同时在每次写入数据之前要先写入提交日志,也就是说提交日志的写入成功才被认为是数据写入成功。
|
||||
|
||||
当数据库节点崩溃恢复后,将备份拿出来,计算出该备份与最新日志之间的差距,然后在该备份上重放这些操作。这样就保证数据库恢复出了最新的数据。
|
||||
|
||||
除了内存和磁盘的取舍,存储引擎还关心数据的组合模式,现在让我们看看两种常见的组合方式:行式与列式。
|
||||
|
||||
行式存储与列式存储
|
||||
|
||||
数据一般是以表格的形式存储在数据库中的,所以所有数据都有行与列的概念。但这只是一个逻辑概念,我们将要介绍的所谓“行式”和“列式”体现的其实是物理概念。
|
||||
|
||||
行式存储会把每行的所有列存储在一起,从而形成数据文件。当需要把整行数据读取出来时,这种数据组织形式是比较合理且高效的。但是如果要读取多行中的某个列,这种模式的代价就很昂贵了,因为一些不需要的数据也会被读取出来。
|
||||
|
||||
而列式存储与之相反,不同行的同一列数据会被就近存储在一个数据文件中。同时除了存储数据本身外,还需要存储该数据属于哪行。而行式存储由于列的顺序是固定的,不需要存储额外的信息来关联列与值之间的关系。
|
||||
|
||||
列式存储非常适合处理分析聚合类型的任务,如计算数据趋势、平均值,等等。因为这些数据一般需要加载一列的所有行,而不关心的列数据不会被读取,从而获得了更高的性能。
|
||||
|
||||
我们会发现 OLTP 数据库倾向于使用行式存储,而 OLAP 数据库更倾向于列式存储,正是这两种存储的物理特性导致了这种倾向性。而 HATP 数据库也是融合了两种存储模式的一种产物。
|
||||
|
||||
当然这里我们要区分 HBase 和 BigTable 所说的宽列存储与列存储在本质上是不同的。宽列存储放在其中的数据的列首先被聚合到了列簇上,列簇被放在不同的文件中;而列簇中的数据其实是按行进行组织的。
|
||||
|
||||
选择行模式与列模式除了以上的区分外,一些其他特性也需要考虑。在现代计算机的 CPU 中,向量指令集可以一次处理很多类型相同的数据,这正是列式存储的特点。同时,将相同类型数据就近存储,还可以使用压缩算法大大减少磁盘空间的占用。
|
||||
|
||||
当然,选择这两种存储模式最重要的因素还是访问模式。如果数据主要是按照行进行读取,比如交易场景、资料管理场景等,那么行式存储应是首选。如果需要经常查询所有数据做聚合,或者进行范围扫描,那么列式存储就很值得一试。
|
||||
|
||||
以上就是常见的数据的组合模式,那么组合好的数据如何存储在物理设备上呢?下面让我们探讨一下数据文件和索引文件两种常用的存放数据的物理原件。
|
||||
|
||||
数据文件与索引文件
|
||||
|
||||
上文介绍了内存与磁盘之间的取舍,从中可看到磁盘其实更为重要的,因为数据库是提供数据持久化存储的服务。故我们开始介绍磁盘上最为重要的两类文件:数据文件和索引文件。
|
||||
|
||||
数据文件和索引文件如名字所示,分别保存原始数据与检索数据用的索引数据。
|
||||
|
||||
但是随着时间的推移,两者的区分也不是那么泾渭分明了。其中以 IOT(索引组织表)模式为代表的数据文件在数据库,特别是分布式数据库中占据越来越重的位置。一种将两者进行融合的趋势已经变得势不可挡。
|
||||
|
||||
数据文件最传统的形式为堆组织表(Heap-Organized Table),数据的放置没有一个特别的顺序,一般是按照写入的先后顺序排布。这种数据文件需要一定额外的索引帮助来查找数据。
|
||||
|
||||
另外有两种数据表形式自带了一定的索引数据能力,即哈希组织表(Hash-Organized Table)和索引组织表(Index-Organized Table)。前者是将数据通过哈希函数分散到一组数据桶内,桶内的数据一般是按照一定规则进行排序,以提高查询效率;而后者一般采用索引文件的形式来存储数据,以 B+树为例,数据被存储在叶子节点上,这样做的目的是减少检索数据时读取磁盘的次数,同时对范围扫描支持友好。
|
||||
|
||||
索引文件的分类模式一般为主键索引与二级索引两类。前者是建立在主键上的,它可能是一个字段或多个字段组成。而其他类型的索引都被称为二级索引。主键索引与数据是一对一关系,而二级索引很有可能是一对多的关系,即多个索引条目指向一条数据。
|
||||
|
||||
这里按照索引与数据之间结合的程度,我们又可以把索引分为聚簇索引和非聚簇索引。前者如哈希组织表和索引组织表那样,数据的分布与索引分布是有关联的,它们被“聚”在一起,这样的查询效率很好。而后者最常见的例子就是针对这两种数据文件的二级索引,因为二级索引要索引的列不是主键,故索引与数据是分割的,查询时需要进行多次磁盘读取。但是对于写入,聚簇索引可能需要进行唯一判断,性能会比简单构建的非聚簇索引低效。
|
||||
|
||||
最后一点需要说明的是,二级索引需要保存指向最终数据的“引用”。从实现层面上,这个引用可以是数据的实际位置,也可以是数据的主键。前者的好处是查询效率高,而写入需要更新所有索引,故性能相对较低。而后者就恰好相反,查询需要通过主键索引进行映射,效率稍低,但写入性能很稳定,如 MySQL 就是选用后者作为其索引模式。
|
||||
|
||||
面向分布式的存储引擎特点
|
||||
|
||||
以上内容为存储引擎的一些核心内容。那分布式数据库相比传统单机数据库,在存储引擎的架构上有什么不同呢?我总结了以下几点。
|
||||
|
||||
内存型数据库会倾向于选择分布式模式来进行构建。原因也是显而易见的,由于单机内存容量相比磁盘来说是很小的,故需要构建分布式数据库来满足业务所需要的容量。
|
||||
|
||||
列式存储也与分布式数据库存在天然的联系。你可以去研究一下,很多列式相关的开源项目都与 Hadoop 等平台有关系的。原因是针对 OLAP 的分析数据库,一个非常大的应用场景就是要分析所有数据。
|
||||
|
||||
而列式存储可以被认为是这种模式的一种优化,实现该模式的必要条件是要有分布式系统,因为一台机器的处理能力是有瓶颈的。如果希望处理超大规模数据,那么将数据分散到多个节点就成为必要的方式。所以说,列模式是由分析性分布式的优化需求所流行起来的。
|
||||
|
||||
至于宽列存储更是分布式数据库场景下才会采用的模式。
|
||||
|
||||
数据文件的组织形式,分布式数据库几乎不会使用堆组织表。因为该形式过于随意,无法有效地分散数据。不知道学习过数据分片那一讲的时候你有没有注意到,另外两种组织表的名字与两种分片算法是有着天然联系的。
|
||||
|
||||
哈希组织表数据经过哈希函数散列到不同的桶,这些桶可以被分散到不同节点。而索引组织表一般叶子节点是按一定顺序排列的,这与范围分片又有着某种契合的关系。所以分布式数据库一般都会采用这两种模式作为其存储引擎,甚至一些分布式数据库直接将数据文件当作索引使用。
|
||||
|
||||
总结
|
||||
|
||||
好了,关于存储引擎我就介绍到这了。这一讲我们首先展示了数据库的整体架构,并点出了存储引擎所在的位置;而后分别讨论了存储引擎中几组概念的对比,并在最后说明了分布式数据库在引擎层面的选择及其原因。
|
||||
|
||||
当然,本讲只是一篇概述。存储引擎中其他重要的概念,我会在本模块随后的几讲中为你详细介绍。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,168 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
08 分布式索引:如何在集群中快速定位数据?
|
||||
索引是数据检错的关键技术,那么在分布式数据库这种体量的数据容量下,如单机数据那样进行数据表全量扫描是非常不现实的,故分布式存储引擎的关键就是要通过索引查找目标数据。
|
||||
|
||||
由于索引在不同的数据库概念里内涵是非常不同的,故本讲首先会定义我们要讨论的索引的内涵;接着会描述数据库的读取路径,从中可以观察到主要索引的使用模式;而后会重点介绍磁盘上与内存中的索引结构;最后会谈谈非主键索引,即二级索引的意义和主要实现形式。
|
||||
|
||||
那么,让我们从什么是分布式索引说起。
|
||||
|
||||
说到分布式索引时,我们在谈论什么?
|
||||
|
||||
首先,我要说明一下谈到分布式索引,需要了解什么样的内容。通过上一讲的学习,你已经知道存储引擎中包含数据文件和索引文件,同时索引文件中又有索引组织表这种主要的形式。目前世界上主要的分布式数据库的数据存储形式,就是围绕着索引而设计的。
|
||||
|
||||
为什么会这样呢?
|
||||
|
||||
由于分布式数据库的数据被分散在多个节点上,当查询请求到达服务端时,目标数据有极大的概率并不在该节点上,需要进行一次甚至多次远程调用才可查询到数据。由于以上的原因,在设计分布式数据库存储引擎时,我们更希望采用含有索引的数据表,从而减少查询的延迟。
|
||||
|
||||
这同时暗含了,大部分分布式数据库的场景是为查询服务的。数据库牺牲了部分写入的性能,在存入数据的时候同时生成索引结构。故分布式数据库的核心是以提供数据检索服务为主,数据写入要服务于数据查询。从这个意义上说,分布式索引就是数据存储的主要形式。
|
||||
|
||||
本讲会以 NewSQL 和 Cassandra 为代表,介绍典型的 NoSQL 的存储引擎中的主要技术,力图帮助你理解此类数据库中存储引擎检索数据的路径。
|
||||
|
||||
读取路径
|
||||
|
||||
掌握分布式数据库存储引擎,一般需要明确其写入路径与读取路径。但如上文讨论的那样,写入是严重依赖读取的,故明确读取路径我们就可以指明写入的规则。
|
||||
|
||||
因此这一部分,我们先来明确存储引擎是如何处理查询请求的。一般的规则如下:
|
||||
|
||||
|
||||
寻找分片和目标节点;
|
||||
检查数据是否在缓存与缓冲中;
|
||||
检查数据是否在磁盘文件中;
|
||||
合并结果。
|
||||
|
||||
|
||||
第一步就是要查找数据在分布式系统的哪个目标节点上。严格说,这一步并不是存储引擎所囊括的部分,但为了表述清楚,我们也将它加入读取路径中来。由于分布式数据库采用分片技术来分散数据,那么查询条件中如果有分片键,就可以应用分片算法来计算出分片,也就是目标节点所在的位置;而如果不包含分片键,就需要“二级索引”来帮忙寻找分片键了,之后的逻辑与使用分片键查找就相似了。
|
||||
|
||||
第二步,既然确定了所在节点,那么剩下的就交给存储引擎了。首先需要在缓存(Cache)中进行查找。缓存包含数据缓存或行缓存,其中包含真实的数据,用于快速检索经常访问的数据,一般元数据和静态配置数据都会放在数据缓存里面。而后再缓冲查找数据,缓冲是为了批量写入数据而预留的一段内存空间,当写满缓冲后,数据会被刷入磁盘中,所以会有部分数据存在缓冲之中。
|
||||
|
||||
第三步,确定了数据并不在内存中,这时就需要检查磁盘了。我们需要在具有索引的数据文件内查找响应的数据。通过之前的学习我们可以知道,每个数据文件都有主键索引,可以直接在其中查找数据。但是,存储引擎为了写入性能,会把数据拆分在众多的数据文件内部。所以我们需要在一系列文件中去查找数据,即使有索引的加成,查找起来的速度也不是能够令人满意的。这个时候我们可以引入布隆过滤,来快速地定位目标文件,提高查询效率。
|
||||
|
||||
最后一步是对结果进行归并。根据执行层的不同需求,这里可以马上返回部分匹配结果,也可以一次性返回全部结果。
|
||||
|
||||
现在我们已经勾勒出存储引擎的一个完整的读取路径,可以看到路径上一些关键技术是保证数据查询与读取的关键点。下面我们就分别介绍其中所涉及的关键技术。
|
||||
|
||||
索引数据表
|
||||
|
||||
我在前文提到过,含有索引的数据表有索引组织表和哈希组织表。其实,我们在分布式数据库中最常见的是 Google 的 BigTable 论文所提到的 SSTable(排序字符串表)。
|
||||
|
||||
Google 论文中的原始描述为:SSTable 用于 BigTable 内部数据存储。SSTable 文件是一个排序的、不可变的、持久化的键值对结构,其中键值对可以是任意字节的字符串,支持使用指定键来查找值,或通过给定键范围遍历所有的键值对。每个 SSTable 文件包含一系列的块。SSTable 文件中的块索引(这些块索引通常保存在文件尾部区域)用于定位块,这些块索引在 SSTable 文件被打开时加载到内存。在查找时首先从内存中的索引二分查找找到块,然后一次磁盘寻道即可读取到相应的块。另一种方式是将 SSTable 文件完全加载到内存,从而在查找和扫描中就不需要读取磁盘。
|
||||
|
||||
从上面的描述看,我们会发现这些键值对是按照键进行排序的,而且一旦写入就不可变。数据引擎支持根据特定键查询,或进行范围扫描。同时,索引为稀疏索引,它只定位到数据块。查到块后,需要顺序扫描块内部,从而获取目标数据。
|
||||
|
||||
下面就是 RocksDB 的 SSTable 结构,可以看到数据是放在前面,后索引作为 metadata 放在文件尾部,甚至 meta 的索引也是放在整个 meta 结构的尾部。
|
||||
|
||||
<beginning_of_file>
|
||||
|
||||
[data block 1]
|
||||
|
||||
[data block 2]
|
||||
|
||||
...
|
||||
|
||||
[data block N]
|
||||
|
||||
[meta block 1: filter block]
|
||||
|
||||
[meta block 2: index block]
|
||||
|
||||
[meta block 3: compression dictionary block]
|
||||
|
||||
[meta block 4: range deletion block]
|
||||
|
||||
[meta block 5: stats block]
|
||||
|
||||
...
|
||||
|
||||
[meta block K: future extended block]
|
||||
|
||||
[metaindex block]
|
||||
|
||||
[Footer]
|
||||
|
||||
<end_of_file>
|
||||
|
||||
|
||||
当然 SSTable 的实现并不一定是通过一个文件,不同的存储引擎会采用不一样的策略去实现它。有的是使用一个文件,如 BigTable 论文中描述的那样,将数据放置在文件开始的部分,索引放在文件结尾。或者将数据和索引分开,放置在不同的文件中。
|
||||
|
||||
数据是按照键的顺序放置的,所以不论索引的实现形式如何,数据文件本身是支持范围扫描的。即使使用没有规律的哈希表,数据部分也可以正常支持范围扫描。
|
||||
|
||||
这里要注意,SSTable 是不可变的,也就是输入一旦写入是不可以更改的,而修改和删除操作一般也是以写入的形式进行的。这就需要进行合并(Compaction),将对同一个数据的操作合并为最终的结果。这个过程类似于上文中数据库面临故障崩溃后恢复的过程,其中日志回放与合并的基本思想是相同的。关于 SSTable 的详细操作,我们会在 LSM 树这种存储引擎的介绍中详细说明。
|
||||
|
||||
当然索引数据表的实现方式不仅仅有 SSTable 一种,对数据库索引有所了解的朋友应该都知道,B 树家族在索引领域扮演着举足轻重的角色。原因是 B 树的每个节点可以有多个数据,所以可以在高度与宽度上进行平衡,从而有效降低磁盘寻道次数。
|
||||
|
||||
但是对 B 树的更新代价是非常高的,故分布式数据库为了写入高效会采用一系列优化手段去提高更新 B 树的效率。这里我们以 MongoDB 的 WiredTiger 存储引擎为例,来介绍其中的一个优化手段。
|
||||
|
||||
这个优化方式就是缓存最近的对索引的操作,而后将操作固化到磁盘中。WiredTiger 使用 B 树来存储数据,在内存页中,B 树节点带有一个修改缓冲,这个缓冲保存的一个指向磁盘原始数据的引用。而后,在读取流程中,原始磁盘数据结合内存缓冲数据后,再返回给用户。这么做的好处是,数据的刷新和内存页更新都是由后台线程完成,不会去阻塞读写操作。
|
||||
|
||||
以上就是两种带有索引性质的数据表实现的逻辑,从中可以看到提高写入速度的关键点,不是采用顺序的形式写入,就是缓存随机写入,从而转变为顺序写入。
|
||||
|
||||
以上介绍的两种数据表都包含内存中的缓冲结构,用以应对内存与磁盘两种设备写入速度差的问题,我在这一讲的后面将会详细介绍其中使用的数据结构。
|
||||
|
||||
下面我们再来看看内存缓冲。
|
||||
|
||||
内存缓冲
|
||||
|
||||
目前有很多种不同的数据结构可以在内存中存储有序的数据。在分布式数据库的存储引擎中,有一种结构因其简单而被广泛地使用,那就是跳表(SkipList)。
|
||||
|
||||
跳表的优势在于其实现难度比简单的链表高不了多少,但是其时间复杂度可以接近负载平衡的搜索树结构。
|
||||
|
||||
跳表在插入和更新时避免对节点做旋转或替换,而是使用了随机平衡的概念来使整个表平衡。跳表由一系列节点组成,它们又由不同的高度组成。连续访问高度较高的节点可以跳过高度较低的节点,有点像蜘蛛侠利用高楼在城市内快速移动一样,这也就是跳表名称的来源。现在我们用一个例子来说明跳表的算法细节。请看下面的图片。
|
||||
|
||||
|
||||
|
||||
如果我们以寻找 15 为例来说明跳表的查找顺序。
|
||||
|
||||
|
||||
首先查找跳表中高度最高的节点,从图中可以看到是10。
|
||||
目标节点 15 比 10 大,从当前高度,也就是最高的高度,向后找没有任何节点,这个时候需要降低一个高度。
|
||||
高度降低后,找到了节点 22,它比 15 要大,这个时候我们又回到了 10 节点,且要继续降低高度。
|
||||
现在降低到了最低,而后顺利地找到了 15。
|
||||
|
||||
|
||||
如果节点需要插入、删除和修改。就需要进行树的平衡,这个时候需要将节点在不同高度上移动,而且高度也会随着节点的数量而变化。要怎么决定变化的数量呢?答案其实很简单,使用随机数来决定这些变量。随机数虽然不是严格均分数据,但是可以做到相对均匀,且代价很小。这也是该算法被广泛使用的原因:用比较小的代价去实现较好的结果,简而言之,其通入产出比非常可观。
|
||||
|
||||
以上就是内存中常用的快速搜索数据结构,那么我们如何判断数据在哪个磁盘文件中呢?答案就是使用布隆过滤。
|
||||
|
||||
布隆过滤
|
||||
|
||||
以上介绍的内容包含了如何在数据文件以及在数据文件缓冲里查找数据。在查询路径中,我们介绍了,除了向所有数据文件请求查询(也被称作读放大)外,还可以利用布隆过滤快速定位目标数据文件。
|
||||
|
||||
布隆过滤的原理是,我们有一个非常大的位数组,首先初始化里面所有的值为 0;而后对数据中的键做哈希转换,将结果对应的二进制表示形式映射到这个位数组里面,这样有一部分 0 转为 1;然后将数据表中所有建都如此映射进去。
|
||||
|
||||
查找的时候,将查询条件传入的键也进行类似的哈希转换,而后比较其中的 1 是否与数组中的匹配,如果匹配,说明键有可能在这个数据表中。
|
||||
|
||||
可以看到,这个算法是一个近似算法,存在误判的可能。也就是所有位置都是 1,但是键也可能不在数据表内,而这些 1 是由于别的键计算产生的。
|
||||
|
||||
但是在查找数据文件的场景中,这个缺陷可以忽略。因为如果布隆过滤判断失败,也只是多浪费一些时间在数据表中查找,从而退化为读放大场景,并不会产生误读的情况。
|
||||
|
||||
布隆过滤的原理简单易懂,它对于 LSM 树存储引擎下所产生的大量 SSTable 的检索很有帮助,是重要的优化查询的手段。
|
||||
|
||||
二级索引
|
||||
|
||||
我以上谈到的所有查询方式都是基于主键索引,但是在真实的场景下,非主键经常需要作为查询条件。这个时候就引入了二级索引的概念。
|
||||
|
||||
二级索引一般都是稀疏索引,也就是索引与数据是分离的。索引的结果一般保存的是主键,而后根据主键去查找数据。这在分布式场景下有比较明显的性能问题,因为索引结果所在的节点很可能与数据不在一个节点上。
|
||||
|
||||
以上问题的一个可行解决方案是以二级索引的结果(也就是主键)来分散索引数据,也就是在数据表创建时,同时创建二级索引。Apache Cassandra 的 SASI 在这方面就是一个很好的例子。它绑定在 SSTable 的生命周期上,在内存缓存刷新或是在数据合并时,二级索引就伴随着创建了。这一定程度上让稀疏的索引有了一定亲和性。
|
||||
|
||||
如果要使用键值对实现二级索引,那么索引结果会有如下几种组合方式。
|
||||
|
||||
|
||||
急迫模式:将索引结果快速合并到一个 value 中,而后一次查询就可以查到所以结果。
|
||||
正常模式:使用多个键值对保留数据。
|
||||
键组合模式:把索引与结果全都放在 key 上,value 是空的。
|
||||
|
||||
|
||||
总体来说,三种模式读取性能接近,但急迫模式的写入性能会低一些。但是对于不同的 key-value 底层实现,其性能会有差别,比如 wisckey(将在第 11 讲中介绍)实现的键值分离模式,使用组合模式就有意义。同时由于键组合模式比较简单,且适合键扫描算法的实现,故是一种比较常见二级索引形式。
|
||||
|
||||
总结
|
||||
|
||||
本讲内容就介绍到这里了。这一讲我们首先说明了分布式索引的概念,实际上它就是分布式数据库存储引擎中用来存储数据的所有技术的总称;而后我介绍了存储引擎的查询路径,帮你在心中建立起存储引擎处理查询的整体概念;最后我又分别介绍了影响查询路径的多个关键技术,并给出了实际的案例。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,112 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
11 事务处理与恢复(下):如何控制并发事务?
|
||||
上一讲,我们介绍了事务的基本概念和数据库恢复流程,其中涉及了事务持久性是如何保证的,那么这一讲,我们就重点介绍事务的隔离性。
|
||||
|
||||
数据库最强的隔离级别是序列化,它保证从事务的角度看自己是独占了所有资源的。但序列化性能较差,因此我们引入了多种隔离界别来提高性能。在本讲的最后我会介绍分布式数据库中常用的并发控制手段,它们是实现隔离级别的有效方案,其中以多版本方式实现快照隔离最为常见。
|
||||
|
||||
现在让我们开始今天的内容。
|
||||
|
||||
隔离级别
|
||||
|
||||
在谈隔离级别之前,我们先聊聊“序列化”(Serializability)的概念。
|
||||
|
||||
序列化的概念与事务调度(Schedule)密切相关。一个调度包含该事务的全部操作。我们可以用 CPU 调度理论来类比,当一个事务被调度后,它可以访问数据库系统的全部资源,同时会假设没有其他事务去影响数据库的状态。这就类似于一个进程被 CPU 调度,从而独占该 CPU 资源(这里的 CPU 指的是时分系统)。但是实际设计调度时,会允许调度事务内部的操作被重新排序,使它们可以并行执行。这些都是优化操作,但只要不违反 ACID 的原则和结果的正确性就可以了。
|
||||
|
||||
那什么是序列化呢?如果一个调度被说成是序列化的,指的是它与其他调度之间的关系:在该调度执行时没有其他被调度的事务并行执行。也就是说,调度是一个接着一个顺序执行的,前一个调度成功完成后,另一个调度再执行。这种方法的一个好处是执行结果比较好预测。但是,我们发现这种做法有明显的缺陷:性能太低。在实现时,一个序列化调度可能会并行执行多个事务操作,但是会保证这样与一个个顺序执行调度有相同的结果。
|
||||
|
||||
以上就是序列化的概念,它揭示了序列化也会存在并发执行的情况。这一点很重要,在隔离理论中,一个隔离概念只是描述了一种行为,而在实现层面可以有多种选择,只要保证这个行为的结果符合必要条件就没有问题了。
|
||||
|
||||
序列化是最强的事务隔离级别,它是非常完美的隔离状态,可以让并行运行的事务感知不到对方的存在,从而安心地进行自己的操作。但在实现数据库事务时,序列化存在实现难度大、性能差等问题。故数据库理论家提出了隔离级别的概念,用来进行不同程度的妥协。在详解隔离级别之前,来看看我们到底可以“妥协”什么。
|
||||
|
||||
这些“妥协”被称为读写异常(Anomalies)。读异常是大家比较熟悉的,有“脏读”“不可重读”和“幻读”。写异常不太为大家所知,分别是“丢失更新”“脏写”和“写偏序”。读异常和写异常是分别站在使用者和数据本身这两个角度去观察隔离性的,我们将成对介绍它们。传统上隔离级别是从读异常角度描述的,但是最近几年,一些论文也从写异常角度出发,希望你能明白两种表述方式之间是有联系的。下表就是经典隔离级别与读异常的关系。
|
||||
|
||||
|
||||
|
||||
从中可以看到序列化是不允许任何读写异常存在的。
|
||||
|
||||
可重读允许幻读的产生。幻读是事务里面读取一组数据后,再次读取这组数据会发现它们可能已经被修改了。幻读对应的写异常是写偏序。写偏序从写入角度发现,事务内读取一批数据进行修改,由于幻读的存在,造成最终修改的结果从整体上看违背了数据一致性约束。
|
||||
|
||||
读到已提交在可重读基础上放弃了不可重读。与幻读类似,但不可重读针对的是一条数据。也就是只读取一条数据,而后在同一个事务内,再读取它数据就变化了。
|
||||
|
||||
刚接触这个概念的同学可能会感觉匪夷所思,两者只相差一个数据量,就出现了两个隔离级别。这背后的原因是保证一条数据的难度要远远低于多条,也就是划分这两个级别,主要的考虑是背后的原理问题。而这个原理又牵扯出了性能与代价的问题。因此就像我在本专栏中反复阐述的一样,一些理论概念有其背后深刻的思考,你需要理解背后原理才能明白其中的奥义。不过不用担心,后面我会详细阐述它们之间实现的差别。
|
||||
|
||||
而不可重读对应的是丢失更新,与写偏序类似,丢失更新是多个事务操作一条数据造成的。
|
||||
|
||||
最低的隔离级别就是读到未提交,它允许脏读的产生。脏读比较简单,它描述了事务可以读到其他事务为提交的数据,我们可以理解为完全没有隔离性。而脏读本身也会造成写异常:脏写。脏写就是由于读到未提交的数据而造成的写异常。
|
||||
|
||||
以上,我们详细阐述了经典的隔离级别。但是这套理论是非常古早的,较新的 MVCC 多版本技术所带来的快照隔离又为传统隔离级别增添一个灵活选型。它可以被理解为可重读隔离级别,也就是不允许不可重读。但是在可重读隔离下,是可以保证读取不到数据被修改的。但快照隔离的行为是:一旦读到曾经读过的数据被修改,将立即终止当前事务,也就是进行回滚操作。在多并发事务下,也就是只有一个会成功。你可以细细品味两者的差异。
|
||||
|
||||
快照隔离可以解决丢失更新的问题,因为针对同一条数据可以做快照检测,从而发现数据被修改,但是不能防止写偏序的问题。
|
||||
|
||||
快照隔离是现代分布式数据库存储引擎最常使用的隔离级别,而解决写偏序问题,也是存储引擎在该隔离级别下需要解决的问题。SSI(Serializable Snaphost Isoltion)正是解决这个问题的方案,我会在“18 | 分布式事务:‘老大难’问题的最新研究与实践”中详细介绍该方案。
|
||||
|
||||
至此我们讨论了典型的隔离级别,隔离级别与分布式一致性的关系我在“”中已经有过阐述,如果需要复习,请出门左转。现在让我们接着讨论如何实现这些隔离级别。
|
||||
|
||||
并发控制
|
||||
|
||||
目前存储引擎引入多种并发模型来实现上面提到的隔离级别,不同的模式对实现不同的级别是有偏好的,虽然理论上每种控制模型都可以实现所有级别。下面我就从乐观与悲观、多版本、基于锁的控制三个方面进行介绍。
|
||||
|
||||
乐观与悲观
|
||||
|
||||
乐观与悲观的概念类似于并发编程中的乐观锁与悲观锁。但是这里你要注意,实现它们并不一定要借助锁管理。
|
||||
|
||||
乐观控制使用的场景是并行事务不太多的情况,也就是只需要很少的时间来解决冲突。那么在这种情况下,就可以使用一些冲突解决手段来实现隔离级别。最常用的方案是进行提交前冲突检查。
|
||||
|
||||
冲突检查有多种实现模式,比如最常用的多版本模式。而另一种古老的模式需要检查并行事务直接操作的数据,也就是观察它们操作的数据是否有重合。由于其性能非常差,已经很少出现在现代存储引擎中了。这里需要你注意的是,乐观控制不一定就是多版本这一种实现,还有其他更多的选择。
|
||||
|
||||
同样的,悲观控制也不仅仅只有锁这一种方案。一种可能的无锁实现是首先设置两个全局时间戳,最大读取时间与最大写入时间。如果一个读取操作发生的时间小于最大写入时间,那么该操作所在的事务被认为应该终止,因为读到的很可能是旧数据。而一个写操作如果小于最大读取时间,也被认为是异常操作,因为刚刚已经有读取操作发生了,当前事务就不能去修改数据了。而这两个值是随着写入和读取操作而更新的。这个悲观控制被称为 Thomas Write Rule,对此有兴趣的话你可以自行搜索学习。
|
||||
|
||||
虽然乐观与悲观分别有多种实现方案,但乐观控制最为常见的实现是多版本控制,而悲观控制最常见的就是锁控制。下面我就详细介绍它们。
|
||||
|
||||
多版本
|
||||
|
||||
多版本并发控制(MVCC,Multiversion concurrency control)是一种实现乐观控制的经典模式。它将每行数据设置一个版本号,且使用一个单调递增的版本号生成器来产生这些版本号,从而保证每条记录的版本号是唯一的。同时给每个事物分为一个 ID 或时间戳,从而保证读取操作可以读到事务提交之前的旧值。
|
||||
|
||||
MVCC 需要区分提交版本与未提交版本。最近一次提交的版本被认为是当前版本,从而可以被所有事务读取出来。而根据隔离级别的不同,读取操作能或者不能读取到未提交的版本。
|
||||
|
||||
使用 MVCC 最经典的用法是实现快照隔离。事务开始的时候,记录当前时间,而后该事务内所有的读取操作只能读到当前提交版本小于事务开始时间的数据,而未提交的数据和提交版本大于事务开始时间点的数据是不能读取出来的。如果事务读取的数据已经被其他事务修改,那么该数据应该在上一讲提到的 undo log 中,当前事务还是能够读取到这份数据的。故 undo log 的数据不能在事务提交的时候就清除掉,因为很可能有另外的事务正在读取它。
|
||||
|
||||
而当事务提交的时候,数据其实已经写入完成。只需要将版本状态从未提交版本改为提交版本即可。所以 MVCC 中的提交操作是非常快的,这点会对分布式事务有很多启示。
|
||||
|
||||
而上文提到的 SSI 模式可以在 MVCC 的基础上引入冲突解决机制,从而解决写偏序问题。当提交发生的时候,事务会检测其修改和读取的数据在提交之前是否已经被其他已提交事务修改了,如果是,则会终止当前事务,并进行回滚。同时这个冲突检测时机会有两个:一个是在事务内进行读取操作时就进行检测,称为前向检测(forward)。而相对的,在提交时进行检测被称为后向检测(backward)。你会明显感觉到,前者会快速失败,但是性能较低;而后者对异常的反应较慢,但速度会有优势。
|
||||
|
||||
这就是经典的 MVCC 并发控制,现在让我接着介绍典型的悲观控制:锁控制。
|
||||
|
||||
基于锁的控制
|
||||
|
||||
基于锁的控制是典型的悲观控制。它会使用显示的锁来控制共享资源,而不是通过调度手段来实现。锁控制可以很容易实现“序列化操作”,但是它同时存在锁竞争和难扩展等问题。
|
||||
|
||||
一个比较知名的锁技术是两阶段锁(2PL),它将锁操作总结为两个阶段。
|
||||
|
||||
|
||||
锁膨胀阶段。在该过程中,事务逐步获得所有它需要的锁,同时不释放任何锁。这期间事务可以对加锁的数据进行操作。
|
||||
锁收缩阶段。该过程中,在上一过程中获得的锁全部被释放。这个事务是逐步的,这期间事务依然可以对还持有锁的数据进行操作。
|
||||
|
||||
|
||||
以上过程简单明了,它是针对一次性加锁提出来的,一次性加锁的缺点是没有并发度,性能低;而两阶段锁可以保证一定的并发度,但其缺点是会有死锁的产生。
|
||||
|
||||
死锁是两个事务互相持有对方的锁,从而造成它们都无法继续运行。解决死锁需要引入超时机制,但超时机制又有明显的性能缺憾。此时,人们会引入死锁检测机制来尽早发现死锁。一般实现手段是将所有事务的锁依赖构建成一棵依赖图,而后使用图算法来发现其中的环形死锁结构,从而快速判断死锁的产生。
|
||||
|
||||
而与锁相对的一个概念就是“闩”(latch,读“shuān”)。一般资料说闩是轻量的,锁是重量的,这其实体现在两个方面。
|
||||
|
||||
一是说它们处理的对象。闩一般用在粒度很小的数据中,比如数据块、索引树的节点等。而锁一般作用在大颗粒操作,如锁定多行数据、事务和修改存储结构等。
|
||||
|
||||
二是它们本身的实现不同。闩一般使用 CAS 执行,是基于比较而后设置的无锁指令级别的操作。如果原始值发生变化就重新进行以上操作,这个过程叫自旋(spin)。而锁是使用独立的资源,且有锁管理器来控制。可想而知,调度锁也是一个比较耗时且复杂的过程。
|
||||
|
||||
这里就要解释上文中隔离级别“序列化”和“可重读”之间实现的差异了。“序列化”由于要保证一组数据重复读取的一致性,就需要引入重量级的锁,其代价是很高的;而“可重读”只需要保证一行数据重复读取是一致的,它可以使用轻量级的闩来实现。故隔离级别将它们分成两种是非常合理的,因为从原理看,它们是完全不同的。
|
||||
|
||||
以上就是关于基于锁的控制的相关内容。
|
||||
|
||||
总结
|
||||
|
||||
本讲内容就介绍到这里了。事务是我们课程到目前为止最长的内容,用了两讲的篇幅来详细介绍。事务的话题在数据库领域一直很热门,我从事务原理层面切入,解释了 ACID 和不同隔离级别所需要的技术手段。这些内容为分布式事务的学习打下坚实的基础,同时你可以将本专栏作为一份参考资料,随时进行查阅。
|
||||
|
||||
从本质出发,事务是一个面向使用者的概念,它向使用者提供一种契约,目的是使人们可以可靠地使用数据库保存和共享数据,这是数据库最核心的功能,且有众多的应用是基于该功能构建的,这也是分布式数据库为什么要实现分布式条件下的事务的根本原因。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,131 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
12 引擎拓展:解读当前流行的分布式存储引擎
|
||||
这一讲是存储引擎模块的最后一讲,通过这一个模块的学习,相信你已经对存储引擎的概念、使用方法与技术细节有了全方位的认识。本讲我们先总结一下模块二的主要内容,并回答大家提到的一些典型问题;而后我会介绍评估存储引擎的三个重要元素;最后为你介绍目前比较流行的面向分布式数据库的存储引擎。
|
||||
|
||||
让我们先进行本模块的内容回顾。
|
||||
|
||||
存储引擎回顾
|
||||
|
||||
存储引擎是数据库的核心组件,起到了物理模型与逻辑模型之间的沟通作用,是数据库重要功能,是数据写入、查询执行、高可用和事务等操作的主要承担者。可谓理解存储引擎也就掌握了数据库的主要功能。
|
||||
|
||||
在这个模块里,我首先向你介绍了存储引擎在整个数据库中的定位,点明了它其实是本地执行模块的组成部分;而后通过内存与磁盘、行式与列式等几组概念的对比,介绍了不同种类的存储引擎的实现差异;并最终说明了分布式数据库存储引擎的特点,即面向内存、列式和易于散列。
|
||||
|
||||
在第 8 讲中,我介绍了分布式数据库的索引。着重说明了存储引擎中大部分数据文件其实都是索引结构;而后带着你一起探讨了典型分布式数据库存储引擎的读取路径,并介绍了该路径上的一些典型技术,如索引数据表、内存跳表、布隆过滤和二级索引等。
|
||||
|
||||
接着我介绍了一个在分布式数据库领域内非常流行的存储引擎:LSM 树。介绍了其具体的结构、读写修改等操作流程;重点说明了合并操作,它是 LSM 树的核心操作,直接影响其性能;最后介绍了 RUM 假说,它是数据库优化的一个经典取舍定律。
|
||||
|
||||
最后,我们探讨了存储引擎最精华的概念,就是事务。我用了两讲的篇幅,详细为你阐述事务的方方面面。总结一下,事务其实是数据库给使用者的一个承诺,即 ACID。为了完成这个承诺,数据库动用了存储引擎中众多的功能模块。其中最重要的事务管理器,同时还需要页缓存、提交日志和锁管理器等组件来进行配合。故在实现层面上,事务的面貌是很模糊的,它同时具备故障恢复和并发控制等特性,这是由其概念是建立在最终使用侧而造成的。
|
||||
|
||||
事务部分我们主要抓住两点:故障恢复+隔离级别。前者保证了数据库存储数据不会丢失,后者保证并发读写数据时的完整性;同时我们要将事务与模块一中的分布式一致性做区别,详细内容请你回顾第 5 讲。
|
||||
|
||||
在事务部分,有同学提到了下面这个问题,现在我来为你解答。
|
||||
|
||||
当内存数据刷入磁盘后,同时需要对日志做“截取”操作,这个截取的值是什么?
|
||||
|
||||
这个“截取”是一个形象的说法,也就是可以理解为截取点之前的数据已经在输入磁盘中。当进行数据库恢复的时候,只要从截取点开始恢复数据库即可,这样大大加快了恢复速度,同时也释放了日志的空间。这个截取点,一般被称为检查点。相关细节,你可以自行学习。
|
||||
|
||||
以上我们简要回顾了本模块的基本知识。接下来,我将带你领略当代分布式数据库存储引擎的一些风采。但是开始介绍之前,我们需要使用一个模型来评估它们的特点。
|
||||
|
||||
评估存储引擎的黄金三角
|
||||
|
||||
存储引擎的特点千差万别,各具特色。但总体上我们可以通过三个变量来描述它们的行为:缓存的使用方式,数据是可变的还是不可变的,存储的数据是有顺序的还是没有顺序的。
|
||||
|
||||
缓存形式
|
||||
|
||||
缓存是说存储引擎在数据写入的时候,首先将它们写入到内存的一个片段,目的是进行数据汇聚,而后再写入磁盘中。这个小片段由一系列块组成,块是写入磁盘的最小单位。理想状态是写入磁盘的块是满块,这样的效率最高。
|
||||
|
||||
大部分存储引擎都会使用到缓存。但使用它的方式却很不相同,比如我将要介绍的 WiredTiger 缓存 B 树节点,用内存来抵消随机读写的性能问题。而我们介绍的 LSM 树是用缓存构建一个有顺序的不可变结构。故使用缓存的模式是衡量存储引擎的一个重要指标。
|
||||
|
||||
可变/不可变数据
|
||||
|
||||
存储的数据是可变的还是不可变的,这是判断存储引擎特点的另一个维度。不可变性一般都是以追加日志的形式存在的,其特点是写入高效;而可变数据,以经典 B 树为代表,强调的是读取性能。故一般认为可变性是区分 B 树与 LSM 树的重要指标。但 BW-Tree 这种 B 树的变种结构虽然结构上吸收了 B 树的特点,但数据文件是不可变的。
|
||||
|
||||
当然不可变数据并不是说数据一直是不变的,而是强调了是否在最影响性能的写入场景中是否可变。LSM 树的合并操作,就是在不阻塞读写的情况下,进行数据文件的合并与分割操作,在此过程中一些数据会被删除。
|
||||
|
||||
排序
|
||||
|
||||
最后一个变量就是数据存储的时候是否进行排序。排序的好处是对范围扫描非常友好,可以实现 between 类的数据操作。同时范围扫描也是实现二级索引、数据分类等特性的有效武器。如本模块介绍的 LSM 树和 B+ 树都是支持数据排序的。
|
||||
|
||||
而不排序一般是一种对于写入的优化。可以想到,如果数据是按照写入的顺序直接存储在磁盘上,不需要进行重排序,那么其写入性能会很好,下面我们要介绍的 WiscKey 和 Bitcask 的写入都是直接追加到文件末尾,而不进行排序的。
|
||||
|
||||
以上就是评估存储引擎特点的三个变量,我这里将它们称为黄金三角。因为它们是互相独立的,彼此并不重叠,故可以方便地评估存储引擎的特点。下面我们就试着使用这组黄金三角来评估目前流行的存储引擎的特点。
|
||||
|
||||
B 树类
|
||||
|
||||
上文我们提到过评估存储引擎的一个重要指标就是数据是否可以被修改,而 B 树就是可以修改类存储引擎比较典型的一个代表。它是目前的分布式数据库,乃至于一般数据库最常采用的数据结构。它是为了解决搜索树(BST)等结构在 HDD 磁盘上性能差而产生的,结构特点是高度很低,宽度很宽。检索的时候从上到下查找次数较少,甚至如 B+ 树那样,可以完全把非叶子节点加载到内存中,从而使查找最多只进行一次磁盘操作。
|
||||
|
||||
下面让我介绍几种典型的 B 树结构的存储引擎。
|
||||
|
||||
InnoDB
|
||||
|
||||
InnoDB 是目前 MySQL 的默认存储引擎,同时也是 MariaDB 10.2 之后的默认存储引擎。
|
||||
|
||||
根据上文的评估指标看,它的 B+ 树节点是可变的,且叶子节点保存的数据是经过排序的。同时由于数据的持续写入,在高度不变的情况下,这个 B+ 树一定会横向发展,从而使原有的一个节点分裂为多个节点。而 InnoDB 使用缓存的模式就是:为这种分裂预留一部分内存页面,用来容纳可能的节点分裂。
|
||||
|
||||
这种预留的空间其实就是一种浪费,是空间放大的一种表现。用 RUM 假设来解释,InnoDB 这种结构是牺牲了空间来获取对于读写的优化。
|
||||
|
||||
在事务层面,InnoDB 实现了完整的隔离级别,通过 MVCC 机制配合各种悲观锁机制来实现不同级别的隔离性。
|
||||
|
||||
WiredTiger
|
||||
|
||||
WiredTiger 是 MongoDB 默认的存储引擎。它解决了原有 MongoDB 必须将大部分数据放在内存中,当内存出现压力后,数据库性能急剧下降的问题。
|
||||
|
||||
它采用的是 B 树结构,而不是 InnoDB 的 B+ 树结构。这个原因主要是 MongoDB 是文档型数据库,采用内聚的形式存储数据(你可以理解为在关系型数据库上增加了扩展列)。故这种数据库很少进行 join 操作,不需要范围扫描且一次访问就可以获得全部数据。而 B 树每个层级上都有数据,虽然查询性能不稳定,但总体平均性能是要好于 B+ 树的。
|
||||
|
||||
故 WiredTiger 首先是可变数据结构,同时由于不进行顺序扫描操作,数据也不是排序的。那么它是如何运用缓存的呢?这个部分与 InnoDB 就有区别了。
|
||||
|
||||
在缓存中每个树节点上,都配合一个更新缓冲,是用跳表实现的。当进行插入和更新操作时,这些数据写入缓冲内,而不直接修改节点。这样做的好处是,跳表这种结构不需要预留额外的空间,且并发性能较好。在刷盘时,跳表内的数据和节点页面一起被合并到磁盘上。
|
||||
|
||||
由此可见,WiredTiger 牺牲了一定的查询性能来换取空间利用率和写入性能。因为查询的时候出来读取页面数据外,还要合并跳表内的数据后才能获取最新的数据。
|
||||
|
||||
BW-Tree
|
||||
|
||||
BW-Tree 是微软的 Azure Cosmos DB 背后的主要技术栈。它其实通过软件与硬件结合来实现高性能的类 B 树结构,硬件部分的优化使用 Llama 存储系统,有兴趣的话你可以自行搜索学习。我们重点关注数据结构方面的优化。
|
||||
|
||||
BW-Tree 为每个节点配置了一个页面 ID,而后该节点的所有操作被转换为如 LSM 树那样的顺序写过程,也就是写入和删除操作都是通过日志操作来完成的。采用这种结构很好地解决了 B 树的写放大和空间放大问题。同时由于存在多个小的日志,并发性也得到了改善。
|
||||
|
||||
刷盘时,从日志刷入磁盘,将随机写变为了顺序写,同样提高了刷盘效率。我们会发现,BW-Tree 也如 LSM 树一样存在读放大问题,即查询时需要将基础数据与日志数据进行合并。而且如果日志太长,会导致读取缓慢。而此时 Cosmos 采用了一种硬件的解决方案,它会感知同一个日志文件中需要进行合并的部分,将它们安排在同一个处理节点,从而加快日志的收敛过程。
|
||||
|
||||
以上就是典型的三种 B 树类的存储引擎,它们各具特色,对于同一个问题的优化方式也带给我们很多启发。
|
||||
|
||||
LSM 类
|
||||
|
||||
这个模块我专门用了一个完整篇章来阐述它的特点,它是典型的不可变数据结构,使用缓存也是通过将随机写转为顺序写来实现的。
|
||||
|
||||
我们在说 LSM 树时介绍了它存储的数据是有顺序的,其实目前有两种无顺序的结构也越来越受到重视。
|
||||
|
||||
经典存储
|
||||
|
||||
经典的 LSM 实现有 LeveledDB,和在其基础之上发展出来的 RocksDB。它们的特点我们之前有介绍过,也就是使用缓存来将随机写转换为顺序写,而后生成排序且不可变的数据。它对写入和空间友好,但是牺牲了读取性能。
|
||||
|
||||
Bitcask
|
||||
|
||||
Bitcask 是分布式键值数据库 Riak 的一种存储引擎,它也是一种典型的无顺序存储结构。与前面介绍的典型 LSM 树有本质上的不同,它没有内存表结构,也就是它根本不进行缓存而是直接将数据写到数据文件之中。
|
||||
|
||||
可以看到,其写入是非常高效的,内存占用也很小。但是如何查询这种“堆”结构的数据呢?答案是在内存中有一个叫作 Keydir 的结构保存了指向数据最新版本的引用,旧数据依然在数据文件中,但是没有被 Keydir 引用,最终就会被垃圾收集器删除掉。Keydir 实际上是一个哈希表,在数据库启动时,从数据文件中构建出来。
|
||||
|
||||
这种查询很明显改善了 LSM 树的读放大问题,因为每条数据只有一个磁盘文件引用,且没有缓存数据,故只需要查询一个位置就可以将数据查询出来。但其缺陷同样明显:不支持范围查找,且启动时,如果数据量很大,启动时间会比较长。
|
||||
|
||||
此种结构优化了写入、空间以及对单条数据的查找,但牺牲了范围查找的功能。
|
||||
|
||||
WiscKey
|
||||
|
||||
那么有没有一种结构,既能利用无顺序带来的高速写入和空间利用率上的优点,又可以支持非常有用的范围查询呢?WiscKey 结构正是尝试解决这个问题的一个手段。
|
||||
|
||||
它的特点是将 Key 和 Value 分别放在两个文件中。Key 还是按照 LSM 树的形式,这样就保证了 Key 是有顺序的,可以进行范围扫描。同时使用 LSM 树,即不需要将所有的 Key 放到内存里,这样也解决了 Bitcask 加载慢的问题。
|
||||
|
||||
而 Value 部分称为 vLogs(value Logs),其中的数据是没有顺序的。这种结构适合更新和删除比较少的场景,因为范围扫描会使用随机读,如果更新删除很多,那么其冲突合并的效率很低。同时在合并操作的时候,需要扫描 Key 而后确定合并方案,这个在普通的 LSM 树中也是不存在的。
|
||||
|
||||
WiscKey 非常适合在 SSD 进行运行,因为读取 Value 需要进行随机读取。目前 dgraph.io 的 Badger 是该模式比较成熟的实现。
|
||||
|
||||
总结
|
||||
|
||||
到这里,这一讲内容就说完了。我带你回顾了第二模块的主要内容,这是一个基础知识普及模块,将为接下来的分布式模块打下基础。同时相对于传统关系型数据库,分布式数据库的存储引擎也有其自身特点,如 LSM 树结构,你需要认真掌握这种结构。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,131 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
13 概要:分布式系统都要解决哪些问题?
|
||||
在学习了存储引擎相关内容之后,从这一讲开始,我们就进入新的模块——分布式数据库最核心的部分,那就是分布式系统。
|
||||
|
||||
分布式数据库区别于传统数据库的一个重要特性就是其分布式的特点,这些特点来源于分布式理论的发展,特别是数据分布相关理论的发展。相比于无状态分布式系统,有状态的数据库在分布式领域中将会面对更多的挑战。
|
||||
|
||||
本讲内容作为整个模块三的引子,我将会向你提出一系列问题,而在后续的课程中,我会逐一回答这些问题。那么现在让我们从失败模型开始,讨论分布式模式下的数据库吧。
|
||||
|
||||
失败模型
|
||||
|
||||
分布式系统是由多个节点参与其中的,它们直接通过网络进行互联。每个节点会保存本地的状态,通过网络来互相同步这些状态;同时节点需要访问时间组件来获取当前时间。对于分布式系统来说,时间分为逻辑时间与物理时间。逻辑时间一般被实现为一个单调递增的计数器,而物理时间对应的是一个真实世界的时间,一般由操作系统提供。
|
||||
|
||||
以上就是分布式系统所涉及的各种概念,看起很简单,实际上业界对分布式系统的共识就是上述所有环节没有一点是可靠的,“不可靠”贯穿了分布式系统的整个生命周期。而总结这些不可靠就成为失败模型所解决的问题。
|
||||
|
||||
在介绍失败模型的具体内容之前,让我们打开思路,看看有哪些具体的原因引起了分布式系统的可靠性问题。
|
||||
|
||||
引起失败的原因
|
||||
|
||||
当讨论分布式系统内的不稳定因素的时候,人们首先会想到网络问题,但是一个最容易让大家忽略的地方就是远程节点处理请求时也可能发生故障。一个比较常见的误区就是认为远程执行会马上返回结果,但这种假设是非常不可靠的。因为远程节点的处理能力、运行环境其实是未知的,我们不能认为它们会一直按照固定的模式去响应我们的请求。
|
||||
|
||||
而另一种情况是,请求到达远程节点后很可能不会被马上处理,而是放在了一个队列里面进行缓冲。这对于远程节点的吞吐量改善是有好处的,但是这在一定程度上带来了延迟,从而深刻地影响了交互模式。处理以上问题的方式就是需要引入故障检测(我会在下一讲介绍),来观察远程节点的运行情况,从而针对不同的问题采取不同的应对手段。
|
||||
|
||||
第二种常见的误解是所有节点时间是一致的,这种误解是非常普遍并且危险的。虽然可以使用工具去同步集群内的时间,但是要保持系统内时间一致是非常困难的。而如果我们使用不同节点产生的物理时间来进行一致性计算或排序,那么结果会非常不靠谱。所以大部分分布式数据库会用一个单独的节点来生成全局唯一的逻辑时间以解决上面的问题。而有些分布式数据库,如 Spanner 会使用原子钟这种精密服务来解决时间一致的问题。
|
||||
|
||||
本地物理时间的另一个问题是会产生回溯,也就是获取一个时间并执行若干步骤后,再去获取当前时间,而这个时间有可能比之前的时间还要早。也就是说我们不能认为系统的物理时间是单调递增的,这就是为什么要使用逻辑时间的另一个重要的原因。
|
||||
|
||||
但是本地物理时间在分布式系统中某些部分依然扮演着重要的作用,如判断远程节点超时等。但是基于以上两点,我们在实现分布式算法时应将时间因素考虑进去,从而避免潜在的问题。
|
||||
|
||||
以上谈到的分布式问题集中在节点层面,而另一大类问题就是网络造成的了。其中最为经典的问题就是网络分区,它指的是分布式系统的节点被网络故障分割为不同的小块。而最棘手的是,这些小块内的节点依然可以提供服务。但它们由于不能很好地感知彼此的存在,会产生不一致的问题,这个我们在模块一“”有过比较详细的论述。
|
||||
|
||||
这里需要注意的是,网络分区带来的问题难以解决,因为它是非常难发现的。这是由于网络环境复杂的拓扑和参与者众多共同左右而导致的。故我们需要设计复杂的算法,并使用诸如混沌工程的方式来解决此类问题。
|
||||
|
||||
最后需要强调的一点是,一个单一读故障可能会引起大规模级联反映,从而放大故障的影响面,也就是著名的雪崩现象。这里你要注意,这种故障放大现象很可能来源于一个为了稳定系统而设计的机制。比如,当系统出现瓶颈后,一个新节点被加入进来,但它需要同步数据才能对外提供服务,而大规模同步数据很可能造成其他节点资源紧张,特别是网络带宽,从而导致整个系统都无法对外提供服务。
|
||||
|
||||
解决级联故障的方式有退避算法和断路。退避算法大量应用在 API 的设计中,由于上文提到远程节点会存在暂时性故障,故需要进行重试来使访问尽可能成功地完成。而频繁地重试会造成远程节点资源耗尽而崩溃,退避算法正是依靠客户端来保证服务端高可用的一种手段。而从服务端角度进行直接保护的方式就是断路,如果对服务端的访问超过阈值,那么系统会中断该服务的请求,从而缓解系统压力。
|
||||
|
||||
以上就是分布式系统比较常见的故障。虽然你可能会觉得这些故障很直观,但是如果要去解决它们思路会比较分散。还好前人已经帮我们总结了一些模型来对这些故障进行分级,从而有的放矢地解决这些问题。接下来我就要为你介绍三种典型的失败模型。
|
||||
|
||||
崩溃失败
|
||||
|
||||
当遭遇故障后,进程完全停止工作被称为崩溃失败。这是最简单的一种失败情况,同时结果也非常好预测。这种失败模式也称为崩溃停止失败,特别强调失败节点不需要再参与回分布式系统内部了。我们说这种模式是最容易预测的,是因为失败节点退出后,其他节点感知到之后可以继续提供服务,而不用考虑它重新回归所带来的复杂问题。
|
||||
|
||||
虽然失败停止模式有以上的优点,但实际的分布式系统很少会采用。因为它非常明显地会造成资源浪费,所以我们一般采用崩溃恢复模式,从而重复利用资源。提到崩溃节点恢复,一般都会想到将崩溃节点进行重启,而后经过一定的恢复步骤再加入网络中。虽然这是一种主流模式,但其实通过数据复制从而生成备份节点,而后进行快速热切换才是最为主流的模式。
|
||||
|
||||
崩溃失败可以被认为是遗漏失败的一种特殊情况。因为从其他节点看,他们很难分清一个节点服务响应是由于崩溃还是由于遗漏消息而产生的。那究竟什么是遗漏失败呢?
|
||||
|
||||
遗漏失败
|
||||
|
||||
遗漏失败相比于崩溃失败来说更为不可预测,这种模式强调的是消息有没有被远程节点所执行。
|
||||
|
||||
这其中的故障可能发生在:
|
||||
|
||||
|
||||
消息发送后没有送达远程节点;
|
||||
远程节点跳过消息的处理或根本无法执行(一种特例就是崩溃失败,节点无法处理消息);
|
||||
后者处理的结果无法发送给其他节点。
|
||||
|
||||
|
||||
总之,从其他节点的角度看,发送给该节点的消息石沉大海,没有任何响应了。
|
||||
|
||||
上文提到的网络分区是遗漏失败的典型案例,其中一部分节点间消息是能正常收发的,但是部分节点之间消息发送存在困难。而如果崩溃失败出现,集群中所有节点都将无法与其进行通讯。
|
||||
|
||||
另一种典型情况就是一个节点的处理速度远远慢于系统的平均水平,从而导致它的数据总是旧的,而此时它没有崩溃,依然会将这些旧数据发送给集群内的其他节点。
|
||||
|
||||
当远程节点遗漏消息时,我们是可以通过重发等可靠连接手段来缓解该问题的。但是如果最终还是无法将消息传递出去,同时当前节点依然在继续提供服务,那么此时遗漏失败才会产生。除了以上两种产生该失败的场景,遗漏失败还会发生在网络过载、消息队列满等场景中。
|
||||
|
||||
下面为你介绍最后一种失败模型,即拜占庭失败。
|
||||
|
||||
拜占庭失败
|
||||
|
||||
拜占庭失败又称为任意失败,它相比于上述两种失败是最不好预测的。所谓任意失败是,参与的节点对请求产生不一致的响应,一个说当前数据是 A,而另一个却说它是 B。
|
||||
|
||||
这个故障往往是程序 Bug 导致的,可以通过严格软件开发流程管理来尽可能规避。但我们都清楚,Bug 在生产系统中是很难避免的,特别是系统版本差异带来的问题是极其常见的。故在运行态,一部分系统并不信任直接从远程节点获得的数据,而是采用交叉检测的方式来尽可能得到正确的结果。
|
||||
|
||||
另一种任意失败是一些节点故意发送错误消息,目的是想破坏系统的正常运行,从而牟利。采用区块链技术的数字货币系统则是使用正面奖励的模式(BFT),来保证系统内大部分节点不“作恶”(做正确事的收益明显高于作恶)。
|
||||
|
||||
以上就是三种比较常见的失败模型。模块三的绝大部分内容主要是面向崩溃恢复的场景的。那么下面我们来梳理一下本模块接下来内容的讲解脉络。
|
||||
|
||||
错误侦测与领导选举
|
||||
|
||||
要想解决失败问题,首先就是要进行侦测。在本模块的开始部分,我们会研究使用什么手段来发现系统中的故障。目前,业界有众多方式来检测故障的产生,他们是在易用性、精确性和性能之间做平衡。
|
||||
|
||||
而错误侦测一个重要应用领域就是领导选举。使用错误侦测技术来检测领导节点的健康状态,从而决定是否选择一个新节点来替代已经故障的领导节点。领导节点的一个主要作用就是缓解系统发生失败的可能。我们知道系统中如果进行对等同步状态的代价是很高昂的,如果能选择一个领导节点来统一进行协调,那么会大大降低系统负载,从而避免一些失败的产生。
|
||||
|
||||
而一旦侦测到失败的产生,如何解决它就是我们需要考虑的内容啦。
|
||||
|
||||
复制与一致性
|
||||
|
||||
故障容忍系统(Fault-tolerant)一般使用复制技术产生多个副本,来提供系统的可用性。这样可以保证当系统总部分节点发生故障后,仍然可以提供正常响应。而多个副本会产生数据同步的需求,一致性就是保证数据同步的前提。就像我在模块一中描述的那样,没有复制技术,一致性与同步就根本不存在。
|
||||
|
||||
模块一我们讨论的是 CAP 理论和强一致性模型,它们都是数据一致的范畴。本模块我们会接着讨论客户端一致,或称为会话一致。同时会讨论最终一致这种弱一致模型,最终一致模型允许系统中存在状态不一致的情况,但我们希望尽可能使系统保持一致,这时候会引入反熵手段来解决副本之间不一致的问题。
|
||||
|
||||
而后我们会接着讨论分布式事务,它与一致性存在着联系但又有很明显的区别。同时相比于模块二中的经典事务,分布式事务由于需要解决上文表述的各种失败情况,其处理是比较特殊的,比如需要进行事务协调来处理脑裂问题。
|
||||
|
||||
共识
|
||||
|
||||
最后我们将介绍分布式系统的精华:共识算法。以上介绍的很多内容,包括错误侦测、领导选举、一致性和分布式事务都涵盖在共识算法内,它是现代分布式数据库重要的组件。
|
||||
|
||||
共识算法是为了解决拜占庭将军问题而产生的。简单来说,在从前,拜占庭将军问题被认为是一个逻辑上的困境,它说明了一群拜占庭将军在试图就下一步行动达成统一意见时,可能存在的沟通问题。
|
||||
|
||||
该困境假设每个将军都有自己的军队,每支军队都位于他们打算攻击的城市周围的不同位置,这些将军需要就攻击或撤退达成一致。只要所有将军达成共识,即协调后决定共同执行,无论是攻击还是撤退都无关紧要。
|
||||
|
||||
基于著名的 FLP 不可能问题的研究,拜占庭将军们面临三种困境:
|
||||
|
||||
|
||||
将军们没有统一的时间(没法对表);
|
||||
无法知道别的将军是否被击败;
|
||||
将军们之间的通讯是完全异步的。
|
||||
|
||||
|
||||
由于以上的困境,我们是没有任何办法使将军们最终在特定时间内达成一致性意见的,也就是说共识算法在上述困境下是完全不可能的。
|
||||
|
||||
但是共识算法使用逻辑时钟来提供统一时间,并引入错误侦测技术来确定参与节点的情况,从而在完全异步的通讯情况下可以实现分布式系统的共识。本模块最后一部分,我会介绍几种经典的共识算法,并介绍它们的使用案例。
|
||||
|
||||
共识可以解决遗漏失败,因为只要系统内大部分节点达成共识,剩下的节点即使遗漏该消息,也能对外提供正确的数据。
|
||||
|
||||
总结
|
||||
|
||||
这一讲是模块三的引导课,我首先为你介绍了失败模型的概念,它是描述分布式数据库内各种可能行为的一个准则;而后根据失败模型为你梳理了本模块的讲解思路。
|
||||
|
||||
分布式算法根据目标不同可能分为下面几种行为模式,这些模式与对应的课时如下表所示。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,139 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
17 数据可靠传播:反熵理论如何帮助数据库可靠工作?
|
||||
上一讲我们完整地介绍了一致性的概念,其中一致性程度最低的是最终一致性。在最终一致性的条件下,节点间需要经过一段时间的数据同步,才能将最新数据在节点间进行分发。这就需要这些最新产生的数据能在节点间稳定地传播。
|
||||
|
||||
但是,现实是非常无情的,数据传播中会遇到各种故障,如节点崩溃失败、网络异常、同步数据量巨大造成延迟高等情况,最终会造成最终一致性集群内部节点间数据差异巨大。随着时间的推移,集群向着越来越混乱的局面恶化。
|
||||
|
||||
以上描述的场景就是“熵增”。这是一个物理学概念,在 2020 年上映的影片“Tenet”中,对“熵”的概念有过普及,其中把熵描述为与时间有关,好像熵增就是正向时间,熵减就是时间倒流。
|
||||
|
||||
其实熵与时间之间是间接关系。19 世纪的时候,科学家发现不借助外力,热力总是从高温物体向低温物理传播,进而出现一个理论:在封闭系统内且没有外力作用下,熵总是增的。而时间也是跟随熵增一起向前流动的。影片假设,如果能将熵减小,时间就应该可以随之倒流。
|
||||
|
||||
熵的概念深入了各个领域中,一般都表示系统总是向混乱的状态变化。在最终一致性系统中,就表示数据最终有向混乱方向发展的趋势,这个时候我们就要引入“反熵”机制来施加“外力”,从而消除自然状态的“熵增”所带来的影响。
|
||||
|
||||
说了这么多,简而言之,就是通过一些外部手段,将分布式数据库中各个节点的数据达到一致状态。那么反熵的手段包含:前台同步、后台异步与 Gossip 协议。现在让我来一一为你介绍。
|
||||
|
||||
前台同步
|
||||
|
||||
前台同步是通过读与写这两个前台操作,同步性地进行数据一致性修复。它们分别称为读修复(Read Repair)和暗示切换(Hinted Handoff)。
|
||||
|
||||
读修复
|
||||
|
||||
随着熵逐步增加,系统进入越来越混乱的状态。但是如果没有读取操作,这种混乱其实是不会暴露出去的。那么人们就有了一个思路,我们可以在读取操作发生的时候再来修复不一致的数据。
|
||||
|
||||
具体操作是,请求由一个总的协调节点来处理,这个协调节点会从一组节点中查询数据,如果这组节点中某些节点有数据缺失,该协调节点就会把缺失的数据发送给这些节点,从而修复这些节点中的数据,达到反熵的目的。
|
||||
|
||||
有的同学可能会发现,这个思路与上一讲的可调节一致性有一些关联。因为在可调节一致性下,读取操作为了满足一致性要求,会从多个节点读取数据从而发现最新的数据结果。而读修复会更进一步,在此以后,会将落后节点数据进行同步修复,最后将最新结果发送回客户端。这一过程如下图所示。
|
||||
|
||||
|
||||
|
||||
当修复数据时,读修复可以使用阻塞模式与异步模式两种。阻塞模式如上图所示,在修复完成数据后,再将最终结果返还给客户端;而异步模式会启动一个异步任务去修复数据,而不必等待修复完成的结果,即可返回到客户端。
|
||||
|
||||
你可以回忆一下,阻塞的读修复模式其实满足了上一讲中客户端一致性提到的读单增。因为一个值被读取后,下一次读取数据一定是基于上一次读取的。也就是说,同步修复的数据可以保证在下一次读取之前就被传播到目标节点;而异步修复就没有如此保证。但是阻塞修复同时丧失了一定的可用性,因为它需要等待远程节点修复数据,而异步修复就没有此问题。
|
||||
|
||||
在进行消息比较的时候,我们有一个优化的手段是使用散列来比较数据。比如协调节点收到客户端请求后,只向一个节点发送读取请求,而向其他节点发送散列请求。而后将完全请求的返回值进行散列计算,与其他节点返回的散列值进行比较。如果它们是相等的,就直接返回响应;如果不相等,将进行上文所描述的修复过程。
|
||||
|
||||
这种散列模式的一个明显好处是在系统处于稳定的状态时,判断数据一致性的代价很小,故可以加快读取速度并有效降低系统负载。常用的散列算法有 MD5 等。当然,理论上散列算法是有碰撞的可能性的,这意味着一些不一致状态无法检测出来。首先,我们要说在真实场景中,这种碰撞概率是很低的,退一万步讲,即使发生碰撞,也会有其他检测方来修复该差异。
|
||||
|
||||
以上就是在读取操作中进行的反熵操作,那么在写入阶段我们如何进行修复呢?下面我来介绍暗示切换。
|
||||
|
||||
暗示切换
|
||||
|
||||
暗示切换名字听起来很玄幻。其实原理非常明了,让我们看看它的过程,如下图所示。
|
||||
|
||||
|
||||
|
||||
客户端首先写入协调节点。而后协调节点将数据分发到两个节点中,这个过程与可调节一致性中的写入是类似的。正常情况下,可以保证写入的两个节点数据是一致的。如果其中的一个节点失败了,系统会启动一个新节点来接收失败节点之后的数据,这个结构一般会被实现为一个队列(Queue),即暗示切换队列(HHQ)。
|
||||
|
||||
一旦失败的节点恢复了回来,HHQ 会把该节点离线这一个时间段内的数据同步到该节点中,从而修复该节点由于离线而丢失的数据。这就是在写入节点进行反熵的操作。
|
||||
|
||||
以上介绍的前台同步操作其实都有一个限制,就是需要假设此种熵增过程发生的概率不高且范围有限。如果熵增大范围产生,那么修复读会造成读取延迟增高,即使使用异步修复也会产生很高的冲突。而暗示切换队列的问题是其容量是有限的,这意味着对于一个长期离线的节点,HHQ 可能无法保存其全部的消息。
|
||||
|
||||
那么有没有什么方式能处理这种大范围和长时间不一致的情况呢?下面我要介绍的后台异步方式就是处理此种问题的一些方案。
|
||||
|
||||
后台异步
|
||||
|
||||
我们之前介绍的同步方案主要是解决最近访问的数据,那么将要介绍的后台异步方案主要面向已经写入较长时间的数据,也就是不活跃的数据。进而使用这种方案也可以进行全量的数据一致性修复工作。
|
||||
|
||||
而后台方案与前台方案的关注点是不同的。前台方案重点放在修复数据,而后台方案由于需要比较和处理大量的非活跃数据,故需要重点解决如何使用更少的资源来进行数据比对。我将要为你介绍两种比对技术:Merkle 树和位图版本向量。
|
||||
|
||||
Merkle 树
|
||||
|
||||
如果想要检查数据的差异,我们一般能想到最直观的方式是进行全量比较。但这种思路效率是很低的,在实际生产中不可能实行。而通过 Merkle 树我们可以快速找到两份数据之间的差异,下图就是一棵典型的 Merkle 树。
|
||||
|
||||
|
||||
|
||||
树构造的过程是:
|
||||
|
||||
|
||||
将数据划分为多个连续的段。而后计算每个段的哈希值,得到 hash1 到 hash4 这四个值;
|
||||
而后,对这四个值两两分组,使用 hash1 和 hash2 计算 hash5、用 hash3 和 hash4 计算 hash6;
|
||||
最后使用 hash5 和 hash6 计算 top hash。
|
||||
|
||||
|
||||
你会发现数据差异的方式类似于二分查找。首先比较两份数据的 top hash,如果不一致就向下一层比较。最终会找到差异的数据范围,从而缩小了数据比较的数量。而两份数据仅仅有部分不同,都可以影响 top hash 的最终结果,从而快速判断两份数据是否一致。
|
||||
|
||||
Merkle 树结合了 checksum 校验与二叉树的特点,可以帮助我们快速判断两份数据是否存在差异。但如果我们想牺牲一定精准性来控制参与比较的数据范围,下面介绍的位图版本向量就是一种理想的选择。
|
||||
|
||||
位图版本向量
|
||||
|
||||
最近的研究发现,大部分数据差异还是发生在距离当前时间不远的时间段。那么我们就可以针对此种场景进行优化,从而避免像 Merkle 树那样计算全量的数据。而位图版本向量就是根据这个想法发展起来的。
|
||||
|
||||
这种算法利用了位图这一种对内存非常友好的高密度数据格式,将节点近期的数据同步状态记录下来;而后通过比较各个节点间的位图数据,从而发现差异,修复数据。下面我用一个例子为你展示这种算法的执行过程,请看下图。
|
||||
|
||||
|
||||
|
||||
如果有三个节点,每个节点包含了一组与其他节点数据同步的向量。上图表示节点 2 的数据同步情况。目前系统中存在 8 条数据,从节点 2 的角度看,每个节点都没有完整的数据。其中深灰色的部分表明同步的数据是连续的,我们用一个压缩的值表示。节点 1 到 3 这个压缩的值分别为 3、5 和 2。可以看到节点 2 自己的数据是连续的。
|
||||
|
||||
数据同步一旦出现不连续的情况,也就是出现了空隙,我们就转而使用位图来存储。也就是图中浅灰色和白色的部分。比如节点 2 观察节点 1,可以看到有三个连续的数据同步,而后状态用 00101 来表示(浅灰色代表 1,白色代表 0)。其中 1 是数据同步了,而 0 是数据没有同步。节点 2 可以从节点 1 和节点 3 获取完整的 8 条数据。
|
||||
|
||||
这种向量列表除了具有内存优势外,我们还可以很容易发现需要修复数据的目标。但是它的一个明显缺点与暗示切换队列 HHQ 类似,就是存储是有限的,如果数据偏差非常大,向量最终会溢出,从而不能比较数据间的差异。但不要紧,我们可以用上面提到的 Merkle 来进行全量比较。
|
||||
|
||||
以上我介绍了一些常见的反熵手段,它们都可以很好地解决数据一致性问题。但是我们会发现相对于传统的领导节点数据同步,它们同步数据的速度是不好度量的,而且会出现部分节点长期不进行同步的状态。那么有没有一种模式可以提高数据同步的效率呢?答案是肯定的,那就是 Gossip 协议。
|
||||
|
||||
Gossip 协议
|
||||
|
||||
Gossip 协议可以说是传播非常广泛的分布式协议。因为它的名字非常地形象,用幽默的东北话来说就是“传闲话”。大家可以想象一个东北乡村,屯头树下大家聚在一起“张家长李家短”。一件事只需一会儿整个村庄的人都全知道了。
|
||||
|
||||
Gossip 协议就是类似于这种情况。节点间主动地互相交换信息,最终达到将消息快速传播的目的。而该协议又是基于病毒传播模型设计的。2020 年是新冠疫情的灾年,大家都对病毒传播有了深刻理解,那么我现在就用病毒传播模型来解释 Gossip 协议的消息传播模式。
|
||||
|
||||
最开始,集群中一个节点产生了一条消息,它的状态为“已感染”。而其他节点我们认为是“易感节点”,这类似于新冠的易感人群。一旦该消息从已感染节点传播到易感节点,这个易感节点把自己的状态转换为已感染,而后接着进行传播。
|
||||
|
||||
这里,选择传播的目标使用一个随机函数,从而可以很好地将“病毒”扩展到整个集群中。当然,如果已感染节点不愿意传染其他节点,类似于它被隔离了起来,在其上的消息经过一段时间后会被移除。
|
||||
|
||||
我们可以看到 Gossip 模式非常适合于无主集群的数据同步,也就是不管集群中有多少节点参与,消息都可以很健壮地在集群内传播。当然,消息会重复传播到同一个节点上,在实现算法的时候,我们需要尽量减少这种重复数据。
|
||||
|
||||
另一个对算法成败重要的影响因素是消息用多快的速度在集群内传播,越快传播不仅会减少不一致的时间,同时可以保证消息不容易丢失。现在我通过几个特性来描述算法的行为。
|
||||
|
||||
|
||||
换出数量。它表示为节点选择多少个相邻节点来传播数据。我们很容易知道,当这个值增大后,数据就能更快地传播。但这个值增大同样会增加重复数据的比例,从而导致集群负载增加吞吐量下降。所以我们需要对重复数据进行监控,来实时调整换出数量。
|
||||
传播延迟。这种延迟与我们之前提到的复制延迟不同,它描述的是消息传播到集群中所有节点所需要的时间。它取决于换出数量和集群规模。在一个规模比较大的集群中,我们应该适当提高换出数量,而降低数据传播的延迟。
|
||||
传播停止阈值。当一个节点最近总是收到重复的数据,我们就应该考虑减弱甚至停止这个数据在集群中的传播了,这种过程被形象地称为“兴趣减弱”。我们一般需要计算每个节点重复的数量,并通过一个阈值来确定该数据是否需要停止传播。
|
||||
|
||||
|
||||
以上就是 Gossip 传播模式的一些特点,但是在实际生产中,我们不能完全用随机的模式构造传播网络,那样的话会造成网络信息过载。我们一般会采用一些网络优化的手段。
|
||||
|
||||
网络优化
|
||||
|
||||
我们刚才提到 Gossip 协议成功的关键之一是控制重复消息的数量,但同时一定程度的重复数量可以保障消息的可用性,从而使集群更加稳健。
|
||||
|
||||
一种平衡的方案是构造一个临时的稳定拓扑网络结构。节点可以通过检测发现与其网络相对稳定的节点,从而构建一个子网。子网之间再互相连接,从而构建一个单向传播且无环的树形拓扑结构。这就达到如存在主节点网络一般的传播结构,这种结构可以很好地控制重复的消息,且保证集群中所有节点都可以安全地接收数据。
|
||||
|
||||
但是这种结构存在明显的弱点,也就是连接子网之间的节点会成为潜在的瓶颈。一旦这类节点失败,那么子网就会变为信息孤岛,从而丧失 Gossip 算法所带来的稳健性特点。
|
||||
|
||||
那有没有一种算法能解决这种孤岛问题呢?我们可以使用混合模式来解决,也就是同时使用树结构与传统 Gossip 随机传播结构。当系统稳定运行时,使用树结构加快信息的传播速度,同时减小重复数据。一旦检测到失败,那么系统退化为 Gossip 模式进行大范围信息同步,来修复失败问题。
|
||||
|
||||
总结
|
||||
|
||||
最终一致性允许节点间状态存在不一致,那么反熵机制就是帮助最终一致性来修复这些不一致情况的。
|
||||
|
||||
我们既可以使用前台的读修复和暗示切换来快速修复最近产生的问题,也可以使用 Merkle 树和位图版本向量这种后台手段来修复全局的一致性问题。如果需要大规模且稳定地同步数据,那么 Gossip 协议将是你绝佳的选择。
|
||||
|
||||
至此我们可以说,所有针对分布式系统复制与一致性的问题都已经介绍完了。下一讲我们将进入分布式数据最核心的领域:分布式事务。希望准时与你相见,谢谢。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,129 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
18 分布式事务(上):除了 XA,还有哪些原子提交算法吗?
|
||||
这一讲我认为是整个课程最为精华的部分,因为事务是区别于数据库与一般存储系统最为重要的功能。而分布式数据库的事务由于其难度极高,一直被广泛关注。可以说,不解决事务问题,一个分布式数据库会被认为是残缺的。而事务的路线之争,也向我们展示了分布式数据库发展的不同路径。
|
||||
|
||||
提到分布式事务,能想到的第一个概念就是原子提交。原子提交描述了这样的一类算法,它们可以使一组操作看起来是原子化的,即要么全部成功要么全部失败,而且其中一些操作是远程操作。Open/X 组织提出 XA 分布式事务标准就是原子化提交的典型代表,XA 被主流数据库广泛地实现,相当长的一段时间内竟成了分布式事务的代名词。
|
||||
|
||||
但是随着 Percolator 的出现,基于快照隔离的原子提交算法进入大众的视野,在 TiDB 实现 Percolator 乐观事务后,此种方案逐步达到生产可用的状态。
|
||||
|
||||
这一讲我们首先要介绍传统的两阶段提交和三阶段提交,其中前者是 XA 的核心概念,后者针对两阶段提交暴露的问题进行了改进。最后介绍 Percolator 实现的乐观事务与 TiDB 对其的改进。
|
||||
|
||||
两阶段提交与三阶段提交
|
||||
|
||||
两阶段提交非常有名,其原因主要有两点:一个是历史很悠久;二是其定义是很模糊的,它首先不是一个协议,更不是一个规范,而仅仅是作为一个概念存在,故从传统的关系统数据库一致的最新的 DistributedSQL 中,我们都可以看到它的身影。
|
||||
|
||||
两阶段提交包含协调器与参与者两个角色。在第一个阶段,协调器将需要提交的数据发送给参与者,同时询问参与者是否能够提交该数据,而后参与者返回投票结果。在第二阶段,协调器根据参与者的投票结果,决定是提交还是取消这次事务,而后将结果发送给每个参与者,参与者根据结果来提交本地的事务。
|
||||
|
||||
可以看到两阶段提交的核心是协调器。它一般被实现为一个领导节点,你可以回忆一下领导选举那一讲。我们可以使用多种方案来选举领导节点,并根据故障检测机制来探测领导节点的健康状态,从而确定是否要重新选择一个领导节点作为协调器。另外一种常见的实现是由事务发起者来充当协调器,这样做的好处是协调工作被分散到多个节点上,从而降低了分布式事务的负载。
|
||||
|
||||
整个事务被分解为两个过程。
|
||||
|
||||
|
||||
准备阶段。协调器向所有参与节点发送 Propose 消息,该消息中包含了该事务的全部信息。而后所有参与节点收到该信息后,进行提交决策——是否可以提交该事务,如果决定提交该事务,它们就告诉协调器同意提交;否则,它们告诉协调器应该终止该事务。协调器和所有参与者分别保存该决定的结果,用于故障恢复。
|
||||
提交或终止。如果有任何一个参与者终止了该事务,那么所有参与者都会收到终止该事务的结果,即使他们自己认为是可以提交该事务的。而只有当所有参与者全票通过该事务时,协调器才会通知它们提交该事务。这就是原子提交的核心理念:全部成功或全部失败。
|
||||
|
||||
|
||||
我们可以看到两阶段提交是很容易理解的,但是其中却缺少大量细节。比如数据是在准备阶段还是在提交阶段写入数据库?每个数据库对该问题的实现是不同的,目前绝大多数实现是在准备阶段写入数据。
|
||||
|
||||
两阶段提交正常流程是很容易理解的,它有趣的地方是其异常流程。由于有两个角色和两个阶段,那么异常流程就分为 4 种。
|
||||
|
||||
|
||||
参与者在准备阶段失败。当协调者发起投票后,有一个参与者没有任何响应(超时)。协调者就会将这个事务标记为失败,这与该阶段投票终止该事务是同样的结果。这虽然保证了事务的一致性,但却降低了分布式事务整体的可用性。下一讲我会介绍 Spanner 使用 Paxos groups 来提高参与者的可用度。
|
||||
参与者在投票后失败。这种场景描述了参与者投赞成票后失败了,这个时候必须保证该节点是可以恢复的。在其恢复流程里,需要首先与协调器取得联系,确认该事务最终的结果。然后根据其结果,来取消或者提交该事务。
|
||||
协调器在投票后失败。这是第二个阶段,此时协调器和参与者都已经把投票结果记录下来了。如果协调器失败,我们可以将备用协调器启动,而后读取那个事务的投票结果,再向所有参与者发送取消或者提交该事务的消息。
|
||||
协调器在准备阶段失败。这是在第一阶段,该阶段存在一个两阶段提交的缺点。在该阶段,协调器发送消息没有收到投票结果,这里所说的没有收到结果主要指结果没有记录到日志里面。此时协调器失败了,那么备用协调器由于缺少投票结果的日志,是不能恢复该事务的。甚至其不知道有哪些参与者参与了这个事务,从而造成参与者无限等待。所以两阶段提交又称为阻塞提交算法。
|
||||
|
||||
|
||||
三阶段相比于两阶段主要是解决上述第 4 点中描述的阻塞状态。它的解决方案是在两阶段中间插入一个阶段,第一阶段还是进行投票,第二阶段将投票后的结果分发给所有参与者,第三阶段是提交操作。其关键点是在第二阶段,如果协调者在第二阶段之前崩溃无法恢复,参与者可以通过超时机制来释放该事务。一旦所有节点通过第二阶段,那么就意味着它们都知道了当前事务的状态,此时,不管协调者还是参与者崩溃都不会影响事务执行。
|
||||
|
||||
我们看到三阶段事务会存在两阶段不存在的一个问题,在第二阶段的时候,一些参与者与协调器失去联系,它们由于超时机制会中断事务。而如果另外一些参与者已经收到可以提交的指令,就会提交数据,从而造成脑裂的情况。
|
||||
|
||||
除了脑裂,三阶段还存在交互量巨大从而造成系统消息负载过大的问题。故三阶段提交很少应用在实际的分布式事务设计中。
|
||||
|
||||
两阶段与三阶段提交都是原子提交协议,它们可以实现各种级别的隔离性要求。在实际生产中,我们可以使用一种特别的事务隔离级别来提高分布式事务的性能,实现非阻塞事务。这种隔离级别就是快照隔离。
|
||||
|
||||
快照的隔离
|
||||
|
||||
我们在第 11 讲中提到过快照隔离。它的隔离级别高于“读到已提交”,解决的是读到已提交无法避免的读偏序问题,也就是一条数据在事务中被读取,重复读取后可能会改变。
|
||||
|
||||
我们举一个快照隔离的读取例子,有甲乙两个事务修改同一个数据 X,其初始值为 2。甲开启事务,但不提交也不回退。此时乙将该数值修改为 10,提交事务。而后甲重新读取 X,其值仍然为 2,并没有读取到已经提交的最新数据 。
|
||||
|
||||
那么并发提交同一条数据呢?由于没有锁的存在,会出现写入冲突,通常只有其中的一个事务可以提交数据。这种特性被称为首先提交获胜机制。
|
||||
|
||||
快照隔离与序列化之间的区别是前者不能解决写偏序的问题,也就是并发事务操作的数据集不相交,当事务提交后,不能保证数据集的结果一致性。举个例子,对于两个事务 T1:b=a+1 和 T2:a=b+1,初始化 a=b=0。序列化隔离级别下,结果只可能是 (a=2,b=1) 或者 (a=1,b=2);而在快照隔离级别下,结果可能是 (a=1,b=1)。这在某些业务场景下是不能接受的。当然,目前有许多手段来解决快照隔离的写偏序问题,即序列化的快照隔离(SSI)。
|
||||
|
||||
实现 SSI 的方式有很多种,如通过一个统一的事务管理器,在提交时去询问事务中读取的数据在提交时是否已经被别的事务的提交覆盖了,如果是,就认为当前事务应标记为失败。另一些是通过在数据行上加锁,来阻止其他事务读取该事务锁定的数据行,从而避免写偏序的产生。
|
||||
|
||||
下面要介绍的 Percolator 正是实现了快照隔离,但是没有实现 SSI。因为可以看到 SSI 不论哪种实现都会影响系统的吞吐量。且 Percolator 本身是一种客户端事务方案,不能很好地保存状态。
|
||||
|
||||
Percolator 乐观事务
|
||||
|
||||
Percolator 是 Google 提出的工具包,它是基于 BigTable 的,并支持刚才所说的快照隔离。快照隔离是有多版本的,那么我们就需要有版本号,Percolator 系统使用一个全局递增时间戳服务器,来为事务产生单调递增的时间戳。每个事务开始时拿一个时间戳 t1,那么这个事务执行过程中可以读 t1 之前的数据;提交时再取一下时间戳 t2,作为这个事务的提交时间戳。
|
||||
|
||||
现在我们开始介绍事务的执行过程。与两阶段提交一样,我们使用客户端作为协调者,BigTable 的 Tablet Server 作为参与者。 除了每个 Cell 的数据存在 BigTable 外,协调者还将 Cell 锁信息、事务版本号存在 BigTable 中。简单来说,如果需要写 bal 列(balance,也就是余额)。在 BigTable 中实际存在三列,分别为 bal:data、bal:lock、bal:write。它们保存的信息如下所示。
|
||||
|
||||
|
||||
bal:write 中存事务提交时间戳 commit_ts=>start_ts;
|
||||
bal:data 这个 map 中存事务开始时间戳 start_ts=> 实际列数据;
|
||||
bal:lock 存 start_ts=>(primary cell),Primary cell 是 Rowkey 和列名的组合,它在提交容错处理和事务冲突时使用,用来清理由于协调器失败导致的事务失败留下的锁信息。
|
||||
|
||||
|
||||
我们现在用一个例子来介绍一下整个过程,请看下图。
|
||||
|
||||
|
||||
|
||||
一个账户表中,Bob 有 10 美元,Joe 有 2 美元。我们可以看到 Bob 的记录在 write 字段中最新的数据是 data@5,它表示当前最新的数据是 ts=5 那个版本的数据,ts=5 版本中的数据是 10 美元,这样读操作就会读到这个 10 美元。同理,Joe 的账号是 2 美元。
|
||||
|
||||
|
||||
|
||||
现在我们要做一个转账操作,从 Bob 账户转 7 美元到 Joe 账户。这需要操作多行数据,这里是两行。首先需要加锁,Percolator 从要操作的行中随机选择一行作为 Primary Row,其余为 Secondary Row。对 Primary Row 加锁,成功后再对 Secondary Row 加锁。从上图我们看到,在 ts=7 的行 lock 列写入了一个锁:I am primary,该行的 write 列是空的,数据列值为 3(10-7=3)。 此时 ts=7 为 start_ts。
|
||||
|
||||
|
||||
|
||||
然后对 Joe 账户加锁,同样是 ts=7,在 Joe 账户的加锁信息中包含了指向 Primary lock 的引用,如此这般处于同一个事务的行就关联起来了。Joe 的数据列写入 9(2+7=9),write 列为空,至此完成 Prewrite 阶段。
|
||||
|
||||
|
||||
|
||||
接下来事务就要 Commit 了。Primary Row 首先执行 Commit,只要 Primary Row Commit 成功了,事务就成功了。Secondary Row 失败了也不要紧,后续会有补救措施。Commit 操作首先清除 Primary Row 的锁,然后写入 ts=8 的行(因为时间是单向递增的,这里是 commit_ts),该行可以称为 Commit Row,因为它不包含数据,只是在 write 列中写入 data@7,标识 ts=7 的数据已经可见了,此刻以后的读操作可以读到版本 ts=7 的数据了。
|
||||
|
||||
|
||||
|
||||
接下来就是 commit Secondary Row 了,和 Primary Row 的逻辑是一样的。Secondary Row 成功 commit,事务就完成了。
|
||||
|
||||
如果 Primary Row commit 成功,Secondary Row commit 失败会怎么样,数据的一致性如何保障?由于 Percolator 没有中心化的事务管理器组件,处理这种异常,只能在下次读操作发起时进行。如果一个读请求发现要读的数据存在 Secondary 锁,它会根据 Secondary Row 锁去检查其对应的 Primary Row 的锁是不是还存在,若存在说明事务还没有完成;若不存在则说明,Primary Row 已经 Commit 了,它会清除 Secondary Row 的锁,使该行数据变为可见状态(commit)。这是一个 Roll forward 的概念。
|
||||
|
||||
我们可以看到,在这样一个存储系统中,并非所有的行都是数据,还包含了一些事务控制行,或者称为 Commit Row。它的数据 Column 为空,但 write 列包含了可见数据的 TS。它的作用是标示事务完成,并指引读请求读到新的数据。随着时间的推移,会产生大量冗余的数据行,无用的数据行会被 GC 线程定时清理。
|
||||
|
||||
该事务另一个问题就是冲突处理。在之前介绍快照隔离时我们提到了对于同一行的冲突操作可以采用先提交获胜的模式,那么后提交的事务就会出现失败。如果数据库在出现高度并发修改相同数据的情况该怎么办呢?现在让我介绍一下根据 Percolator 模型实现乐观事务的 TiDB 是如何处理的。
|
||||
|
||||
TiDB 乐观事务冲突处理
|
||||
|
||||
首先在 TiDB 中写入冲突是在提交阶段进行检测的。在 11 讲中我们介绍了 MVCC 类数据库的冲突处理模式,分别为前项检测与后向检测。而 TiDB 由于使用 Percolator 模式,采用的是提交阶段的后向检测。这其实从原理上看是完全没有问题的,但 TiDB 声明自己完全兼容 MySQL。而众所周知,MySQL 使用的分布式事务是悲观模式。故在 SQL 执行阶段就能检测冲突,也就是前向模式。如此,就造成了用户如果从 MySQL 迁移到 TiDB,就必须好好审视其使用数据库是否依赖了此种模式,从而提高了用户的迁移成本。
|
||||
|
||||
基于以上的原因,TiDB 提供了以下几种方案来解决后向检测与前向检测的差异。
|
||||
|
||||
|
||||
重试。顾名思义,在遇到冲突时,TiDB 可以重试失败的事务中的非查询操作。这是非常简洁而高效的方案,但却不是万能的。如果事务中存在根据读取结果更新数据的情况,很可能造成数据异常。因为读取操作没有重试,从而破坏了“可重读”隔离级别。故重试只能应用在非读取的场景,特别是小事务中,即每个 SQL 是单独的事务。
|
||||
冲突预检。另一个思路是在 prewrite 阶段就执行冲突预检,将后向检查变为前向检查。TiDB 依赖的 TiKV 使用了内存来存储事务中的 key,从而检查 key 是否存在其他事务,避免并发修改 key 的情况。这样做的原因是,TiDB 本身是无状态阶段,从而导致事务之间无法感知彼此,故只能通过底层手段解决。这种结构是一种内存锁,如果事务过多,会造成获取锁的操作阻塞写入,从而导致吞吐量下降的情况。
|
||||
悲观事务。最后,为了完整实现 MySQL 的特性,还可以使用悲观事务。
|
||||
|
||||
|
||||
以上就是 TiDB 在实践 Percolator 模型时所给出的解决思路。从而使用户方便从 MySQL 迁移过来。另外随着 TiDB 此类数据库的面世,Percolator 事务模式也越来越得到业界的认可。
|
||||
|
||||
总结
|
||||
|
||||
好了,这一讲我们介绍了典型的原子提交:两阶段提交。它是 XA 的基础,但是两阶段提交存在天然的问题,且性能很低。在快照隔离下,我们可以使用 Percolator 模式描述的方案去实现新的原子提交,在冲突较低的场景下,该方案具有很好的性能。
|
||||
|
||||
下一讲,我们将介绍一对分布式事务方案的竞争对手 Spanner vs Calvin。感谢学习,希望下次与你准时相见。
|
||||
|
||||
00:00
|
||||
|
||||
24讲吃透分布式数据库
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,147 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
19 分布式事务(下):Spanner 与 Calvin 的巅峰对决
|
||||
上一讲我们介绍了分布式事务最重要的概念——原子提交,并介绍了两阶段、三阶段提交和 Percolator 模型。
|
||||
|
||||
而这一讲我将要为你揭示目前业界最著名的两种分布式事务模型,同时它们的作者和追随者之间的论战又为这两种模型增加了一定的传奇性,这一讲让我们来看看它们最终谁能胜出呢?
|
||||
|
||||
首先,让我介绍一下参战的两位“选手”,它们分别是 Spanner 和 Calvin。它们背后分别有广泛引用的论文,可以说都拥有比较深厚的理论基础。那么我们先从 Spanner 开始说起。
|
||||
|
||||
Spanner 及其追随者
|
||||
|
||||
Spanner 最早来自 Google 的一篇论文,并最终成为 Google Cloud 的一个服务。Spanner 简单来讲是一种两阶段提交的实现,你可以回忆一下,上一讲中我介绍了两阶段提交 4 种失败场景,其中有一种是参与者准备阶段无响应,从而造成事务的可用性下降。而 Spanner
|
||||
|
||||
利用共识算法保证了每个分片(Shard)都是高可用的,从而提高了整体事务的可用性。
|
||||
|
||||
Spanner 的整体架构很复杂,包含的内容非常多。但核心主要是两个部分,分别是 TrueTime 和 Paxos Group,而这场论战也是针对其中的一个部分展开的。
|
||||
|
||||
TrueTime
|
||||
|
||||
我在模块三“13 | 概要:分布式系统都要解决哪些问题”中介绍过,分布式系统获取时间有两种方式:物理时间与逻辑时间。而由于物理时间不靠谱,分布式系统大部分使用逻辑时间。逻辑时间往往由一个节点生成时间戳,虽然已经很高效,但是如果要构建全球系统,这种设计就捉襟见肘了。
|
||||
|
||||
而 TrueTime 是一种逻辑与物理时间的融合,是由原子钟结合 IDC 本地时间生成的。区别于传统的单一时间点,TrueTime 的返回值是一个时间范围,数据操作可能发生在这个范围之内,故范围内的数据状态是不确定的(uncertainty)。系统必须等待一段时间,从而获得确定的系统状态。这段时间通常是比较短暂的,且多个操作可以并行执行,通常不会影响整体的吞吐量。
|
||||
|
||||
事务过程
|
||||
|
||||
Spanner 提供了三种事务模式。
|
||||
|
||||
|
||||
读写事务:该事务是通过分布式锁实现的,并发性是最差的。且数据写入每个分片 Paxos Group 的主节点。
|
||||
只读事务:该事务是无锁的,可以在任意副本集上进行读取。但是,如果想读到最新的数据,需要从主节点上进行读取。主节点可以从 Paxos Group 中获取最新提交的时间节点。
|
||||
快照读:顾名思义,Spanner 实现了 MVCC 和快照隔离,故读取操作在整个事务内部是一致的。同时这也暗示了,Spanner 可以保存同一份数据的多个版本。
|
||||
|
||||
|
||||
了解了事务模型后,我们深入其内部,看看 Spanner 的核心组件都有哪些。下面是一张 Spanner 的架构图。
|
||||
|
||||
|
||||
|
||||
其中我们看到,每个 replica 保存了多个 tablet;同时这些 replica 组成了 Paxos Group。Paxos Group 选举出一个 leader 用来在多分片事务中与其他 Paxos Group 的 leader 进行协调(有关 Paxos 算法的细节我将在下一讲中介绍)。
|
||||
|
||||
写入操作必须通过 leader 来进行,而读取操作可以在任何一个同步完成的 replica 上进行。同时我们看到 leader 中有锁管理器,用来实现并发控制中提到的锁管理。事务管理器用来处理多分片分布式事务。当进行同步写入操作时,必须要获取锁,而快照读取操作是无锁操作。
|
||||
|
||||
我们可以看到,最复杂的操作就是多分片的写入操作。其过程就是由 leader 参与的两阶段提交。在准备阶段,提交的数据写入到协调器的 Paxos Group 中,这解决了如下两个问题。
|
||||
|
||||
|
||||
整个事务的数据是安全的。协调者崩溃不会影响到事务继续运行,我们可以从 Paxos Group 中恢复事务数据。
|
||||
参与者崩溃不会影响事务。因为 Paxos Group 可以重新选择节点来继续执行未完成的事务操作。
|
||||
|
||||
|
||||
在隔离方面,Spanner 实现了 SSI,也就是序列化的快照隔离。其方法就是上文提到的 lock table。该锁是完全的排他锁,不仅仅能阻止并发写入数据,写入也可以阻止读取,从而解决快照隔离写偏序的问题。
|
||||
|
||||
在整个过程中,事务开始时间和提交事务时间(数据可见时间)都是通过 TrueTime 获取的时间范围。Spanner 获取这些范围后,必须等待范围中描述的时间,而后才可以执行操作。否则,系统就会读取到不一致的数据。比如未能读取到当前时间之前的数据,或者读取到事务部分产生的数据等异常数据。
|
||||
|
||||
同时,Spanner 声明自己的事务特性是外部一致性(External Consistency)。其描述为首先并发的事务是序列化的,如上文所示,Spanner 实现了 SSI。同时它还是线性一致的,也就是“真实”时间下,事务 A 在事务 B 前提交,那么事务 A 的时间一定小于事务 B。对一致性部分掌握比较深的同学会发现,这就是我们在该部分提到的事务与一致性之间的联系。任何分布式数据库都要描述其事务特性(并发操作)与一致性特性(非并发操作),而 Spanner 所谓的外部一致就是序列化+线性一致。
|
||||
|
||||
Spanner 不仅仅有 Google Cloud 的一种商业产品可供大家选择,同样有众多开源数据库是源自 Spanner 的理念而设计的,如 CockroachDB、YugaByte DB 等。故Spanner 被认为是一类从开源到商业、本地部署到云端的成熟解决方案。
|
||||
|
||||
以上我讲解了 Spanner 的特性,下面接着看看它的对手 Calvin 的一些特点吧。
|
||||
|
||||
Calvin 与 FaunaDB
|
||||
|
||||
Spanner 引入了很多新技术去改善分布式事务的性能,但我们发现其流程整体还是传统的二阶段提交,并没有在结构上发生重大的改变,而 Calvin 却充满了颠覆性。让我们来看看它是怎么处理分布式事务的。
|
||||
|
||||
首先,传统分布式事务处理使用到了锁来保证并发竞争的事务满足隔离级别的约束。比如,序列化级别保证了事务是一个接一个运行的。而每个副本的执行顺序是无法预测的,但结果是可以预测的。Calvin 的方案是让事务在每个副本上的执行顺序达到一致,那么执行结果也肯定是一致的。这样做的好处是避免了众多事务之间的锁竞争,从而大大提高了高并发度事务的吞吐量。同时,节点崩溃不影响事务的执行。因为事务执行步骤已经分配,节点恢复后从失败处接着运行该事务即可,这种模式使分布式事务的可用性也大大提高。目前实现了 Calvin 事务模式的数据库是 FaunaDB。
|
||||
|
||||
其次,将事务进行排序的组件被称为 sequencer。它搜集事务信息,而后将它们拆解为较小的 epoch,这样做的目的是减小锁竞争,并提高并行度。一旦事务被准备好,sequencer 会将它们发送给 scheduler。scheduler 根据 sequencer 处理的结果,适时地并行执行部分事务步骤,同时也保证顺序执行的步骤不会被并行。因为这些步骤已经排好了顺序,scheduler 执行的时候不需要与 sequencer 进行交互,从而提高了执行效率。Calvin 事务的处理组件如下图所示。
|
||||
|
||||
|
||||
|
||||
Calvin 也使用了 Paxos 算法,不同于 Spanner 每个分片有一个 Paxos Group。Calvin 使用 Paxos 或者异步复制来决定哪个事务需要进入哪个 epoch 里面。
|
||||
|
||||
同时 Calvin 事务有 read set 和 write set 的概念。前者表示事务需要读取的数据,后者表示事务影响的数据。这两个集合需要在事务开始前就进行确定,故Calvin 不支持在事务中查询动态数据而后影响最终结果集的行为。这一点很重要,是这场战争的核心。
|
||||
|
||||
在你了解了两种事务模型之后,我就要带你进入“刺激战场”了。在两位实力相当的选手中,Calvin 一派首先挑起了战争。
|
||||
|
||||
对 Spanner 的批评
|
||||
|
||||
来自马里兰大学的 Daniel Abadi 教授是 Calvin 论文的联合作者、FaunaDB 的咨询师,可以说他非常有资格代表 Calvin 一派向 Spanner 发起挑战。
|
||||
|
||||
一开始 Abadi 教授主要探讨了 Spanner 和 Calvin 之间的架构带来的性能差异,他从如下几个方面给出了比较。
|
||||
|
||||
|
||||
传统读写事务:如果是对于分片内部的事务(非分布式场景),两者的性能是类似的;但是对于跨分片,他认为 Calvin 的性能要远好于 Spanner。原因是 Spanner 相对来说有两点性能损耗,第一就是 TrueTime 返回的是时间范围,我们必须等待一段时间后才可以做提交操作,当然这部分是可以并行的;第二就是 Spanner 是两阶段提交,相比于 Calvin 的“一阶段”来讲,理论上延迟会高。
|
||||
快照读:这部分两者原理类似,故延迟都不高。
|
||||
只读事务:这部分就是 Spanner 要更高效。因为它只从 leader 节点去读取数据,而 Calvin 做全局的一致性读,故延迟更大。
|
||||
|
||||
|
||||
除了以上的比较,Calvin 还在日志复制上存在优势。主要是 Spanner 的日志复制也是 Paxos 过程,而 Calvin 由于预处理加持,可以简单高效地进行复制。这种优势在理论上随着节点间物理距离的扩展而变得更加明显。
|
||||
|
||||
当然,我们知道 Calvin 提到了它的预处理机制会限制事务内的操作,这个限制 Abadi 教授也注意到了。
|
||||
|
||||
以上就是 Abadi 教授在两者性能方面的比较,其论调还是比较客观中立,且冲突性不强。但紧接着,他指出了 Spanner 一个非常具有争议的问题,这个问题关系到了 TrueTime。TrueTime 由于不是在理论层面上证明它的时间不会倒流(skew),而是通过大量的工程实践证明了这种可能性非常低。而这个概率就是一个攻击点。
|
||||
|
||||
教授在这里比较聪明,或可以说是明智。他没有攻击 TrueTime 本身,而是表明 TrueTime 由于依赖原子钟这种硬件,提高了其他人复制该技术的难度。从而引出了一个技术圈的老话题——Google 的技术出了 Google 就失效了。
|
||||
|
||||
而 Abadi 要挑战的就是基于 Spanner 想法的其他开源或商业数据库,如上文提到的 CockroachDB 和 YugaByteDB。它们的 TrueTime 是用软件实现的,相比于硬件,上文描述的时间倒流概率被提高了。CockroachDB 还好,它声明了这种异常的可能;而 YugaByte 却没有,故它被教授集中火力攻击。
|
||||
|
||||
最后教授提到了,Calvin 和 FaunaDB 在理论层面上证明了其可以很好地实现一致性。
|
||||
|
||||
既然 Calvin 引战,特别是主要集中在 YugaByteDB 上,于是后者发起了绝地反击。
|
||||
|
||||
Spanner 追随者的反击
|
||||
|
||||
既然 YugaByte“祸从天上来”,那么必然由它们发起反击。
|
||||
|
||||
上文中,教授的观点总结为:
|
||||
|
||||
|
||||
性能上,Calvin 由于锁持有时间短,吞吐量会大于 Spanner;
|
||||
一致性上,基于硬件的 TrueTime 具有一定概率会发生时间倒流,而软件实现的“TrueTime”更是无法保证时间单调递增。
|
||||
|
||||
|
||||
针对第一个问题,YugaByte 首先承认了 Calvin 吞吐量的优势。但是画风一转,YugaByte 抛出了著名的分布式事务模式研究,该研究通过多 AWS Dynamo 用户使用事务的模式进行分析。得出的结论是:90%的事务是发生在单行和单分片的,只有 10%左右才是多分片的。据此,YugaByte 把前者称为主要负载,后者称为次要负载。
|
||||
|
||||
那么在主要负载方面,上文中教授也承认 Spanner 和 Calvin 性能间没有明显差别,而 Calvin 具有优势的场景变为了次要负载。我们都听说过,“脱离剂量谈毒性都是耍流氓”。而 Calvin 的优势却在次要负载上,这大大降低了该优势的重要程度。
|
||||
|
||||
而第二个问题其实才是核心问题。我很欣赏此处 YugaByte 没有回避,而是大方地承认 YugaByte 等软件实现 TrueTime 的模式无法做到如 Calvin 那种严格序列化,而是所谓“最大可能”序列化。一旦 TrueTime 时间范围超过了阈值,序列化就被破坏了。但是 YugaByte 指出了两点让用户去思考:
|
||||
|
||||
|
||||
上文中主要负载场景两者都不会有一致性问题,只有在次要场景 Spanner 类方案才会有问题;
|
||||
随着 AWS、阿里云等公有云服务逐步提供原子钟服务,YugaByte 这类数据库也可以使用真正的 TrueTime,这大大降低了发生时间倒流的概率。
|
||||
|
||||
|
||||
从以上的解释看出,软件的 NTP 计时器确实存在问题,但如果用户场景对此要求不严格,也是可以使用的。
|
||||
|
||||
除了上面针对教授提到的问题,YugaByte 也提出了 Calvin 类数据库的一些较为“致命”的缺陷。
|
||||
|
||||
|
||||
上文教授已经承认的读性能 Calvin 是要弱于 Spanner 的。
|
||||
静态化的 write set 和 read set 导致了二级索引和会话内事务的问题。会话内事务我们上文提到过,简单说 Calvin 的事务的写入不能依赖于事务内的读取;而二级索引的列如果频繁修改,会导致 Calvin 的事务反复重试,从而降低吞吐量。
|
||||
Calvin 另一个缺憾就是其缺乏开源的实现。目前只有 FaunaDB 这个闭源商业版本,使得习惯使用开源技术栈的用户没有别的选择。
|
||||
FaunaDB 没有使用 SQL,而是使用了一个 GraphQL 风格的新语言 FQL。这为原本使用 SQL 语言的团队切换到 FaunaDB 上带来了很大挑战。
|
||||
|
||||
|
||||
可以看到 YugaByte 团队针对其批评也给出了自己的回应,那么他们之间的争论有确定的结果吗?
|
||||
|
||||
谁胜利了?
|
||||
|
||||
从目前发展的角度来说,并没有一方可以完全替代另一方。Calvin 在高度竞争的事务场景中有明显优势,而 Spanner 在读取、会话内事务中的优势不可代替。从它们的原理看,谁最终也无法胜出。而我们其实也不期待一个最终赢家,而是希望未来的事务模型能够从这两个模式中吸取灵感,为我们带来更高效的分布式事务解决方案 。
|
||||
|
||||
到此,我们用了两讲的内容,详细介绍了面向数据库的分布式事务。下一讲要说的是模块三的最后一个知识点:共识算法。它是现代分布式系统的核心算法,希望到时和你准时相见。
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,149 @@
|
||||
|
||||
|
||||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||||
|
||||
|
||||
20 共识算法:一次性说清楚 Paxos、Raft 等算法的区别
|
||||
现在,我们进入了分布式系统的最后一讲:共识算法。前面我们学习了各种分布式的技术,你可以和我一起回忆一下,其中我们讨论了失败模型、失败检测、领导选举和一致性模型。虽然这些技术可以被单独使用,但我们还是希望用一个技术栈就能实现上述全部功能,如果这样,将会是非常美妙的。于是,整个分布式数据库,乃至分布式领域的研究人员经过多年的努力,终于在这个问题上有所突破——共识算法由此诞生。
|
||||
|
||||
虽然共识算法是分布式系统理论的精华,但是通过之前的学习,其实你已经知道共识算法包含的内容了。它首先是要解决分布式系统比较棘手的失败问题,通过内置的失败检测机制可以发现失败节点、领导选举机制保证数据高效处理、一致性模式保证了消息的一致性。
|
||||
|
||||
这一讲,我会为你介绍几种常用的共识算法的特色。我不会深入到每种算法的详细执行过程,因为这些过程抽象且对使用没有特别的帮助。这一讲我的目的是从更高的维度为你解释这些算法,希望给你形象的记忆,并帮助你能够学以致用。至于算法实现细节,感兴趣的话你可以自行学习。
|
||||
|
||||
在介绍共识协议之前,我们要来聊聊它的三个属性。
|
||||
|
||||
|
||||
正确性(Validity):诚实节点最终达成共识的值必须是来自诚实节点提议的值。
|
||||
一致性(Agreement):所有的诚实节点都必须就相同的值达成共识。
|
||||
终止性(Termination):诚实的节点必须最终就某个值达成共识。
|
||||
|
||||
|
||||
你会发现共识算法中需要有“诚实”节点,它的概念是节点不能产生失败模型所描述的“任意失败”,或是“拜占庭失败”。因为数据库节点一般会满足这种假设,所以我们下面讨论的算法可以认为所有节点都是诚实的。
|
||||
|
||||
以上属性可以换个说法,实际上就是“15 | 领导选举:如何在分布式系统内安全地协调操作”介绍的安全性(Safety)和活跃性(Liveness),其中正确性(Validity)和一致性(Agreement)决定了安全性(Safety),而终止性(Termination)就是活跃性(Liveness)。让我们复习一下这两个特性。
|
||||
|
||||
|
||||
安全性(Safety):在故障发生时,共识系统不能产生错误的结果。
|
||||
活跃性(Liveness):系统能持续产生提交,也就是不会永远处于一个中间状态无法继续。
|
||||
|
||||
|
||||
基于以上的特性,我们开始聊聊目前常见的共识算法。
|
||||
|
||||
原子广播与 ZAB
|
||||
|
||||
广播协议是一类将数据从一个节点同步到多个节点的协议。我在“17 | 数据可靠传播:反熵理论如何帮助数据库可靠工作”介绍过最终一致性系统通过各种反熵手段来保证数据的一致性传播,特别是其中的 Gossip 协议可以保障大规模的数据同步,而 Gossip 在正常情况下就是采用广播模式传播数据的。
|
||||
|
||||
以上的广播过程产生了一个问题,那就是这个协调节点是明显的单点,它的可靠性至关重要。要保障其可靠,首先要解决的问题是需要检查这个节点的健康状态。我们可以通过各种健康检查方式去发现其健康情况。
|
||||
|
||||
如果它失败了,会造成消息传播到一部分节点中,而另外一部分节点却没有这一份消息,这就违背了“一致性”。那么应该怎解决这个问题呢?
|
||||
|
||||
一个简单的算法就是使用“漫灌”机制,这种机制是一旦一个消息被广播到一个节点,该节点就有义务把该消息广播到其他未收到数据节点的义务。这就像水田灌溉一样,最终整个系统都收到了这份数据。
|
||||
|
||||
当然以上的模式有个明显的缺点,就是会产生N2的消息。其中 N 是目前系统剩下的未同步消息的节点,所以我们的一个优化目标就是要减少消息的总数量。
|
||||
|
||||
虽然广播可以可靠传递数据,但通过一致性的学习我们知道:需要保证各个节点接收到消息的顺序,才能实现较为严格的一致性。所以我们这里定义一个原子广播协议来满足。
|
||||
|
||||
|
||||
原子性:所有参与节点都收到并传播该消息;或相反,都不传播该消息。
|
||||
顺序性:所有参与节点传播消息的顺序都是一致的。
|
||||
|
||||
|
||||
满足以上条件的协议我们称为原子广播协议,现在让我来介绍最为常见的原子广播协议:Zookeeper Atomic Broadcast(ZAB)。
|
||||
|
||||
ZAB
|
||||
|
||||
ZAB 协议由于 Zookeeper 的广泛使用变得非常流行。它是一种原子广播协议,可以保证消息顺序的传递,且消息广播时的原子性保障了消息的一致性。
|
||||
|
||||
ZAB 协议中,节点的角色有两种。
|
||||
|
||||
|
||||
领导节点。领导是一个临时角色,它是有任期的。这么做的目的是保证领导角色的活性。领导节点控制着算法执行的过程,广播消息并保证消息是按顺序传播的。读写操作都要经过它,从而保证操作的都是最新的数据。如果一个客户端连接的不是领导节点,它发送的消息也会转发到领导节点中。
|
||||
跟随节点。主要作用是接受领导发送的消息,并检测领导的健康状态。
|
||||
|
||||
|
||||
既然需要有领导节点产生,我们就需要领导选举算法。这里我们要明确两个 ID:数据 ID 与节点 ID。前者可以看作消息的时间戳,后者是节点的优先级。选举的原则是:在同一任职周期内,节点的数据 ID 越大,表示该节点的数据越新,数据 ID 最大的节点优先被投票。所有节点的数据 ID 都相同,则节点 ID 最大的节点优先被投票。当一个节点的得票数超过节点半数,则该节点成为主节点。
|
||||
|
||||
一旦领导节点选举出来,它就需要做两件事。
|
||||
|
||||
|
||||
声明任期。领导节点通知所有的跟随节点当前的最新任期;而后由跟随节点确认当前任期是最新的任期,从而同步所有节点的状态。通过该过程,老任期的消息就不会被跟随节点所接受了。
|
||||
同步状态。这一步很关键,首先领导节点会通知所有跟随节点自己的领导身份,而后跟随节点不会再选举自己为领导了;然后领导节点会同步集群内的消息历史,保证最新的消息在所有节点中同步。因为新选举的领导节点很可能并没有最新被接受的数据,因此同步历史数据操作是很有必要的。
|
||||
|
||||
|
||||
经过以上的初始化动作后,领导节点就可以正常接受消息,进行消息排序而后广播消息了。在广播消息的时候,需要 Quorum(集群中大多数的节点)的节点返回已经接受的消息才认为消息被正确广播了。同时为了保证顺序,需要前一个消息正常广播,后一个消息才能进行广播。
|
||||
|
||||
领导节点与跟随节点使用心跳算法检测彼此的健康情况。如果领导节点发现自己与 Quorum 节点们失去联系,比如网络分区,此时领导节点会主动下台,开始新一轮选举。同理,当跟随节点检测到领导节点延迟过大,也会触发新一轮选举。
|
||||
|
||||
ZAB 选举的优势是,如果领导节点一直健康,即使当前任期过期,选举后原领导节点还会承担领导角色,而不会触发领导节点切换,这保证了该算法的稳定。另外,它的节点恢复比较高效,通过比较各个节点的消息 ID,找到最大的消息 ID,就可以从上面恢复最新的数据了。最后,它的消息广播可以理解为没有投票过程的两阶段提交,只需要两轮消息就可以将消息广播出去。
|
||||
|
||||
那么原子广播协议与本讲重点介绍的共识算法是什么关系呢?这里我先留下一个“暗扣”,先介绍一下典型的共识算法 Paxos,而后再说明它们之间的关系。
|
||||
|
||||
Paxos
|
||||
|
||||
所谓的 Paxos 算法,是为了解决来自客户端的值被发送到集群中的任意一点,而后集群中的所有节点为该值达成共识的一种协调算法。同时这个值伴随一个版本号,可以保证消息是有顺序的,该顺序在集群中任何一点都是一致的。
|
||||
|
||||
基本的 Paxos 算法非常简单,它由三个角色组成。
|
||||
|
||||
|
||||
Proposer:Proposer 可以有多个,Proposer 提出议案(value)。所谓 value,可以是任何操作,比如“设置某个变量的值为 value”。不同的 Proposer 可以提出不同的 value。但对同一轮 Paxos 过程,最多只有一个 value 被批准。
|
||||
Acceptor:Acceptor 有 N 个,Proposer 提出的 value 必须获得 Quorum 的 Acceptor 批准后才能通过。Acceptor 之间完全对等独立。
|
||||
Learner:上面提到只要 Quorum 的 Accpetor 通过即可获得通过,那么 Learner 角色的目的就是把通过的确定性取值同步给其他未确定的 Acceptor。
|
||||
|
||||
|
||||
这三个角色其实已经描述了一个值被提交的整个过程。其实基本的 Paxos 只是理论模型,因为在真实场景下,我们需要处理许多连续的值,并且这些值都是并发的。如果完全执行上面描述的过程,那性能消耗是任何生产系统都无法承受的,因此我们一般使用的是 Multi-Paxos。
|
||||
|
||||
Multi-Paxos 可以并发执行多个 Paxos 协议,它优化的重点是把 Propose 阶段进行了合并,这就引入了一个 Leader 的角色,也就是领导节点。而后读写全部由 Leader 处理,同时这里与 ZAB 类似,Leader 也有任期的概念,Leader 与其他节点之间也用心跳进行互相探活。是不是感觉有那个味道了?后面我就会比较两者的异同。
|
||||
|
||||
另外 Multi-Paxos 引入了两个重要的概念:replicated log 和 state snapshot。
|
||||
|
||||
|
||||
replicated log:值被提交后写入到日志中。这种日志结构除了提供持久化存储外,更重要的是保证了消息保存的顺序性。而 Paxos 算法的目标是保证每个节点该日志内容的强一致性。
|
||||
state snapshot:由于日志结构保存了所有值,随着时间推移,日志会越来越大。故算法实现了一种状态快照,可以保存最新的日志消息。当快照生成后,我们就可以安全删除快照之前的日志了。
|
||||
|
||||
|
||||
熟悉 Raft 的同学会发现,上面的结构其实已经与 Raft 很接近了。在讨论完原子广播与共识之后 ,我们会接着介绍 Raft。
|
||||
|
||||
原子广播与共识
|
||||
|
||||
就像我开篇所说的,本讲不是介绍算法细节的,而是重点关注它们为什么是今天这个样子。从上面的粗略介绍中,我们已经发现:ZAB 其实与 Multi-Paxos 是非常类似的。本质上,它们都需要大部分节点“同意”一个值,并都有 Leader 节点,且 Leader 都是临时的。真是越说越相似,但本质上它们却又是不同的。
|
||||
|
||||
简单来说,ZAB 来源于主备复制场景,就是我们之前介绍的复制技术;而共识算法是状态机复制系统。
|
||||
|
||||
所谓状态机复制系统,是指集群中每个节点都是一个状态机,如果有一组客户端并发在系统中的不同状态机上提交不同的值,该系统保证每个状态机都可以保证执行相同顺序的客户端请求。可以看到请求一旦被提交,其顺序是有保障的。但是未提交之前,顺序是由 Leader 决定的,且这个顺序可以是任意的。一旦 Leader 被重选,新的 Leader 可以任意排序未提交的值。
|
||||
|
||||
而 ZAB 这种广播协议来自主备复制,强调的是消息的顺序是 Leader 产生的,并被 Follower 严格执行,其中没有协调的关系。更重要的区别是,Leader 重选后,新 Leader 依然会按照原 Leader 的排序来广播数据,而不会自己去排序。
|
||||
|
||||
因此可以说 ZAB 可以实现严格的线性一致性。而 Multi-Paxos 由于只是并发写,所以也没有所谓的线性一致,而是一种顺序一致结构,也就是数据被提交时才能确定顺序。而不是如 ZAB 那样有 Leader 首先分配了顺序,该顺序与数据提交的先后顺序保持了一致。关于线性一致和顺序一致,请参考“05 | 一致性与 CAP 模型:为什么需要分布式一致性?”
|
||||
|
||||
由于共识算法如 Paxos 为了效率的原因引入了 Leader。在正常情况下,两者差异不是很大,而差异主要在选举 Leader 的流程上。
|
||||
|
||||
那么学习完 ZAB 和 Multi-Paxos 后,我将要介绍这一讲的主角 Raft 算法,它是目前分布式数据库领域最重要的算法。
|
||||
|
||||
Raft 的特色
|
||||
|
||||
Raft 可以看成是 Multi-Paxos 的改进算法,因为其作者曾在斯坦福大学做过关于 Raft 与 Multi-Paxos 的比较演讲,因此我们可以将它们看作一类算法。
|
||||
|
||||
Raft 算法可以说是目前最成功的分布式共识算法,包括 TiDB、FaunaDB、Redis 等都使用了这种技术。原因是 Multi-Paxos 没有具体的实现细节,虽然它给了开发者想象空间,但共识算法一般居于核心位置,一旦存在潜在问题必然带给系统灾难性的后果。而 Raft 算法给出了大量的实现细节,且处理方式相比于 Multi-Paxos 有两点优势。
|
||||
|
||||
|
||||
发送的请求的是连续的,也就是说 Raft 的写日志操作必须是连续的;而 Multi-Paxos 可以并发修改日志,这也体现了“Multi”的特点。
|
||||
选主必须是最新、最全的日志节点才可以当选,这一点与 ZAB 算法有相同的原则;而 Multi-Paxo 是随机的。因此 Raft 可以看成是简化版本的 Multi-Paxos,正是这个简化,造就了 Raft 的流行。
|
||||
|
||||
|
||||
Multi-Paxos 随机性使得没有一个节点有完整的最新的数据,因此其恢复流程非常复杂,需要同步节点间的历史记录;而 Raft 可以很容易地找到最新节点,从而加快恢复速度。当然乱序提交和日志的不连续也有好处,那就是写入并发性能会大大提高,从而提高吞吐量。所以这两个特性并不是缺点,而是权衡利弊的结果。当然 TiKV 在使用 Raft 的时候采用了多 RaftGroup 的模式,提高了单 Raft 结构的并发度,这可以被看作是向 Multi-Paxos 的一种借鉴。
|
||||
|
||||
同时 Raft 和 Multi-Paxos 都使用了任期形式的 Leader。好处是性能很高,缺点是在切主的时候会拒绝服务,造成可用性下降。因此一般我们认为共识服务是 CP 类服务(CAP 理论)。但是有些团队为了提高可用性 ,转而采用基础的 Paxos 算法,比如微信的 PaxosStore 都是用了每轮一个单独的 Paxos 这种策略。
|
||||
|
||||
以上两点改进使 Raft 更好地落地,可以说目前最新数据库几乎都在使用该算法。想了解算法更多细节,请参考https://raft.github.io/。你从中不仅能学习到算法细节,更重要的是可以看到很多已经完成的实现,结合代码学习能为你带来更深刻的印象。
|
||||
|
||||
总结
|
||||
|
||||
共识算法是一个比较大的话题。本讲聚焦于常见的三种共识类算法,集中展示其最核心的功能。我通过比较它们之间的异同,来加深你对它们特性的记忆。
|
||||
|
||||
共识算法又是现代分布式数据库的核心组件,好在其 API 较为易懂,且目前有比较成熟的实现,所以我认为算法细节并不是本讲的重点。理解它们为什么如此,才能帮助我们理解数据库的选择依据。
|
||||
|
||||
到此,我们学习完了这个模块的所有知识点。下一讲我将会带领你复习这一模块的内容,同时通过几个案例来展示典型分布式数据库特性与咱们所学的知识点之间的关系,到时候见。
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user