这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战
We will next learn how to build software that manages a database.
课程涉及的范围:
- 存储
- 执行
- 并发控制
- 恢复
- 分布式数据库
- 以及其他
1. Overview of storage
存储层次和访问速度
DBMS的设计基于这样的假定:数据库的基本存储,都是放在磁盘上的(non-volatile,非易失)。这种设计,称为disk-oriented DBMS。
DBMS管理数据在内外存之间的移动。disk IO代价很大,所以要尽量避免较大规模的内存失效和性能降级,尽可能的好好利用内存。
2. Disk-oriented
读取数据
- client告知DBMS需要page2
- 先从内存中查找
- 内存没有,去disk查找
- 将disk中的目录加载到内存,然后找到page2,加载到内存
- 返回给client
其实这个逻辑跟操作系统加载数据是类似的,先在内存缓冲区中查找,若未命中就从外存中加载。若缓冲区已满,则使用内存置换算法来移出一块区域中的数据。
Why not use the OS
理论上,可以直接用mmap将数据库文件映射到进程的地址空间中,由OS来负责数据在内外存间的移入移出。
WHY NOT?
DBMS通常更了解你的数据
- 把脏页根据正确的顺序写入disk
- 特定的预加载
- buffer替换策略
- 进程调度和线程调度
The OS is not your friend.
3. Database storage
How the DBMS represents the database in files on disk?
3.1 File storage
在磁盘上如何存放数据库 -> 将数据库中的内容按照文件的形式,存放在磁盘上。
- OS对文件的内容一无所知。
- 早期的一些DBMS会直接使用文件系统来进行存储。
Storage manager
维护数据库文件,有些有自己的读写调度逻辑,来尽可能的利用pages的时间和空间局部性;
将文件按照page的集合的形式维护,数据的读写都面向pages,并且维护可用空间;
3.2 Database pages
- fixed-size的数据块
- tuples,meta-data,indexes,log records都可以按照page的形式来存储
- 每个page有自己的标识符
- DBMS将page id映射到物理存储位置
- 不同的DBMS的page size有所区别(512B-16KB不等,mysql是16KB)
完全类似于操作系统的逻辑地址和物理地址,逻辑地址中的页表、页号和物理地址中的帧。但是这里的物理存储位置一般是外存。
About page
在DBMS中会涉及到三个page的概念:
- Hardware page(4KB usually)
- OS page
- Database page
需要注意的是,存储设备只能确保每一个hardware page的写入时原子操作;所以,当database page比hareware page要大的时候,DBMS就需要确保整个database page大小的数据在写入时的原子性。
Page storage architecture
不同的DBMS有不同的管理pages的方法:
- Heap File Organization
- Sequential / Sorted File Organization
- Hashing File Organization
我们首先考虑page级别的管理,而非关心page中的内容。
Heap file
pages的无序集合。需要元数据来获知page的存在情况和是否有空间。
实现方式:
- Linked List
- Page Directory
如下,链表方式,两个HEAD节点来各自维护free page链表 和 data page链表。
每页中的空白位置(free slots)由page各自负责维护。
如下,目录页方式。通过一个特殊的目录页(Directory)来记录data pages的位置,也同时记录每个页中的free slots。
3.3 Page Layout
page header
每个页内,包括header和data两部分。header部分存储当前页的元数据:
Page size, checksum, DBMS version, transaction visibility, compression information
Self-contained: means that all the information needed to read each page is on the page itself.
page layout
介绍数据在页内如何存储:
tuple-oriented
- 稻草人模型:
直接按顺序存储各个tuple,带来的问题是:删除某个tuple会导致乱序;tuple中存在不定长的属性会导致无法寻址;
- 正确的方案:slotted pages
有一个slot array,将slots映射到每一个tuple的初始存放位置。slot array从前往后、tuple从后往前插入。
如下图,header中需要额外维护已经使用了的slots的数量;最后一个使用的slot的offset(来用于插入新数据);当slot array与data相遇时,我们认为这个page已经存满了。
(但是如果删除某一个tuple,如何解决碎片问题?)
log-structured
存储日志信息,每一次db有修改的时候,就追加操作记录到该文件中。
- insert:存储整个元组
- delete:标记被删除
- update:只标记修改的部分
回溯日志表,找到所有和这个id相关的记录(通过索引),来复现这条记录。
可以构建索引,来便于查找记录。
定期压缩日志(将对同一条数据的操作的最终结果记录下来,替换多条相关日志)。
显然,这种方式对只有追加写的场景非常友好。读相对慢。
3.4 Tuple layout
tuple本质上就是字节序列。DBMS需要把字节给翻译成对应的属性类型和值。
Tuple header
tuple也有元数据,称为 tuple header,包含可见性信息(用于并发控制,例如哪一个事务正在创建/修改了这个tuple)以及标记null值的bit map。
不需要在这里存储关于数据库schema的元数据。
Tuple data
属性通常都是按照创建表时的顺序存储;多数数据库不允许tuple size > page size。
Unique identifier
每一个tuple都有唯一标识符,通常是pageid + (offset / slot)
应用程序不可以使用这些标识符来做其他事情 。
Denormalized tuple data
将相关的属性自动存放到一起(类似join?)
一定程度上减少了I/O数量,但update的开销大大增加。
4. 总结
以上讨论了 寻找page的方法,page的存储方法,元组的存储方法。
数据库中的数据,究其根本,是以文件的形式存储在磁盘上的。
那么就要考虑以下几个问题:
- DBMS如何管理这些数据库文件?包括数据定位、数据拆分、如何高效的访问这些文件等等;
- 每一条数据在文件中具体是如何存储的?
首先,为了管理方便,数据按照固定大小的块来进行组织,每一块称为一页(page);每个文件中可以有多个页。那么我们需要有一套逻辑,来维护和定位文件中的页。这里详细介绍了heap file这种方式,以及从文件中定位页的两种方法:链表结构和使用目录页。
现在我们找到了所需的页,然后思考每一个页是如何设计的,也就是页的布局。就像计算机领域很多的设计一样,页的最开头,需要一个header部分来存储元数据(包括页的大小、校验和、版本信息、压缩信息等),然后就是data部分存放的具体数据,这里介绍了两种data部分的布局方法:朴素的直接存储元组,和类似append-only files的记录操作日志,用复现的方式来获取数据。
现在我们找到了所需的元组(tuple),也就是某一条数据,现在思考每个tuple是如何设计的。既然细化到了某一条,那么它需要一个标识符id,以及数据部分按顺序记录每一个属性的值。要注意的是,每一个tuple仍然有自己的header,用来进行并发控制,以及记录该条记录中的空值。
通过以上三个步骤,我们能够找到文件中的页,页中的元组,然后查看元组的各个属性。但是disk IO总是慢的。database file存放在外存中,我们每次要访问某个文件中的某个页时,总是需要将这个页(页也是进行数据库操作的基本存储单位)加载到内存中。 所以就像操作系统加载数据到内存中一样,我们也希望能尽量好的利用数据的空间和时间局部性,利用相对有效的页面置换算法,来减少从外存中加载页到内存的次数。显然,我们可以直接调用OS的既有逻辑来进行页面管理(mmap),但是我们有DBMS,它既然是专门负责管理数据库的,一定是对数据更了解的,更可以因地制宜的进行策略的选择。所以,通常都是由DBMS来设计这一套操作逻辑。
毕竟,OS is not your friend.