后台开发那些常用技术再次小结(四):缓存部分

  缓存技术在计算机体系中使用的极为广泛:CPU内部多级缓存、硬盘缓存、Linux文件系统缓存、数据库查询缓存、DNS解析缓存、浏览器和应用APP缓存等,缓存在加快系统响应的同时,也降低了对某些资源的消耗,善用缓存会整个系统的性能将会有极大的改善。
  因为缓存通常可以从源头重建,所以即使被删除或者丢失了也不会产生太大的问题,不过也有一些诸如缓存失败导致服务器雪崩的问题。缓存最主要考量的是命中率,这个指标收到缓存键集合大小、内存空间大小和缓存寿命相互制约。

一、基于HTTP缓存

  HTTP层的缓存一个重要特性就是其为通读缓存,通读缓存通常作为中介(代理)的角色,透明地给HTTP链接添加缓存功能,他可以拦截客户端请求并用缓存对象返回给客户端作为响应,只有当缓存没有合适的数据的生活,才会链接到原始服务器并转发客户端请求给他。因为服务和通读缓存的接口是一样的,这种情况下客户端也可以直接连接到原始服务器而跳过缓存。通读缓存很具有吸引力,因为他的存在对客户端来说是透明的,客户端无法区分响应来自缓存还是原始服务器,所以通读缓存扮演着可插拔的角色,客户端无须因为他的存在与否而做任何修改。

1.1 HTTP规范的缓存头

  HTTP协议虽然广为使用,而且其可被缓存特性也广受赞誉,不过协议上其相关选项和实现缺较为的混乱。HTTP协议中和缓存相关的参数有:
  Cache-Control
  private: 指出请求结果对请求用户是特定的,响应不能提供给其他用户。该用法也就意味着只有浏览器可以缓存响应,因为中间缓存无法识别特定用户。
  public: 只要它为过期,指示响应可以在用户间被共享。注意这个选项和private是互斥的。
  no-store: 指示响应不能被任何中间缓存存储到磁盘上,即响应可以被缓存在内存,但是不能持久化到磁盘。当你在响应中包含用户敏感信息的时候应该包含这个选项,使得缓存信息不回存储这些数据到磁盘上。
  no-cache: 指示响应不能被缓存,更精确的意思是针对这个请求,缓存在每次用户请求相同资源的时候都需要询问服务器此时是否仍然合法,其效果和 Cache-Control:max-age=0相当。
  max-age: 指示响应在缓存失效前,客户端可以保持缓存多少秒(即响应的TTL)。作者推荐不适用该选项,因为它向后兼容不好,而且还依赖Expires这个HTTP头。
  no-transform: 指示响应不做任何修改直接用缓存提供。比如CDN的提供商可能通过对图片转码来减少大小,降低质量或者改变压缩算法。
  must-revalidate: 指示一旦响应过于陈旧,不重新验证就不能返回给客户端。出现这个参数主要是客户端可以允许返回陈旧信息,此时通过这个参数告知缓存必须停止提供陈旧响应数据,每当客户端请求陈旧对象,缓存会强制向原始服务器请求数据。
  Expires
  指定一个缓存对象失效的绝对时间点,超过这个时间点缓存对象就“陈旧”了。这个头其实同Cache-Control:max-age有些重复的,参考说Cache-Control 中指定的缓存过期策略优先级会高于Expires,不过同时定义两个变量可能会导致潜在不一致行为,建议只用一个并固定使用它。
  Vary
  这个头告知缓存你需要基于某些HTTP请求头,生成响应的多个变体。

  除此之外,还可以在Web页面中使用meta标签控制Web页面缓存,但是书的作者不推荐使用,因为他们不能用来控制中间缓存,而且容易引起混乱,经验不足还是使用HTTP头来控制缓存。

1.2 HTTP的缓存类型

  因为HTTP缓存是通读缓存,在客户端和服务端交互中的任何环节都可以插入中间缓存因而比较灵活。HTTP缓存常见于四种形式:浏览器本地缓存、缓存代理、反向代理和CDN。
  浏览器缓存
  浏览器缓存和本机的内存、文件系统协同工作,当HTTP请求将要发出的时候,浏览器会检查缓存是否具有该资源的合法版本,如果响应存在于缓存中并且仍然有效,浏览器就可以直接重用之而不用发出一个HTTP请求。
  缓存代理
  通常由大公司或者ISP作为通读缓存部署,用于加快用户响应的同时降低网络流量。但是近些年本地代理的做法有所限制,一方面是网络带宽变便宜了,二则越来越多的Web站点采用HTTPS部署,客户端和服务器之间的通信无法被识别和拦截,缓存也就无从谈起了(一大批运营商的缓存服务器要吃灰了)。
  反向代理
  其工作方式和常见的缓存代理差不多,不过是开发者自己部署和控制的,具有极大的灵活性。反向代理本身可以缓存页面缓存,降低对后端Web服务器的请求压力;还可以在反向代理中覆盖HTTP头,更好的控制请求的缓存策略和有效时间。
  CDN
  CDN在之前也说的比较多了,其是基于智能DNS解析+缓存技术实现的分布式网络加速。
  现在较好的CDN提供商可以通过配置,同时提供网站的静态内容和动态内容,这样客户端就可以不用直接向数据中心或者原始服务器发送请求而直接向CDN的地址请求,动态内容的请求可以穿透CDN缓存服务器回源到原始服务器。即使看似有些麻烦,但是这一方面可以隐藏原始服务器的地址,而且可以减轻DDoS攻击力度,再则某些动态的内容也是可以被CDN缓存的。
  在我之前体验的几家国内的CDN提供商,在提供HTTP服务的同时还提供HTTPS类型的服务,不过后者的费用会高一些。CDN会要求用户提供域名证书,所以相比于缓存代理而言,他们是完全可以缓存HTTPS加密通信的内容的

