草庐IT

浅谈HTTP缓存与CDN缓存的那点事

Jcloud 2023-03-28 原文

HTTP缓存与CDN缓存一直是提升web性能的两大利器,合理的缓存配置可以降低带宽成本、减轻服务器压力、提升用户的体验。而不合理的缓存配置会导致资源界面无法及时更新,从而引发一系列的衍生问题。本文将分别将从HTTP缓存与cdn缓存的规则、流程、配置入手,能让大家了解基础概念的同时,可对自己的项目配置定制化的缓存调优方案,以及在遇到缓存问题时如何快速定位解决。

 

首先,让我们来了解一下缓存在实际场景中的应用

用户第一次访问网站时,浏览器会从服务器获取所有的资源。在传输过程中,浏览器会通过一些约定好的响应头,从而确定是否需要将这个资源保存一份到本地作为缓存,当用户第二次访问该网站的时候,浏览器就会从缓存中加载资源,不用向服务器请求资源,从而提高了网站的访问速度,而若使用了CDN,当浏览器本地缓存的资源过期之后,浏览器不是直接向源站点请求资源,而是向CDN边缘节点请求资源,CDN边缘节点中也存在缓存,若CDN中的缓存也过期,那就由CDN边缘节点向源站点发出回源请求来获取最新资源。

HTTP缓存

简介

http缓存是一种客户端缓存,当Web浏览器向服务器发起资源请求时,服务器可以在响应报文头中包含缓存相关的信息。这些HTTP Header会告诉浏览器是否以及如何缓存资源,再次请求时如果命中缓存将直接读取本地缓存不再发出请求。

缓存规则

http缓存规则由响应头中Expires,Cache-Control ,Last-Modified ,Etag 这四个关键字段控制。其中Expires和Cache-Control为强缓存用来确定确定缓存的存储时间,Last-Modified 和Etag为协商缓存则用来确定缓存是否要被更新,接下来我们简单来看一下区别。

强缓存

•expires: HTTP1.0中用来控制缓存时间的参数,header里包含日期/时间,用GMT格式的字符串表示, 即在此时间之后,响应过期。

•cache-control: HTTP1.1中用来控制缓存时间的参数

◦public: 表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存。

◦private: 表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器例如CDN不能缓存它)。

◦max-age=<seconds>: 设置缓存存储的最大周期,相对于请求的时间缓存seconds秒,在此时间内,访问资源直接读取本地缓存,不向服务器发出请求。(与expires同时出现时,max-age优先级更高)

◦s-maxage=<seconds>: 规则等同max-age,覆盖max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),并且私有缓存中它被忽略。(与expires或max-age同时出现时,s-maxage优先级更高)

◦no-store: 不缓存服务器响应的任何内容,每次访问资源都需要服务器完整响应

◦no-cache: 缓存资源,但立即过期,每次请求都需要跟服务器对比验证资源是否被修改。(等同于max-age=0)

协商缓存

•Last-modified: 源头服务器认定的资源做出修改的日期及时间。精确度比Etag低。包含有If-Modified-Since ( 资源修改的时间 )或 If-Unmodified-Since首部的条件请求会使用这个字段,Last-Modified优先级低于Etag。

•Etag: HTTP响应头是资源的特定版本的标识符,如果客户端想再次请求相同的URL,将会发送一个包含已保存的ETag和“If-None-Match”(标识符字符串)字段的请求。客户端请求之后,服务器可能会比较客户端的ETag和当前版本资源的ETag(只要文件内容改动,ETag就会重新计算)。如果ETag值匹配,这就意味着资源没有改变,服务器便会发送回一个极短的响应,包含HTTP “304 未修改”的状态。304状态告诉客户端,它的缓存版本是最新的,并应该使用它。

我们通过chrome控制台可以很轻松的找到一个案例:

 

图中配置

1.Cache-control: max-age=3600代表相对于请求时间,缓存3600秒,即1小时,在此时间内,再次访问资源直接读取本地缓存,不向服务器发送请求.

2.Last-modified: Mon...上次修改时间,如果缓存时间过期,该字段将用于与请求中的If-Modified-Since字段进行对比,一致则继续使用之前缓存,不一致则认定缓存失效

3.Expires: 在http1.0版本下被cache-control覆盖,此处意为缓存至Mon, 07 Nov ...

4.Etag: Web服务器会返回资源和其相应的ETag值,该字段将用于与当前客户端版本资源的ETag进行对比,一致则继续使用之前缓存,不一致则认定缓存失效

缓存流程

 

 

缓存规则在其中是如何起作用的呢,我们来看几个重点关注部分

