一.磁盘上存储数据
1.磁盘介绍
2.磁盘读取数据
3.提高磁盘读取性能
4.RAID(提高磁盘容灾性)
二.数据库在磁盘中的组织形式
1.块组织型式(block-level)
1.1 Disk Map
可以在header块上存储,后面每个块的信息,0表示使用,1表示未使用。
1.2 Free List
Free List 是由一组 chunks 组成的链表,每个chunks 都是一些空闲块组成。分为一个 header 块和多个其它的块。
以图中为例,block 0 存储第一个 chunk 的块号 2,block 0 不能被分配(header)。后面 block 2 就是大小为1的chunk,在 block 2 的下一个chunk 的起始块号是5。block 10 表示终止块。
2.文件组织型式(file-level)
OS 能够提供给用户一个更高级的接口,叫做 文件系统(File system)。一个文件由存储在磁盘上的多个 chunk 组成,每个 chunk 都是多个 block,根据存储方式的不同,组成文件的 chunk 可能是不连续的。通过文件来访问磁盘,也可以看成提供了一层虚拟层,用户以为文件的存储是连续的,而实际上磁盘可以将文件分成多个块存储在不连续的位置。可以通过文件名+offset访问文件的任意位置。
文件组织型式一共有三类:
- 连续分配(Continuous Allocation)
- 基于 Extent 的分配(Extent-Based Allocation)
- 索引分配(Indexed Allocation)
2.1 连续分配
在磁盘上找到一段连续的空闲块。磁盘空间趋于碎片化,连续分配方式难以扩展。当文件长度增加时,可能需要分配新的空间(费时)
2.2 基于Extent的分配
Extents:OS会将一定固定数量的连续空闲块称为extents
缺点:
-
每个文件的大小需要 = extent 的整数倍,即使只用到一个 extent 的一小部分,OS 也会将这个extent
分配给他。这样子就会带来内部碎片。 -
由于每次分配都是分配 extent,因此没有外部碎片。
2.3 索引分配
不再以extents作为分配单位,而是以block作为分配单位。
- 外部碎片减少
- 最大内部碎片从一个 extent 减少到一个 block
3.数据库和操作系统
Block-level:block-level 能够让磁盘和disk-blocks完全由DB开发者自己控制。
- adavantage:内存中关于磁盘的缓存由用户自己控制,可以指定自己的置换策略,而不是使用OS的置换策略,能够提升效率;不受 OS对文件的大小限制,能够比 OS 的 maximum file size 更大;性能一般能够提升 10% ~ 15%。
- disadvantage:难以实现;工程难度大;磁盘需要被格式化,需要挂载一个有文件系统的磁盘;实现自己的文件系统可能会无法移植到其他服务器上。
File-Level:DB 通过 OS 提供的 open 、close 等系统调用来打开和控制文件。
- adavantage:易于实现;DB 知道文件的大小、能更好的组织数据
- disadvantage:缓存由OS控制,不能够最优置换。
可以采用折中的方式以减少工作量。
4.page
- page 是一个固定大小的数据块,在一个系统中,所有 page 的大小都是相同的,然而在不同的 DBMS 中,page 大小可能是不同的。
- page 可以存储 DBMS 的许多信息,例如 tuple、元数据、indexes、日志记录等。 每个文件的每个 page 有自己的标识符page id,对应于磁盘中的 一个/几个 磁盘块。一个 page 由 文件名 + page_id 标识。
- 存储引擎会追踪 page的读写信息,以及一些空间信息。许多系统都需要 self-contained,就是空间信息和一些日志信息都保存在 page 的 header中。还有一些系统需要用一些 page 单独存储上面的信息。
三、BufferPool Basic
然后用户访问一个虚拟内存地址时,磁盘上的数据就会被调进物理内存中,每次访问这个数据都是在物理内存上访问,最后写回到磁盘中。DBMS 比 OS 拥有更多、更充分的方法来决定数据移动的时机和数量。具体包括:
- 将 dirty pages 按正确地顺序写到磁盘,也可以控制日志写回的时间。
- 根据具体情况预获取数据
- 定制化缓存置换(buffer replacement)策略
可以从时间和空间两个角度考虑这个问题:
- Spatial Control:空间控制策略通过决定将 pages 写到磁盘的哪个位置,使得常常一起使用的 pages能离得更近,从而提高 I/O 效率。就预取存储在磁盘上的 pages。
- Temporal Control:时间控制策略通过决定何时将pages 读入内存,写回磁盘,使得读写的次数最小,从而提高 I/O 效率。
1 BufferPool Manager
核心思想:减少IO次数
1.1 Buffer Pool
- 首先缓冲池会查看当前缓冲池内有没有对应的 page
- 如果有的话直接返回这个 page 的指针(由于都在内存中,不需要继续cache),如果没有的话就需要找到一个 frame,通过磁盘管理器请求对应的 page,然后将 page 的内容复制到 frame中。
- 最后返回对应 page 的指针通过上面的过程。
可以发现一个缓冲池需要几个额外的元数据:page
- table:快速找到当前缓冲池中 page id 对应的 page
- mutex:互斥,保证一个获取、释放一个 page的过程是原子的,不会被其他进程干扰
1.2 buffer organization
- dirty flag:一个符号表示当前 buffer 是否被修改过,如果被修改过的 buffer被置换出缓冲池,就需要写回到磁盘中保证数据一致性。如果没有被修改过,还是不要浪费一次io操作的时间了。
- pin_count:pin_count也可以叫做 reference count,如果 pin count > 0, 就不应该被置换出缓冲区,否则指向这个 buffer的指针就会指向错误的 page(被置换出去之后,frame 的内容被替换了,对应的 page id 也被替换了),对他的读入和修改都是不正确的。
1.3 locks vs latches
- lock:用来保护事务的内容(表、tuple、记录、索引),保证事务的持久性,需要考虑回滚
- latch:用来保护底层数据结构,让各个线程互斥访问,保证操作原子性,不需要考虑回滚
1.4 page table vs page directory
DBMS可能会维持一个 page directory,能够将 page id 映射到磁盘中文件的位置。而 page table 是缓冲池中为了方便知道当前 page 是不是在缓冲池的数据结构。
1.5 allocation policies
- Global Policies:对所有查询都共用一个缓冲池,在同一个缓冲池里面置换。
- Local Policies:对每个查询都分配一个缓冲池,planner 就可以根据每个查询的特点分配对应的缓冲置换策略,也不会污染缓冲池。
2.Buffer Replacement Policies
当 Buffer Pool 请求一个 page 时,会从磁盘中读入数据,然后分配一个 frame,将数据复制到 frame 里面。frame 的选择可以是任意的,类似于 cpu 的全相联。frame 的选择优先级如下:
- 首先,先找到第一个空的frame,用来读入page数据
- 如果没有空的frame,通过特定的缓冲置换策略移除一个page,然后在被移除的位置上写入新的page,更新元数据(例如 page table 将 pageid 映射给他)
它的主要目标是:
- Correctness:操作过程中要保证脏数据同步到 disk
- Accuracy:尽量选择不常用的 pages移除
- Speed:决策要迅速,每次移除 pages 都需要申请latch,使用太久将使得并发度下降
- Meta-data overhead:决策所使用的元信息占用的量不能太大
替换策略:
- LRU
- CLOCK
- LRU-K
- …
四、BufferPool Adavance
1.多缓冲池(Multiple Buffer Pools)
使用多个缓冲池,每个缓冲池有自己的置换策略,或者让每个 DB 拥有自己的缓冲区。
2.预取(Pre-fetching)
缓冲池能够通过预取来进行优化。
3.Scan Sharing
一个查询可以和其他查询共用一个游标,增加查询效率。
4. Buffer Pool Bypass
当遇到大数据量的 Sequential Scan 时,如果将所需 pages 顺序存入 Buffer Pool,将造成后者的污染,因为这些 pages 通常只使用一次,而它们的进入将导致一些可能在未来更需要的 pages 被移除。因此一些 DBMS 做了相应的优化,在这种查询出现时,为它单独分配一块局部内存,将其对 Buffer Pool 的影响隔离。
五、Shadow Paging
数据库的操作都是以 事务(Transaction) 作为一个单位,每个事务都是一组 SQL 语句。事务可以进行读操作(select),也可以进行写操作(update、insert)。事务具有很重要的四个性质,简称为 ACID:
- Atomicity:一个操作要么全做、要么不做,过程是不可中断的。
- Consistency:数据要确保一致性,不会获取到数据的中间状态。比如说,事务A不会读取到事务B运行到一半的中间数据。
- Isolation:数据库会给每个事务提供一个假设,好像此时只有一个事务在运行。如果多事务并发,数据库要保证他们之间彼此隔离。
- Durability:已提交的事务所做的操作要保存在磁盘中。
每个数据库会有两个工具用来维护上面的四个特性:
- Recovery manager:atomicity & durability,通过使用恢复系统,可以让未提交的事务回滚操作,让已提交的事务操作永久保存在磁盘中。
- Concurrency manager:ioslation & consistency,通过使用并发系统,保证事务之间运行不冲突、保证数据的一致性。
1. Why we need Recovery system
- atomicity:当一个事务需要回滚时,数据库需要恢复撤销它的所有操作
- durability:一个已经提交的事务所有修改都要落地到磁盘上
事务A的修改后先提交到内存,但是发生故障中断,未满足durability,或者一个语句执行到一半发生故障,未满足atomicity。
2.Failure Classification(故障分类)
2.1事务故障
- logical errors:由于一些内部原因,比如 数据完整性约束:主键约束、外键约束、unique约束,导致事务没办法正常完成。
- internal state errors:由于数据库的内部调度、并发控制,如死锁,导致事务没办法正常提交。
发生事务故障时,DBMS 会中止事务,将状态设置为 abort,然后 undo 他之前的所有操作。
2.2系统故障
- Software Failure:如 DBMS 本身的实现问题 (NPE, Divide-by-zero)
- Hardware Failure:DBMS 所在的宿主机发生崩溃,如断电。且一般假设非易失性的存储数据在宿主机崩溃后不会丢失
软件问题会让整个数据库系统软件都崩溃,然后需要重启系统软件。硬件问题需要重启宿主机并且重启数据库系统软件。无论哪种原因,重启之后缓冲池上的数据都会消失,只会保留在磁盘上的存储数据。数据库系统要做的就是根据磁盘上的数据进行如下操作:
- 在崩溃之前 commit 的事务,需要保证所有修改都落盘
- 在崩溃之前 未commit的事务,因为它无法继续原来的操作了,可以认为这个事务是abort,需要保证所有修改都撤销
2.3存储介质故障
备份
3. Buffer Pool Policies
修改数据时,DBMS 需要先把对应的数据页从磁盘中读到内存中,然后在内存中根据写请求修改数据,最后将修改后的数据写回到磁盘。在整个过程中,DBMS 需要保证两点:
- 所有事务的修改都已经持久化到磁盘后,DBMS才能告诉用户提交成功
- 如果事务中止,任何数据修改都不应该持久化
如果真的遇上事务故障或者系统故障,DBMS 有两种基本思路来恢复数据一致性,向用户提供上述两方面保证:
- Undo:发生崩溃时,回退中止或未完成的事务中已经执行的操作
- Redo:发生崩溃时,重做提交的事务执行的操作
有 2 个并发事务 T1 和 T2,T1 要修改 A 数据,T2 要修改 B 数据,A、B 数据都位于同一块 page 上。在 T2 事务提交时,T1 事务尚未结束,这时 T1 已经修改了 A 数据。
- 此时 DBMS 会允许被修改但未提交的 A 数据进入持久化存储吗?
- 在 T2 事务提交时,是否需要将他的数据改动持久化?
对于两个问题分别有行和不行两种策略。
4.shadow paging
- No-steal:不允许未提交的数据落盘。事务中止时,就不需要 undo 回滚数据。
- Force:强制事务提交后所有数据落盘。数据库恢复时,就不需要 redo 恢复数据。
shadow paging 是 No-Steal + Force 策略的典型代表,解决了 write-set 无法放进内存中的情况。它会维护两份数据库数据在磁盘中:
- Master:包含所有已经提交事务的数据
- Shadow:在 Master 之上增加未提交事务的数据变动
未提交的事务的 buffer 被置换出去时,不是落盘,而是落地到一个 temporary database(shadow)。当事务提交时,原子地切换 db-root 指针,将 shadow 变成新的 master。
六、WAL(Write Ahead Log)预写日志
在上节提到了 shadow paging,影响 shadow paging 性能效率的主要原因是:
- 在事务提交时,所有被该事务修改的缓冲页都要立即落盘。
- 由于 shadow paging 的特性,pageid 接近的 page实际上的物理位置可能相差极大。
- 同时恢复策略要访问的 page table 的页表项很可能也是不相邻的。
访问他们属于 随机 IO,会影响事务提交时的效率,影响并发事务的数量。
为了改善 shadow paging 的效率问题:
- 我们希望能够延缓 commit 之后脏页的落盘时间。
- 并且不会改变 page 的物理位置。
- 同时恢复策略所采用的 IO 尽量都是随机IO。
1.Write Ahead Log
WAL 指的是 DBMS 除了维持正常的数据文件外,额外地维护一个日志文件,上面记录着所有事务对 DBMS 数据的完整修改记录,这些记录能够帮助数据库在恢复数据时执行 undo/redo。使用 WAL 时,DBMS 必须先将操作日志持久化到独立的日志文件中,然后才修改其真正的数据页。
WAL基于一个假设:每个修改操作都有一条对应的日志文件。
WAL 策略的简单流程:
- DBMS 先将事务的操作记录放在内存中
- 然后修改内存中的 data page
- 如果该 data page 要落盘,需要强制刷新 data page 的所有日志到磁盘中
- 当一个事务提交时,需要把该事务所有的日志(包括 commit 记录)都落盘,而对应的 data page 不需要立即落盘。只要所有日志都落盘,才能认为这个事务提交成功
- 当一个事务终止时,需要把该事务所有的日志(包括 abort 记录)都落盘。
2.Log Records
简单地说,日志类型能够分为三种:
- 关于单个事务状态的描述:start records,commit records,rollback records
- 单个事务执行的操作:update records
- 对恢复策略效率优化:checkpoint records(下一节)
每个日志记录着数据修改的完整信息,如:
- Transaction Id (事务 id)
- Object Id (数据记录 id):filename 和 pageid/blocknum
- Before Value (修改前的值),用于 undo 操作
- After Value (修改后的值),用于 redo 操作
3.Recovery
下面,讨论在没有 checkpoint 时发生的 recovery 流程。由于不考虑 checkpoint,因此我们需要遍历整个日志文件。
先发生redo再发生undo
- redo:从第一条日志开始从前往后遍历,根据 new value 来 redo 每条日志,包括已经提交或者没有提交、终止的事务。
- undo:对于没有打出 txn-end 的事务,会在 redo 阶段存储下来他们的最后一条日志的LSN,根据这些 LSN 和prevLSN,从后往前遍历,根据 old value 来 undo 没有写出 CLR 日志的记录,写出 CLR 日志。对于已经写出CLR 日志的记录,已经在 redo 阶段 undo 完成。
先发生undo再发生redo
- undo:从最后一条日志开始从后往前遍历,记录所有未打出 txn-end 的事务,然后根据 old value undo这些事务。记录所有已经 commit 的事务,在 redo 阶段遍历。
- redo:然后根据 new value 从前往后,redo所有已经 commit 的事务和 CLR 记录(或者可以在undo时就redo这些记录)。
一个日志可能不能同时满足能够 从前往后遍历 和 从后往前遍历,一个处理方法就是存储下来 LSN 和 日志文件中 offset 的映射。比如第二种方案在 undo 时记录下每个 commit 的事务的所有 lsn 和 offset 的映射,然后根据 lsn 递增的顺序 redo 每个日志。
undo 为了保证正确性,需要从后往前遍历。redo 为了保证正确性,需要从前往后遍历。
七、Recovery
由于我们并不知道数据库崩溃前,到底执行了哪些操作,因此系统恢复时通常需要遍历整个日志文件进行恢复。假如日志文件很大,恢复就需要很长的时间。不仅如此,在前面的恢复阶段可以看到即使是一些很久之前就 commited 、rollback 的事务,由于 WAL 的特性,我们不能确保所有脏页都已经落盘,因此需要重新 redo、undo,这无疑浪费了很多时间。
因此,checkpoint 就出现了。通过 checkpoint 我们可以简单地认为,在 checkpoints 之前的所有已经提交的事务,都已经落盘,不需要再进行 redo 了。
1.checkpoint
为了避免上述的情况发生,DBMS 需要定期的记录 checkpoint,也就是此时将所有日志记录和数据页都持久化到存储设备中,然后在日志中写入一条 ,表示前面所有的日志都不需要 redo。
也就是说一个 checkpoint 保证:
- 在 checkpoint 之前提交的事务的所有日志记录都已经落盘
- 这些日志记录对应的脏页都已经落盘
- 在checkpoint之前未提交的事务,根据不同 checkpoint 策略,可能还需要 redo
八、Transaction & 2PL
1.ACID
在 SQL语句中:
- 一个事务通过 BEGIN 开始执行
- 一个事务通过 COMMIT or ABORT 结束执行
如果事务 commit,那么 DBMS 需要永久保存这个事务的所有修改。如果事务 abort,那么 DBMS 需要撤销这个事务所有的修改。为了实现这些要求,事务需要4个特性:
- Atomicity:事务的操作是原子的,要么全部发生,要么全部不发
- Consistency:事务的结果要保持一致
- Isolation:每个事务都认为只有他一个事务在DBMS中允许
- Durability:如果一个事务commit,他所有的操作都要保存在磁盘中
1.1 Atomicity 原子性
事务执行只有两种可能:nothing or all。假如事务 abort 或者事务执行到一半时数据库发生崩溃,那么 DBMS 就需要回滚这个事务,以确保这个事务好像没有发生过。
1.2 Consistency 一致性
如果 DBMS 在 transaction 开始之前是 consistent,那么在执行完毕后也应当是 consistent。Transaction consistency 是 DBMS 必须保证的事情。
1.3 Isolation
DBMS 中同一时刻会有多个事务并发,每一个事务应该保证是相互隔离互不影响的,每个事务都会认为此时 DBMS 只为我服务!为了控制多个事务,DBMS 需要采用一些合理的并发策略,来保证事务执行的正确性,主要有两种策略:
- Pessimistic:不让问题出现,将问题扼杀在摇篮
- Optimistic:假设问题很罕见,一旦问题出现了再行处理
当事务并发时,假如事务并发执行的顺序能够等于这些事务按某种顺序顺序执行的结果,那么称这些事务的并发是 serializable schedule,称这两个 schedule 是 Serializable Schedule。
1.3.2 Conflicting Operations
在对 schedules 作等价分析前,需要了解 conflicting operations。当两个 operations 满足以下条件时,我们认为它们是 conflicting operations:
- 来自不同的 transactions
- 对同一个数据对象操作
- 两个 operations 至少有一个是 write 操作
那么就能够列举出集中情况:
- Read-Write Conflicts (R-W)
- Write-Read Conflicts (W-R)
- Write-Write Conflicts (W-W)
1.4 Durality
持久性就是 RecoveryManager 保证的,对于一个已经 commit 成功的事务,应该保证他的所有操作都落在磁盘中。假如采用的是 WAL 策略,为了保证持久性,在崩溃恢复的时候就需要 redo 一些操作。
2. Two Phase Locking
前面介绍了通过 WW、WR、RW conflicts 来判断一个 schedule 是否是 serializable 的方法,但使用该方法的前提是预先知道所有事务的执行流程,这与真实的数据库使用场景并不符合,主要原因在于:
请求连续不断。时时刻刻都有事务在开启、中止和提交
显式事务中,客户端不会一次性告诉数据库所有执行流程
通过并发策略就能够管理事务,是他们自动保证一个 schedule 是 serializable。不难想到,保证 schedule 正确性的方法就是合理的加锁 (locks) 策略,2PL 就是其中之一。
注意:2PL 保证正确性的方式是通过获得锁的顺序实现的,也就说我们没办法强制要求事务按照某个 serial schedule 并发,只能保证最终结果是正确的。
2PL,顾名思义,有两个阶段:growing 和 shrinking:
- 在 growing 阶段中,事务可以按需获取某条数据的锁,lock manager 决定同意或者拒绝。一旦开始释放锁就进了
- shrinking 阶段 在 shrinking 阶段中,事务只能释放之前获取的锁,不能获得新锁,即一旦开始释放锁,之后就只能释放锁。
3. DeadLock
2PL 和 强2PL 都无法解决死锁的问题,但是当死锁发生时,不解决就会出现无休止的等待。解决死锁一般有两种方法:
- Detection:事后检测
- Prevention:事前阻止