本笔记知识沿用之前DBNotes: Buffer Pool对于缓冲页的链表式管理的部分知识
目录
- 获取一个空闲页的源码逻辑
- Page_Cleaner_Thread
- LRU_Manager_Thread
- Hazard Pointer作为驱逐算法改进
- 参考
获取一个空闲页的源码逻辑
任何一个读写请求都需要从Buffer pool来获取所需页面。如果需要的页面已经存在于Buffer pool,那么直接利用当前页面进行操作就行。但是如果所需页面不在Buffer pool,比如UPDATE操作,那么就需要从Buffer pool中新申请空闲页面,将需要读取的数据放到Buffer pool中进行操作。
如何从buffer pool中获取一个页面呢?这依赖于buf_LRU_get_free_block函数,该函数会循环尝试去淘汰LRU list上的页面。每次循环都会访问freelist,查看是否有足够的空闲页面,如果没有,就继续从LRUlist去淘汰。这样的循环在负载较高的时候会加剧对freelist以及LRUlist的mutex的竞争。可以设置buf_pool->try_LRU_scan是做了一个优化,如果当前用户线程扫描的时候 发现没有空闲页面,那么其他用户线程就不需要进行同样的扫描。
MySQL的free页面的获取依赖于Page_Cleaner_Thread的刷新能力,如果刷新不及时,那么系统就会使用上面所说的循环逻辑来为用户线程申请空闲页面,可以看出是十分耗时间的。而如果刷新过快,也会导致性能问题,因为刷新是需要io操作的。
所以引入独立的线程负责LRU list的刷脏。目的是为了让独立线程根据系统负载动态调整LRU的刷脏能力。由于LRU list的刷脏从page cleaner线程中脱离出来,调整LRU list的刷脏能力不再会影响到page cleaner。
同时由于单线程LRUlist刷脏存在问题,设计者进行了改进。继续将LRU list独立于page cleaner threads并将LRU list单线程刷脏增加为多线程刷脏。page cleaner只负责flush list的刷脏,lru_manager_thread只负责LRU List刷脏。这样的分离,可以使得LRU list刷脏和Flush List刷脏并行执行。
Page_Cleaner_Thread
主要负责flushlist的刷脏,避免用户线程同步刷脏页。
也是每隔一定时间刷一次脏页,sleep time是自适应的,依赖于当前的lsn,flushlist中的oldest_modification以及当前的同步刷脏点。
与LRU_Manager_Thread不同,该线程每次执行刷的脏页数量也是自适应的,依赖于当前系统中脏页的比率,日志产生的速度以及几个参数。
LRU_Manager_Thread
一个系统线程,随着InnoDB启动而work,作用是定期清理出空闲的数据页(数量为innodb_LRU_scan_depth)并加入到Freelist中,防止用户线程去做同步刷脏影响效率。
该线程每隔一段时间就去FLUSH。先尝试从LRU中驱逐部分数据页,如果数量不够就从Flushlist中驱逐。
线程执行频率是自适应的:
设定max_free_len = innodb_LRU_scan_depth * innodb_buf_pool_instances。
如果Freelist中的数量小于max_free_len 的1%,则sleep time = 0,表示这时候空闲页太少了,需要一直执行buf_flush_LRU_tail操作,从而腾出空闲的数据页。
如果Free List中的数量介于max_free_len的1%-5%,则sleep time减少50ms(默认为1000ms),如果Free List中的数量介于max_free_len的5%-20%,则sleep time不变,如果Free List中的数量大于max_free_len的20%,则sleep time增加50ms,但是最大值不超过rds_cleaner_max_lru_time
。
Hazard Pointer作为驱逐算法改进
在学术上,Hazard Pointer是一个指针,如果这个指针被一个线程所占有,在它释放之前,其他线程不能对他进行修改,但是在InnoDB里面,概念刚好相反,一个线程可以随时访问Hazard Pointer,但是在访问后,他需要调整指针到一个有效的值,便于其他线程使用。我们用Hazard Pointer来加速逆向的逻辑链表遍历。 先来说一下这个问题的背景,我们知道InnoDB中可能有多个线程同时作用在Flush List上进行刷脏,例如LRU_Manager_Thread和Page_Cleaner_Thread。同时,为了减少锁占用的时间,InnoDB在进行写盘的时候都会把之前占用的锁给释放掉。这两个因素叠加在一起导致同一个刷脏线程刷完一个数据页A,就需要回到Flush List末尾(因为A之前的脏页可能被其他线程给刷走了,之前的脏页可能已经不在Flush list中了),重新扫描新的可刷盘的脏页。另一方面,数据页刷盘是异步操作,在刷盘的过程中,我们会把对应的数据页IO_FIX住,防止其他线程对这个数据页进行操作。我们假设某台机器使用了非常缓慢的机械硬盘,当前Flush List中所有页面都可以被刷盘(buf_flush_ready_for_replace
返回true)。我们的某一个刷脏线程拿到队尾最后一个数据页,IO fixed,发送给IO线程,最后再从队尾扫描寻找可刷盘的脏页。在这次扫描中,它发现最后一个数据页(也就是刚刚发送到IO线程中的数据页)状态为IO fixed(磁盘很慢,还没处理完)所以不能刷,跳过,开始刷倒数第二个数据页,同样IO fixed,发送给IO线程,然后再次重新扫描Flush List。它又发现尾部的两个数据页都不能刷新(因为磁盘很慢,可能还没刷完),直到扫描到倒数第三个数据页。所以,存在一种极端的情况,如果磁盘比较缓慢,刷脏算法性能会从O(N)退化成O(N*N)。 要解决这个问题,最本质的方法就是当刷完一个脏页的时候不要每次都从队尾重新扫描。我们可以使用Hazard Pointer来解决,方法如下:遍历找到一个可刷盘的数据页,在锁释放之前,调整Hazard Pointer使之指向Flush List中下一个节点,注意一定要在持有锁的情况下修改。然后释放锁,进行刷盘,刷完盘后,重新获取锁,读取Hazard Pointer并设置下一个节点,然后释放锁,进行刷盘,如此重复。当这个线程在刷盘的时候,另外一个线程需要刷盘,也是通过Hazard Pointer来获取可靠的节点,并重置下一个有效的节点。通过这种机制,保证每次读到的Hazard Pointer是一个有效的Flush List节点,即使磁盘再慢,刷脏算法效率依然是O(N)。 这个解法同样可以用到LRU List驱逐算法上,提高驱逐的效率。
参考
MySQL · 源码分析 · Innodb缓冲池刷脏的多线程实现
MySQL · 源码分析 · InnoDB LRU List刷脏改进之路
MySQL · 引擎特性 · InnoDB Buffer Pool