说说HTTP的缓存

  前两天吐槽了自己的博客加载太慢,其中最主要是因为有一个4M大小的search.xml索引文件,而每次访问网站的时候都完全需要下载这个文件,这样一个1M带宽的小水管的确是扛不住,十几秒的加载时间严重影响了用户的体验
  虽然使用CDN可以解决这个问题,毕竟一般的CDN是按照流量而不是带宽收费的,但是仔细想想觉得,我的博客内容都是静态文件,只有每次更新博客的时候网页和资源文件才可能部分更新,所以完全可以告知浏览器缓存文件一段时间,或者需要该文件的时候向服务器确认该文件是否Modified,依照结果决定是否真正下载该文件即可。呵呵,这就牵扯出了HTTP中缓存的概念了。
  随着互联网的不断发展,用户体验和运营成本显得越来越重要,合适地使用HTTP缓存可以:
  (1) 降低访问延迟:通过客户端缓存可以让客户端立即从本地加载数据,或者同客户端网络最接近的服务器上加载缓存副本,而不用向原始服务器发起请求,客户访问响应资源的速度将会有很大的提升,这对于国内电信、联通骨干网络划江而治的清晰有奇效;
  (2) 减少网络带宽:国内服务器的租赁费用绝大多数成本来源于带宽,合理利用缓存可以重用缓存副本,减少实际数据的网络传输量,从而节省带宽或者流量费用。而且如果使用云计算成熟的CDN解决方案,按照流量计费则可以获得很大的访问带宽,而且这些云计算厂商同电信运营商的议价能力更强,相对用户来说也更为的合算;
  (3) 提高服务的可用性:缓存可以降低后台服务端的压力,对服务端来说本身就起到一定的保护作用,比如现在的CDN可以设置访问带宽达到某个限额回源或者拒绝访问。在某些情况下后台服务端不可用的时候,还可以设置缓存服务器返回陈旧的缓存信息,不至于让整个网站不可用。
  整个网络的访问过程,从服务端到客户端之间任何环节都可能有缓存的存在,比如:
  (1) 浏览器缓存:记得当初网上列举优化Windows的一项内容,就是设置浏览器的缓存大小和位置(Ramdisk内存盘中),浏览器可以把认为可以缓存的东西保存到本地磁盘,下次再需要对应内容的时候只要检查本地缓存合法后直接加载就可以了,对于频繁访问的网站,以及浏览器返回这种情形十分有效。
  (2) 代理缓存:一般存在于大型公司或者ISP等机构,为其所服务的客户提供可配置(比如浏览器设置代理地址)的或者无感知的缓存服务,他们扮演者一种中间人的角色,其提供的缓存是一种shared cache。比如我们下载大尺寸的软件或者视频,看似国外的地址但是却急速下载完成,就是运营商把一些热点资源缓存来下的结果,而天威、长城这类宽带浏览网页慢但是看视频飞快,也是这么个原因。
  (3) 网关缓存:也被叫做反向代理缓存,通常是服务提供着向外部署的一套缓存服务,用以提高自身服务的可伸缩性、可靠性和响应性能。服务的请求通过负载均衡等技术路由到缓存服务器上,缓存服务器将本地有效的缓存直接返回给客户端,或者将无效、不存在的缓存向原始服务器更新后,再回传给客户端,我们常说的CDN就是这么个角色。其实上面一点就是我们常说的正向代理,这里说的是反向代理,他们都是加载在客户端和真实服务端之间的网关,只是某些角度不同罢了。
  当然缓存是一个思路,各个行业各个服务都可以借鉴,这里我们就着重关注HTTP中的缓存了。

一、HTTP中缓存相关的字段

  在HTTP 1.x协议中,缓存主要是通过HTTP头字段来控制缓存行为的。
  HTTP中的HTML文件,可以在中通过meta标签来标明文件是不可缓存的,或者文件在多久之后失效。虽然meta标签容易去写,但是并不推荐使用,只有很少的浏览器缓存会检查它,代理服务基本不会去读取分析HTML报文体,所以该缓存的使用范围很有限。
  HTTP头信息会被所有的中间服务器读取和处理,而且缓存的功能也更为的强大,主要包含:

1.1Expires

  Expires头指明了相关资源的有效时间点,到该时间点之后缓存需要一直向原始服务器确认该资源是否被修改了。该字段被广为使用因而几乎所有的浏览器和缓存服务器都会处理它,其值是一个绝对时刻的HTTP date,同时还必须是标准的GMT时间,如果其值不是将来的某个时刻那么其对应的资源就认为是不可缓存(或者缓存过期)了。
  Expires虽然好用,但是也有着缺陷:如果对缓存有效性要求较高,那么服务端和缓存服务器的时间就需要是同步的;更严重的问题是缓存有效时间是一个绝对时间点,很多情况下如果忘记更新其值,那么该资源就一直是不可缓存的状态,所有的请求就都回源到原始服务器上了。

