(学习笔记-内存管理)如何避免预读失效和缓存污染的问题?

这篇具有很好参考价值的文章主要介绍了(学习笔记-内存管理)如何避免预读失效和缓存污染的问题?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

传统的LRU算法存在这两个问题:

  • 预读失效 导致的缓存命中率下降
  • 缓存污染 导致的缓存命中率下降

Redis的缓存淘汰算法是通过实现LFU算法来避免 [缓存污染] 而导致缓存命中率下降的问题(redis 没有预读机制)

Mysql 和 Linux操作系统是通过改进LRU算法来避免 [预读失效和缓存污染] 而导致缓存命中率下降的问题。


Linux和MySQL的缓存

Linux操作系统的缓存

在应用程序读取文件的数据的时候,Linux操作系统是会对读取的文件数据进行缓存的,会缓存在文件系统中的 Page Cache(页缓存):

(学习笔记-内存管理)如何避免预读失效和缓存污染的问题?,操作系统,缓存

 Page Cache 属于内存空间里的数据,由于内存访问比磁盘访问快很多,在下一次访问相同的数据就不需要通过I/O了,命中缓存就直接返回数据即可。

因此,Page Cache 起到了加速访问数据的作用。

MySQL的缓存

MySQL的数据是存储在磁盘里的,为了提升数据库的读写性能,Innodb存储引擎设计了一个缓冲池( Buffer Pool),Buffer Pool 属于内存空间里的数据。

(学习笔记-内存管理)如何避免预读失效和缓存污染的问题?,操作系统,缓存

 有了缓冲池后:

  • 当读取数据时,如果数据存在于Buffer Pool中,客户端就会直接读取Buffer Pool中的数据,否则再去磁盘中读取。
  • 当修改数据时,首先是修改Buffer Pool中数据所在的页,然后将其页设置为脏页,最后由后台线程将脏页写入到磁盘。

传统LRU是如何管理内存数据的?

Linux的Page Cache 和 MySQL的Buffer Pool的大小是有限的,并不能无限地缓存数据,对于一些频繁访问的数据我们希望可以一直留在内存中,而一些很少访问的数据希望可以在某些时机可以淘汰掉,从而保证内存不会因为满了而导致无法再缓存新的数据,同时还能保证常用数据留在内存中。

最容易想到的就是LRU(Least recently used)算法。

LRU算法一般是用 [链表] 作为数据结构来实现的,链表头部的数据是最近使用的,而链表末尾的数据是最久没被使用的。当空间不够了,就淘汰最久没被使用的节点,也就是链表末尾的数据,从而腾出内存空间。

因为 Linux 的 Page Cache 和 MySQL 的 Buffer Pool 缓存的基本数据单位都是页(Page)单位,所以后续以「页」名称代替「数据」

传统的LRU算法的实现思路:

  • 当访问的页在内存中,就直接把该页对应的LRU链表节点移动到链表的头部
  • 当访问的页不在内存里,除了要把该页放入到LRU链表的头部,还要淘汰LRU链表末尾的页。

比如下图,假设LRU链表长度为 5 ,LRU链表从左到右有编号为1,2,3,4,5的页。

(学习笔记-内存管理)如何避免预读失效和缓存污染的问题?,操作系统,缓存

 如果访问了3号页,因为3号页已经在内存了,所以把3号页移动到链表头部即可,表示最近被访问了。

(学习笔记-内存管理)如何避免预读失效和缓存污染的问题?,操作系统,缓存

 接下来如果访问了 8 号页,因为8号页不在内存中,而且LRU链表的长度为5,所以必须要淘汰数据,以腾出内存空间缓存 8 号页,于是就会淘汰末尾的 5 号页,然后再将 8 号页插入到头部:

(学习笔记-内存管理)如何避免预读失效和缓存污染的问题?,操作系统,缓存

 传统的LRU算法并没有被Linux和Mysql使用,因为传统的LRU算法无法避免两个问题:

  • 预读失效导致缓存命中率下降
  • 缓存污染导致缓存命中率下降

预读失效怎么办?

什么是预读机制?

