《构建高性能的web站点》读书笔记--缓存

时间:2023-01-31 19:48:53

      其实在说缓存之前,还有其它关于网络和服务器硬件、系统的基础知识,其中在网络一节中:着重介绍了网络模型和带宽的概念,提供了一个我们去计算一次网络传输时间的方法,以及在当前联通、电信网络的情况下,如何部署服务器,做好互联互通。在服务器硬件、系统能力方面,突出了一个服务器能力的指标:吞吐率,介绍了各个主要部件和系统的基础知识。了解并熟悉这方面的知识,对我们构建一个优秀的系统是不可缺少的,笔者所有的这些这方面的知识也是各种资料,没有太多的实践的经历,这里就不摘抄了,推荐大家去看此书或其它相关资料了解这方面的知识。

      下面进入本篇的主题,缓存我们再熟悉不过了,不仅体现空间换时间的体现,也能节省不少的资源开销,提高服务器吞吐率,但是使用所有缓存之前,我们要做寿好更新或过期策略,并且任何时候都要做好去重新计算的准备。

 

首先,从网站方面说一下缓存的应用场景:

1,动态内容缓存

      无论你的页面是php、aspx或jsp,最后输出内容都必须是html,而这些动态页面都要通过一系列的计算最终输出html的结果,这样我们就可以把这个结果缓存,当有下次相同的请求时,直接返回,这种缓存叫页面缓存,在asp.net里我们可以轻松的实现全部或局部的页面缓存(控件缓存)。

      而有些情况下,我们将缓存持久化到硬盘上,这样我们可以很廉价的缓存大量的文件,像我们可以根据url参数不同的缓存不同的结果。但是如果相同缓存太多,就可能造成硬盘I/O开销巨大,我们就要分组目录或其它算法来分目录:我们用时间来创建分级目录,或根据物品ID对一个基数求余的结果来把物品信息缓存放到不同的目录。

      除此之外,我们可以把结果(计算的结果或提取的数据)放到内存中,如.net的cache,或memcache,这些以key/value形式存在的缓存,这种恐怕应该是我们最常用的了。

      还有一种更彻底的方式:静态化页面,让用户直接访问生成的html页面,这种是所有缓存里效率最高的,因为用户的请求不经过我们的程序直接返回,我们的程序只是在后台管理着这些页面。

2,脚本加速

       书中称之为opcode缓存,解释型语言在执行时,由解释器分析代码之后,直接生成操作码operate code,然后直接执行。这样的语言主要有如php、python、ruby,所谓opcode缓存,就是像动态内容那样,把operate code缓存起来,从而节省代码解释过程的时间,这些可以通过php的opcode缓存扩展如APC、xCache等来实现。

       而如C++的编译就没有这一过程,它编译和运行是两个过程,编译成机器可以直接运行的目标程序之后,运行则是由目标程序来控制。

       我们知道C#也是一种编译型语言,不过它的过程却是比较不一样的,它首先是一个C#编译器生成IL,在真正执行时,IL被JIT编译器再次生成机器码来执行。而在IL编译成可运行的代码的过程中,它是以函数为单位编译的,并且每个函数只会被编译一次,以后的运行也是直接拿成生成后的机器码来运行的,这与上面所说的opcode缓存是同一个思想,不过这不用我们去费力气再去关心它,我们关心的如何利用这个机制?在effictive C#中有一条:尽量实现短小简洁的函数,其实这也是我们程序时应该注意的一点,尽量提取相同代码到方法,而不是分散的内联代码,同名函数只会被JIT编译一次。

3,浏览器缓存

       这个可能大家都比较熟悉了,在园子里也看到有很多文章讨论过。浏览器的缓存主要是对单个请求url来说的,如页面和页面的组件(图片、CSS、脚本等)。相同的URL再次请求时,才有可能用到浏览器缓存。

       首先浏览器会把页面的内容缓存到一个特定的位置,但是使用不使用确不是浏览器自作主张的,这里有一个协商的过程,而这些协调信息是包含在http的头信息中的。

LAST-MODIFIED协商

一个url第一次请求,动态程序我们可以轻松在http头信息中加入last-modified信息,而静态文件,系统会自动把最后修改时间在http头信息中,如Last-Modified: Wed, 13 Jun 2012 09:15:45 GMT;

如果再次请求这个url时,浏览器发现有此缓存时,就会再请求中加入:If-Modified-Since: Wed, 13 Jun 2012 09:15:45 GMT这样格式,时间就是上次请求返回的最后修改时间;

这时如果是动态程序我们就可以检查是否有改变,然后决定是输出新内容或304,如果是静态文件,系统会比较这个时间,返回200还是304;

浏览器接到响应,如果是304就使用浏览器的缓存,200则使用新接收的数据。

ETAG协商

这个请求的过程和LAST-MODIFIED是一样的,只不过标识是使用etag,而不再是last-modified.