1.2 Cache-Control

  因为Expires有着上述的问题,而且控制不够灵活,所以现在基本很少再使用了。HTTP 1.1引入了Cache-Control响应头以便更好的控制内容的缓存属性。该响应头包含的属性有:
  max-age=[secs]: 指明了某个资源相对于请求时间记起最大的有效时长(秒),所以服务提供者再不用繁琐的修改Expires了;
  public: 表明经过认证响应是可以缓存的,任意的中间缓存和本地浏览器都可以缓存该内容,因为默认HTTP对于需要认证的请求的响应是默认private的;
  private: 只允许缓存给某一个用户(比如本地浏览器缓存),所有的中间代理缓存都不允许缓存这个资源;
  no-cache: 在缓存服务器接收每一个客户端请求的时候,都强制将请求发回给原服务器验证缓存是否有效,然后缓存才可以向客户端发送缓存副本。其主要作用是和public合作确保验证机制的执行,或者确保客户端能得到最新的缓存副本;
  no-store: 任何情况下都不允许缓存该资源;
  must-revalidate|proxy-revalidate: 告知缓存必须准守缓存机制的约束,其主要针对的情况是,某些时候原始服务器不可用的时候,缓存服务器可以蜕化返回一些陈旧的缓存副本以不间断服务。

1
2
3
Cache-Control: max-age=600
Expires: Sat, 02 Dec 2017 08:05:18 GMT
Last-Modified: Thu, 30 Nov 2017 08:14:25 GMT

  如果Cache-Control和Expires都存在,那么Cache-Control优先级更高,毕竟Cache-Control就是针对Expires的更新版本。

1.3 缓存的验证

  缓存的验证非常的重要,主要是缓存服务器需要确保缓存是有效的,同时如果得知当前缓存的副本和原始服务器是一致的,就不需要重新下载一模一样的副本了。如果缓存的资源有Last-Modified头信息,那么缓存就可以向原始服务器发送一个附带If-Modified-Since头的请求,以确保本地的缓存是否还可以继续使用。
  HTTP 1.1还引入了ETag,作为一个由服务器端为特定文件的特定版本产生的唯一性标识符,当资源被更新后该标识符也会被同步更新。所以缓存可以向服务端发送带有If-None-Match的请求,以确认本地的资源是否和服务器端的资源是一样的,如果资源没有更改则返回一个标准的304 Not Modified的响应。
  所有的缓存都会处理Last-Modified,同时ETag也被越来越广泛的支持了,不过ETag不应当脱离Cache-Control单独使用,否则每一个请求都需要向服务器进行If-None-Match确认,这个请求响应的来回也会增加网络流量和响应时间。现代的服务端针对静态资源,基本都会自动产生ETag和Last-Modified响应头信息,以便有缓存需求的访问者能够受益,网站管理者不需要做其他额外的配置。

二、缓存友好的服务端建设

  要让缓存发挥作用,HTTP服务端需要尽量准守一些约定,形成一个缓存友好的网站。

2.1 缓存友好网站的一般性准则

  (1). 保持url的一致性,如果是同一个资源,那么在不同的页面被引用到、针对不同的访问者都应该使用一致性的url,而且同一个资源不要在网站中保存多个副本,那真的是浪费。在配置缓存服务端的时候我们知道url基本都默认成为缓存键的生成因素,所以同一个资源就需要一致的url去引用它;
  (2). 对于不会更新的静态资源或者页面,max-age可以适量设置大一点的值;对于常规更新的资源,根据更新的频率设置合适的过期时长;
  (3). 对于资源类的文件(比如可下载的文件),也可以设置一个很长的过期时间,而如果文件更新了就更新它的文件名即可,这样能够确保用户总能下载到正确版本的资源,只需要引用该资源的页面设置一个过期时间相对较短的值就可以了。所以现在很多网站开发都看见uri中附带v1、v2……这样的版本号,甚至可以将版本控制的摘要附加到资源名当中
  (4). 只对必要更新的资源进行更新,随意拷贝移动资源可能会让服务器修改Last-Modified值,导致不必要的缓存失效;
  (5). 只对必要的动态资源设置cookie,带cookie的资源是很难被缓存的。
  (6). 默认很多情况下,对于脚本语言生成的动态响应页面都是没有validator(Last-Modified、ETag)或者freshness(Expires、Cache-Control)信息的,但是:如果同一个脚本对相同的请求在将来的一段时间(几秒钟甚至几天)内能够生成相同的响应,则可以考虑其为cacheable;如果一个脚本的响应内容只跟url相关,那么其也是cacheable的;如果脚本的输出依据于cookie、认证信息等,则为不可缓存的。
  (7). 非必要不要使用POST方法,GET方法的响应是可缓存的,但默认POST不是!