Linux操作系统为基于Page Cache的读缓存机制提供预读机制

  • 应用程序只想读取磁盘文件A的offset 为 0-3KB范围内的数据,由于磁盘的基本读写单位为block(4KB),于是操作系统至少会读 0-4KB内容,这恰好可以在一个page中装下
  • 但是操作系统出于空间局部性原理(靠近当前被访问数据的数据,在未来很大概率会被访问到),会选择将磁盘块offset[4KB ,8KB]、[8KB, 12KB]、[12KB, 16KB]都加载到内存中,于是额外在内存中申请了 3 个page;

(学习笔记-内存管理)如何避免预读失效和缓存污染的问题?,操作系统,缓存

 上图中,应用程序利用read系统调用读取4KB数据,实际上内核使用预读机制完成了16KB数据的读取,也就是通过一次磁盘顺序读将多个Page数据装入Page Cache。

这样下次读取 4KB 数据后面的数据的时候,就不用从磁盘读取了,直接在Page Cache即可命中数据。因此,预读机制的好处是减少了磁盘I/O次数,提高系统磁盘I/O吞吐量

MYSQL Innodb存储引擎的Buffer Pool也有类似的预读机制,MySQL从磁盘加载页时,会提前把它邻近的页一并加载进来,目的是为了减少磁盘 IO。

预读失效会带来什么问题?

预读失效提前加载进来的页,并没有被访问,相当于这个预读工作白做了。

如果使用传统的LRU算法,就会把 [预读页] 放到LRU链表头部,而当内存空间不够的时候,还需要把末尾的页淘汰掉。

如果这些 [预读页]一直不被访问到,就会出现一个奇怪的问题:不会被访问的预读页占用了LRU链表前排的位置,而末尾淘汰的页可能是热点数据,这样就大大降低了缓存命中率

如何避免预读失效造成的影响?

要避免预读失效带来的影响,最好就是让预读页停留在内存里的时间尽可能要短,让真正被访问的页才移动到LRU链表的头部,从而保证真正被读取的热数据留在内存里的时间尽可能长

Linux操作系统和MySQL Innodb通过改进传统的LRU链表来避免预读失效带来的影响,具体的改进分别如下:

  • Linux操作系统实现了两个LRU链表:活跃LRU链表非活跃LRU链表
  • MySQL的Innodb存储引擎是在一个 LRU 链表上划分 2 个区域:young区域old区域

这两个改进方式,设计思想类似,都是将数据划分为冷数据和热数据,然后分别进行LRU算法

不再像传统的LRU算法一样,所有数据都只用一个LRU算法管理。

Linux如何避免预读失效带来的影响?

Linux系统实现两个LRU链表:活跃LRU链表 和 非活跃LRU链表

  • active list :存放的是最近被访问过(活跃)的内存页
  • inactive list:存放的是很少被访问(非活跃)的内存页

有了这两个LRU链表后,预读页就只需要加入到 inactive list区域的头部,当页被真正访问时,才将页插入到active list 的头部。如果预读的页一直没有被访问,就会从inactive list移除,这样就不会影响active list中的热点数据。

MySQL是如何避免预读失效带来的影响?

MySQL 的 Innodb 存储引擎是在一个 LRU 链表上划分来 2 个区域,young 区域 和 old 区域

young 区域在 LRU 链表的前半部分,old 区域则是在后半部分,这两个区域都有各自的头和尾节点,如下图:

(学习笔记-内存管理)如何避免预读失效和缓存污染的问题?,操作系统,缓存

 young区域与old区域在LRU链表中的占比关系并不是一比一的,而是63:37(默认比例)的关系。

划分这两个区域后,预读的页就只需要加入到old区域的头部,当页被真正访问的时候,才将页插入young区域的头部。如果预读的页一直没有被访问,就会从 old 区域移除,这样就不会影响 young 区域中的热点数据。


缓存污染怎么办?

虽然Linux和 MySQL通过改进传统的LRU数据结构,避免了预读失效的影响。

但是如果还是使用 [只要数据被访问一次,就将数据加入到活跃的LRU链表头部(或young区域)] 这种方式的话,那么还存在缓存污染的问题

当我们在批量读取数据的时候,由于数据被访问了一次,这些大量数据就会被加入到 [活跃LRU链表] 里,然后之前缓存在活跃LRU链表里的热点数据就全部被淘汰了,如果这些大量的数据在很长一段时间内都不会被访问的话,那么整个活跃LRU链表(Young区域)就被污染了

缓存污染会带来什么问题?

缓存污染带来的影响是很致命的,等这些热数据又被再一次访问的时候,由于缓存没有命中,就会产生大量的磁盘I/O,系统性能会急剧下降。

我们以MySQL为例,Linux发生缓存污染的现象也是类似。

当某个SQL语句扫描了大量的数据时,在Buffer Pool空间比较有限的情况下,可能会将Buffer Pool里的所有页都替换出去,导致大量热数据都被淘汰了,等这些热数据又被再一次访问的时候,由于缓存未命中,就会产生大量的磁盘I/O,MySQL的性能会急剧下降。

注意,缓存污染并不只是查询语句查询出了大量数据才出现的问题,即使查询出的结果很小,也会造成混存污染

比如,在一个数据量非常大的表,执行以下语句:

select * from t_user where name like "%name%";

可能这个查询出来的结果只有几条,但是由于这条语句会发生索引失效,所以这个查询过程是全表扫描,接着会发生如下过程:

  • 从磁盘读的页加入到LRU链表的old区域头部
  • 当从页里读取行记录时,也就是页被访问的时候,就要将页放到young区域头部
  • 接下来拿行记录的name字段与查询条件进行模糊查询,如果符合条件,就加入到结果集里
  • 如此往复,直到扫描完表中所有的记录

由于这条 SQL 语句访问的页非常多,每访问一个页,都会将其加入 young 区域头部,那么原本 young 区域的热点数据都会被替换掉,导致缓存命中率下降。那些在批量扫描时,而被加入到 young 区域的页,如果在很长一段时间都不会再被访问的话,那么就污染了 young 区域。

举个例子,假设需要批量扫描:21,22,23,24,25 这五个页,这些页都会被逐一访问(读取页里的记录)

(学习笔记-内存管理)如何避免预读失效和缓存污染的问题?,操作系统,缓存

 在批量访问这些页的时候,会被逐一插入到 young 区域头部

(学习笔记-内存管理)如何避免预读失效和缓存污染的问题?,操作系统,缓存

 原本在young区域的 6 和 7 号页都被淘汰了,而批量扫描的页基本占满了young区域,如果这些页在很长一段时间内都不会被访问,那么就对young区域造成了污染。

如果 6和 7 号页是热点数据,那么在淘汰后,后续有SQL再次读取 6 和 7 号页时,由于未被命中。就要从磁盘中重新读取,降低了MySQL的性能,这就是缓存污染带来的影响。

如何避免缓存污染造成的影响?

前面的LRU算法只要数据被访问一次,就将数据加入活跃LRU链表(young 区域),这种LRU算法进入活跃LRU链表的门槛太低了。正是因为门槛太低,才导致在发生缓存污染的时候,很容易就将原本在活跃LRU链表里的热点数据淘汰了。

所以,只要我们提高进入到活跃LRU链表(young 区域)的门槛,就能有效地保证活跃LRU链表(young 区域)里的热点数据不会被轻易替换掉

Linux操作系统和MySQL Innodb的存储引擎分别是这样提高门槛

  • Linux操作系统:在内存页被访问第二次的时候,才将页从inactive list 升级到active list里
  • MySQL Innodb:在内存页被访问第二次的时候,并不会马上将该页从old区域升级到young区域,因为还要进行停留在old区域的时间判断
    • 如果第二次访问时间与第一次访问的时间在1秒内,那么该页就不会被从old区域升级到young区域
    • 如果第二次的访问时间与第一次访问时间超过 1 秒,那么该页就从 old 区域升级到 young 区域。

提高了进入活跃LRU链表(或 young区域)的门槛后,就很好地避免了缓存污染带来的影响。

在批量读取数据的时候,如果这些大量数据只会被访问一次,那么它们就不会进入到活跃LRU链表(或 young 区域),也就不会把热点数据淘汰,只会待在非活跃LRU链表(old 区域中),后续很快也被淘汰。文章来源地址https://www.toymoban.com/news/detail-621926.html

