极客时间已完结课程限时免费阅读

22 | 缓存架构:如何减少不必要的计算?

22 | 缓存架构:如何减少不必要的计算?-极客时间

22 | 缓存架构:如何减少不必要的计算?

讲述:李智慧

时长11:27大小9.19M

上一篇我们讲到,互联网应用的主要挑战就是在高并发情况下,大量的用户请求到达应用系统服务器,造成了巨大的计算压力。互联网应用的核心解决思路就是采用分布式架构,提供更多的服务器,从而提供更多的计算资源,以应对高并发带来的计算压力及资源消耗。
那么有没有办法减少到达服务器的并发请求压力呢?或者请求到达服务器后,有没有办法减少不必要的计算,降低服务器的计算资源消耗,尽快返回计算结果给用户呢?
有,解决的核心就是缓存。
所谓缓存,就是将需要多次读取的数据暂存起来,这样在后面,应用程序需要多次读取的时候,就不必从数据源重复加载数据了,这样就可以降低数据源的计算负载压力,提高数据响应速度。
一般说来,缓存可以分成两种,通读缓存和旁路缓存。
通读(read-through)缓存,应用程序访问通读缓存获取数据的时候,如果通读缓存有应用程序需要的数据,那么就返回这个数据;如果没有,那么通读缓存就自己负责访问数据源,从数据源获取数据返回给应用程序,并将这个数据缓存在自己的缓存中。这样,下次应用程序需要数据的时候,就可以通过通读缓存直接获得数据了。
通读缓存在架构中的位置与作用如下图:
旁路(cache-aside)缓存,应用程序访问旁路缓存获取数据的时候,如果旁路缓存中有应用程序需要的数据,那么就返回这个数据;如果没有,就返回空(null)。应用程序需要自己从数据源读取数据,然后将这个数据写入到旁路缓存中。这样,下次应用程序需要数据的时候,就可以通过旁路缓存直接获得数据了。
旁路缓存在架构中位置与作用如下图:

通读缓存

互联网应用中主要使用的通读缓存是 CDN 和反向代理缓存。
CDN(Content Delivery Network)即内容分发网络。我们上网的时候,App 或者浏览器想要连接到互联网应用的服务器,需要网络服务商,比如移动、电信这样的服务商为我们提供网络服务,建立网络连接才可以上网。
而这些服务商需要在全国范围内部署骨干网络、交换机机房才能完成网络连接服务,这些交换机机房可能会离用户非常近,那么互联网应用能不能在这些交换机机房中部署缓存缓存服务器呢?这样,用户就可以近距离获得自己需要的数据,既提高了响应速度,又节约了网络带宽和服务器资源。
当然可以。这个部署在网络服务商机房中的缓存就是 CDN,因为距离用户非常近,又被称作网络连接的第一跳。目前很多互联网应用大约 80% 以上的网络流量都是通过 CDN 返回的。
CDN 只能缓存静态数据内容,比如图片、CSS、JS、HTML 等内容。而动态的内容,比如订单查询、商品搜索结果等必须要应用服务器进行计算处理后才能获得。因此,互联网应用的静态内容和动态内容需要进行分离,静态内容和动态内容部署在不同的服务器集群上,使用不同的二级域名,即所谓的动静分离,一方面便于运维管理,另一方面也便于 CDN 进行缓存,使 CDN 只缓存静态内容。
反向代理缓存也是一种通读缓存。我们上网的时候,有时候需要通过代理上网,这个代理是代理我们的客户端上网设备。而反向代理则代理服务器,是应用程序服务器的门户,所有的网络请求都需要通过反向代理才能到达应用程序服务器。既然所有的请求都需要通过反向代理才能到达应用服务器,那么在这里加一个缓存,尽快将数据返回给用户,而不是发送给应用服务器,这就是反向代理缓存。
用户请求到达反向代理缓存服务器,反向代理检查本地是否有需要的数据,如果有就直接返回,如果没有,就请求应用服务器,得到需要的数据后缓存在本地,然后返回给用户。

旁路缓存