2.2 其他注意事项

  (1). 需要认证的页面默认是不会被shared cache缓存的,但是通过Control-Cache参数可以实现缓存,这不会造成安全问题,因为缓存服务发送这个缓存之前,还是需要把每个用户的授权信息发送到原始服务器,只有原始服务器认证通过之后才会将缓存的内容发送给用户。

1
Cache-Control: public, no-cache

  虽然有这个安全机制但是不要滥用,把需要认证访问的资源最小化,因为需要原始服务器进行认证检查会有性能和响应时间的损失,一些非敏感的静态资源应该配置服务器使其不用授权就可以访问,那么这些资源就会被自动的缓存了。
  (2). HTTPS的资源是无法被缓存服务器解密和缓存的,HTTP的资源是天然缓存友好的,所以现在的CDN都需要用户配置SSL证书以支持HTTPS缓存,这里CDN就是一个真正的中间人角色了,他扮演着普通用户向原始服务器请求资源,然后以HTTPS协议和客户端通信的时候再将资源加密送给最终的用户。此时你和用户之间通信内容不再是端到端绝对安全的了,对于敏感的内容还是直接回源访问吧。

2.3 个人小站的压缩和缓存配置

  针对个人小站的优化,一方面是开启了gzip压缩,这样即使图片等资源也不能压缩太大的内容(图片本身就是压缩格式了,如果nginx还能无损压缩算法,那干嘛不直接用于创建新的图片格式),但是对于xml等大尺寸文本文件其收益还将回事很客观的。

2.3.1 针对压缩的配置

1
2
3
4
5
6
7
8
9
10
11
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_comp_level 7;
gzip_min_length 4096;
gzip_buffers 512 16k;
gzip_proxied any;
gzip_types
text/plain text/css text/js text/xml text/javascript
application/javascript application/x-javascript application/json application/xml application/rss+xml
image/svg+xml;

  上面的参数设置都很显而易见,其中有一个比较重要的参数gzip_vary on,就是用于生成Vary: Accept-Encoding头部的。现在的网站如果启用了CDN,CDN会缓存相应资源的副本,但是访问的客户端有千千万万,有的支持压缩有的不支持压缩特性,那么只有通过上面的参数,CDN同时缓存压缩版本和不压缩版本的资源类型,以便在和客户端协商的时候提供正确的版本。

2.3.2针对缓存的配置

  Nginx不会去读取或者解析资源文件的内容,而是根据资源文件的扩展名确定文件的MIME类型,通过对资源的扩展名进行匹配,就可以针对相应类型的资源进行定制化的操作,比如增加缓存相关的文件头信息。