重点关注1: 缓存是否过期

基于该资源上次响应缓存规则同时满足下列条件则视为缓存未过期,不发请求直接从本地缓存读取该文件。需要注意的是,判断缓存是否过期只跟客户端有关系,与服务端无关。1&2&3同时满足即认为缓存未过期,相反则是已过期

1.cache-control值为max-age

2.max-age > 0

3.当前 date < 上次请求时的date + max-age

注:如果HTTP为1.0时,则用expires判断是否过期,如果HTTP为1.1及其以上时,则查看cache-control。

重点关注2: 询问服务器资源是否修改

判断资源是否修改,需要客户端与服务器共同协作,客户端在首次拿到资源缓存后会存储Etag(若有)和Last-Modified(若有),在下次缓存过期时会将Etag写在请求头部中的If-None-Match中,将Last-Modified值写在请求头部中的If-Modified-Since中,服务端优先对Etag进行对比,然后再对比Last-Modified,一致即视为缓存没有修改,命中协商缓存,返回304,不一致则返回新文件并带上新的Etag或Last-Modified值。

重点关注3: 缓存规则

参考上文缓存规则,不在赘述。

 

小结

对于http缓存的配置,我们只有在了解http缓存的原理、规则、流程后,才能根据不同的情况定制不同的规则,真正的发挥http缓存在实际业务中的价值。

 

CDN缓存

cdn缓存是一种服务端缓存,cdn服务商可以将源站上的资源缓到其各地的边缘服务器节点上。当用户访问该资源时,cdn再通过负载均衡将用户的请求调度到最近的缓存节点上,有效减少了链路回源,提高了资源访问效率及可用性,降低带宽消耗。

缓存规则

与http缓存规则不同的是,这个规则并不是规范性的,而是由cdn服务商来制定,我们以JD内部CDN举例,打开cdn接入界面,面板如下。

 


可以看到,提供给我们的配置项只有文件类型(或文件目录)和Http2,在cdn节点上缓存默认遵循源站设置缓存时长。

运作流程

 

由图我们可以看出CDN的主要处理逻辑集中在缓存处理阶段,除了关注CDN缓存的文件类型及时间外,我们还需要引入一个概念——回源,客户端请求访问资源时,如果CDN节点上未缓存该资源,或者部署预热任务给CDN节点时,CDN节点会回源站获取资源。如图中所示,接入cdn后,我们提供服务的服务器就是源站,源站一般情况下只会在cdn节点没有资源或cdn资源失效时接收到cdn节点的资源请求,其他时间,源站并不会接收请求。简单的概括就是,没有资源就去源站读取,有资源就直接发送给用户。值得注意的是cdn中有s-maxage=0、max-age=0、no-cache、no-store、private中的任一种时候,该类型文件就被认定为不缓存文件,就是所有请求直接转发源站,只有当缓存时间大于0且缓存过期的时候,才会与源站对比缓存是否被修改。

缓存配置

与在Web浏览器中的缓存规则类似,可通过发送缓存指令标头来控制缓存在CDN中的执行方式。尽管大部分标头最初都旨在解决客户端浏览器中的缓存问题,但现在所有中间缓存(如 CDN)也会使用这些标头,可使用两个标头来定义缓存刷新:Cache-Control 和 Expires。 如果两者都存在,则 Cache-Control 为最新且优先于 Expires。 还有两种用于验证的标头类型(称为验证程序):ETag 和 Last-Modified。 如果两者均已定义,则 ETag 为最新且优先于 Last-Modified。以OSS对象存储为例,在缓存配置的文档中特别有以下说明。

缓存继承

当用户请求您某一业务资源时,源站对应的Response HTTP Header中存在Cache-Control字段,此时默认策略如下:

•Cache-Control字段为max-age,对该资源的缓存时间以配置的缓存时间为主,对于小于1小时的缓存时长,不继承max-age指定时间。

•Cache-Control字段为s-maxage=0、max-age=0、no-cache、no-store、private、nil或无 Cache-Control字段时,对象存储会源节点会为CDN默认添加:Cache-Control: max-age=3600头部字段,已确保提高缓存的命中率,同时应对高并发回源流量带来的风险与成本的增加。

缓存影响

1.如果http缓存设置cache-control: max-age=600,即缓存10分钟,但对象存储cdn缓存配置中设置文件缓存时间默认为1小时,那么就会出现如下情况,文件被访问后第20分钟修改并上传到服务器,用户重新访问资源,响应码会是304,对比缓存未修改,资源依然是旧的,一个小时后再次访问才能更新为最新资源