二、缓存应用对象

  对象缓存区别于HTTP缓存,其为旁路缓存类型。旁路缓存中,应用需要意识到缓存对象的存在,而不是透明的存在于应用和数据源之间。
  旁路缓存会应用视为一个独立的键值存储服务,应用程序代码会询问需要的对象是否存在,如果存在就获取之,否则(不存在或者已过期)则需要从头构建该对象,并将结果保存在缓存中以便将来使用。使用这种缓存的动机就是节省对象从头构建所化的时间和资源。

2.1 对象的缓存类型

  客户端缓存
  主要还是浏览器的,据说用JS玩起来比较溜。
  本地缓存
  本地缓存是相对于原始服务器而言的,其常见的本地缓存形式有:
  (1) 对象直接缓存在应用内存
  应用可以创建一个缓存对象池并不在释放分配的内存,这种缓存的速度最快,因为是以执行代码相同的格式存储在进程的内存空间中的。
  (2) 对象存储在共享内存
  这种形式主要是方便同一台机器的多个进程可以同时访问的需要。
  (3) 缓存服务器作为独立应用部署在Web服务器上
  这种形式的缓存服务器,需要应用程序使用专门的接口与之交互,而不能直接访问内存。虽然和上面的形式相比有一定的性能消耗,但好处是可以使用统一的访问接口,后续进行伸缩也较为的方便。这里讨论的本地缓存强调的是应用服务和缓存服务服务在同一台机器上,这种样式的缓存也有一定的问题:a. 如果在多个机器组成的应用环境中,各个机器上的缓存没有联系,也就可能同一个缓存对象会被缓存成多个副本;b. 这种分散式的缓存管理起来也比较麻烦,比如要更新或者删除某条缓存记录的话,要通知到所有的缓存服务器将会非常的棘手。
  分布式缓存
  其实就是相对于上面描述的本地缓存中,将缓存服务器和应用服务器进行分离,通过网络接口的方式进行交互,可为所有的应用服务提供一个集中的缓存服务(变更和删除缓存条目比上面多个本地缓存类型要方便的多)。这样的缓存服务具有很强的伸缩性,管理的方式也多种多样,而且通常也会提供多种语言的访问接口库,是近年来开发和研究的热点,老牌的缓存服务如Memcached和Redis使用非常广泛。

2.2 对象缓存的伸缩

  针对上面描述的对象缓存类型,通常只有分布式缓存才具有伸缩的价值和可行性。常见的Memecached可以采用Ketama一致性hash算法算法的客户端库实现水平伸缩(再次提到last.fm的这个C库 ),这里强调一下Memcached一致性hash实现的伸缩性是在客户端一侧实现的;而Redis允许进行主从复制部署,对某些高热度的数据可以采用读副本的形式增加并发访问量。

三、缓存使用的经验法则

  缓存整个调用栈
  通常一个请求从客户端到最终服务端之间会经历多个角色,在整个调用栈中任意部分都可以考虑插入缓存,而且缓存的调用栈越高,能节省的资源也就越大,响应这个请求的代价也就越小。
  用户间缓存重用
  缓存使用的另外一个要点就是尽可能多地在不同请求或者用户之间重用相同的缓存对象!
  比如下面的例子,通过经度、维度数据查询某个结果,此时过高精度的参数将导致几乎每个调用都是不相同的,而如果将请求参数进行精度降低,那么很多请求就可以共享结果,也就增加了缓存的命中率。
  缓存的入手点
  缓存的实施不是凭感觉的,通常的2-8定律也可以用在这里:可以通过日志查看访问量高的页面或者接口,然后从这些位置入手看是否能通过缓存改善性能,在重要、热点页面上的改进带来收益的提升可能性更大。
  缓存失效的困难
  可以说缓存失效的问题是缓存体系中的大难题,虽然这也同业务类型具有比较大的相关性。
  首先缓存服务可以设置在整个调用栈的任何可能位置,要让整个调用链在最大化缓存命中率的同时还能及时感知缓存失效是比较困难的;再则有些缓存的结果是通过多个数据源进行计算得来的,当最初的某个或者某些数据源变更将会导致该缓存失效,但是怎么样跟踪这种依赖关系是个难点。
  缓存失效简单的方案就是对重要缓存对象上设置一个较短的TTL,这样可以保证数据不会过于的陈旧;为了降低数据库和服务的压力,某些动态数据的展示仍然是可以使用缓存的,只是在最终产生业务的时候进行实际的数据校验操作(虽然体验上不见得多好,比如12306的那种缓存,但至少可以帮助服务器抗住强大的访问压力)。

本文完!

参考