1
2
3
4
5
6
7
8
location ~* \.(?:xml)$ {
expires 48h;
add_header Cache-Control "public, no-cache";
}
location ~* \.(?:jpg|jpeg|png|ico|gif)$ {
expires 96h;
add_header Cache-Control "public";
}

  上面的expires和HTTP 1.0的Expires没有半毛钱的关系,其为Nginx内部用来控制过期时间的表示方式的。其值为当前时间和其参数time的和(,而如果添加了modified关键字则为文件修改的时间和参数的和。

1
2
expires [modified] time;
expires epoch | max | off;

  如果time的值为负数,则Nginx会自动设置“Cache-Control: no-cache”,如果time的值为0或者正数,则会自动设置“Cache-Control: max-age=t”,不过上面还可以进行更精细的控制。epoch会将Expires设置成1970年的Unix诞辰,max参数会将Expires设置成“Thu, 31 Dec 2037 23:55:55 GMT”,也就是Unix的世界末日,而max-age则被设置为10year;如果设置为off则表示禁止增加或者修改Expires和Cache-Control头信息。

三、Nginx搭建代理缓存服务器

3.1 简介

  Nginx无愧为反向代理服务器的王者,将其配置成一个缓存服务器也是其十分常见的应用形式。此时Nginx部署在负载均衡器后面(当然Nginx本身也是个很好的负载均衡器),Web/Application Server的前面充当网关的角色,将用户的请求缓存在磁盘当中,而相同的请求再次过来时候就可以直接从磁盘中加载返回,而不用再向原始服务器发起相同的请求。据称Varnish是一个更专业的缓存服务器,但是Nginx流行很多,针对大多数的缓存需求也能轻松胜任。
  为了描述方便,在缓存使用场景中,我们分化出了两个角色——Original Server和Cache Server。
  Origin Server: 主要指静态拥有或者动态产生需要被访问的资源的服务器,除了提供资源还需要产生合适的HTTP头部对缓存进行控制。要注意原始服务器产生的响应的头部信息可以对缓存进行绝对的控制,认定某些资源不允许被缓存、某些资源可以被缓存、缓存的时间有多久。
  Cache Server: 从客户端获取原始的HTTP请求,让后判断:要么本地是否有一个存在的并且未过期的缓存结果也快速返回,否则将客户端的请求转发给Origin Server,再根据得到的响应决定该结果是需要缓存在本地还是不缓存,然后将结果转发给原始的客户端。

3.2 Nginx缓存服务器的配置

  搭建Nginx缓存代理服务器的配置,其实就是在Nginx反向代理的配置上增加proxy_cache的缓存配置就可以工作了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
proxy_cache_path /tmp/nginx levels=1:2 keys_zone=my_cache_zone:10m max_size=1g inactive=60m;
proxy_cache_key "$scheme$request_method$host$request_uri$cookie_user";
location /static {
proxy_cache my_cache_zone;
add_header X-Cache-Status $upstream_cache_status;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_revalidate on;
proxy_cache_min_uses 3;
proxy_cache_background_update on;
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504 updating;
proxy_cache_lock on;
proxy_pass http://127.0.0.1:8080;
}
}

  上面的proxy_cache_path是一个有点复杂的参数,后面紧跟的是本机的本地目录位置;levels的参数指明了两级目录结构,默认是单个的目录,但是文件多了访问起来会很慢;keys_zone声明了一个共享内存映射的区域,其被用于存储共享键和metadata元数据信息,内存映射的方式可以快速检索一个请求是否在缓存中而不用进行磁盘IO,1M的空间就可以大约存储8k个缓存的相关信息;max_size表明cache所用的最大磁盘空间极限值,当缓存的内容超过这个值的时候就会根据least-recently-used算法删除旧的缓存信息;inactive指明一个缓存持续多久不被命中后就置为inactive,默认值是10min,然后inactive的缓存条目会被cache managert process自动删除掉,而不管这个缓存在HTTP层面是否已经expired。
  proxy_cache_key指明了哪些请求参数用于组织成缓存的唯一键。上面的cookie_user只是一个例子,实际中要谨慎使用,因为很多资源如果不是跟cookie相关的,那么你的缓存服务器将会缓存大量实际内容相同的冗余副本。
  通过add_header增加X-Proxy-Cache,则Nginx返回客户端的响应中就会增加这个头部表明缓存的状态:HIT、MISS、BYPASS……,这个名字可以随便取的,比如X-Cache-Status也很常用。
  默认Nginx会将缓存永久的保留,直到缓存的大小超过max_size后最旧被访问的资源将会被清除,通过proxy_cache_valid可以设置缓存的valid时长。
  proxy_cache_revalidate打开的时候,Nginx可以使用conditional GET请求,即当客户端请求的缓存不存在或者失效了,则Nginx向源服务器请求附带If-Modified-Since请求,如果源服务器的内容没有更新返回304 Not-Modified响应,就不必返回响应的实体了。
  proxy_cache_min_uses用于设置同一个请求至少被访问多少次,Nginx才会缓存它,默认的值是1。
  通过配置proxy_cache_use_stale,当Origin Server不可用(挂掉或者繁忙等)而Nginx无法向后端服务器请求更新资源的时候,是否仍然可以向客户端返回stale陈旧的缓存内容。上面的配置就是当Nginx向后端服务器请求收到error、timeout或者其他的5xx错误码的时候,仍然可以向客户端返回历史缓存的响应内容,这在某种程度上增加了服务的容错性。上面的updating参数表明当Nginx正在向源服务器获取更新资源的时候,相同的请求再次过来,则向后续的请求返回stale资源,直到之前请求触发的更新完成,才向后续的请求返回更新后的资源。
  当proxy_cache_lock被打开,则表明当多个请求同时过来但是资源没有被缓存的时候,只允许第一个请求发送到源端服务器获取资源,其他的请求被阻塞等到直到第一个请求获得的资源被缓存可用。

3.3 Nginx缓存服务器非核心配置

  上面的配置基本够用了,同时还可以通过一些其他选项进行定制化的缓存操作。
  (1). 默认Nginx是准守源服务器的缓存控制的,所以对于Cache-Control设置为private、no-cache、no-store,或者含有 Set-Cookie的响应是不会缓存的,而且只对GET、HEAD方法进行缓存。但是这些限制都可以被修改:

1
2
3
proxy_ignore_headers Cache-Control;
proxy_cache_valid any 30m;
proxy_cache_methods GET HEAD POST;

  (2). Nginx的缓存也可以对特定资源不进行缓存,通过proxy_cache_bypass指令可以将客户端的请求直接转发到源服务器:

1
proxy_cache_bypass $http_x_no_cache $cookie_nocache $arg_nocache;

  通过上面的参数,请求HTTP头当中有X-No-Cache、或者Cookie中有nocache、或者GET参数中有nocache,则对应的请求会直接被转发给源服务器处理。

本文完!

参考