当前位置: 首页 > 科技观察

掌握HTTP缓存——从请求到响应的一切过程(上)

时间:2023-03-20 12:12:22 科技观察

类CDN网站一度霸占Alexa域名排行榜前100。过去,一些小网站根本不需要使用CDN,或者根本负担不起它的价格,但近几年这种现象发生了很大的变化。CDN市场上有很多按次付费和非企业的提供商,这使得CDN成为了一项人人都能负担得起的服务。本文介绍如何使用这个简单易用的缓存服务。要使用内容分发网络(CDN),您需要正确理解HTTP响应头:哪些标签与HTTP响应头相关?它们是如何工作的?如何使用它们?我将在本文中回答这些问题。这篇文章的内容并不像教科书那么精确。其实在某些情况下,为了描述的清楚简洁,我会根据自己的理解对一些问题进行简化。本文将通过一些实际例子来介绍缓存理论。在这篇文章的基础上,还会写一些文章来介绍如何使用CDN作为一些指定的CMS或框架的缓存层。为什么要使用CDN?CDN是一个全球分布的网络,可以将网站内容更快地传送到世界各地的特定位置,通常远离实际的内容服务器。例如,您的网站托管在爱尔兰,而您的用户从澳大利亚访问它。这时候,当你的用户访问你的网站时,延迟会很高。将你的(静态)数据放在澳大利亚,用CDN将大大改善用户访问网站的体验。但是,CDN的使用不限于此。其实CDN可以理解为一个普通的缓存,比如代理缓存(edgecache)。即使您不关心用户的具体地理位置,您也应该考虑使用CDN的代理缓存来改善您的用户体验。为什么要使用代理缓存?简而言之,代理缓存缓存您网站的某些页面,并通过缓存非常快速地提供“静态”内容。举个简单的例子,假设您有一个博客,其起始页列出了所有最近的博客。为此,PHP脚本从数据库中获取最新的文章实体,将它们转换为HTML结果页面,并将它们返回给用户。因此,对于一个请求(访问)包含:一次PHP执行+一组数据库查询。对于1000个请求(访问),它包含:1000次PHP执行+1000组数据库查询。每个PHP执行都需要CPU、内存和I/O操作,数据库操作也是如此。请求的需求与访问用户的数量成线性比例。听上去怎么样?与其说不是,是因为这种线性关系是有限制的:磁盘只能提供一定量的I/O,CPU和内存都不是最优的。这样发展到一定程度,就是说当某个资源到了瓶颈的时候,就会出现一个问题:你的网站访问起来会很慢,甚至所有人都访问不了。其实此时其他资源还没有完全填满。诚然这时候你可以扩大你的硬件规模来突破这个瓶颈,但这会让项目变得更复杂,成本也更高。实际上有更简单和更便宜的解决方案。中间加一层代理缓存,会减少对你的资源限制。以前面的例子为例,使用代理缓存只需要第一次请求执行一个PHP脚本,查询数据库,生成一个HTML结果页面。所有后续请求都会从这个缓存中获取内容,读取缓存几乎和直接从内存中读取一样快。也就是说,上面的线性刻度瓶颈问题解决了!100个用户或1000个用户都无所谓,仍然只有1次PHP执行、1次数据库查询和1次结果页面生成。CDN!=CDNCDN也有不同的类型。站长可能很好奇数据是怎么存储的?它存储在哪里?数据在CDN上是如何分布的?它是如何分布的?这篇文章不是写给站长的,而是写给开发者的,所以我只能告诉你有“经典CDN”和“点对点CDN”,后者是现在主流的方式。对于开发者来说,他们更感兴趣的是如何把数据放到CDN中,而不是拿到数据到CDN之后做什么。说起来,有pushCDN和pullCDN之分。顾名思义,“推送CDN”就是你要向CDN提供内容;“pullCDN”是指如何从CDN中获取内容。本文将主要介绍拉取式CDN,因为很多时候拉取式CDN更容易使用,可以毫不费力地集成到现有网站中。拉取式CDN如何工作?让我们举个例子,假设您有一个可访问的网站https://www.foobar.tld。在这种情况下,域名http://www.foobar.tld将放置在拉取CDN服务器上,而不是您的Web服务器上。CDN充当您的Web服务器的代理。还有一个未公开的域名指向实际的网络服务器。假设在这个例子中是direct.foobar.tld,实际的Web服务器称为origin。此CDN将接受所有请求。如果其缓存中有结果,则直接返回给用户,否则,请求将托管在您实际的Web服务器上,然后将返回的结果缓存起来以供以后的请求使用,并将结果返回给用户同时。最简单的拉取CDN操作流程如下:获取一个页面的请求,这个页面:http://www.foobar.tld/some/page使用some/page作为缓存key检查缓存中是否存在,然后直接从缓存中返回结果给用户,如果不在缓存中,则请求http://direct.foobar.tld/some/page,将返回结果以some/page为key写入缓存,并将结果返回给用户上面的静态内容VS动态内容这个过程非常适合完全静态的内容。静态内容是指如果用户访问同一个URL地址,返回的所有数据都是一样的。例如,CSS文件就具有这样的特性。http://www.foobar.tld/public/css/main.css这个文件是一个普通的文件,对于所有访问该网站的用户来说都是一样的,所以特别适合Cache它。与静态文件相反的是动态文件。内容在运行时确定也很常见。比如多语言问题,需要根据浏览器语言返回内容。还有一些与“用户会话”相关的内容。例如,当用户登录时,“登录”按钮应该替换为“注销”按钮。你绝对不希望它被缓存。这些高度活跃的内容(例如每小时或更短时间更新的页面)无法缓存,或者不能在缓存中停留太久。这就是缓存变得有趣的地方,它不难理解和实现。绝大多数拉取式CDN以“每页”缓存的形式处理动态内容。实现此目的的一种简单方法是HTTP响应缓存标头。首先你要知道缓存头有“旧版”和“新版”两种,也就是说一开始并没有设计成现在的版本,还有一个逐渐演变的过程。较新的版本指的是HTTP/1.1,而较旧的版本指的是HTTP/1.0。它的选项之多,让每个人都为之头疼。我认为这是人们不愿意使用缓存头的最重要原因。言归正传,我们只关注ETag和Cache-Control这两个标签。大多数CDN还支持旧版本(Expires、Pragma和Age),但这些仅用于向后兼容。ETag标头我们从最简单的ETag开始:它是文档版本的标识符。通常是内容的MD5值,但也可以包含其他内容,代表文档的版本/日期,如:1.0或2017-02-27。这里要注意一点,一定要用双引号括起来,比如:ETag:"d3b07384d113edec49eaa6238ad5ff00"。二次认证现在让我们考虑ETags的实际应用:二次认证。暂时不考虑之前的proxy+source架构模式,只考虑简单的client-server模式。如下图所示:假设客户端请求http://www.foobar.tld/hello.txt,然后服务端返回如下响应内容:#REQUESTGET/hello.txtHTTP/1.1Host:www.foobar.tld#RESPONSEHTTP/1.1200OKDate:Sun,05Feb201712:34:56UTCServer:ApacheLast-Modified:Sun,05Feb201710:34:56UTCETag:"8a75d48aaf3e72648a4e3747b713d730"Content-Length:8Content-Type:twoTheretextchar/plain设置了UTC响应中有趣的头标识:一个是ETag,内容的MD5值,另一个是Last-Modified,也就是hello.txt文件最后一次被修改的时间。这里双因素认证就派上用场了:当客户端短时间内再次访问上述URL时,客户端浏览器会使用If-*请求头。比如If-None-Match,检查ETag的内容是否发生变化。也就是说,如果ETag发生变化,客户端会收到一个完整的新响应;如果ETag没有改变,客户端会收到一个标识符,表明内容没有改变。GET/hello.txtHTTP/1.1If-None-Match:"8a75d48aaf3e72648a4e3747b713d730"Host:www.foobar.tld如果ETag没有改变,服务器会返回:HTTP/1.1304NotModifiedDate:Sun,05Feb201712:34:57UTCServer:ApacheLast-Modified:Sun,05Feb201710:34:56UTCETag:"8a75d48aaf3e72648a4e3747b713d730"Content-Length:8Content-Type:text/plain;charset=UTF-8如上所示,这次服务器的响应不是200ok,而是304NotModified,这意味着它会跳过数据包主体,让客户端直接去自己的缓存中获取数据。本例中包体内容为body,比较小,效果不明显。但是想象一下,如果是一个很大的内容,或者是一个非常复杂的动态生成的内容,价值会很大。作为开发者,你可能会想:“没那么好用,我还得掌握IF类头标签,比以前还麻烦”。别着急,这只是对共享缓存的介绍,这就是代理缓存的由来。先看原架构:,proxy根据自己的缓存返回304NotModified给client,下一章详细介绍,在介绍之前,我想说一下Last-Modfied头.在处理上面的hello.txt静态文件示例时,客户端也可以使用If-Not-Modified-Since:Sun,05Feb201710:34:56UTC来达到相同的效果(返回304响应)。这也适用于静态文件,因为响应标头中的Last-Modified标志是根据服务器磁盘上的“修改时间戳”自动生成的。但是,“更改时间戳”一般对动态文件没有用,因为动态生成的文件更新频繁,时间戳很难确定。我们都知道最想缓存的是内容,而生成内容的成本是最低的,所以ETag头是更好的选择。Cache-ControlheaderCache-Controlheader比较难。两个原因:***,Cache-Control既可以用于请求头,也可以用于响应头。本文重点介绍响应头,因为这是开发者必须掌握的。其次,它控制两个缓存:本地缓存(也称为私有缓存)和共享缓存。本地缓存是指客户端本地机器中的缓存。从开发者的角度来看,它并不完全在你的控制之下,通常浏览器自己决定是否将某些内容放入缓存中,这意味着:不要依赖本地缓存。用户也有可能在您不知情的情况下关闭浏览器时清除所有缓存。您不会意识到这一点,除非您监控到用户的流量持续增加,从而导致缓存的内容迅速过期。共享缓存,也就是本文介绍的:客户端和服务器之间的缓存。即CDN。您对共享缓存拥有绝对控制权,应该加以利用。现在让我们以一些代码为例来深入挖掘。Cache-Control:publicmax-age=3600Cache-Control:privateimmutableCache-Control:no-cacheCache-Control:publicmax-age=3600s-maxage=7200Cache-Control:publicmax-age=3600proxy-revalidate代码乍看之下很混乱,但别担心,这并不难,我会带您逐步了解它。首先你要知道Cache-Control有3个属性:缓存容量、过期时间和二次验证。首先是缓存能力,重点是缓存在什么地方,是否应该缓存。他的几个重要属性是:private:表示它应该只存在于本地缓存中;public:表示它可以同时存在于共享缓存和本地缓存中;no-cache:表示无论是本地缓存还是共享缓存,在使用之前,都必须用缓存中的值重新生效;no-store:表示不允许缓存。其次是过期时间,这显然是关心内容可以缓存多长时间。它的几个重要属性是:max-age=:设置缓存时间,设置单位为秒。本地缓存和共享缓存都可以;s-maxage=:覆盖max-age属性。仅适用于共享缓存。最后一个是二次验证,也就是精细化把控。它的几个重要属性是:immutable:表示文档不能更改。must-revalidate:表示客户端(浏览器)必须检查它是否存在于代理服务器上,即使它已经缓存在本地。proxy-revalidat:表示共享缓存(CDN)必须检查源是否存在,即使已经有缓存。通过上面的具体解释,更容易理解上面Cache-Control代码表达的意思:本地缓存和CDN缓存都缓存1小时;它们不能缓存在CDN中,只能缓存在本地。并且一旦缓存,就无法更新;它不能被缓存。如果必须缓存它,请确保对其进行双重验证;本地缓存1小时,CDN缓存2小时;在本地和CDN缓存1小时。但是如果CDN收到请求,即使缓存了1小时,它也会检查源站的文档是否有变化。示例理论会很乏味,现在用一个简短的示例来演示如何自动注入ETag和Cache-Control标头。例子是一个Apache的.htaccess文件,但希望大家能够领会要点,根据自己的实际情况将其应用到自己的web应用中。#为所有图片设置ETag,缓存时间为1天FileETag-INodeMTimeSizeHeadersetCache-Control"max-age=86400public"#为所有CSS文件和JS文件设置ETag,缓存时间为2小时,并保证二次校验FileETag-INodeMTimeSizeHeadersetCache-Control"max-age=7200publicmust-revalidate"HeaderunsetLast-Modified上面的例子是对URL的响应:http://www.foobar.tld/baz.jpg。包含由更改时间和文件大小组成的ETag标头,以及用于将缓存时间设置为1天的Cache-Control标头。请参阅下面的请求和响应:#REQUESTGET/baz.jpgHTTP/1.1Host:www.foobar.tld#RESPONSEHTTP/1.1200OKDate:Tue,07Feb201715:01:20GMTLast-Modified:Tue,07Feb201715:01:15GMTETag:"4-547f20501b9e9"Content-Length:123Cache-Control:max-age=86400publicContent-Type:image/jpeg对URL的响应:http://www.foobar.tld/dist/css/styles.css还包括ETag标头。由更改时间、文件大小和Cache-Control组成,限制为2小时。Last-Modfied标头也被删除,以确保只有ETag用于二次身份验证。请参阅下面的请求和响应:#REQUESTGET/styles.cssHTTP/1.1Host:www.foobar.tld#RESPONSEHTTP/1.1200OKDate:Tue,07Feb201715:00:00GMTServer:ApacheETag:"20-547f1fbe02409"Content-Length:32Cache-Control:max-age=7200publicmust-revalidateContent-Type:text/css总结在这篇文章中,我们介绍了:为什么应该使用CDN及其工作原理。静态内容和动态内容有什么区别。HTTP标头如何解决缓存问题。那么想象这样一个场景,假设你有一个网站需要保存用户的登录状态,需要对不同状态的用户进行不同的展示。通常,我们使用cookie来解决用户特征问题。这时候,问题就来了。如果cookie也缓存在CDN中,会导致所有用户都拥有相同的cookie,这不是我们希望看到的。那么如何解决呢?我们会在《掌握 HTTP 缓存——从请求到响应过程的一切(下)》中详细介绍。商店《掌握 HTTP 缓存——从请求到响应过程的一切(上)》阅读原文。【本文为专栏作者“虎子打哈”原创文章,转载请联系作者获得授权】点此阅读更多该作者好文