分布式 DBMS 将单个逻辑数据库划分为多个物理资源。应用程序(通常)并不知道数据被分割在不同的硬件上。系统依靠单节点 DBMS 的技术和算法来支持分布式环境中的事务处理和查询执行。设计分布式 DBMS 的一个重要目标是容错(即避免单个节点故障导致整个系统瘫痪)。
DBMS 的系统架构规定了 CPU 可直接访问的共享资源。它影响 CPU 之间的相互协调,以及 CPU 在数据库中检索和存储对象的位置。
单节点 DBMS 使用所谓的shared everything架构。单节点在本地 CPU 上执行worker,并拥有自己的本地内存地址空间和磁盘。
分布式 DBMS 的目标是保持数据的透明度,在单节点 DBMS 上运行的 SQL 查询在分布式 DBMS 上也能运行。
同构节点:集群中的每个节点都能执行同一套任务(尽管可能是不同的数据分区),因此非常适合shared nothing。这使得配置和故障切换变得 “更容易”。失败的任务会分配给可用的节点。
异构节点:节点被分配特定的任务,因此节点之间必须进行通信才能执行特定的任务。这样,单个物理节点就可以承载多个 "虚拟 "节点类型,用于执行专用任务,并可从一个节点独立扩展到其他节点。MongoDB 就是一个例子,它有路由器节点将查询路由到分片,有配置服务器节点存储从键到分片的映射。
分布式系统必须在多个资源(包括磁盘、节点、处理器)上对数据库进行分区。 在 NoSQL 系统中,这一过程有时被称为分片。DBMS 收到查询后,首先会分析查询计划需要访问的数据。DBMS 可能会将查询计划的片段发送到不同的节点,然后将结果合并,生成一个单一的答案。
分区方案的目标是最大限度地增加单节点事务,或只访问一个分区中包含的数据的事务。这样,数据库管理系统就无需协调其他节点上运行的并发事务的行为。另一方面,分布式事务会访问一个或多个分区的数据。这就需要昂贵而困难的协调工作。
对于逻辑分区节点,特定节点负责访问共享磁盘中的特定tuple。
对于物理分区节点,每个shared nothing节点读取和更新自己本地磁盘上的数据元组。
对表进行分区的最简单方法是 naive data partitioning。假设给定节点有足够的存储空间,则每个节点存储一个表。这种方法很容易实现,因为查询只需路由到特定的分区。但这并不好,因为它不具备可扩展性。如果经常查询一个表,而不是使用所有可用节点,那么一个分区的资源就会耗尽。
另一种分区方式是 vertical partitioning 垂直分区,它将表的属性分割成不同的分区。 每个分区还必须存储元组信息,以便重建原始记录。
更常见的是 horizontal partitioning 水平分区,它将表中的元组分割成互不相关的子集。选择在大小、负载或使用方面等分数据库的列,记为分区键。
DBMS 可以根据散列、数据范围或谓词对数据库进行物理分区(shared nothing)或逻辑分区(共享磁盘)。散列分区的问题在于,当节点被添加或删除时,大量数据必须被洗牌。解决这个问题的方法就是一致性哈希。
一致性哈希将每个节点分配到某个逻辑环上的某个位置。然后,每个分区key的散列映射到环上的一个位置。顺时针方向上最靠近的节点负责该键。当节点添加或删除时,密钥只会在与新节点/删除节点相邻的节点之间移动,因此只有 1/n 部分的键会被移动。复制因子为 k 意味着每个键在顺时针方向最近的 k 个节点上被复制。
逻辑分区:节点负责一组键,但实际上并不存储这些键。这通常用于共享磁盘架构。
物理分区:节点负责一组键,并实际存储这些密钥。这通常用于无共享架构。
分布式事务访问一个或多个分区的数据,这就需要昂贵的协调工作。
如果一个事务访问多个节点上的数据,那么它就是 “分布式” 事务。执行这些事务比单节点事务更具挑战性,因为现在事务提交时,DBMS 必须确保所有节点都同意提交事务。DBMS 要确保数据库提供与单节点 DBMS 相同的 ACID 保证。
我们可以假设,分布式 DBMS 中的所有节点都很乖巧,并处于同一管理域之下。换句话说,如果没有节点故障,被告知要提交事务的节点就会提交事务。如果分布式 DBMS 中的其他节点不可信,那么 DBMS 就需要为事务使用容错协议(如区块链)。
当多节点事务结束时,DBMS 需要询问所有相关节点是否可以安全提交。根据协议的不同,可能需要大多数节点或所有节点提交。如两(三)阶段提交、raft、paxos、ZAB等。
如果协调器在发送准备信息后发生故障,两阶段提交(2PC)就会阻塞,直到协调器恢复。另一方面,如果大多数参与者都活着,只要有足够长的时间不再发生故障,Paxos 就不会阻塞。如果节点在同一个数据中心,不经常发生故障,也没有恶意,那么 2PC 通常比 Paxos 更受青睐,因为 2PC 通常会减少往返次数。
DBMS 可以在冗余节点上复制数据,以提高可用性。换句话说,如果某个节点宕机,数据不会丢失,而且系统仍然存活,无需重启。我们可以使用 Paxos 来决定向哪个副本写入数据。
在主副本中,每个对象的所有更新都会发送到指定的主副本。主服务器在不使用原子提交协议的情况下将更新传播到其副本,协调所有更新。如果不需要最新信息,可以允许只读事务访问副本。如果主服务器宕机,则会进行选举,选出新的主服务器。
在多副本中,事务可以在任何副本中更新数据对象。副本之间必须使用原子提交协议(如 Paxos 或 2PC。
K-safety 是确定复制数据库容错性的阈值。K 值表示每个数据对象必须始终可用的副本数量。如果副本数量低于这个阈值,DBMS 就会停止执行并下线。K 值越大,丢失数据的风险就越小。它是确定系统可用程度的阈值。
当事务在复制数据库中提交时,DBMS 会决定是否必须等待该事务的更改传播到其他节点,然后才能向应用程序客户端发送确认。有两种传播级别:同步(强一致性)和异步(最终一致性):
传播定时:
向副本应用更改:
CAP 定理解释了分布式系统不可能始终保持一致性、可用性和分区容忍性(Consistent, Available, and Partition Tolerant)。这三个属性中只能选择两个。
一致性:一旦写入完成,所有未来的读取都应返回该写入应用或后续写入应用的值。此外,一旦读取返回,以后的读取都应返回该值或以后应用的写入值。NoSQL 系统在这一属性上有所妥协,更倾向于后两个属性。其他系统则偏向于这一属性和后两者之一。
可用性:所有正常运行的节点都能满足所有请求。
分区容错:尽管在试图就数值达成共识的节点之间会有一些信息丢失,但系统仍能正常运行。如果系统选择了一致性和分区容错,那么在大多数节点重新连接之前,将不允许进行更新。
现代版本考虑了一致性与延迟的权衡:PACELC 定理。如果分布式系统中存在网络分区 §,则必须在可用性 (A) 和一致性 ? 之间做出选择,否则 (E),即使系统在没有网络分区的情况下正常运行,也必须在延迟 (L) 和一致性 ? 之间做出选择。
对于只读 OLAP 数据库来说,通常会有一个分支环境,即有多个 OLTP 数据库实例从外部世界获取信息,然后将这些信息输入后端 OLAP 数据库(有时称为数据仓库)。中间的步骤称为 ETL,即Extract、Transform和Load,它是将 OLTP 数据库合并为数据仓库的通用模式。
决策支持系统(Decision support systems,DSS)是服务于组织的管理、运营和规划层面的应用程序,通过分析存储在数据仓库中的历史数据,帮助人们就未来的问题做出决策。
建立分析数据库模型的两种方法是星形模式和雪花模式:
分布式 DBMS 的执行模型规定了查询执行期间节点之间的通信方式。执行查询的两种方法是 pushing 和 pulling。
将查询推送到数据:DBMS 将查询(或部分查询)发送到包含数据的节点,然后在通过网络传输之前,尽可能在数据所在的位置进行过滤和处理。然后将结果发回正在执行查询的节点,该节点使用本地数据和发送给它的数据完成查询。这种情况在无共享系统中更为常见。
为查询调用数据:DBMS 会将数据调用到正在执行查询的节点,以处理需要的数据。换句话说,节点会检测它们可以对哪些数据分区进行计算,并相应地从存储中提取数据。然后,本地操作被传播到一个节点,由该节点对所有中间结果进行操作。这通常是共享磁盘系统的做法。这样做的问题是,相对于查询的大小,数据的大小可能会有很大差异。也可以发送过滤器,以便只从磁盘检索所需的数据。
节点从远程来源接收的数据会缓存在缓冲池中。这样,DBMS 就能支持大于可用内存量的中间结果。但是,短暂页面在重启后不会被持久化。
大多数无共享分布式 OLAP DBMS 在设计时都假定节点在查询执行过程中不会发生故障。如果一个节点在查询执行过程中发生故障,那么整个查询就会失败。DBMS 可以在执行过程中对查询的中间结果进行快照,以便在节点发生故障时恢复。不过,这种操作的成本很高,因为将数据写入磁盘的速度很慢。
之前谈到的所有优化方法仍然适用于分布式环境,包括 predicate pushdown、early projections、optimal join orderings。分布式查询优化更加困难,因为它必须考虑数据在集群中的物理位置和数据移动成本。一种方法是生成一个单一的全局查询计划,然后将物理算子分发到节点,将其分解为特定于分区的片段。大多数系统都采用这种方法。
另一种方法是使用 SQL 查询,并将原始查询重写为特定分区查询。 这样就可以在每个节点上进行局部优化。SingleStore 和 Vitess 就是使用这种方法的系统实例。
对于分析型workload来说,大部分时间都花在连接和从磁盘读取数据上。分布式连接的效率取决于目标表的分区方案。
一种方法是将整个表放在一个节点上,然后执行连接。但是,这样 DBMS 就失去了分布式 DBMS 的并行性,也就失去了使用分布式 DBMS 的意义。这种方法还需要在网络上进行昂贵的数据传输。
要连接表 R 和表 S,DBMS 需要在同一节点上获取适当的数据元组。一旦连接成功就会执行连接算法。我们应该始终发送计算连接所需的最小数据量,有时甚至是整个数据元组。
分布式连接算法有四种情况:
半连接是一种连接操作,其结果只包含左表中的列。分布式 DBMS 使用半连接来尽量减少连接过程中发送的数据量,它与自然连接类似,只是限制了右表中未用于计算连接的属性。
供应商提供了_database-as-a-service_ (DBaaS)来提供受管理的DBMS环境。
云系统分为两种类型:
服务器无感知数据库(Serverless Databases):
数据湖(Data Lakes):
一些可以帮助构建 distributed database 的组件库/系统:
大多数 DBMS 为其数据库使用专有的磁盘二进制文件格式。 在系统之间共享数据的唯一方法是将数据转换为通用的基于文本的格式,包括 CSV、JSON 和 XML。云供应商和分布式数据库系统支持新的开源二进制文件格式,这使得跨系统访问数据变得更加容易。
通用数据库文件格式的著名示例:
到目前为止,我们一直认为应用程序的所有逻辑都位于应用程序本身。大多数应用程序使用 “对话式” 应用程序接口(如 JDBC、ODBC)与数据库管理系统交互。应用程序向数据库管理系统发送查询请求,然后等待响应。DBMS 发送响应后,就会等待应用程序对该连接的下一个请求。
可以将复杂的应用逻辑移入 DBMS,以避免多次网络往返。 这样做可以提高应用程序的效率、响应速度和可重用性。 这些方法的缺点是语法通常无法在不同的 DBMS 之间移植。
用户定义函数(User Defined Function,UDF)是由应用程序开发人员编写的函数,用于扩展系统的功能,超越其内置操作。每个函数接受标量输入参数,执行一些计算,然后返回一个结果(标量或表)。UDF只能作为SQL语句的一部分来调用。
返回类型
函数体
优点
缺点
触发器(Trigger)是指示DBMS在数据库发生某个事件时调用用户定义函数(UDF)的机制。触发器的一些示例用途包括在表中的元组被修改时进行约束检查或审计。
每个触发器都具有以下属性:
变更通知(Change Notification)类似于触发器(Trigger),但是不同之处在于DBMS会向外部实体发送消息,告知数据库中发生了重要事件。它们可以与触发器一起链接,以在发生更改时传递通知。通知是异步的,这意味着只有在它们与DBMS互动时,才会被推送到正在监听的连接。一些ORM(对象关系映射)工具会定期使用轻量级的“SELECT 1”轮询DBMS,以获取新的通知。
通知通常包括以下命令:
大多数 DBMS 都支持 SQL 标准中定义的基本原始类型(如 ints、floats、varchars)。但有时应用程序希望存储由多个基本类型组成的复杂类型。或者,这些复杂类型可能对各种算术运算符具有不同的行为。
一种可能的解决方案是将复杂类型拆分存储,并将其每个基本元素作为自己的属性存储在表中。这样做的问题是,必须确保应用程序知道如何拆分/合并复合类型。另一种解决方案是让应用程序将复杂类型序列化(例如 Java “serialize”、Python “pickle”、Google Protobufs),并将其作为 blob 存储在数据库中。这种方法的问题是,如果不先反序列化整个 blob,就无法编辑类型中的子属性。
同样,DBMS 的优化器也无法估量访问序列化数据的谓词的选择性。 一种更好的方法是使用用户自定义类型(UDT)。这是一种特殊的数据类型,由应用程序开发人员定义,DBMS 可以原生存储。
创建一个包含 SELECT 查询输出的“虚拟”表。 然后可以像访问真实的表一样访问该视图。这允许程序员简化经常执行的复杂查询。(但它不会让 DBMS 运行得更快)通常还用作隐藏表的某些属性对特定用户不可见的机制。
与 SELECT…INTO 不同,视图不分配表来存储视图的结果。**物化视图(materialized view)**在内部维护视图的结果,当底层表发生变化时,视图的结果可能会自动更新。