Skip to content

Lec 4 数据库内部

阅读资料:

我们的目标是关注整体系统设计(关系型数据库)

截屏2024-07-04 02.19.57

总览

  • 一个查询的来龙去脉
  • 进程模型
  • 并行架构: 进程和内存协调
  • 关系查询处理器
  • 存储管理
  • 事务:并发控制和恢复
  • 共享组件

一个查询的来龙去脉

执行SQL后内部发生了什么?

sql
-- case 1
SELECT * FROM animals WHERE species = 'Giraffe'
-- case 2
SELECT image, imgid FROM planet_images WHERE likelyPlanet(image) = TRUE
  1. 首先客户端调用API(可能基于ODBC或JDBC协议)与DBMS的客户端通信管理器(Client Communications Manager),建立连接。 CCM还为客户端建立安全证书,为新的连接细节和客户端SQL命令分配空间,并将请求往下传送
    • 上述为二层模式。如果客户端与中间层服务器(Web、事务处理系统等)建立联系,称为三层模式
  2. 收到请求后,DBMS为之分配一个计算线程,这个工作由进程管理器(Processing Manager)管理,这部分中,主要工作是准入控制,即是否应该立即处理该查询,或是等待系统有足够资源时再处理该查询
  3. 分配工作线程后,查询便可以处理了。处理工作借助与关系查询处理器(Relational Query Processor)实现。这些模块检查用户是否有权进行该查询,然后将sql编译为中间查询计划。编译后,查询计划被交给查询执行器。查询执行器包括一些列处理查询的操作:连接、排序、投影、聚集等等(算法的实现),也包括从底层读取需要的数据。‘
  4. 在查询计划的底层,需要从数据库请求数据,这些操作通过调用(call),从DBMS的存储管理器(transactional storage manager)中收集数据,它负责所有数据的CRUD。存储系统包括用于管理磁盘数据的基本算法和数据结构,比如基本的表和索引。它还包括一个缓冲管理器, 用来控制内存缓冲区和磁盘之间的数据传输。查询必须调用事务管理代码来保证“ACID”性质。在获 取数据之前,需要通过锁管理器来确保并发情况下运行的正确性。如果登机口客户端的查询 包含对数据库的更新操作,那么,它需要与日志系统进行交互
  5. 当获取数据后,访问方法把控制权转交给查询处理器,查询处理器将数据库的数据组织成结果元组,结果元组生成后被放到客户通信管理器的缓冲区中,然后将结果发送给调用者。
  6. 在查询的最后,事务结束、连接关闭,事务管理器中的结果被清空,进程管理器释放无用的数据结构,通信管理器将状态清空。

QUIZ

整体系统设计图中,我们为什么需要访问控制admission control?

为什么scheduling很重要?

  1. 客户端建立连接并传输 SQL
  2. 访问控制防止过多的传入工作
  3. 检查基于角色的权限
  4. 查询被分配给一个“工作线程”
  5. 应该如何进行工作?
    1. 每个工作线程使用一个进程
    2. 每个工作线程使用一个线程
    3. 使用进程池

进程模型

从简单到复杂

  • 每个工作线程使用一个进程
  • 每个工作线程使用一个线程
  • 使用线程/进程池

每个工作线程使用一个进程

  • 优点:易于实现和调试,系统有进程保护机制
  • 缺点:广泛使用共享内存(比如锁机制和缓冲池),高并发并不搞笑
  • 代表产品:Oracle(都支持,默认是进程)、PostgreSQL

每个工作线程为系统线程

一个多线程进程负责所有的worker的工作,当结束时,将结果返回给客户端,并等待客户端下一个请求

  • 优点: 高并发支持好
  • 缺点:无线程保护机制,调试困难
  • 代表产品: Mysql

使用线程/进程池

每个worker一个进程的变体。不必为每个worker分配一个进程。

  • 优点:易于实现和调试,只需要少量进程,内存使用效率高
  • 代表产品:SQLServer

准入控制

高并发请求带来负载 的压力,应通过准入控制优雅地回退。比如排序和Hash Join造成大量的内存消耗,锁冲突:事务处理发生死锁需要回滚并重启。

  • 如果拥有一个好的准入控制器,系统将 在过载情况下发生比较优雅的性能衰退:事务延迟将随着到达率的增加而适当增加,但吞吐 量一直保持在峰值
  • 两个层面实现
    • 一个简单的准入控制可以通过进程来确保客户端连接数处于一个临界值之下
    • 直接在 DBMS 内核关系查询处理器上实现。准入控制器在查询语句转换和优化完成后执行这一步操作。
    • 特殊情况下,优化器的查询计划可以:(1)确定查询所需要的磁盘设备以及对磁盘的 I/O 请求数;(2)根据查询中的操 作以及要求的元组数目判断 CPU 负载;(3)评估查询数据结构的内存使用情况,包括在连 接和其他操作期间的排序和哈希所消耗的内存。上面的第 3 点对于准 入控制而言是最为关键的,因为,内存压力通常是引起“抖动”的主要原因

