大家好,我是 Snow Hide,作为《MySQL 实战》这个专栏的学员之一,这是我打卡的第 28 天,也是我第 91 次进行这种操作。
今天我温习了该专栏里一篇叫《我查这么多数据,会不会把数据库内存打爆?》的文章。
关键词总结:全表扫描对 server 层的影响(取数据和发数据的流程、流程细节、边读边发、Sending to client、查询结果不多的情况、Sending data、查询语句状态变化、)、全表扫描对 InnoDB 的影响(内存数据页管理、Buffer Pool 对查询加速所依赖的指标、系统当前 BP 命中率、InnoDB Buffer Pool 大小设置、InnoDB 内存管理所使用的算法、LRU 算法的链表实现、InnoDB 改进之后的 LRU 算法(流程、逻辑))。
所学总结:
全表扫描对 server 层的影响
取数据和发数据的流程
- 获取一行,写到
net_buffer
中。这块内存的大小是由参数net_buffer_length
定义的,默认是 16K; - 重复获取行,直到
net_buffer
写满,调用网络接口发出去; - 如果发送成功,就清空
net_buffer
,然后继续取下一行,并写入net_buffer
; - 如果发送函数返回
EAGAIN
或WSAEWOULDBLOCK
,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送。
流程细节
- 一个查询在发送过程中,占用的 MySQL 内部的内存最大就是 net_buffer_length 的大小,并不会达到表的大小;
- socket send buffer 也不可能达到表的大小(默认定义
/proc/sys/net/core/vmem_default
),如果 socket send buffer 被写满,就会暂停读数据的流程。
边读边发
如果客户端接收得慢,会导致 MySQL 服务端由于结果发不出去,事务执行时间变长。
Sending to client
等待客户端接收结果,表示服务器端的网络栈写满了。
将 net_buffer_length
参数设置为一个更大的值是一个可选方案。
查询结果不多的情况
建议使用 mysql_store_result
接口,直接把查询结果保存到本地内存。
Sending data
正在执行,可能是处于执行器过程中的任意阶段。
查询语句状态变化
- MySQL 查询语句进入执行阶段后,首先把状态设置成 “Sending data”;
- 然后,发送执行结果的列相关的信息(meta data)给客户端;
- 再继续执行语句的流程;
- 执行完成后,把状态设置成空字符串。
全表扫描对 InnoDB 的影响
内存数据页管理
内存数据页是在 Buffer Pool(BP)中管理的,在 WAL 里 Buffer Pool 起到了加速更新的作用。而实际上,Buffer Pool 还有一个更重要的作用,就是加速查询。
Buffer Pool 对查询加速所依赖的指标
内存命中率。
系统当前 BP 命中率
- 在
show engine innodb status
结果中查看系统当前的 BP 命中率; - 一个稳定服务的线上系统,要保证响应时间符合要求的话,内存命中率要在 99% 以上;
- 执行
show engine innodb status
后看到的 “Buffer pool hit rate” 滋养就是当前的命中率。
InnoDB Buffer Pool 大小设置
由参数 innodb_buffer_pool_size
确定的。一般建议设置成可用物理内存的 60%~80%。
InnoDB 内存管理所使用的算法
最近最少使用(LRU,Least Recently Used)算法,淘汰最久未使用的数据。
LRU 算法的链表实现
- 链表头部是最近刚刚被访问过的数据页;假设内存里只能放下一定量的数据页;
- 这时有个读请求访问排名第二的数据页,因此其被移到最前;
- 当访问的数据页不存在于链表中时,需要在 Buffer Pool 中新申请一个数据页并添加到链表头部。但由于内存已经满了,不能申请新的内存。于是,会清空链表末尾数据页的内存,存入新的内容,然后放到链表头部;
- 效果上是最久没有被访问的数据页被淘汰了。
InnoDB 改进之后的 LRU 算法
流程
- 要访问排名 3 的数据页,由于其在 young 区域,因此和优化前的 LRU 算法一样,将其移到链表头部;
- 之后要访问一个新的不存在于当前链表的数据页,这时候依然是淘汰掉最后的数据页,但是新插入的数据页,是放在 LRU_old 处;
- 处于 old 区域的数据页,每次被访问的时候都要做下面的判断:
- 若这个数据页在 LRU 链表中存在的时间超过了 1 秒,就把它移动到链表头部;
- 如果这个数据页在 LRU 链表中存在的时间短于 1 秒,位置保持不变。1 秒这个时间,是由参数
innodb_old_blocks_time
控制的。其默认值是 1000,单位毫秒。
逻辑
- 扫描过程中,需要新插入的数据页,都被放到 old 区域;
- 一个数据页里面有多条记录,这个数据页会被多次访问到,但由于是顺序扫描,这个数据页第一次被访问和最后一次被访问的时间间隔不会超过 1 秒,因此还是会被保留在 old 区域;
- 再继续扫描后续的数据,之前的这个数据页之后也不会再被访问到,于是始终没有机会移到链表头部(也就是 young 区域),很快就会被淘汰出去。
末了
重新总结了一下文中提到的内容:MySQL 的查询结果、发送给客户端的过程、边算边发的逻辑、不在 server 端保存完整的结果集、会堵住 MySQL 查询过程但不会打爆内存、InnoDB 引擎内部由于淘汰策略的存在所以大查询不会导致内存暴涨、InnoDB 对 LRU 算法做的改进使冷数据全表扫描对 Buffer Pool 的影响可控、业务高峰期还是不能在主库执行全表扫描。