CDN 和反向代理缓存通常会作为系统架构的一部分,很多时候对应用程序是透明的。而应用程序在代码中主要使用的是对象缓存,对象缓存是一种旁路缓存。
不管是通读缓存还是旁路缓存,缓存通常都是以 <key, value> 的方式存储在缓存中,比如,CDN 和反向代理缓存中,每个 URL 是一个 key,那么 URL 对应的文件内容就是 value。而对象缓存中,key 通常是一个 ID,比如用户 ID,商品 ID 等等,而 value 则是一个对象,就是 ID 对应的用户对象或者商品对象。
对于 <key, value> 的数据格式,我们在前面在数据结构讨论过,比较快速的存取方式是使用 Hash 表。因此通读缓存和旁路缓存在实现上,基本上用的是 Hash 表
程序中使用的对象缓存,可以分成两种。一种是本地缓存,缓存和应用程序在同一个进程中启动,使用程序的堆空间存放缓存数据。本地缓存的响应速度快,但是缓存可以使用的内存空间相对比较小,但是对于大型互联网应用所需要缓存的数据通以 T 计,这时候就要使用远程的分布式缓存了。
分布式缓存是指将一组服务器构成一个缓存集群,共同对外提供缓存服务,那么应用程序在每次读写缓存的时候,如何知道要访问缓存集群中的哪台服务器呢?我们以 Memcached 为例,看看分布式缓存的架构:
Memcached 将多台服务器构成一个缓存集群,缓存数据存储在每台服务器的内存中。事实上,使用缓存的应用程序服务器通常也是以集群方式部署的,每个程序需要依赖一个 Memcached 的客户端 SDK,通过 SDK 的 API 访问 Memcached 的服务器。
应用程序调用 API,API 调用 SDK 的路由算法,路由算法根据缓存的 key 值,计算这个 key 应该访问哪台 Memcached 服务器,计算得到服务器的 IP 地址和端口号后,API 再调用 SDK 的通信模块,将 <key, value> 值以及缓存操作命令发送给具体的某台 Memcached 服务器,由这台服务器完成缓存操作。
那么,路由算法又是如何计算得到 Memcached 的服务器 IP 端口呢?比较简单的一种方法,和 Hash 算法一样,利用 key 的 Hash 值对服务器列表长度取模,根据余数就可以确定服务器列表的下标,进而得到服务器的 IP 和端口。

缓存注意事项

使用缓存可以减少不必要的计算,能够带来三个方面的好处:
缓存的数据通常存储在内存中,距离使用数据的应用也更近一点,因此相比从硬盘上获取,或者从远程网络上获取,它获取数据的速度更快一点,响应时间更快,性能表现更好。
缓存的数据通常是计算结果数据,比如对象缓存中,通常存放经过计算加工的结果对象,如果缓存不命中,那么就需要从数据库中获取原始数据,然后进行计算加工才能得到结果对象,因此使用缓存可以减少 CPU 的计算消耗,节省计算资源,同样也加快了处理的速度。
通过对象缓存获取数据,可以降低数据库的负载压力;通过 CDN、反向代理等通读缓存获取数据,可以降低服务器的负载压力。这些被释放出来的计算资源,可以提供给其他更有需要的计算场景,比如写数据的场景,间接提高整个系统的处理能力。
但是缓存也不是万能的,如果不恰当地使用缓存,也可能会带来问题。
首先就是数据脏读的问题,缓存的数据来自数据源,如果数据源中的数据被修改了,那么缓存中的数据就变成脏数据了。
主要解决办法有两个,一个是过期失效,每次写入缓存中的数据都标记其失效时间,在读取缓存的时候,检查数据是否已经过期失效,如果失效,就重新从数据源获取数据。缓存失效依然可能会在未失效时间内读到脏数据,但是一般的应用都可以容忍较短时间的数据不一致,比如淘宝卖家更新了商品信息,那么几分钟数据没有更新到缓存,买家看到的还是旧数据,这种情况通常是可以接受的,这时候,就可以设置缓存失效时间为几分钟。
另一个办法就是失效通知,应用程序更新数据源的数据,同时发送通知,将该数据从缓存中清除。失效通知看起来数据更新更加及时,但是实践中,更多使用的还是过期失效。
此外,并不是所有数据使用缓存都有意义。在互联网应用中,大多数数据访问都是有热点的,比如热门微博会被更多阅读,热门商品会被更多浏览。那么将这些热门的数据保存在缓存中是有意义的,因为缓存通常使用内存,存储空间比较有限,只能存储有限的数据,热门数据存储在缓存中,可以被更多次地读取,缓存效率也比较高。
相反,如果缓存的数据没有热点,写入缓存的数据很难被重复读取,那么使用缓存就不是很有必要了

