MySQL数据查询太多会OOM吗?

线上 MySQL 直接 Select 千万条的100G数据,服务器会裂开吗?

假设对某100G表t执行全表扫描,把扫描结果保存在客户端:

# 该语句无任何判断条件,所以全表扫描,查到的每行都可直接放到结果集,然后返给客户端
mysql -h$host -P$port -u$user -p$pwd -e 
	"select * from t" > $target_file

那这“结果集”存在哪的?

服务端其实无需保存一个完整结果集,实际上MySQL取、发数据流程如下:

  1. 获取一行,写到net_buffer

    该内存大小由参数net_buffer_length定义,默认16k

  2. 继续获取行,直到写满net_buffer,然后发出去

  3. 若发送成功,则清空net_buffer,继续读取下一行,并写入net_buffer

  4. 若发送函数返回EAGAINWSAEWOULDBLOCK,表示本地网络栈(socket send buffer)写满,进入等待。直到网络栈重新可写,再继续发送

以上过程执行流程图如下:

可以看出:

  • 一个查询在发送过程中,占用MySQL内部的内存最大就是net_buffer_length,根本达不到100G
  • 同理,socket send buffer 也不会达到100G(默认定义/proc/sys/net/core/wmem_default),若socket send buffer被写满,就会暂停读数据的流程

所以MySQL其实是边读取边发送,若客户端接收得比较慢,会导致MySQL Server由于结果发不出去,该事务的执行时间就会变得很长。如下状态,即当Client不读socket receive buffer内容时,在Server端show processlist的结果 — 服务端发送阻塞:

IdUserHostDbCommandTimeStateInfo
11rootLocalhost:64001TestQuery0Startingshow processlist
12rootlocalhost:64003TestQuery10sending to clientselect * from citizen

若State一直“Sending to client”,说明Server端网络栈满。若客户端使用–quick参数,会使用mysql_use_result:读一行处理一行。假设某业务较复杂,每读一行数据后要处理的逻辑很慢,就会导致客户端要过很久才能读取下一行数据,可能就会出现上表的结果。

正常的线上业务,若一个查询的返回结果不多,推荐使用mysql_store_result接口,直接把查询结果保存到本地内存。这前提是查询返回结果不多,若太多,因执行一个大查询,导致客户端占用内存近20G,这时就需改用mysql_use_result接口。

若你看到MySQL有很多线程处于“Sending to client”,就得优化业务的查询结果了。若想快速减少处于该状态下的线程,可将net_buffer_length设大些。

有时会看到很多查询语句的state是“Sending data”,这又是为啥?我们得知晓一个查询语句的状态变化:

  1. MySQL查询语句进入执行阶段后,先把状态置为 Sending data
  2. 然后,发送执行结果的列相关的信息(meta data) 给客户端(Sending data)
  3. 再继续执行语句的流程(Sending data)
  4. 执行完成后,把状态置成空串(“”)

所以“Sending data”并不一定指“正在发送数据”,可能是处于执行器过程中的任意阶段。可构造锁等待场景—读全表被锁:

session1session2
begin
select * from citizen where id=1 for update;
select * from citizen lock in share mode;
(blocked)

执行show processlist:

IdUserHostDbCommandTimeStateInfo
11rootLocalhost:64001TestSleep59StartingNull
12rootlocalhost:64003TestQuery3Sending dataselect * from citizen lock in share mode
13rootLocalhost:64004TestQuery0Startingshow processlist

可见session2在等锁,state为Sending data,即

  • 仅当一个线程处“等待客户端接收结果”的状态,才会显示"Sending to client"
  • 若显示成“Sending data”,只是表明“正在执行”

所以,查询结果是分段发给客户端的,因此扫描全表,即使查询返回大量数据,也不会把内存搞满。

以上都是server层处理逻辑,InnoDB引擎层又是如何处理的呢?

InnoDB如何处理全表扫描?

InnoDB内存的一个作用,是保存更新的结果,再配合redo log,避免随机写盘。

内存的数据页是在Buffer Pool (简称为BP)管理,在WAL里BP起加速更新的作用。
BP还能加速查询。

  • 由于WAL,当事务提交时,磁盘上的数据页是旧的,若这时马上有个查询来读该数据页,是不是要马上把redo log应用到数据页?
    不需要。因为此时,内存数据页的结果是最新的,直接读内存页即可。这时查询无需读磁盘,直接从内存取结果,速度很快。所以,Buffer Pool能加速查询。