并行架构:进程与内存协调

  • 共享内存
    • UMA(均衡访问模型)
    • NUMA(非均衡访问模型)
  • 共享磁盘
  • 分布式(无共享)

共享内存

截屏2024-08-12 17.11.53

所有处理器共享相同的内存地址空间。

服务器硬件成本会比较高,但是大部分商业DB都支持。

UMA(”乌麻“)

每个处理器核心共享相同的内存空间,随着CPU核数增加,总线带宽成为了瓶颈,访问同一内存冲突

NMUA(“努麻”)

截屏2024-08-12 17.15.55

  • QPI的延迟 >> IMC BUS
    • 访问本地内存非常快,远程访问慢
  • 商业上不是很成功,裸金属机器? 针对可能需要远程的访问,可以进行绑核化解

共享磁盘

截屏2024-08-12 17.19.38

  • 管理成本低
  • 单节点故障不会影响到其他节点
  • 依赖分布式锁管理设备,高速缓存一致性协议
  • 代表产品:Amazon Aurora

分布式架构

多个独立计算机组成集群,本质上是做水平分区。

引入新问题:

  • 分区方式
  • 分布式事务
    • 2PC-3PC-Percolator(谷歌)
  • 负载均衡
    • 快速打散热点
  • 故障处理

特点: 可扩展性好,成本低

代表铲平:TiDB,Spanner

关系查询处理器

一个关系查询处理器以一个 SQL 语句作为输入,然后进行验证,优化成为一个程序数据流执行计划,获得准入后就可以代表一个客户程序执行数据流程序了。然后,客户程序“拉”结果元组,通常一次一个或一小批。关系查询处理可以被看作是一个单用户、单线程任务。并发控制由系统较低层透明控制,唯一的例外是操作缓冲池页面时,DBMS必须明确unpin和pin。

截屏2024-08-12 17.31.06

查询解析和授权

Query Parsing and Authorization子模块。

其中解析器的作用:

  1. 检查这个查询是否被正确地定义
  2. 解决名字和引用
  3. 将这个查询转化为优化器使用的内部形式
  4. 核实这个用户是否 被授权执行这个查询

给定一个SQL查询,查询处理器执行步骤

  1. 解析器考虑FROM子句中每个表的引用,表名规范化“服务器,数据库,模式,表名”,简称四部分名称。
    • 不支持跨服务器执行查询的系统,去掉服务器
    • 只支持一个数据库的系统,去掉服务器
  2. 规范化表名后,查询处理器开始调用目录管理器,检查表是否被注册到系统目录,检查属性引用是否正确,以及对属性的操作是否正确
  3. 被成功解析后,进行授权检查,以确保用户对查询中引用到的这些表、 用户自定义的函数以及其他对象具有适当的权限(CRUD)。
  4. 一个查询被解析且通过验证,那么就可以传递到重写模块

查询重写

Query Rewriter,负责简化和标准化查询。他只能依靠查询目录中的元数据,而不能访问表中的数据。将SQL语句文本重写成内部表示。

重写器的作用:

  1. 视图展开
  2. 常量运算
  3. 谓词逻辑重写
  4. 子查询平面化
  5. 语义优化
    • 冗余连接消除

查询优化器

Query Optimizer, 将内部的查询表达转换为高效的查询计划。

可以优化的点:

  • 计划空间
  • 选择代价估计
  • 搜索算法
  • 并行
  • 自动调优

查询执行器

负责执行一个具体的查询计划,是一个把多个操作连接在一起的数据流程图,其中封装了基本表的访问和各种查询执行算法。执行器常用的模型: 迭代器模型。 只需要单一的DBMS线程来执行一个完整的查询图。

迭代器

迭代器的一个重要性能就是,它们连接了数据流和控制流。get_next()调用是一个标准 过程调用,该调用通过调用堆栈给调用者返回一个元组引用。因此,当一个控制返回的时候, 一个元组就返回给数据流图的父类

数据在哪里

  • 第一种是,那个元组驻留在缓冲池的页面中,我 们称这些为缓冲池元组。如果一个迭代器构造了一个引用缓冲池元组的元组描述符,那么它 就必须增加那个元组所在的缓冲池页面的引脚数(pin count),即在那个页面上的元组的活 跃(active)引用数量
  • 一个通用的方法就是总是将缓冲池中的数据复制到内存元组中。这个设计使用内存元 组作为唯一的查询时使用的元组结构,从而简化了执行器代码,门内存元组会成为一个主要性能问题,因为,在一个高 性能系统中,内存副本通常是一个瓶颈。

访问方法

访问方法是用来对系统支持的基于磁盘的数据结构的访问进行管理的,通常包括无序的 文件(“堆”)和各种各样的索引。所有的商业系统都实现了堆和 B+树索引。PostgreSQL 支持一种叫做 Generalized Search Tree[39] 的可扩展性索引,当前使用它来实现多维数据的 R-树索引以及针对文本数据的 RD-树索引

