我的数据库内存只有10G,现在我要对一个20G的大表做全表扫描,会不会把数据库内存(buffer pool)占满然后报OOM的错误?
答案是不会的。
InnoDB的数据是保存在主键索引上面的(主键索引最全),所以全表扫描扫描的是主键索引,由于没有其他的判定条件,所以查询到的每一行都放到结果集里面,然后返回给客户端。
这个结果集在哪呢?
实际上,服务端并不会保存一个完成的结果集之后在发送。 取数据和发数据的流程是这样的:
这个过程对应的流程图如下:
看完上面的流程图,可以得到:
也就是说 MySQL 是边读边发的,如果客户端接收的速度跟不上 MySQL 服务端发送的速度,就会造成发送的时间变长。
如果你看到 State 的值一直处于“Sending to client”,就表示服务器端的网络栈写满了。
如果客户端使用–quick
参数,会使用 mysql_use_result 方法。这个方法是读一行处理一行。你可以想象一下,假设有一个业务的逻辑比较复杂,每读一行数据以后要处理的逻辑如果很慢,就会导致客户端要过很久才会去取下一行数据,可能就会出现如图所示(Sending to client)的这种情况。
因此,对于正常的线上业务来说,如果一个查询的返回结果不会很多的话,建议你使用 mysql_store_result 这个接口,直接把查询结果保存到本地内存(客户端本地)。
如果要快速减少处于这个状态的线程的话,将 net_buffer_length 参数设置为一个更大的值是一个可选方案。
与“Sending to client”长相很类似的一个状态是“Sending data” ,但是经过排查网络没有问题。
实际上,一个查询语句的状态变化是这样的:
也就是说,“Sending data”并不一定是指“正在发送数据”,而可能是处于执行器过程中的任意阶段。
仅当一个线程处于“等待客户端接收结果”的状态,才会显示"Sending to client";而如果显示成“Sending data”,它的意思只是“正在执行”。
在 server 层的处理逻辑我们都清楚了,在 InnoDB 引擎里面又是怎么处理的呢? 扫描全表会不会对引擎系统造成影响呢?
全表扫描对 InnoDB 的影响
内存的数据页是在 Buffer Pool (BP) 中管理的,在 WAL 里 Buffer Pool 起到了加速更新的作用。而实际上,Buffer Pool 还有一个更重要的作用,就是加速查询。
而 Buffer Pool 对查询的加速效果,依赖于一个重要的指标,即:内存命中率。
可以在 show engine innodb status
结果中,查看一个系统当前的 BP 命中率。一般情况下,一个稳定服务的线上系统,要保证响应时间符合要求的话,内存命中率要在 99% 以上。
InnoDB Buffer Pool 的大小是由参数 innodb_buffer_pool_size 确定的,一般建议设置成可用物理内存的 60%~80%。
在大约十年前,单机的数据量是上百个 G,而物理内存是几个 G;现在虽然很多服务器都能有 128G 甚至更高的内存,但是单机的数据量却达到了 T 级别。
所以,innodb_buffer_pool_size 小于磁盘的数据量是很常见的。如果一个 Buffer Pool 满了,而又要从磁盘读入一个数据页,那肯定是要淘汰一个旧数据页的。
InnoDB淘汰的逻辑是怎样的呢?
InnoDB 内存管理用的是最近最少使用 (Least Recently Used, LRU) 算法,这个算法的核心就是淘汰最久未使用的数据。
下图是一个 LRU 算法的基本模型。
InnoDB 管理 Buffer Pool 的 LRU 算法,是用链表来实现的。
这个算法乍一看上去没什么问题,但是如果考虑到要做一个全表扫描,会不会有问题呢?
假设按照这个算法,我们要扫描一个 20G 的表,而这个表是一个历史数据表,平时没有业务访问它。那么,按照这个算法扫描的话,就会把当前的 Buffer Pool 里的数据全部淘汰掉,存入扫描过程中访问到的数据页的内容。
也就是说 Buffer Pool 里面主要放的是这个历史数据表的数据。对于一个正在做业务服务的库,这可不妙。你会看到,Buffer Pool 的内存命中率急剧下降,磁盘压力增加,SQL 语句响应变慢。
所以,InnoDB 不能直接使用这个 LRU 算法。实际上,InnoDB 对 LRU 算法做了改进。
在 InnoDB 实现上,按照 5:3 的比例把整个 LRU 链表分成了 young 区域和 old 区域。图中 LRU_old 指向的就是 old 区域的第一个位置,是整个链表的 5/8 处。也就是说,靠近链表头部的 5/8 是 young 区域,靠近链表尾部的 3/8 是 old 区域。
改进后的 LRU 算法执行流程变成了下面这样。
可以看到,这个策略最大的收益,就是在扫描这个大表的过程中,虽然也用到了 Buffer Pool,但是对 young 区域完全没有影响,从而保证了 Buffer Pool 响应正常业务的查询命中率。