第一次请求时,你可以生成一个etag值,做为http头信息:ETag: "b09ce81c4549cd1:155db" 发出;再次发出这个请求时,浏览器会在请求头信息中加入:If-None-Match: "b09ce81c4549cd1:155d7";这样你可以,再次比较这个值,来决定返回200或304.

 

     当然这两种方式,结果是一样的,要根据不同的场景来决定如何使用:如我们如果采用定期更新静态文件策略来实现的静态化,一些页面经常会被重新生成也就是最后修改时间经常变化,但是内容可能没有改变,这样我们就可以内容生成的标识使用etag来实现缓存协商;如果我们有多台服务器,每一次请求可能是不同服务器来处理,用根据内容生成的etag值比最后修改时间更合适。

 

      此外,还有一个不得不说的那就是:我们还可以设置http输出头信息中的expires和Cache-Control,如Cache-Control: max-age=31536000,单位是s,来设置过期时间。就是下次的同一个url,浏览器会先检查Cache-Control: max-age,这个是相对过期时间;如果不存在,检查expires设置的时间,这是一个绝对的过期时间。

 

      实际上更多时候,我们不大可能准确判断出这个过期时间,有一个良好的方法:设置一个较大的过期时间,如果文件有改变时,我们就替换一个新的文件名或者加一个新的参数,还让浏览器去请求新的资源。

 

      服务器端拥有绝对的能力来决定浏览器是否使用缓存的文件,同样浏览器不同的刷新方式也决定浏览器是否使用缓存:输入url转向,这个会使用缓存的全部功能,会使用过期时间的设置;f5/刷新,这不会考虑expires但会去询问服务器是否可以使用缓存;ctrl+f5/强制刷新,不使用缓存,不询问,直接请求文件。

4,服务器缓存

      说起这个缓存,我们一定要和之前的动态内容缓存区分开,上述的动态内容缓存,服务器接到一个url的请求,交到我们的动态程序,程序去判断是否有请求内容的缓存,如果有,加入到输出的html;而服务器缓存是,服务器接到一个url的请求,服务器发现有这个url可用的缓存,直接返回,这样就不会再走我们精心构建的程序代码。

5,反向代理缓存

     这个其实应该归到服务器缓存里,但是这里有个反向代理的概念,需要说明一下。代理服务器,这个大家肯定都很清楚,平常所谓的“FQ”:例如我们无法访问网站A,而B可以访问A,我们可以访问B,我们就可以把B做为代理服务器,把对A的请求发给B,B去请求A,然后把A返回内容给B,B再把内容返回给我们。

     而反向代理服务器,就是服务器的代理,就是我们请求一个url,这个反向代理服务器,接到这个请求后,会根据请求的信息分发到相应的实际的请求处理服务器,实际服务器处理完毕,返给代理,再返给浏览器。

     这样的代理服务器相当于一个中枢的位置,当然就可以缓存一些响应的内容,当有相同的请求时,不会到后端实际处理的服务器而直接把内容返回给浏览器。

 

写操作的缓存(缓冲)

      上面所说的都是为了减少服务器读资源使用,减少数据库的读操作或服务器的计算时间。其实我们可以用缓存来实现一些写操作的缓冲:书中提到一个例子,例如我们对于页面流量次数统计的这样不太重要且不需要及时的数据,可以在缓存中缓存访问次数,书中使用memcache实现,因为memcache支持原子操作,当次数到达1000或你设定的值时,才更新一次数据库,这样可以大减少即时更新的资源消耗,可以让数据库干更多的其它重要的事务。

 

分布式缓存和扩展

      随着网站规模变大,可能会有单独的缓存服务器,就以memcache为例,这也是我们最常用的选择,越来越,当一台缓存器不能支持我们的日常运行,你可能会再加一台缓存服务器,我们可以按业务来划分:一台负责用户相关缓存,一台用于物品相关缓存。越来越,当一台无法负责到用户相关缓存,还可能再按业务拆分吗,总之,我们将不可避免的会出现多台服务器用于缓存同类的数据项,但是我们应该把缓存放到哪个服务器,我们要取一个缓存项时,去哪个服务器去取呢?

     书中提到一个解决方案,运用基本的散列算法:我们把key值进行MD5运算,得到一个32位的散列码,同时它也是一个16进制的长整数,这样我们可以取前几位转换为长整数,然后对你缓存服务器的台数取余,来决定去哪台服务器进行该Key缓存的操作。

     按这样的方法,如果我们再增加缓存服务器时呢,这样就不得不做出牺牲,把之前的缓存数据清理掉,重新部署缓存规则:当然清理之前你可能要发出公告,并且暂停网站服务一会,进行升级,然后重新生成缓存或者等到用户访问时自动生成缓存。

 

最后

     我们不能只是为了使用缓存而去使用缓存,如何使用好缓存,使用缓存的时机、如何提高缓存的命中率、缓存的更新策略、过期策略以及缓存内容的监控等等一系列的问题要我们去考虑。