1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| InnoDB使用buffer pool来缓存从磁盘读取到内存的数据页.buffer pool通常由数个内存块加上一组控制结构体对象组成.内存块的个数取决于buffer pool instance的个数,不过在5.7版本中开始默认以128M(可配置)的chunk单位分配内存块,这样做的目的是为了支持buffer pool的在线动态调整大小.
Buffer pool的每个内存块通过mmap的方式分配内存,因此你会发现,在实例启动时虚存很高,而物理内存很低.这些大片的内存块又按照16KB划分为多个frame,用于存储数据页.
虽然大多数情况下buffer pool是以16KB来存储数据页,但有一种例外: 使用压缩表时,需要在内存中同时存储压缩页和解压页,对于压缩页,使用Binary buddy allocator算法来分配内存空间.例如我们读入一个8KB的压缩页,就从buffer pool中取一个16KB的block,取其中8KB,剩下的8KB放到空闲链表上,如果紧跟着另外一个4KB的压缩页读入内存,就可以从这8KB中分裂4KB,同时将剩下的4KB放到空闲链表上.
为了管理buffer pool,每个buffer pool instance使用如下几个链表来管理: * LRU链表包含所有读入内存的数据页 * Flush_list包含被修改过的脏页 * unzip_LRU包含所有解压页 * Free list上存放当前空闲的block
另外为了避免查询数据页时扫描LRU,还为每个buffer pool instance维护了一个page hash,通过space id和page no可以直接找到对应的page
一般情况下,当我们需要读入一个Page时,首先根据space id 和page no找到对应的buffer pool instance.然后查询page hash,如果page hash中没有,则表示需要从磁盘读取.在读盘前首先我们需要为即将读入内存的数据页分配一个空闲的block.当free list上存在空闲的block时,可以直接从free list上摘取,如果没有,就需要从unzip_lru 或者 lru上驱逐page.
这里需要遵循一定的原则(参考函数buf_LRU_scan_and_free_block , 5.7.5): 首先尝试从unzip_lru上驱逐解压页,如果没有,再尝试从Lru链表上驱逐Page;如果还是无法从Lru上获取到空闲block,用户线程就会参与刷脏,尝试做一次SINGLE PAGE FLUSH,单独从Lru上刷掉一个脏页,然后再重试.Buffer pool中的page被修改后,不是立刻写入磁盘,而是由后台线程定时写入,和大多数数据库系统一样,脏页的写盘遵循日志先行WAL原则,因此在每个block上都记录了一个最近被修改时的Lsn,写数据页时需要确保当前写入日志文件的redo不低于这个Lsn.
然而基于WAL原则的刷脏策略可能带来一个问题: 当数据库的写入负载过高时,产生redo log的速度极快,redo log可能很快到达同步checkpoint点.这时候需要进行刷脏来推进Lsn.由于这种行为是由用户线程在检查到redo log空间不够时触发,大量用户线程将可能陷入到这段低效的逻辑中,产生一个明显的性能拐点.
|