这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战
OLAP, OLTP, HTAP都是数据库的workloads的不同类型。
How the DBMS manages its memory and move data back-and-forth from disk?
- 空间上:尽量将可能一起使用的数据磁盘上物理距离接近
- 时间上:何时可以从内存读取,何时需要写入disk;减少不得不从磁盘读取数据的次数
1. locks & latches
lock
- 在事务中保护数据逻辑内容
- 在事务过程中要held
- 需要能够回滚
latch
- 隔离数据,mutex访问
- 操作过程中要held
- 不需要回滚
latch是轻量级的锁,它锁的是资源,比如内存中的某一块区域;只工作在内存中;用来快速短时间的锁定资源;阻止多个进程同时访问某个共享资源;只能被当前实例访问,是瞬间的占用。非入队,没有死锁。
lock是更高层级的锁,它直接锁DB中的数据,比如一个table,一个tuple等等。 需要等到事务正确的结束才会释放,可能需要回滚;需要入队,存在死锁;可以理解为os中的mutex。
2. allocation policy
global policy
针对所有active txns(事务,transactions)进行调度优化
local policy
需要对特定某个txn进行优化,就只考虑他,而不考虑其他并发中的txns。
3. Buffer pool
Buffer pool organization
内存中的区域也是按照固定大小的块来划分,和外存中的page大小相同,称为帧(frame)。
读取page到内存的时候,是把每个page的一个副本存入物理内存的栈帧中。使用页表进行索引。
和OS的内存管理思路相似。
Buffer pool meta-data
页表来记录当前内存中的page,通过页表来检索在内存中实际的位置。
Buffer pool存储元数据:
-
page table: 页表
- 注意,前面的 page directory是用来记录page id到page location的映射的。这里的location指的是在db file中的位置。
- 这里页表是用来记录page id到内存中(buffer pool)中的对应page 位置的映射的。
每个page各自存储的元数据:
- dirty-flag:脏标志位。需要写回disk
- pin/Reference counter:当前正在访问该page的线程数。
Buffer pool potimizations
multiple buffer pools
通常用多个缓冲池实例,比如每个DB,每种page-type;
用于减少latch冲突:可能多个线程在同时访问多个不同的表,这样它们可以一起进行,而不用竞争latch。
和增强局部性。
带来的问题是,我们需要知道每个buffer pool在管理哪些数据。两种方式:
- 在record id中嵌入指向特定buffer pool的标识符(这样每一个id就记录了object id, page id, slotNum);
- 使用一个hash表来维护page id到buffer pool的映射关系;
pre-fetching
预先读取当前正在读的page的后面的page,一次性加载到内存中。局部性比较好的查询,或者一般需要顺序scan多个page的操作,都很友好。
这可以依赖query plan来实现。
- 例如,query plan发现需要读page0 - page4,那么首先page0读入内存,在处理page0中的数据的时候,就可以预先把剩余的部分也读入内存,这样当处理完page0之后,想访问page1中的数据就不用等着它载入内存了。
- 实际上,操作系统就可以完成这个工作,mmap就可以预加载后面的内容。但这是因为这种情况比较简单,这个query要求的内存是连续的,所以OS即使不知道我们query的内容也可以(阴差阳错的)完成优化。
如果query需要的是某个特定的数据范围,而我们知道每一个index-page中的数据范围,如下图,我们按照树结构查询page0, page1之后,我们知道随后需要page3和page5中的数据,这时候就可以去预加载page3和page5;但是OS不知道每一个page中的范围,所以OS在读完page1之后会(顺理成章的)预加载page2和page3,这时候page2的加载就是一次无用的、浪费的操作。
所以这是DBMS可以完成,但OS不能完成的事情,的一个例子。
——也对应了前面所说的,OS is not our friend.
select * from A
where val between 100 and 250
scan sharing
不同的查询可以共享某些scan或操作的中间结果。
两个查询A,B 都在 FROM t1上进行scan。A执行到scan了5个page以后,B开始启动,那么B直接跟着A的cursor,从page 5开始遍历,记录遍历后的结果,A完成以后,B回到头,再去加载0-4的page。
buffer pool bypass(旁路)
顺序扫描的时候,通常每个page只使用一次,短期内不会重复使用;所以这种查询出现的时候,为他单独分配一块小的buffer pool区域,已经使用过的page不再存储,从而防止整个内存被这一个顺序扫描占满,引发后续频繁的内存空间置换。
(avoid)OS page cache
多数磁盘操作都依赖OS API。
多数DBMS使用direct IO来避免经过os的page cache,以防止在OS的cache中存储冗余的page,以及二者不同的数据回收策略。
4. Buffer replacement policies
替换的目标:正确性,准确性,速度,元数据覆盖。
- LRU
- clock 类似LRU,访问置1,置换的时候遇到1就改成0,遇到0就丢弃。
这两种方法是OS中常用的算法,但是在顺序scan上是非常不好的,因为通常刚刚读过的page是最不需要的。
Sequential flooding 问题,也就是前面buffer pool bypass部分提到的那种情况。
LRU-K
保留最后k次的时间戳(每个PAGE)
Localization
(感觉翻译不准确,直接贴个slides原文)
The DBMS chooses which pages to evict on a per txn/query basis. This minimizes the pollution of the buffer pool from each query.
这就需要维护每个query已经访问的pages列表。
Priority hints
DBMS知道每个page的上下文信息,可以用来作为参考,以判断每个page的优先级和重要程度。
dirty page
- 对于内存中的非脏页,可以直接丢弃,不用写回磁盘
5. 总结
这一节的内容,通过一些实际的场景,说明了为什么相对操作系统来说,DBMS可以更好的掌控内存和数据。
访问内存远远快于访问磁盘,但内存的空间是有限的,所以我们想好好的利用内存。于是有了一些优化措施:
- 多设置几个内存的缓冲池,这样在访问不同的表时,就不用每次都对同一个缓冲区上锁进行并发控制,减少并发等待的时间;
- 预加载,当从磁盘中读取某个page的时候,DBMS用它的“智慧”来判断有哪些page是之后会需要但还没有装到内存中的。为什么DBMS有这个智慧呢?因为它知道每个page中存储了哪些数据,以及它通过query plan知道哪些数据是能够用得到的。OS能干啥,OS只能傻眼看着。
- 共享结果,如果多个query都要scan同一个表,那么后来的query可以借用前面的query的scan result,前人栽树,后人乘凉。
- 有的时候用得到局部性,有的时候用不到。对于顺序扫描某个表这样的需求,显然每一个page在这段时间内只需要用到一次,扫描完就完了,那么就没必要将扫描过的page还存在内存中。所以我的缓冲区只给这样的扫描任务提供一小块地方,用过的page就丢掉,没有价值了。防止你占满整个内存,耽误事儿。
- OS既然没用,那么OS自作多情的访问磁盘数据加载到内存时自动的cache也不需要了。
The DBMS can manage that sweet, sweet memory better than the OS.
--by leveraging the semantics about the query plan to make better decisions: Evictions, Allocations, Pre-fetching.