而BP对查询的加速效果,依赖于一个重要的指标,即:内存命中率。
可以在show engine innodb status结果中,查看一个系统当前的BP命中率。一般情况下,一个稳定服务的线上系统,要保证响应时间符合要求的话,内存命中率要在99%以上。

执行show engine innodb status ,可以看到“Buffer pool hit rate”字样,显示的就是当前的命中率。比如下图命中率,就是100%。

若所有查询需要的数据页都能够直接从内存得到,那是最好的,对应命中率100%。

InnoDB Buffer Pool的大小是由参数 innodb_buffer_pool_size确定,一般建议设置成可用物理内存的60%~80%。

在大约十年前,单机的数据量是上百个G,而物理内存是几个G;现在虽然很多服务器都能有128G甚至更高的内存,但是单机的数据量却达到了T级别。

所以,innodb_buffer_pool_size小于磁盘数据量很常见。若一个 Buffer Pool满了,而又要从磁盘读入一个数据页,那肯定是要淘汰一个旧数据页的。

InnoDB内存管理

使用的最近最少使用 (Least Recently Used, LRU)算法,淘汰最久未使用数据。

  • 基本LRU算法
    TODO

InnoDB管理BP的LRU算法,是用链表实现的:

  • state1,链表头部是P1,表示P1是最近刚被访问过的数据页
  • 此时,一个读请求访问P3,因此变成状态2,P3被移到最前
  • 状态3表示,这次访问的数据页不存在于链表,所以需要在BP中新申请一个数据页Px,加到链表头。但由于内存已满,不能申请新内存。于是清空链表末尾Pm数据页内存,存入Px的内容,放到链表头部

最终就是最久没有被访问的数据页Pm被淘汰。
若此时要做一个全表扫描,会咋样?若要扫描一个200G的表,而这个表是一个历史数据表,平时没有业务访问它。

那么,按此算法扫描,就会把当前BP里的数据全部淘汰,存入扫描过程中访问到的数据页的内容。也就是说BP里主要放的是这个历史数据表的数据。

对于一个正在做业务服务的库,这可不行呀。你会看到,BP内存命中率急剧下降,磁盘压力增加,SQL语句响应变慢。

所以,InnoDB不能直接使用原始的LRU。InnoDB对其进行了优化。

  • 改进的LRU算法

InnoDB按5:3比例把链表分成New区和Old区。图中LRU_old指向的就是old区域的第一个位置,是整个链表的5/8处。即靠近链表头部的5/8是New区域,靠近链表尾部的3/8是old区域。

改进后的LRU算法执行流程:

  1. 状态1,要访问P3,由于P3在New区,和优化前LRU一样,将其移到链表头部 =》状态2
  2. 之后要访问一个新的不存在于当前链表的数据页,这时依然是淘汰掉数据页Pm,但新插入的数据页Px,是放在LRU_old
  3. 处于old区的数据页,每次被访问的时候都要做如下判断:
    • 若该数据页在LRU链表中存在的时间超过1s,就把它移动到链表头部
    • 若该数据页在LRU链表中存在的时间短于1s,位置保持不变。1s是由参数innodb_old_blocks_time控制,默认值1000,单位ms。

该策略,就是为了处理类似全表扫描的操作量身定制。还是扫描200G历史数据表:
4. 扫描过程中,需要新插入的数据页,都被放到old区域
5. 一个数据页里面有多条记录,这个数据页会被多次访问到,但由于是顺序扫描,这个数据页第一次被访问和最后一次被访问的时间间隔不会超过1秒,因此还是会被保留在old区域
6. 再继续扫描后续的数据,之前的这个数据页之后也不会再被访问到,于是始终没有机会移到链表头部(New区),很快就会被淘汰出去。

可以看到,这个策略最大的收益,就是在扫描这个大表的过程中,虽然也用到了BP,但对young区完全没有影响,从而保证了Buffer Pool响应正常业务的查询命中率。

总结

MySQL采用的是边算边发的逻辑,因此对于数据量很大的查询结果来说,不会在server端保存完整的结果集。所以,如果客户端读结果不及时,会堵住MySQL的查询过程,但是不会把内存打爆。

而对于InnoDB引擎内部,由于有淘汰策略,大查询也不会导致内存暴涨。并且,由于InnoDB对LRU算法做了改进,冷数据的全表扫描,对Buffer Pool的影响也能做到可控。

全表扫描还是比较耗费IO资源的,所以业务高峰期还是不能直接在线上主库执行全表扫描的。

参考

  • 《MySQL实战45讲》
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值