访问方法提供的基本 API 是一种迭代器 API。Int()例程会被扩展,从而可以接受一种列 操作符常量形式的“搜索参数”(或者在 System R 术语中被称为 SARG)。一个 NULL SARG 被看成一个扫描表中所有元组的请求。当再也没有满足搜索参数的元组时,在访问方法层调 用 get_text()将返回 NULL 值。

存储管理

有两种基本类型的DBMS存储系统。

  • DBMS直接语底层面向磁盘的块模式设备驱动进行交互(通常称为原始模式访问)
  • 使用标准的OS文件系统

这个决定会在空间和时间上同时影响 DBMS 控制存储的能力。

空间控制

当磁盘中读取和写入数据时,顺序读写带宽要比随机读写带宽快10到100倍。如何把数据块放置在磁盘上就显得尤其重要,从而使得需要访问大量数据的查询可以顺序地访问磁盘

对于 DBMS 而言,控制数据空间局部性的最好的方式,就是将数据直接存储到“原始” 磁盘设备中,完全绕过文件系统。原始设备地址通常对应于存储位置的物理临近性。由于存储区域网络、逻辑卷(volumne)管理器,RAID已经非常普及。在许多应用场景中,“虚拟”磁盘设备已经成为规范——“原始”设备接口实际上已经被很多硬件和软件拦截掉,它们会在一个或多个物理磁盘之间重新定位数据

原始磁盘访问的一种替代方式是,由 DBMS 在操作系统的文件管理系统中创建一个非常大的文件,然后采用数据在文件中的地址偏移量来定位数据。这个文件在本质上可以视为 磁盘页面的一个线性阵列。大多数虚拟化存储系统在设计时,都会把文件中 临近的地址放置在临近的物理位置中。因此,随着时间的推移,使用大文件而不是原始磁盘 的相对控制代价,已经变得越来越不明显了

时间控制:缓冲

一个 DBMS 还必须控制数据什么时候被物理地写 入到磁盘中。OS提供的I/O缓冲机制会带来数据库的ACID事务承诺的正确性问题,以及性能问题。其中性能问题有两个方面,一个是DBMS能够预测未来读请求,第二个是“双缓冲问题”(执行读取操作时, 数据会首先从磁盘复制到操作系统的缓冲区,然后再复制到 DBMS 缓冲池,写操作时则相反)

缓冲管理

现在大多数商业 DBMS 会根据系统需要和可用资源来动态调整缓冲池大小。缓冲池会被组织成一个帧数组,其中,每一帧是内存中的一段区域,帧的大小是数据库磁盘块的大小。块从磁盘直接复制到缓冲池中,不会发生格式的变化,在内存中也是以这种原生的格式进行修改操作,然后, 写回磁盘。这种不需要转换的方法,避免了 CPU 瓶颈。

和缓冲池中的帧数组相关联的是一个哈希表,它会对以下内容进行哈希映射:(1)把内存中当前的页面编号映射到它们在帧表中的位置;(2)页面在备份磁盘存储中的位置;(3) 关于该页面的一些元数据

元数据包括一个“脏”标记位,用来表示页面在从磁盘中读取出来以后是否已经发生改 变;元数据还包括页面替换策略所需要的任何信息,当缓冲区满的时候,页面替换策略会选 择被驱逐出缓冲区的页面。大多数系统还包括一个引脚计数器(pin count),来标记该页面。当引脚数是非零时,页面被“钉”(pin)在内存中,不会 被强行写入到磁盘或丢失。

事务:并发控制和恢复

数据库系统真正庞大而复杂的部分是事务存储管理器。该部分由四个彼此紧密关联的组 件组成。

  • 用于并发控制的锁管理器
  • 用于事务恢复的日志管理器
  • 数据库I/O缓冲池
  • 用于组织磁盘数据的访问方法

可串行化

在多用户事务存储管理器中实现并发概念的模块,1)锁; 2)锁存器

定义: 多个事务相互交错的一 组并发执行,必须与该组事务的一个串行执行结果相对应——即执行结果与没有并发的结果相同。

实现方法:

  • 严格的两段锁:事务在读任何数据之前需要一个共享锁,而在写之前需要一 个排他锁。一个事务所拥有的锁,会一直保持到事务结束时才自动释放。
  • 多版本控并发控制:事务不使用锁,为过去的某一时间点的数据库状态保存一致的副本,即使在固定时间之后数据库状态发生改变,也可以读到一个过去状态。
  • 乐观并发控制(OCC):该方法允许多个事务在无阻塞的情况下读或者更新一个数 据项。事务会保存自身的读写历史,在一个事务提交前,必须通过检查其读写历史 来判断是否发生了隔离性冲突;若发生,则发生冲突的其中一个事务必须回滚。

锁和锁存器(latching)