静态内容缓存的七种方法
Posted 架构决定未来
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了静态内容缓存的七种方法相关的知识,希望对你有一定的参考价值。
文章导读:静态内容指的是那些不经常改变的文本、图像和视频,例如各种新闻网站和视频应用。动态内容是指随着时间的推移,将会不断发生变化的内容,例如支付公司每天处理的各种支付交易。这里将要讨论的主要是针对静态内容实现缓存的七种不同技术手段。
在商业世界,人们常说“现金为王”;
在支付行业,一般常说“场景为王”;在未来的世界,将会是“数据为王”;然而,在互联网技术里,我们却说“缓存为王”。 从用户端的浏览器到服务端的应用层、业务层和数据层,每个架构层次都可以通过缓存技术来显著地提高系统的扩展能力,改善系统的响应速度,以及大幅度减少系统的负担。
我们一般把互联网平台上的内容分为静态和动态两种。静态内容指的是那些不经常改变的文本、图像和视频,例如各种新闻网站和视频应用。动态内容是指随着时间的推移,将会不断发生变化的内容,例如支付公司每天处理的各种支付交易。这里将要讨论的主要是针对静态内容实现缓存的七种不同技术手段。
CDN(Contents Distribution Network
)即内容分发网络,是通过骨干网络把一组计算机连接起来以存储客户数据或内容的副本。通过在不同的网络,CDN策略性地部署边缘服务器,并应用大量技术和算法,把用户的请求转发到最佳的响应节点。这种优化的逻辑既可以是基于最少网络跳转数,也可以是基于系统的最高可用性或者最少的请求数。这种优化常常聚焦在减少最终用户、请求者或服务可以感知的响应时间。
正如本例所示,在网站服务器的前面使用CDN的效果是由CDN负责处理所有的请求,只有当缓冲内容需要更新时才会访问源服务器。因此,只需要购买少量低配置的服务器和网络带宽,以及配置少数维护基础设施的技术人员。无论网站的网页是动态还是静态,我们都可以考虑加入CDN形成混合缓存。该层缓存可以提供快速交付的好处,有非常高的可用性,因此网站的服务器只需要处理较少的流量。
HTTP Header提供了有关代理缓存的有效机制。在 html中,我们看不到这些 HTTP Header,它们通常是由网络服务器或生成页面的代码动态生成。通过服务器配置或代码来控制。一个典型的 HTTP Header 响应看起来可能像下面这样:
HTTP Status Code: HTTP/1.1 200 OK
Date: Thu, 15 Oct 2020 20:03:38 GMT
Server: Apache/2.2.9 (Fedora)
Expires: Mon, 15 Nov 2020 05:00:00 GMT
Last-Modified: Thu, 15 Oct 2020 20:03:38 GMT
Vary: Accept-Encoding, User-Agent
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8
与缓存的关系最为密切的Header是 Expires和Cache-Control 。Expires Header的实体字段提供网站响应的有效期信息。如果想要把网站的响应标记为“永不过期”
,那么源服务器可以发送-1, 或者响应时间算起一年后的日期。在前面的例子中,我们可以注意到 Expires Header的标识日期为 2020 年 11 月 15 日 05:00GMT。如果今天是 2020 年 10 月 15 日,请求的页面将在大约一个月后过期,浏览器将不得不在那个时候再次访问服务器以获取数据来刷新缓存的内容。
Cache-Control 为通用的Header字段,用于按照 RFC2616 第 14 节定义的 HTTP1.1 协议来定义指令,沿请求/响应链的所有缓存机制必须遵守这些指令,
可以发出许多指令,包括 public、private,、no-cache 和 max-age。如果响应同时包含Expires Header和 max-age 指令,即使 Expires 限制较多,max-age 指令的优先级同样会高过 Expires Header。以下是一些有关 Cache-Control 指令的定义:
public—响应可以由任何缓存、共享或非共享缓存来处理;
private—响应针对单用户,不能放在共享缓存;
no-cache—在与源服务器确认之前,不得使用缓存来满足后续的其他请求;
max-age—如果当前数值大于在请求时给定的值(秒),那么响应过时。
有几种设置 HTTP Header的方式,其中包括通过网络服务器和代码设置。有关Apache2.2 的配置设置定义在 httpd.conf 文件中。Expires Headerr要求把 mod_expires 模块添加到 Apache。Expires 模块有三条基本指令。
第一条
ExpiresActive 告诉服务器激活该模块。
第二条 ExpiresByType 设置 Expires 服务对象的特定类型(如图片或文本)。
第三条
ExpiresDefault 设置如何处理所有未指定类型的对象。
ExpiresByType image/png "access plus 1 day"
ExpiresByType image/gif "modification plus 5 hours"
ExpiresByType text/html "access plus 1 month 15 days 2 hours"
ExpiresDefault "access plus 1 month"
除了在 HTTP 中设置 Expires、Cache-Control 和其他Header以外,另外一种方法是在代码中具体实现。例如PHP 直接利用 Header() 命令发送原始的 HTTP Header。在任何输出前都必须要通过 HTML 标签或从 PHP 代码中调用 Header() 命令。关于如何设置Header,请参考下面的 PHP 示例代码。其他语言也有类似的Header设置方法。
Header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
Header("cache-control: no-store, no-cache, must-revalidate");
Header("Pragma: no-cache");
最后一个主题涉及到调整网络服务器的配置,以优化其性能与可扩展性。参数keep-alives或HTTP常链接允许多个HTTP请求复用TCP连接。在HTTP / 1.1 中,所有的链接都是持久的常链接,大多数网络服务器默认允许保持常链接。根据 Apache文档的记载,使用常链接可以使HTML页面的时间延迟减少50%。在 Apache的httpd.conf文件中,参数keep-alives的默认设置为打开,但KeepAliveTimeOut 的默认值只设置为 5 秒。设置较长超时的好处是不必建立、使用和终结TCP连接就可以处理更多的 HTTP 请求;设置较短超时的好处是不会被捆占用网络服务器的线程,服务器可以继续服务其他请求。可以根据应用或网站的具体情况在前述两者之间寻找平衡点。
下面有一个具体的实例,我们可以利用 AOL 研发的开源网页测试工具webpagetest.org对Wiki网站做测试。测试对象为运行在Apache HTTP 2.2 版服务器的网站。
上图给出了在关闭 keep-alives,不设置 Expires Header的情况下,测试 Wiki 页面的结果。
页面的初始加载时间为 3.8 秒,重复浏览时间为 2.3 秒。
上图显示的是在打开 keep-alives, 设置 Expires Header的情况下,测试 Wiki 页面的结果。页面的初始加载时间为 2.6 秒,重复浏览时间为 1.4 秒。简单的一个参数调整减少了 32% 的页面初始加载时间和 37% 的重复页面加载时间!
2005 年杰西·詹姆斯·加勒特在他的文章《 Ajax :一种网络应用的新方法》中创造了 Ajax 这个术语。Ajax 是 Asynchronous javascript and XML 的缩写。虽然我们经常把它作为一种技术,但更为贴切的描述是,它是一组技巧、语言和在浏览器或客户端上使用的方法,有助于为最终用户带来更为丰富的内容和更加实时的用户体验。
因为可以减少数据在网络上不必要的往复传递,从而使用户与浏览器之间的互动更容易,因此用户交互可以更为迅速地发生。用户不必用等待服务器的响应就可以放大或缩小图片,下拉菜单可以根据以前的输入预先安排好,当用户在搜索栏中输入要查询的关键词时,马上可以看到那些可能会感兴趣并起到引导作用的潜在搜索词。
Ajax
的异步特性还可以帮助我们在往客户端浏览器查看股票交易时,不必点击刷新就可以看到变化的市场行情
。
但是,其中的一些动作不利于平台的扩展,以用户在网站上输入某个特定商品的搜索关键词为例。我们可能想用商品目录来填充搜索建议,即那些当用户键入搜索条件时出现的关键词。Ajax 可以通过用户后续的每个按键向服务器发送请求,根据已键入的词返回搜索结果,并在不需要用户介入刷新浏览器的情况下,把搜索结果填充到下拉菜单。有可能返回的是基于用户不完全键入的字符串而获得完整搜索结果!许多搜索引擎和电商网站都可以找到这种案例。以每个后续按键为基础,最终形成搜索服务器需要的查询语句,可能既昂贵,也浪费后台系统资源。例如,当用户输入 “Beanie Baby” 时,可能会带来连续 11 次的搜索,而真正的输入只需要一次。虽然用户的体验可能很奇妙,但是如果用户敲击按键的速度很快,在打完字之前,实际上可能有多达 8-10 次的搜索永
远都没有返回结果的机会。
我们的目标是要减少在网络上来回传输的数据量,以减少用户可以感知响应时间并降低服务器的负载。因此,响应Header中Expires
设置的有效期应该足够长,这样浏览器会在本地缓存第一次查询的结果,并在后续请求中反复使用。诸如公司商标或者简介图片这样的静态或半静态的对象,其有效期应该设置成几天或更长。某些对象对时间的敏感性可能很强,例如阅读好友在微信或者Facebook上的状态更新。在这些情况下,应该把 Expires Header设置为数秒甚至数分钟,以给用户实时的感觉,同时减轻网络服务整体的负载。
数据集是静态甚至半动态的情况,例如有限的或对上下文敏感的产品目录就很容易解决。从客户端看,以异步的方式获取这些结果,然后缓存起来供同一客户端以后使用,或者更重要的是确保 CDN、中间缓存或代理存储它们,以利于其他的用户进行类似的搜索。
下面给出了一个不太好的 Ajax 调用案例和一个比较好的响应案例。不太好的调用案例看起来可能像下面这样:
HTTP Status Code: HTTP/1.1 200 OK
Date: Thu, 21 Oct 2015 20:03:38 GMT
Server: Apache/2.2.9 (Fedora)
Expires: Mon, 26 Jul 1997 05:00:00 GMT
Last-Modified: Thu, 21 Oct 2015 20:03:38 GMT
Vary: Accept-Encoding,User-Agent
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8
从上面的信息中可以发现 Expires Header发生在过去,完全丢失 Cache-Control Header,Last-Modified Header与响应发送的时间一致。这些设置迫使所有的 GET 都得抓取新的内容。一个更容易缓存 Ajax 结果的比较好的响应该是这样的:
HTTP Status Code: HTTP/1.1 200 OK
Date: Thu, 21 Oct 2015 20:03:38 GMT
Server: Apache/2.2.9 (Fedora)
Expires: Sun, 26 Jul 2020 05:00:00 GMT
Last-Modified: Thu, 31 Dec 1970 20:03:38 GMT
Vary: Accept-Encoding,User-Agent
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8
在本例中,Expires 设置为遥远的将来,Last-Modified 设置为沧桑的过去,并通过Cache-Control: public 告诉中间代理,他们可以缓存并在其他系统复用对象。
页面缓存是部署在网络服务器前面的专用缓存服务器,目的是用来减少静态和动态对象对这些服务器的请求。这样的系统或服务器的其他常见名称是反向代理缓存、反向代理服务器和反向代理。
页面缓存处理部分或者所有的请求,直到所存储的页面或者数据过期,或者在服务器上查询不到用户请求需要的数据。我们把请求失败称为缓存丢失,可能是缓存池满没有空间存储最近的请求,或者缓存池不满,但是请求率很低或者系统最近刚刚重新启动过。缓存丢失传递给网络服务器,后者应答请求并填充缓存,要么更新最近最少使用的记录或者填补一个未占的可用空间。我们特别用页面缓存这个术语,因为代理还负责负载均衡或 SSL 加速,代理缓存的架构如下所示,我们在这里强调三点。
首先,在网络服务器前面实施页面缓存或反向代理,这样做可以获得显著的扩展效益,生成动态内容的网络服务器的工作量因此大为减少,因为计算结果会在适当的时间内缓存。服务静态内容的网络服务器不再需要查找内容,因此要减少服务器的数量。然而,我们认为静态页面的缓存所带来的好处绝非动态内容那么大。
其次,需要使用适当的 HTTP Header,以确保发挥内容缓存和结果缓存的最大潜在作用。为此,请参考前面对 Cache-Control,Last-Modified 和 Expires 的简要讨论。RFC2616 第 14 节对这些Header文件、相关参数及其预期结果有更为完整的描述。
再次,我们尽可能包括 RFC2616 中定义的其他 HTTP Header,这有助于最大限度地提高内容缓存。
这个新的Header被称为 ETag。定义 ETag 或实体标记的目的是方便 If-None-Match 方法,用户对服务器有条件的 GET 请求。ETag是服务器在浏览器首次请求时,为对象发出的唯一标识。如果服务器的资源发生变化,就会重新分配新的Etag。假设浏览器(客户端)提供适当的支持,对象及其ETag 由浏览器缓存,后续浏览器向网络服务器发出的 If-None-Match 请求将包含此标签。如果标签相符,服务器会返回 HTTP304,即内容未修改的响应。如果标签与服务器上的不一致,服务器将会发出更新后的对象及其相应的 ETag。
要想保持缓存长期有效,我们必须从系统架构的视角出发来制定方案。对平台架构我们可以从功能上按照服务或资源拆分(Y 轴拆分),或者按照请求者或客户的某些属性拆分(Z 轴拆分),这些拆分会在服务请求的数据缓存能力上受益匪浅。问题在于我们到底实施哪种拆分才可以获得最大利益。随着新功能或新特性的开发而产生新的数据要求,所以这个问题的答案可能随着时间的推移而改变。实施的方法,也需要随着时间的推移而改变,以适应业务需求的不断变化。需要不断地分析生产流量、每笔交易的成本、用户感知的响应时间,以识别在生产环境中出现瓶颈的早期迹象,并将数据交给负责架构的团队。
从可扩展性和成本的角度来看,我们需要回答的关键问题是:什么类型的拆分或进一步的细分可以让我们获得最大的利益?通过适当的拆分和由此给应用服务器所带来的数据缓存能力,我们完全有可能以较少的生产服务器来处理现有生产系统2倍、3倍、甚至10倍的流量。以电商网站为例,电商网站有很多功能,包括搜索、浏览、图片、帐户、登录、购物车、结帐、建议等。现有生产流量的分析表明 80%的交易都集中在使用搜索、浏览和推荐产品等几个功能,并聚焦在不到20% 的库存上。我们可以利用80-20规则,对这些服务实施功能拆分,利用与整个用户群相比,相对较少对象上的高命中率。可缓存的机会应该很高,动态系统可以从类似的早期请求交付结果中受益。
我们也可能会发现有些发出频繁请求的高级用户。对这些特定的用户功能,我们可能会决定针对用户的特定功能,诸如登录、购物车、帐户等实施按用户属性的拆分。另外一个例子,假设我们做的是 SaaS 业务,通过托管电话服务、电子邮件服务、聊天服务和关系管理系统来为公司的客户提供支持。系统中有大量与特定业务相关的规则。以每个业务为基础考
虑,可能需要大量的内存来缓存这些规则和一些业务操作所需的数据。如果你马上得出结论,以客户为导向拆分是人间正道,那么恭喜你答对了。
最后一个例子涉及到社交网络或互动网站。你可能会猜到,我们会再次应用80-20原则并依靠生产环境的信息来作出决定。社交网络往往涉及到少量拥有令人不可思议的大份额流量。这些用户有时是活跃的消费者,有时是活跃的生产者,有时两者兼而有之。首先确定是否有一小部分信息或者子站点有超过正常比例的读流量。在社交网络中,这样的节点对我们思考架构有很好的指导意义,它可以引导我们对这些生产者实施 Z 轴拆分,这样从读的角度他们节点的活动是高度可缓存的。假设我们的80-20原则正确,现在可以由少数服务器来为将近80%的读流量提供服务。
在社交网络中,内容或更新非常活跃的生产者会怎么样?答案可能会有所不同,取决于内容是否有很高的消费率(读),还是大多数都处于休眠的状态。当用户既有高生产率(写入/更新)又有高消费(读)率的时候,我们可以直接把内容发布到正在读取的泳道或节点。如果读写冲突导致节点变热,并开始成为一个问题,那么我们就可以采用读复制和水平扩展的技术来解决。
对象缓存是用来存储每个对象的哈希摘要的数据存储。这些缓存主要用于存储那些可能需要很大计算资源才能重新获得的数据,例如数据库复杂查询的结果集。哈希函数将一个可变长度的大数转换成一个小的散列值。这个散列值也称为散列和/或校验和,通常是一个可以用作数组中索引的整数。
# echo 'AKF Partners' | md5sum
90c9e7fd09d67219b15e730402d092eb[em][em]-
# echo 'Hyper Growth Scalability AKF Partners' | md5sum
faa216d21d711b81dfcddf3631cbe1ef[em][em]-
对象缓存有许多种,如目前流行的 Redis、Memcached、Apache 的 OJB 和 Ncache等数不胜数,实施方式之多胜过工具选择的多样性。我们通常把对象缓存部署在数据层和应用层之间,以用来缓存 SQL 查询的结果集。然而,有些人则把对象缓存用于复
杂应用计算的结果,例如用户推荐、产品优先级或基于最近表现的广告重排序等。最常见的实施是把对象缓存放在数据层前面,因为通常数据库扩展起来最困难和最昂贵,而实施对象缓存才是人间正道。
除了我们要留意数据库的CPU和内存使用率之外,SQL查询排行榜是表明系统需要目标缓存的最具代表性指标。SQL查询排行榜是根据那些在数据库上运行得最频繁和资源最密集查询所生成报表的通称。Oracle的Enterprise Manager Grid Control有个内置的SQL查询评估工具,用来识别那些使用SQL资源最密集的语句。除了可以确定执行很慢的查询和改善它们的优先级之外,这个数据还可以用来显示哪个查询可以通过添加缓存而从数据库的活动中消除。所有常见的数据库都有类似的报告或工具,通过内置或附加的工具提供相应的服务。
一旦决定了要实施的对象缓存,我们就需要选择最适合的方案。提醒那些可能会考
虑自建解决方案的技术团队。有太多生产级别的对象缓存方案可供选择。例如Facebook采用800多台服务器为其系统提供超过28TB的内存。虽然你有可能作出决定自建而不是购买,或者使用开源的对象缓存,但是这个决定需要详细斟酌。
下一步是实施对象缓存,通常这个决策是直截了当的。Memcached支持许多不同编程语言的客户端,如 Java、Python 和 PHP。PHP 有get和set两个基本命令。从下面的例子可以看到,我们连接到Memcached服务器。如果连接失败,就通过dbquery函数来查询数据库。如果Memcached连接成功,则尝试检索与特定的$key相关联的$data。如果get失败,那么我们将查询db并把$data存入Memcached,这样我们在下次查询时,就可以期待在缓存中能够找到它。set 命令中的 false 标识用于压缩,90是以秒计算的缓存有效期。
$memcache = new Memcache;
If ($memcache->connect('127.0.0.1', 11211)) {
[em][em]If ($data = $memcache->get('$key')) {
[em][em][em][em]$data = dbquery($key);
[em][em][em][em]$memcache->set('$key',$data, false, 90);
[em][em]$data = dbquery($key);
实施对象缓存的最后一步是监控缓存命中率。这是能在缓存系统找到请求对象的次数与请求总次数的比率。在理想的情况下,该比率应该是85%或更高,这意味着请求对象不在缓存或者缓存对象过期的机会仅有15%或者更少。如果缓存的命中率下降,我们需要考虑添加更多的对象缓存服务器。
许多公司从网络或应用服务器开始实施对象缓存。这样的实施简单有效,不必投入额外硬件或云平台虚拟机就可以实现对象缓存。缺点是对象缓存占用服务器中的大量内存,结果造成对象缓存无法在应用或网络层以外去独立扩展。
更好的选择是把对象缓存配置在本层的服务器上。如果使用对象缓存来存储查询结果集,那么将部署在应用层和数据层之间。如果缓存对象创建在应用层,那么对象缓存层就部署在网络层和应用层之间。这里的对象缓存层可能是物理服务器,用来缓存数据库对象和应用对象。
对这些层进行分离的优点是,可以根据对内存和 CPU 的要求适当地选择服务器。此外还可以在其他服务器池以外独立地扩展对象缓存池中的服务器。正确地评估服务器可以大幅降低成本,因为对象缓存通常需要大量内存来存储对象和键,但是需要相对较低的计算能力。不必拆分应用或网络服务器,在必要时添加服务器,让对象缓存使用额外的容量。
《架构真经》书籍介绍:
本书旨在帮助工程师、架构师和管理者研发及维护可扩展的互联网产品。本书给出了一系列规则,每个规则围绕着不同的主题展开讨论。大部分的规则聚焦在技术上,少数规则涉及一些关键的思维或流程问题,每个规则对构建可扩展的产品都是至关重要的。
如果您想了解更多技术干货,NETSTARS CTO 陈斌翻译的图书《架构即未来》《架构真经》《数据即未来》《区块链启示录》《AI战略》《Python机器学习》正在京东和亚马逊热卖中!
扫描左侧←二维码,
在工作中成长,
在学习中进步!
以上是关于静态内容缓存的七种方法的主要内容,如果未能解决你的问题,请参考以下文章
一个完整的PHP类包含的七种语法说明
常用的七种传值方式
单例模式的七种实现
MFC对话框控件访问的七种方式
深入解析单例模式的七种实现
linux查看文件行号的七种方法