小结

缓存是优化软件性能的杀手锏,任何需要查询数据、请求数据的场合都可以考虑使用缓存。缓存几乎是无处不在的,程序代码中可以使用缓存,网络架构中可以使用缓存,CPU、操作系统、虚拟机也大量使用缓存,事实上,缓存最早就是在 CPU 中使用的。对于一个典型的互联网应用而言,使用缓存可以解决绝大部分的性能问题,如果需要优化软件性能,那么可以优先考虑哪里可以使用缓存改善性能。
除了本篇提到的系统架构缓存外,客户端也可以使用缓存,在 App 或者浏览器中缓存数据,甚至都不需要消耗网络带宽资源,也不会消耗 CDN、反向代理的内存资源,更不会消耗服务器的计算资源。

思考题

我们从 Memcached 路由算法讲到余数 Hash 算法,但是,这种算法在 Memcached 服务器集群扩容,也就是增加服务器的时候,会遇到较大的问题,问题是什么呢?应该如何解决?
欢迎你在评论区写下你的思考,也欢迎把这篇文章分享给你的朋友或者同事,一起交流进步一下。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 16

提建议

上一篇
21丨分布式架构:如何应对高并发的用户请求
下一篇
23 | 异步架构:如何避免互相依赖的系统间耦合?
unpreview
 写留言

