实现tab页缓存_缓存系统设计精要 1:多级缓存

本文探讨了系统缓存设计的关键点,包括多级缓存、缓存淘汰策略和缓存安全。详细讲解了存储器层次结构、局部性原理以及CPU高速缓存的原理和设计。特别提到了PHP7数组设计如何利用CPU缓存优化性能,并介绍了浏览器的Service Worker、Memory Cache、Disk Cache和Push Cache四级缓存机制,强调了缓存设计中局部性原理的重要性。
摘要由CSDN通过智能技术生成

点击上方蓝色“Go语言中文网”关注我们,领全套Go资料,每天学习 Go 语言

作者:绘你一世倾城 

链接:https://juejin.im/post/5e9ad171518825738f2b30de  

来源:掘金 

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在计算机领域,缓存在程序设计过程中扮演着重要角色。浏览器的资源缓存策略是影响web网站性能的一个关键因素;mysql的Buffer Pool极大的提高了数据库的查询效率;redis作为被广泛应用的缓存数据库,提供了丰富的数据结构和缓存策略来满足开发者的需求。缓存在现代计算机系统中无处不在,是一个非常深入、广泛的话题,本文的目的是从众多已有的系统设计中,提取出有关系统缓存设计的精要,主要从三个方面展开:

  • 多级缓存
  • 缓存淘汰策略
  • 缓存安全

在讨论这些话题的同时,笔者会结合很多实际的例子阐述,其中会涉及到PHP源码、redis源码、浏览器的缓存策略、mysql数据存储设计、缓存淘汰算法等偏底层的内容,如果你是一个比较资深的开发工程师,对这些内容都比较了解,本文将非常适合您。为了照顾初学者,涉及到难懂重要的知识点笔者也将会尽可能的描述清楚。本文篇幅有点长,请读者做好心理预期!

1.多级缓存

1.1 存储器层次结构

计算机使用不同的存储技术对数据进行访问,时间差异很大。速度较快的技术每个字节的成本要比速度较慢的技术高,而且容量较小。CPU和主存(内存)之间的速度差距在增大。基于这种特性,所有的现代计算机系统都使用了金字塔式的存储器层次结构。如下图所示:

99789463d203dfdf7e7bfc1ed3fa0c8f.png
存储器层次结构

从高层往低层走,存储设备变得更慢、更便宜、容量更大。最高层(L0)是少量快速的CPU寄存器,CPU可以在一个时钟周期内访问它们 (一个时钟周期通常小于1ns);接下来是一个或多个中小型SRAM高速缓存存储器,可以在几个CPU时钟周期内访问它们;然后是一个大的基于DRAM的主存,可以在几十到几百个CPU时钟周期内访问它们;接下来是慢速但是容量很大的本地磁盘(通常是SSD);最后有些系统甚至包括了一层附加的网络文件系统(Network File System,NFS),比如说腾讯云提供的CFS服务就可以通过挂载到多个服务器实现分布式的数据共享。

下面我们从 CPU 的角度以具体的数据来量化对比CPU、磁盘、网络的速度,对计算机各个组件不同的速度差异有个更直观的认识。

类型缓存内容缓存位置读取数据需要时长对比基本单位时长
CPU寄存器4字节 & 8字节芯片上的CPU寄存器0.38ns1s
L1高速缓存64字节块芯片上的L1高速缓存0.5ns1.3s
L2高速缓存64字节块芯片上的L2高速缓存7ns18.2s
L3高速缓存64字节块芯片上的L3高速缓存35ns91s
内存4KB页主存100ns4分钟
固态硬盘SSD磁盘扇区磁盘控制器150us4.5天
网络磁盘(NFS本地挂载)部分文件本地磁盘1.5ms一个半月

CPU 和内存之间的瓶颈被称为冯诺依曼瓶颈, 它们之间至少有着几百倍的速差,可以想象成天上人间,天上一天,地下一年。普通的磁盘读取数据就更慢了,寻址时间10ms,对比CPU的基本单位时长是10个月,可以生一次孩子了!所以说IO 设备是计算机系统的瓶颈。以此想到我们网络中的API请求,动不动就是一百多毫秒,对比CPU的基本单位就是十几年!

