熟肉视频地址:
1. 课程大纲
这门课主要是关于如何开发一个功能全面的数据库管理系统,而不是如何编写复杂的 SQL 查询以及设计出最合理的关系模型数据库表。这门课会告诉你从低往上设计一个数据库管理系统需要的这些技术栈层:
- 磁盘管理(Disk Manager)
- 缓存池管理(Buffer Pool Manager)
- 访问方法(Access Method)
- 操作执行(Operator Execution)
- 查询计划(Query Plan)
涉及的主题包括:
- 关系数据库(Relational Databases,这个前面两节课已经过了一遍,咱们这个系列从第三节课开始,这部分就省略了)
- 存储(Storage)
- 执行(Execution)
- 并发控制(Concurrency Control)
- 恢复(Recovery)
- 分布式数据库(Distributed Databases)
- 其他一些有意思的实战(Potpourri)
2. 存储介质与为何不用 mmap
面向磁盘的数据库系统是这样一个系统,软件假定数据库的主要搜索位置在磁盘上。这意味着执行一个查询,它可能要访问不在内存中的数据,它需要将数据从 non-volatile 存储(例如磁盘)加载到 volatile 存储(例如内存)中。
计算机存储层次结构如上图所示,越往上,大小越小,成本越昂贵但是访问速度越快,越往下,大小越大,成本越低但是访问速度更慢,这些从顶部往下包括:
- Volatile 存储:断电后,存储的数据会丢失。支持字节访问,即可以直接读取取任意字节大小的数据,也可以直接更新。随机访问与顺序访问的速度差不多。
- CPU 寄存器
- CPU 缓存(L1,L2,L3)
- DRAM(Dynamic Random Access Memory 动态随机存取存储器)
- non-volatile 存储:断电后,存储的数据不会会丢失。不支持字节访问,只支持块访问,即如果你要读取某一字节的数据,必须将这字节所在的块或者页(page)的数据一起读取出来,并且一起更新。随机访问速度远小于顺序读取,所以对于 non-volatile 存储必须尽量用顺序访问。
- SSD(Solid State Disk,固态硬盘)
- HDD(Hard Disk Drive,硬盘驱动器):这里他经常管这块叫做 Spinning Disk Hard Drive,其实就是机械硬盘,一个磁头在磁盘上面不断移动寻找数据。
- Network Storage(网络存储):像是 AWS 的 EBS 以及 S3 这种网络存储服务。
在这门课中,不会关心 CPU 寄存器以及缓存,将 DRAM 看做内存(Memory),将 SSD、HDD、Network Storage 看做磁盘(Disk)。我们只关心内存和磁盘。
这里其实还有一些新型的存储,可能打破这些边界,例如:
- Non-volatile Memory:可以像内存一样支持字节访问,同时掉电也不会丢失数据。目前发布的产品是 Intel® Optane™ Memory:https://www.intel.com/content/www/us/en/products/details/memory-storage/optane-memory.html
- Fast Network Storage:快速网络存储,例如 NAS(Network Attached Storage)
这些新型存储可能会打破现有的设计,但是他们还没有被大幅度的采用,这门课还不会涉及这些。
下面我们再来看看访问这些不同存储大概的耗时(网上各种数字很多,我们只要关注数量级即可)
因此,我们数据库管理系统的目标,虽然我们想要存储一个超过可用内存容量的数据库,但是我们想给应用程序提供一种错觉,即我们有足够的内存将整个数据库存储在内存中,用一些缓存,预先计算一些数据,允许不同的线程或不同的查询同时运行来避免每次读或写的时候都因为写入或者读取磁盘导致执行效率低。
我们要在这门课上设计的是一个基于磁盘持久化的 DBMS,如上图所示:
- 最下面一层是磁盘,放着单个文件或者多个文件构成的数据库文件。
- 用不同的块或页(Page)来划分文件中的内容,页是比较学术的说法。
- 同时文件中还包含页表或者页目录(Directory),类似于文件内容与页与文件位置的索引。
- 在上层内存中,有一个缓冲池(Buffer Pool),里面使用一些缓存算法将文件中的页和页目录缓存在内存中。
- 最上层是执行引擎(Execution Engine),直接将读取页的请求发送给缓冲池,缓冲池不存在就会从磁盘查找,根据页目录定位到页,加载到缓冲池,返回内存中页的地址。之后执行引擎用这个内存中的地址做一些事情。
对于前面提到的内存与缓冲池这一层,想那些学过操作系统或者对操作系统熟悉的人可能知道,操作系统有类似的机制,为啥不通过系统调用使用操作系统现有的机制实现缓冲池呢?例如 mmap() (内存映射文件的系统调用),我们来看下上图所示的这个场景:
- 假设我们磁盘上有四页,物理内存最多容纳两页
- 内存分配是先 reverse 虚拟内存,虚拟内存是很大的,基本用不完。在实际使用的时候,commit 映射实际的物理内存。对于 mmap,是先将文件映射到进程的虚拟地址空间,实际使用到这个地址的时候,如果不在内存中(也就是没有实际映射物理内存),就会发生缺页中断(Page Fault),需要阻塞等待加载这页实际映射到物理内存中。
- 假设我们先读取的是第一页,在虚拟内存中查找我们发现第一页实际没有映射物理内存,发生了缺页中断,阻塞加载磁盘第一页数据到内存
- 之后读取的是第三页,和上一步一样
- 如果这时候我们读取第二页,物理内存不够了,我们需要删除内存中的某一页。这个缓存过期策略,我们是全权交给系统了,但是系统并不知道我们的业务,可能不如我们自己管理做得好
如果我们使用 mmap(),我们就是将这种缓存过期以及内存管理全权交给操作系统来执行了,操作系统并不知道我们的业务场景,以及哪些缓存被过期掉是更加合适的。从应用程序的角度来看,我可能需要读取不在内存中的东西,也就会发生缺页中断,我们可以将它交给另一个线程来做,不阻塞当前线程,这样当前线程就可以去处理其他请求,这样可以增加吞吐量。对于只读的场景这样的优化已经足够了,但是如果还涉及写入的话,这个优化也就不够用了:
因为操作系统被告知要写一个数据,操作系统不会管是否合适(比如批量写,聚合内存块在一起的,减少内存中断)就直接写了。
我们还可以通过一些系统调用来优化: