在构建需要高吞吐量和最小响应时间的系统的API时,缓存几乎是不可避免的。每个在分布式系统上工作的开发人员都曾在某个时候使用过某种缓存机制。在本文中,我们将探讨如何使用CDN构建读取缓存设计,不仅可以优化您的API,还可以降低基础架构成本。
了解一些关于缓存和CDN的知识将有助于理解本文。如果您对此一无所知,建议您先了解一些相关知识,然后再回到这里。
作为后端开发人员,我们始终在努力构建高度优化的API,以为用户提供良好的体验。故事从这里开始,我们如何面对一个特定的问题,然后如何解决它。我希望您在阅读本文后能够从中学到一些关于大规模系统设计的东西。
我们需要开发一些API,这些API具有以下特征:
1.数据不会经常更改。2.对所有用户来说,响应是相同的,没有意外的查询参数,只是简单的GET API。3.响应数据量最多为约600 KB。4.我们预计API的吞吐量非常高(最终约为每秒5-6万次查询)。
当您第一次看到这个问题时,你的第一反应是什么?对我来说,首先想到的是,只需在节点上添加内存缓存(例如Google Guava),使用Kafka发送失效消息(因为我喜欢Kafka,它很可靠),设置服务实例的自动缩放(因为流量在一天中不均匀)。类似于下面的示意图:
嘭!问题解决了!很容易对吧?嗯,事实并非如此,像任何其他设计一样,这个设计也带来了一些缺陷。例如,对于一个简单的用例来说,这个设计略微复杂,基础架构成本将会增加,因为现在我们必须生成一个Kafka + Zookeeper集群,而且为了处理每秒5-6万次请求,我们需要水平扩展服务实例(对于我们来说是Kubernetes Pod),这意味着需要增加更多的物理节点或虚拟机。
因此,我们寻找了一种更简单和经济有效的方法,这就是我们最终开发了一种具有“使用CDN构建读取缓存”的解决方案。不久之后,我将讨论架构的细节以及权衡。
但在进一步探讨之前,让我们先了解设计的构建块。
标准的缓存更新策略有:
1.旁路缓存(Cache-Aside)2.读取通过缓存(Read-Through)3.写入通过缓存(Write-Through)4.写入后缓存(Write-Behind)5.提前刷新(Refresh-Ahead)
我将不详细讨论其他策略,而只关注读取缓存,因为本文只涉及此内容。让我们深入研究并了解它的工作原理。
上图很容易理解,但简要总结一下:
1.应用程序永远不直接与数据库交互,而始终通过缓存进行。2.在缓存未命中时,缓存将从数据库中读取数据并丰富缓存存储。3.在缓存命中时,数据将从缓存中提供。
您可以看到,数据库很少被频繁访问,响应速度很快,因为缓存主要是内存中的(如Redis或Memcached)。已经解决了很多问题。
互联网上关于CDN的定义是:“内容传递网络(CDN)是一种全球分布的代理服务器网络,用于在离用户更近的位置提供内容,并用于提供静态文件,如图像、视频、HTML、CSS文件”。但我们将违反潮流,使用CDN提供动态内容(JSON响应,而不是JSON文件)。
此外,从概念上说,通常有两种CDN:
1.推送CDN(Push CDN):您负责将数据上传到CDN服务器。2.拉取CDN(Pull CDN):CDN将从您的服务器(源服务器)拉取数据。
我们将使用拉取CDN,因为使用推送方法,我必须处理重试、幂等性和其他内容,这对我来说是一个额外的麻烦,而且对于这个用例并没有真
正添加任何价值。
这个想法很简单,我们将CDN作为用户和实际后端服务之间的缓存层。如下图所示:
正如您所看到的,CDN位于客户端和我们的后端服务之间,并成为缓存。数据流顺序如下:
让我们深入探讨一下,因为这是设计的精髓。
•T1 -> 时间实例1 + 毫秒数•T2 -> 时间实例1 + 1分钟+某些毫秒数•TTL -> 存活时间•源服务器 -> 您实际的后端服务
1.T1:客户端请求获取user1。2.T1:请求着陆在CDN上。3.T1:CDN发现在其缓存存储中没有user1相关的键。4.T1:CDN到上游,即实际的后端服务,以获取user1。5.T1:后端服务返回user1作为标准的JSON响应。6.T1:CDN接收到JSON,现在它需要存储它。7.所以现在需要决定这个数据的TTL,它是如何做到的?8.通常有两种设置TTL的方式,要么源服务器指定数据应该被缓存多长时间,要么在CDN配置中设置了一个恒定值,它使用该时间来设置TTL。9.最好让源服务器控制TTL,这样我们有能力根据需要控制TTL或具有条件的TTL。10.那么问题就产生了,源服务器如何指定TTL。缓存控制头(Cache-Control headers)来拯救。来自源服务器的响应可以包含像 cache-control: public, max-age: 180
这样的缓存控制头。这将转化为该数据可以被公开缓存,有效期为180秒。11.T1:现在CDN看到这一点并使用180秒的TTL缓存了数据。12.T1:CDN向调用者响应user1 JSON。13.T2:另一个客户端请求user1。14.T2:请求着陆在CDN上。15.T2:CDN看到它的缓存中有user1键,因此不会到源服务器,而是返回缓存的JSON响应。16.T3:CDN在180秒后缓存失效。17.T4:某个客户端请求user1,但由于缓存为空,流程再次从第3步开始。这种情况一直重复。
不一定要将TTL设置为180秒。选择TTL是根据您能够提供过期数据多长时间以及是否接受它而选择的。如果这引发了一个问题,为什么不能在数据更改时使缓存失效,那么请稍等,我马上在缺点部分回答。
但还有一个问题,CDN承担了所有负载,我们不必进行扩展。但我们的吞吐量达到了每秒60,000次查询,这意味着在缓存未命中的情况下,会有60,000个请求同时命中我们的源服务(假设需要1秒来填充CDN缓存),这可能会使服务不堪重负。
这就是请求合并的工作方式:
顾名思义,它基本上将具有相同查询参数的多个请求合并在一起,并将很少的请求发送到源服务器。
我们设计的美妙之处在于,我们不必自己执行请求合并,CDN将帮助我们执行。正如我已经提到的,我们使用的是Google Cloud CDN,它有请求合并的概念,这只是请求合并的另一种名称。因此,当在同一时间进行大量的缓存填充请求时,CDN会识别出这一点,每个CDN节点只发送一个请求到源服务器,然后从该响应中响应所有这些请求。这就是如何保护我们的源服务器免受高流量的影响。
好的,我们现在接近结束了,任何设计在没有经过利弊分析之前都是不完整的。因此,让我们稍微分析一下这个设计,看看它如何有所帮助,以及它的不足之处。
1.简单性: 这个设计非常简单,易于实现和维护。2.响应时间: 您已经知道CDN服务器的地理位置优化了数据传输,因此我们的响应时间也变得非常快。例如,忽略TCP连接建立时间,60毫秒听起来如何?3.减少负载: 由于实际的后端服务器现在只收到约每180秒1个请求,负载非常低。
1.缓存失效: 缓存失效是计算机科学中最难正确执行的事情之一,而且由于CDN成为缓存,它变得更加困难。在CDN上的任意即兴的缓存失效是一个昂贵的过程,通常不会实时发生。如果数据发生更改,由于我们无法使CDN上的缓存失效,您的客户端可能会在一段时间内获得旧数据。但这又取决于您设置的TTL,如果TTL为几小时,那么您也可以在CDN上调用缓存失效。但如果TTL以秒/分钟为单位,这可能会有问题。此外,请记住,并非所有CDN提供商都提供API以使CDN缓存失效。2.控制较少: 由于请求现在不会着陆在我们的服务器上,因此会有这样一种感觉,即作为开发人员,您对系统没有足够的控制。可观察性可能会受到轻微影响,您可以在CDN上设置日志记录和监控,但这通常会带来一定的成本。
在分布式世界中的任何设计都具有一定程度的主观性,并且总会有一些权衡。作为开发人员/架构师,我们的职责是权衡各种权衡,并选择适合我们的设计。说到这里,没有哪种设计足够具体以继续下去,因此鉴于约束条件,我们选择了一种设计,根据它的运作方式,我们可能会进一步演化它。