相信通过以上描述,读者对计算机组件之间数据处理速度有了深入的印象。

1.2 局部性原理

一个编写良好的计算机程序通常具有良好的局部性(locality)。也就是,它们倾向于引用邻近于其他最近引用过的数据项的数据项,或者最近引用过的数据项本身。这种倾向性,被称为局部性原理(principle of locality),是一个持久的概念,对硬件和软件系统的设计和性能都有着极大的影响。

局部性通常有两种不同的形式: 时间局部性(temporal locality)空间局部性(spatial locality)

  • 时间局部性:被引用过一次的内存位置很可能在不远的将来再被多次引用。
  • 空间局部性:如果一个内存位置被引用了一次,那么程序很可能在不远的将来引用附近的一个内存位置。

Mysql数据库Innodb引擎的设计就很好的考虑了局部性原理。

Innodb引擎以页的形式组织数据,页的默认大小是16KB,存放用户数据记录的页叫“数据页”,为了实现数据更快的查找,InnoDB使用B+树的结构组织存放数据,最下层的叶子节点存放“数据页”,在“数据页”的上层抽出相应的“目录页”,最终形成的基本结构如下图所示:

fc9465f42067ed9bf400df17358719ba.png
InnoDB数据存储

数据页中记录是连续存放的,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,也就是说即使我们只需要访问一个页的一条记录,那也需要先把整个页的数据加载到内存中。这就利用了局部性原理中的“空间局部性”。将整个页加载到内存中后就可以进行读写访问了,在进行完读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存到Buffer Pool中,这样将来有请求再次访问该页面时,就可以省去磁盘IO的开销了,这又利用了局部性原理中的“时间局部性”。

局部性原理是系统缓存设计中非常重要的理论基石,下文还会多次提到。

1.3 Cpu 高速缓存

本节我们来聊一聊Cpu 高速缓存,Cpu 高速缓存是影响程序性能的一个关键因素。

1.3.1 什么是Cpu 高速缓存?

笔者直接引用维基百科中对Cpu 高速缓存的描述:

在计算机系统中,CPU高速缓存(英语:CPU Cache,在本文中简称缓存)是用于减少处理器访问内存所需平均时间的部件。在金字塔式存储体系中它位于自顶向下的第二层,仅次于CPU寄存器。其容量远小于内存,但速度却可以接近处理器的频率。当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。缓存之所以有效,主要是因为程序运行时对内存的访问呈现局部性(Locality)特征。这种局部性既包括空间局部性(Spatial Locality),也包括时间局部性(Temporal Locality)。有效利用这种局部性,缓存可以达到极高的命中率。在处理器看来,缓存是一个透明部件。因此,程序员通常无法直接干预对缓存的操作。但是,确实可以根据缓存的特点对程序代码实施特定优化,从而更好地利用缓存。

1.3.2 为什么需要有Cpu 高速缓存?

上文中我们提到冯诺依曼瓶颈。随着工艺的提升,最近几十年CPU的频率不断提升,而受制于制造工艺和成本限制,目前计算机的内存在访问速度上没有质的突破。因此,CPU的处理速度和内存的访问速度差距越来越大,这种情况下传统的 CPU 直连内存的方式显然就会因为内存访问的等待,导致计算资源大量闲置,降低 CPU 整体吞吐量。同时又由于内存数据访问的热点集中性,在 CPU 和内存之间用较为快速而成本较高(相对于内存)的介质做一层缓存,就显得性价比极高了。

1.3.3 Cpu 高速缓存架构

早期计算机系统的存储器层次结构只有三层:CPU寄存器、DRAM主存储器和磁盘存储。由于CPU和主存之间逐渐增大的差距,系统设计者被迫在CPU寄存器文件和主存之间插入了一个小的SRAM高速缓存存储器,称为L1高速缓存(一级缓存),如下图所示。L1高速缓存的访问速度几乎和寄存器相差无几。

1ca2c89821c9d813940fdfd95cc4d564.png
CPU芯片示意图

随着CPU和主存之间的性能差距不断增大,系统设计者在L1高速缓存和主存之间又插入了一个更大的高速缓存,称为L2高速缓存,可以在大约10个时钟周期内访问到它。现代的操作系统还包括一个更大的高速缓存,称为L3高速缓存,在存储器的层次结构中,它位于L2高速缓存和主存之间,可以在大约50个周期内访问到它。下图是简单的高速缓存架构:

67ad41b245a44df550b127cb6c569e0e.png
高速缓存架构

数据的读取和存储都经过高速缓存,CPU 核心与高速缓存有一条特殊的快速通道;主存与高速缓存都连在系统总线上(BUS),这条总线还用于其他组件的通信。

1.3.4 PHP7数组的设计

关于Cpu 高速缓存的应用,PHP7数组的设计是一个非常经典的案例。在PHP中数组是最常用的一种数据类型,提升数组的性能会使程序整体性能得到提升。我们通过对比PHP7和PHP5 数组的设计,来学习一下PHP 设计者为了提升PHP数组的性能,都进行了哪些思考。

我们先来总结一下PHP数组的特性:

  • php中的数组是一个字典dict,存储着key-value对,通过key可以快速的找到value,其中key可以是整形,也可以是字符串(hash array 和 packed array)。
  • 数组是有序的:插入有序、遍历有序。

PHP中数组使用hashTable实现,我们先来了解一下什么是hashTable。

hashTable是一种通过某种hash函数将特定的key映射到特定value的一种数据结构,它维护着键和值的一一对应关系,并且可以快速的根据键找到值(o(1)). 一般HashTable的示意图如下:

452d8df76581625fc5fdca11d99a6201.png

  1. key: 通过key可以快速找到value。一般可以为数字或者字符串。
  2. value: 值可以为复杂结构
  3. bucket: 桶,HashTable存储数据的单元,用来存储key/value以及其他辅助信息的容器
  4. slot:槽,HashTable有多少个槽,一个bucket必须属于具体的某一个slot,一个slot可以有多个 bucket
  5. 哈希函数:一般都是自己实现(time33),在存储的时候,会将key通过hash函数确定所在的slot
  6. 哈希冲突: 当多个key经过哈希计算后,得到的slot位置是同一个,那么就会冲突,一般这个时候会有2种解决办法:链地址法和开放地址法。其中php采用的是链地址法,即将同一个slot中的bucket通过链表连接起来

为了实现数组的插入有序和遍历有序,PHP5使用hashTable + 双链表实现数组,如下图是将key分别为:“a”,"b","c","d",值为“1”,"2","3","4" 插入到数组中后的效果:

3937f71fd7c891483502e51775a36b98.png
PHP5数组实现

上图中b,c,d所在的bucket被存分配到了同一个slot,为了解决hash冲突,slot中多个bucket通过双向链表关联,称为局部双向链表;为了实现有序,slot之间也通过双向链表关联,称为全局双向链表

了解php5数组的实现原理之后,我们发现其中影响性能的两个关键点:

  1. 频繁的内存分配!每向数组中添加一个元素,都需要申请分配一个bucket大小的内存,然后维护多个指针。虽然php是基于内存池的管理方式(预先申请大块内存进行按需分配),但是内存分配带来的性能损耗是不可忽略的。
  2. Cpu 高速缓存命中率低!因为bucket与bucket之间是通过链表指针连接的,bucket随机分配,内存基本不连续,导致Cpu cache降低,不能给遍历数组带来性能提升。

针对以上两点,php7对hashTable的设计进行了改造。既然是字典,还是要使用hashTable,但php7数组的实现去掉了slot,bucket的分配使用了连续内存;bucket间不再使用真实存在的指针进行维护,bucket只维护一个在数组中的索引,bucket与bucket之间通过内存地址进行关联,如下图所示:

4753aecfe56a148d59c3b04652db84a4.png
PHP7中hashTable实现

PHP7中数组是如何解决hash冲突的

PHP7对zval进行了改造,zval中有一个u2的union联合体,占用4字节大小,存放一些辅助信息。PHP7数组中的value也是一个个的zval指针,当发生hash冲突时,zval中u2部分存放的next指针存放指向下一个bucket数组中的索引,通过这样一种逻辑上的链表就巧妙解决hash冲突。关于PHP7中zval的设计,推荐大家阅读鸟哥的文章:深入理解PHP7内核之zval[1]