2.如果不设置cache-control呢,在http缓存中我们说过,如果不设置cache-control,那么会有默认的缓存时间,但在这里,对象存储cdn服务商明确会在没有cache-control字段时主动帮我们添加cache-control: max-age=3600。

注:针对问题1,也并非没有办法,当我们必须要在缓存期内修改文件,并且不向想影响用户体验,那么我们可以使用cdn服务商提供的强制更新缓存功能,主要注意的是,这里的强制更新是更新服务端缓存,http缓存依然按照http头部规则进行自己的缓存处理,并不会受到影响。

小结

cdn缓存的配置并不复杂, 复杂的情况在于cdn缓存配置会受到http缓存配置的影响,并且不同的cdn运营商有各自的运营规则计费标准,结合来看才能让cdn缓存在业务中发挥最大的效能。

HTTP缓存与CDN缓存的结合

在我们分别了解http缓存配置和cdn缓存配置后,让我们再结合引言看一次二者结合的请求过程

 

当用户访问我们的业务服务器时,首先进行的就是http缓存处理,如果http缓存通过校验,则直接响应给用户,如果未通过校验,则继续进行cdn缓存的处理,cdn缓存处理完成后返回给客户端,由客户端进行http缓存规则存储并响应给用户。再回到开篇缓存在实际场景中的应用,当我们分析缓存问题时,一定要将两个流程独立开来分析判断,是由于http缓存配置的不合理,还是cdn缓存未及时更新引起的问题。

 

实战场景推荐(懒人版)

不同访问场景下的缓存规则选择

1.不更新文件内容

优先使用http的本地缓存,配置cache-control: max-age=seconds //seconds > 0(且设置为较大值31536000,即1年):强缓存,缓存当前资源,在配置时期内,再次请求资源直接读取本地缓存。

使用cdn缓存,当本地缓存无法使用时,配置较大的cache-control:同样可以让业务直接访问cdn资源,且配置时间内不会再发生回源请求。

2.很少更新文件内容

对于img,css,js,fonts等非html资源,我们可以直接考虑配置cache-control: max-age=seconds //seconds > 0,并且max-age配置的时间可以相对久一些,类似于缓存规则案例中,cache-control: max-age=36000配置10小时的缓存,需要注意的是,这样配置并不代表这些资源就一定十小时不变,其根本原因在于目前前端构建工具在静态资源中都会加入戳的概念(例如,webpack中的[hash],gulp中的gulp-rev),每次修改均会改变文件名或增加query参数,本质上改变了请求的地址,也就不存在缓存更新的问题。

3.频繁更新文件内容

对于html资源,作为前端资源的入口文件,一旦被强缓存,那么相关的js,css,img等均无法更新。对于高频维护的业务类项目,建议配置cache-control: no-cache或cache-control: max-age=0:采用协商缓存,缓存当前资源,但每次访问都需要跟服务器对比,检查资源是否被修改。但是基于流量和成本的考虑更推荐于max-age设置一个较小值,例如3600,一小时过期。对于一些活动项目,上线后不会进行较大改动,建议业务配置一个较小的max-age值,否则一旦出现bug或是未知问题,用户无法及时更新。

除了以上考虑,有时候其他因素也会影响缓存的配置,例如春晚红包除夕活动,高并发大流量很容易给服务器带来极大挑战,这时我们作为前端开发,就可以采用静态页面提前加载兜底来避免用户多次进入带来的流量压力。

 

如何减少缓存规则带来的访问影响

1.通过清理缓存控制

我们可以使用cdn服务商提供的强制更新缓存功能,主要注意的是,这里的强制更新是更新服务端缓存,http缓存依然按照http头部规则进行自己的缓存处理,并不会受到影响。

2.通过url带版本号或者版本发布时间

我们在使用对象存储发布资源时,可根据版本号或发布时间定义url,以作为区分。

例:
https://storage.jd.com/xxx/xx?verson=1.1.1

例:
https://storage.jd.com/xxx/xx?verson=20221111

 

线上禁止的访问策略

对于访问同一资源,url带时间戳、uuid等具有唯一性参数会直接回源至后端服务,带来极高的带宽成本及击穿底层服务的风险,业务在使用CDN域名访问时,请务必谨慎使用带有时间戳、唯一性参数的url。

 

总结

技术行业发展到今天,海量的流量已然成为常态,而http缓存和cdn缓存分别作为客户端缓存和服务端缓存基石更是值得我们去深入学习、思考。

作者:管宸昊