到了这里,关于(学习笔记-内存管理)如何避免预读失效和缓存污染的问题?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Objective-C学习笔记(内存管理、property参数)4.9

    1.引用计数器retainCount: 每个对象都有这个属性,默认值为1,记录当前对象有多少人用。    为对象发送一条retain/release消息,对象的引用计数器加/减1,为对象发一条retainCount,得到对象的引用计数器值,当计数器为0时自动调用对象的dealloc方法。    手动发送消息:-(id)perf

    2024年04月13日
    浏览(45)
  • 【嵌入式环境下linux内核及驱动学习笔记-(10-内核内存管理)】

    对于包含MMU(内存管理单元)的处理器而言,linux系统以虚拟内存的方式为每个进程分配最大4GB的内存。这真的4GB的内存空间被分为两个部分–用户空间 与 内核空间。用户空间地地址分布为0~3GB,剩下的3 ~ 4GB 为内核空间。如下图。 用户进程通常只能访问用户空间的虚拟地址

    2024年02月11日
    浏览(58)
  • 系统内存管理:虚拟内存、内存分段与分页、页表缓存TLB以及Linux内存管理

    虚拟内存是一种操作系统提供的机制,用于将每个进程分配的独立的虚拟地址空间映射到实际的物理内存地址空间上。通过使用虚拟内存,操作系统可以有效地解决多个应用程序直接操作物理内存可能引发的冲突问题。 在使用虚拟内存的情况下,每个进程都有自己的独立的虚

    2024年02月11日
    浏览(45)
  • Linux内存从0到1学习笔记(6.12 应用程序是如何申请内存的呢?)

        前面提到了应用程序大多基于glibc的malloc/free进行内存的分配。这里不讨论共享内存,因为共享内存都是预先分配好的,所以由共享内存mmap和shm所设计的内存泄漏比较少见。     接下来我们从应用调用的维度来看下,应用程序都有哪些调用入口,以及它们是如何申请和释

    2024年02月03日
    浏览(64)
  • Jtti:Linux内存管理中的slab缓存怎么实现

    在Linux内存管理中,slab缓存是一种高效的内存分配机制,用于管理小型对象的内存分配。slab缓存的实现是通过SLAB分配器来完成的,它在Linux内核中对内存分配进行优化。 SLAB分配器将内存分为三个区域:slab、partial、和empty。 Slab区域: Slab区域用于保存完整的内存对象。当有

    2024年02月15日
    浏览(47)
  • 如何在 Linux 中清空缓冲区和缓存内存?

    在 Linux 系统中,缓冲区和缓存内存起着重要的作用,用于提高系统性能和优化磁盘访问。然而,有时候我们可能需要清空缓冲区和缓存内存,以释放系统资源或解决某些性能问题。本文将详细介绍如何在 Linux 中清空缓冲区和缓存内存,并提供一些实际的示例。 在深入讨论如

    2024年02月06日
    浏览(71)
  • (学习笔记-进程管理)多线程冲突如何解决

    对于共享资源,如果没有上锁,在多线程的环境里,很有可能发生翻车。 在单核 CPU 系统里,为了实现多个程序同时运行的假象,操作系统通常以时间片调度的方式,让每个进程每次执行一个时间片,时间片用完了,就切换下一个进程运行,由于这个时间片的时间很短,于是

    2024年02月13日
    浏览(129)
  • LLM 推理优化探微 (3) :如何有效控制 KV 缓存的内存占用,优化推理速度?

    编者按: 随着 LLM 赋能越来越多需要实时决策和响应的应用场景,以及用户体验不佳、成本过高、资源受限等问题的出现,大模型高效推理已成为一个重要的研究课题。为此,Baihai IDP 推出 Pierre Lienhart 的系列文章,从多个维度全面剖析 Transformer 大语言模型的推理过程,以期

    2024年03月15日
    浏览(75)
  • 03.C++内存管理笔记

    1、C/C++内存分布 ①内存分那么多区的原因:不同的数据,有不同的存储需求,各区域满足了不同的需求。 ②存放: 临时变量等临时用的变量:栈区; 动态申请的变量:堆区; 全局变量和静态变量等整个程序期间都使用的变量:数据段; 常量、可执行代码等只读数据:代码

    2024年01月23日
    浏览(30)
  • 强缓存与协商缓存、缓存失效的问题、缓存nginx配置、缓存存在哪里

    前端缓存,这是一个老生常谈的话题,也常被作为前端面试的一个知识点。今天我们来总结一下。 分类:前端缓存分为强缓存和协商缓存两种。 强缓存主要使用 Expires、Cache-Control 两个头字段,两者同时存在 Cache-Control 优先级更高。当命中强缓存的时候,客户端不会再求,直

    2024年01月25日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包