14 MYSQL 全表扫描
全表扫描对 MySQL 有什么影响
1. 对 server 层的影响
1.1 发送数据的流程
取数据和发数据的流程是这样的:
- 获取一行,写到 net_buffer 中。这块内存的大小是由参数 net_buffer_length 定义的,默认是 16k。
- 重复获取行,直到 net_buffer 写满,调用网络接口发出去。
- 如果发送成功,就清空 net_buffer,然后继续取下一行,并写入 net_buffer。
- 如果发送成功,就清空 net_buffer,然后继续取下一行,并写入 net_buffer。
可以看到:
- 一个查询在发送过程中,占用的 MySQL 内部的内存最大就是 net_buffer_length 这么大,并不会达到 200G;
- socket send buffer 也不可能达到 200G(默认定义 /proc/sys/net/core/wmem_default),如果 socket send buffer 被写满,就会暂停读数据的流程。
MySQL 是“边读边发的”,这就意味着,如果客户端接收得慢,会导致 MySQL 服务端由于结果发不出去,这个事务的执行时间变长。
1.2 查询语句的状态变化
一个查询语句的状态变化是这样的:
- MySQL 查询语句进入执行阶段后,首先把状态设置成“Sending data”;
- 然后,发送执行结果的列相关的信息(meta data) 给客户端;
- 再继续执行语句的流程;
- 执行完成后,把状态设置成空字符串。
也就是说:
- “Sending data”并不一定是指“正在发送数据”,而可能是处于执行器过程中的任意阶段。比如,锁等待的场景
- 仅当一个线程处于“等待客户端接收结果”的状态,才会显示"Sending to client"
1.3 客户端接收数据慢对事务的影响
如果客户端使用–quick 参数,会使用 mysql_use_result 方法。这个方法是读一行处理一行。假设有一个业务的逻辑比较复杂,每读一行数据以后要处理的逻辑如果很慢,就会导致客户端要过很久才会去取下一行数据,可能就会出现如图 2 所示的 “Sending to client” 这种情况。
对于正常的线上业务来说,如果一个查询的返回结果不会很多的话,我都建议你使用 mysql_store_result 这个接口,直接把查询结果保存到本地内存。当然前提是查询返回结果不多。
另一方面,如果你在自己负责维护的 MySQL 里看到很多个线程都处于“Sending to client”这个状态,就意味着你要让业务开发同学优化查询结果,并评估这么多的返回结果是否合理。
而如果要快速减少处于这个状态的线程的话,将 net_buffer_length 参数设置为一个更大的值是一个可选方案。
2. 全表扫描对 InnoDB 的影响
Innodb 内存的数据页是在 Buffer Pool (BP) 中管理的,在 WAL 里 Buffer Pool 起到了两个作用:
- 加速更新
- 加速查询
而 Buffer Pool 对查询的加速效果,依赖于一个重要的指标,即:内存命中率。一般情况下,一个稳定服务的线上系统,要保证响应时间符合要求的话,内存命中率要在 99% 以上。
show engine innodb status
的 Buffer pool hit rate
显示的就是当前的命中率。
2.1 innodb_buffer_pool_size
InnoDB Buffer Pool 的大小是由参数 innodb_buffer_pool_size 确定的,一般建议设置成可用物理内存的 60%~80%。
InnoDB 内存管理用的是最近最少使用 (Least Recently Used, LRU) 算法,这个算法的核心就是淘汰最久未使用的数据。为了避免一次全表扫描导致所有的缓存失效,InnoDB 对 LRU 算法做了改进。
在 InnoDB 实现上,按照 5:3 的比例把整个 LRU 链表分成了 young 区域和 old 区域。图中 LRU_old 指向的就是 old 区域的第一个位置,是整个链表的 5/8 处。也就是说,靠近链表头部的 5/8 是 young 区域,靠近链表尾部的 3/8 是 old 区域。LRU 算法执行流程如下:
- 图 7 中状态 1,要访问数据页 P3,由于 P3 在 young 区域,将其移到链表头部,变成状态 2
- 之后要访问一个新的不存在于当前链表的数据页,这时候依然是淘汰掉数据页 Pm,但是新插入的数据页 Px,是放在 LRU_old 处
- 处于 old 区域的数据页,每次被访问的时候都要做下面这个判断:
- 若这个数据页在 LRU 链表中存在的时间超过了 1 秒,就把它移动到链表头部;
- 如果这个数据页在 LRU 链表中存在的时间短于 1 秒,位置保持不变。
- 1 秒这个时间,是由参数 innodb_old_blocks_time 控制的。其默认值是 1000,单位毫秒
因此进入yong区域的缓存需要满足两个条件:
- 已经存在 old 缓存内
- 被第二次访问
- 两次访问之间的间隔超过 1s
这个策略,就是为了处理类似全表扫描的操作量身定制的。扫描大表的过程中,虽然也用到了 Buffer Pool,但是对 young 区域完全没有影响,从而保证了 Buffer Pool 响应正常业务的查询命中率。
全表扫描还是比较耗费 IO 资源的,所以业务高峰期还是不能直接在线上主库执行全表扫描的。