有关浅谈HTTP缓存与CDN缓存的那点事的更多相关文章

  1. ruby - 如何模拟 Net::HTTP::Post? - 2

    是的,我知道最好使用webmock,但我想知道如何在RSpec中模拟此方法:defmethod_to_testurl=URI.parseurireq=Net::HTTP::Post.newurl.pathres=Net::HTTP.start(url.host,url.port)do|http|http.requestreq,foo:1endresend这是RSpec:let(:uri){'http://example.com'}specify'HTTPcall'dohttp=mock:httpNet::HTTP.stub!(:start).and_yieldhttphttp.shou

  2. ruby - 如何在 Ubuntu 中清除 Ruby Phusion Passenger 的缓存? - 2

    我试过重新启动apache,缓存的页面仍然出现,所以一定有一个文件夹在某个地方。我没有“公共(public)/缓存”,那么我还应该查看哪些其他地方?是否有一个URL标志也可以触发此效果? 最佳答案 您需要触摸一个文件才能清除phusion,例如:touch/webapps/mycook/tmp/restart.txt参见docs 关于ruby-如何在Ubuntu中清除RubyPhusionPassenger的缓存?,我们在StackOverflow上找到一个类似的问题:

  3. ruby-on-rails - Ruby on Rails 计数器缓存错误 - 2

    尝试在我的RoR应用程序中实现计数器缓存列时出现错误Unknownkey(s):counter_cache。我在这个问题中实现了模型关联:Modelassociationquestion这是我的迁移:classAddVideoVotesCountToVideos0Video.reset_column_informationVideo.find(:all).eachdo|p|p.update_attributes:videos_votes_count,p.video_votes.lengthendenddefself.downremove_column:videos,:video_vot

  4. ruby - Net::HTTP 获取源代码和状态 - 2

    我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur

  5. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

    1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

  6. ruby-on-rails - Rails - 从命名路由中提取 HTTP 动词 - 2

    Rails中有没有一种方法可以提取与路由关联的HTTP动词?例如,给定这样的路线:将“users”匹配到:“users#show”,通过:[:get,:post]我能实现这样的目标吗?users_path.respond_to?(:get)(显然#respond_to不是正确的方法)我最接近的是通过执行以下操作,但它似乎并不令人满意。Rails.application.routes.routes.named_routes["users"].constraints[:request_method]#=>/^GET$/对于上下文,我有一个设置cookie然后执行redirect_to:ba

  7. ruby-on-rails - Heroku 吃掉了我的自定义 HTTP header - 2

    我正在使用Heroku(heroku.com)来部署我的Rails应用程序,并且正在构建一个iPhone客户端来与之交互。我的目的是将手机的唯一设备标识符作为HTTPheader传递给应用程序以进行身份​​验证。当我在本地测试时,我的header通过得很好,但在Heroku上它似乎去掉了我的自定义header。我用ruby​​脚本验证:url=URI.parse('http://#{myapp}.heroku.com/')#url=URI.parse('http://localhost:3000/')req=Net::HTTP::Post.new(url.path)#boguspara

  8. ruby-on-rails - 使用 HTTP.get_response 检索 Facebook 访问 token 时出现 Rails EOF 错误 - 2

    我试图在我的网站上实现使用Facebook登录功能,但在尝试从Facebook取回访问token时遇到障碍。这是我的代码:ifparams[:error_reason]=="user_denied"thenflash[:error]="TologinwithFacebook,youmustclick'Allow'toletthesiteaccessyourinformation"redirect_to:loginelsifparams[:code]thentoken_uri=URI.parse("https://graph.facebook.com/oauth/access_token

  9. ruby - HTTP 请求中的用户代理,Ruby - 2

    我是Ruby的新手。我试过查看在线文档,但没有找到任何有效的方法。我想在以下HTTP请求botget_response()和get()中包含一个用户代理。有人可以指出我正确的方向吗?#PreliminarycheckthatProggitisupcheck=Net::HTTP.get_response(URI.parse(proggit_url))ifcheck.code!="200"puts"ErrorcontactingProggit"returnend#Attempttogetthejsonresponse=Net::HTTP.get(URI.parse(proggit_url)

  10. ruby - 如何使用 Ruby HTTP::Net 处理 404 错误? - 2

    我正在尝试解析网页,但有时会收到404错误。这是我用来获取网页的代码:result=Net::HTTP::getURI.parse(URI.escape(url))如何测试result是否为404错误代码? 最佳答案 像这样重写你的代码:uri=URI.parse(url)result=Net::HTTP.start(uri.host,uri.port){|http|http.get(uri.path)}putsresult.codeputsresult.body这将打印状态码和正文。

随机推荐