精选留言(14)

  • holybell
    2020-01-13
    关于思考题,使用Memcached服务器数量参与hash余数算法,会导致服务器数量增减的时候,相同的路由被重新计算到不同的机器上,这就会导致每次增删Memcached服务器的时候,会导致大量的缓存失效,针对这个问题可以采用一致性哈希算法,将所有的服务器连接成一个逻辑上的环,采用一个不会改变运算逻辑的hash函数,当一个key计算出hash值的时候落到环上的某一个点,之后顺时针找到最近的一个服务器即可,这样即使删除Memcached服务器,只会导致该服务器的缓存转移到环上下一个点的服务器上,不会导致几乎整个Memcached集群缓存的失效;同时如果是增加Memcached机器,那么相当于从环路上的某个机器匀了一部分数据到新机器上,旧机器的缓存可以配合过期策略自动删除。
    展开
    共 3 条评论
    50
  • 桂冠远航
    2020-03-22
    这个专栏大而全是好事,但限于篇幅每个点都比较浅。
    共 2 条评论
    21
  • 探索无止境
    2020-03-02
    老师您好,文中提到缓存解决脏数据的方式失效通知,立即清理缓存,为什么实际采用的少?

    作者回复: 大部分业务场景可以接受短时间的读取脏数据,通知清理的方式带来的系统复杂性和压力得不偿失。

    共 2 条评论
    8
  • Paul Shan
    2020-02-25
    缓存是将数据的生产和消费解耦,在消费次数明显高于生产次数的现实中,让网络各个节点共享数据以减少无谓的数据生产。通路缓存节点必然位于数据生产者和数据消费者之间,当通路缓存不存在需要的数据的时候,就主动向生产者要数据,最终更新自己并把数据返回下家。通读缓存通常离数据消费者更近,因为这样才有效率。通读缓存在数据生产和消费者插入节点,有时也会增加生产者和消费者的距离,并不是万能的。CDN只适合于静态数据,因为动态数据更新快,缓存意义不大。反向代理也是在服务器和应用间插入一个节点,来代理服务器,个人觉得反向代理对于动态数据意义也不大。 旁路缓存则是数据消费者的衍生,数据消费者可能自己存不下那么多数据,就用旁路缓存来暂存结果,下次访问的时候先从旁路缓存读取,如果没有,再向数据生产者要数据,然后更新旁路缓存。旁路缓存需要存储大量数据,这个时候就需要将数据分配给不同的服务器,分配最好均匀,这和hash算法本质相同,因而有很多应用。 总结,通读代理是数据生成者的衍生,可以代理生产者。旁路缓存是数据消费者的衍生,可以想象成数据消费者外挂的内存或硬盘。通读缓存都存在于生产者和消费者之间的关键节点上,旁路缓存则是把这些关键节点扩容,让其有更好的性能。缓存是数据生产的一个快照,当其过了时效,也就失效了。
    展开
    共 1 条评论
    4
  • escray
    2020-10-07
    推荐去看一下隔壁《高并发系统设计 40 问》的缓存部分(可以试读有关缓存的 4 篇),我是为了最后的思考题,Memcached 的哈希一致性算法,而去看的。 缓存大概可以分为以下三类 旁路缓存 Cache Aside: 通路缓存 Read/Write Through: 写回缓存 Write Back:计算机体系结构中的设计,比如操作系统的 Page Cache 这篇专栏偏重于缓存架构的概要介绍,而高并发专栏里面提到了一些实际使用中的注意事项,配合服用疗效更好。当然,最终还是要到真实的使用场景中去学以致用。 除了 Memcached,也有把 Redis 来当做缓存使用的。 我觉的缓存受到限制的原因主要还是速度比较快的存储,费用昂贵——从 CPU 中的 L1 Cache、L2 Cache,一直到 Memcached 的内存数据库,CDN 和其他缓存技术,如果真的不考虑性价比的话,那么大量使用内存或者 SSD 等高速存储应该就可以了。
    展开
    3
  • 草原上的奔跑
    2020-01-16
    通读缓存、旁路缓存,第一次对缓存有了明确的认知。hash算法在扩缩容时会导致绝大多数缓存失效,这是它的问题。使用一致性hash算法可以降低失效缓存的数量,改善缓存的效率。但一致性hash也有自己的问题,比如平衡性问题等,又会针对这些问题提出解决方案。 之前看一些架构性的内容,不知道为何要这样做。听了李老师娓娓道来的讲述,明白了架构的演进,心里有些底,不再是飘在空中的感觉
    展开
    2
  • 俊杰
    2020-01-13
    会导致大量缓存不命中,可以用一致性哈希算法解决
    2
  • zqnba
    2022-09-06 来自日本
    谢谢。提个建议: "首先就是数据脏读的问题,缓存的数据来自数据源,如果数据源中的数据被修改了,那么缓存中的数据就变成脏数据了。" 意思可以理解。不过这里的“脏读”,跟数据库事物的“脏读”定义不一致。有点容易混淆。
  • java小霸王
    2022-06-30
    一致性哈希,或者redis的哈希槽机制
  • 秋天
    2021-01-26
    最后的问题 一致性hash算法可以解决吧 另一个就是 总算是懂了read-through 和 cache-aside
  • 不要挑战自己的智商
    2020-07-31
    旁路缓存的几段有点confusing..请问老师: 旁路缓存都是一个个的对象(id, 对象)是吗? 旁路缓存全都是本地缓存是吗?缓存和应用程序在同一个进程中启动,使用程序的堆空间存放缓存数据。这个缓存是由客户端应用来管理的,比如浏览器的缓存就是旁路缓存,是吗?

    作者回复: 旁路缓存都是一个个的对象(id, 对象)是吗? 是的 旁路缓存全都是本地缓存是吗? 分布式对象缓存也是 浏览器的缓存就是旁路缓存,是吗? 是的

  • 非同凡想
    2020-03-28
    淘宝一般会在CDN放商品图片么?

    作者回复: 是的

    共 2 条评论
  • 布衣骇客
    2020-03-09
    扩容后可以导致部分缓存不命中,这个怎么处理呢?再用一致性hash路由算法的话,肯定会导致缓存不命中的情况的。还有老师,缓存雪崩的一般怎么处理好呢?缓存预热?或者是漏斗算法?
  • 旅途
    2020-01-30
    一致性hash