可以看到,php7中hashTable对性能提升最重要的改动是bucket的分配使用了连续内存。这样不仅省去了数组元素插入时频繁的内存分配开销;遍历数组也只需要一个bucket一个bucket的访问就好了,Cpu 高速缓存命中率非常高,极大的提升了数组性能;符合局部性原理。

更多关于PHP5和PHP7数组的设计细节,推荐大家研究这篇文章:【PHP7源码学习】系列之数组实现[2]

1.4 浏览器的多级缓存机制

举一个多级缓存的应用案例:浏览器对我们生活的影响是不言而喻的,现代的浏览器已经不同往昔了,快速的信息加载,顺畅的用户交互,这一切都得益于网络技术的普及、H5标准特性的推广应用,当然还有一个重要因素,那就是浏览器的缓存策略。本节我们就来简单介绍一下浏览器的多级缓存机制。

对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。

浏览器有四种缓存级别,它们各自都有优先级。

  • Service Worker
  • Memory Cache
  • Disk Cache
  • Push Cache

当依次查找缓存且都没有命中的时候,才会去请求网络。

1.4.1 Service Worker

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的

Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。

当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

1.4.2 Memory Cache

Memory Cache 也就是内存中的缓存,主要包含的是当前页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。

当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存

7c418599d4229f6a071ce7c4741508c6.png
浏览器内存缓存

内存缓存中有一块重要的缓存资源是preloader相关指令(例如)下载的资源。众所周知preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件,一边网络请求下一个资源。

需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验。

为什么浏览器数据不都存放在内存中

这个问题就算我不回答,读者肯定也都想明白了,计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。谷歌浏览器是公认的性能出众的,但是它占用的内存也是很大的。

1.4.3 Disk Cache

Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。

在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache。

浏览器会把哪些文件丢进内存中?哪些丢进硬盘中

关于这点,网上说法不一,不过有些观点比较靠得住:对于大文件来说,大概率是不存储在内存中的,另外如果当前系统内存使用率高的话,文件优先存储进硬盘。

1.4.4 Push Cache

Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。

Push Cache 在国内能够查到的资料很少,也是因为 HTTP/2 在国内不够普及。这里推荐阅读Jake Archibald的 HTTP/2 push is tougher than I thought[3] 这篇文章,文章中的几个结论是:

  • 所有的资源都能被推送,并且能够被缓存,但是 Edge 和 Safari 浏览器支持相对比较差
  • 可以推送 no-cache 和 no-store 的资源
  • 一旦连接被关闭,Push Cache 就被释放
  • 多个页面可以使用同一个HTTP/2的连接,也就可以使用同一个Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的tab标签使用同一个HTTP连接。
  • Push Cache 中的缓存只能被使用一次
  • 浏览器可以拒绝接受已经存在的资源推送
  • 你可以给其他域名推送资源

如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。

浏览器的多级缓存机制到这里就介绍完了,大家可以看到,浏览器多级缓存机制的实现思路,和我们前边说到的计算机系统多级缓存的知识是相呼应的,并且也满足局部性原理。浏览器缓存还有更多值得我们深入学习的内容,感兴趣的读者可以研究一下这篇文章:深入理解浏览器的缓存机制[4]

参考资料

[1]

深入理解PHP7内核之zval: https://www.laruence.com/2018/04/08/3170.html

[2]

【PHP7源码学习】系列之数组实现: https://www.jianshu.com/p/ed7e0d4e020a

[3]

HTTP/2 push is tougher than I thought: https://jakearchibald.com/2017/h2-push-tougher-than-i-thought/

[4]

深入理解浏览器的缓存机制: https://www.jianshu.com/p/54cc04190252

77b0ca9b1becc7f78228c2781b0f6fb4.png

Hi,此公众号由全栈技术交流群维护,持续发布关于Web全栈的技术知识。欢迎投稿/关注个人公众号:火丁笔记。

 714bdacefaafea15427d23b2ee675548.png

欢迎加微信申请:polaris_6196 进入 “全栈技术交流群“ 讨论全栈技术。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值