Apache 缓存指南
简介
Apache HTTP Server 拥有一系列缓存功能,旨在以各种方式改进服务器的性能。
三状态 RFC2616 HTTP 缓存
mod_cache 及其提供模块 mod_cache_disk 可实现智能的、HTTP 感知的缓存功能。内容本身保存在缓存中,mod_cache 的目标是尊重所有控制可缓存性内容的 HTTP 标头和选项。mod_cache 的目标是兼顾简单和复杂的缓存配置,用它来处理代理内容、动态本地内容,或加速本地文件的访问。
双状态键/值共享对象缓存
共享对象缓存 API 及其提供模块可以实现整个服务端的、基于键/值的共享对象缓存。这些模块旨在缓存低阶数据,如 SSL 会话及身份验证凭据。后台允许将数据存储在共享内存中的服务器范围内,或在高速缓存 (如 memcache 或 distcache) 中的大数据中心中。
专用文件缓存
mod_file_cache 可实现在服务端启动时将文件预加载到内存中,并且可以加速访问,在经常访问的文件上保存文件句柄,因为没必要为每个请求都去访问磁盘。
三状态 RFC2616 HTTP 缓存
相关模块 :mod_cache,mod_cache_disk
相关指令 :CacheEnable,CacheDisable,UseCanonicalName,CacheNegotiatedDocs
HTTP 协议支持在线缓存机制,而且是内置的,mod_cache 模块可用于使用该机制。
在简单的双状态键/值缓存中,当内容过期时会彻底消失掉。而 HTTP 缓存与之不同,它有一种保留过期内容的机制,先是询问源服务端该过期内容是否发生了改变,如果没变则让其重新生效。
HTTP 缓存中的条目的状态
新鲜
如果内容足够新,在使用期之内,则认为它是新鲜的。HTTP 缓存可以提供新鲜的内容,而无需对源服务端进行任何调用。
过期
如果内容太旧,过了使用期,则认为它是过期的。在把过期内容提供给客户端之前,HTTP 缓存应该联系源服务端,以检查该内容是否仍然新鲜。如果不再新鲜,源服务端会返回替换的内容;如果仍然新鲜,服务端会返回一个代码告知缓存,无需重新生成内容,也无需把原内容重发一遍。内容重新变成新鲜的,循环继续。
在某些情况下,HTTP 协议不允许缓存提供过期数据。比如,当对源服务端刷新数据的尝试失败时,返回 5xx 错误;或者,当另一个对相同条目刷新的请求正在进行中时,在这些情况下,回复中会加入一个 Warning 标头。
不存在
如果缓存满了,它会从中删除内容来腾出空间。什么时候都可以删除内容,删除的可以是过期的,也可以是新鲜的。
htcacheclean 程序可以在需要时使用,也可以部署为服务,使缓存始终保持在特定大小之内,或特定数量的 inode 之内。该程序会尽量先删除过期内容,不得以才会删除新鲜内容。
缓存与服务端的交互
根据 CacheQuickHandler 指令的值,mod_cache 模块在两个可能的位置挂钩到服务器。
快速处理程序阶段
此阶段在请求处理过程中很早就发生,就在请求被解析之后。如果在缓存中找到了内容,它将立即送达,几乎所有请求处理都会被绕过。
在这种情况下,缓存的行为就好像它被锚到服务器前面一样。
此模式提供了最佳性能,因为大多数服务端处理都被绕过了。但是,此模式也绕过服务器处理的身份验证和授权阶段,因此,如果安全是特别要考虑的因素时,应谨慎选择此模式。
当 mod_cache 在此阶段运行时,具有 “授权” 标头 (例如,HTTP 基本身份验证) 的请求既不会被缓存,也不能用缓存来完成。
一般处理阶段
此阶段在请求处理后期发生,在所有请求阶段完成之后。
此时,缓存的行为就好像它被锚到服务器后面一样。
此模式提供了最大的灵活性,因为在筛选链中的精确控制点上存在缓存的可能性。在发送给客户端之前,可以对缓存内容进行筛选或个性化。
…
如果缓存中没有找到 URL,mod_cache 会在过滤栈中添加一个过滤器,以便将响应记录到缓存中,然后退出,允许正常请求处理继续进行。如果将内容确定为可缓存,则内容将保存到缓存中以供使用,否则内容会被忽略。
如果在缓存中找到的内容陈旧,则 mod_cache 模块将请求转换为条件请求。如果源服务端正常响应,则缓存正常响应,替换已缓存的内容。如果源服务器响应的是 304 未修改的响应,则内容将再次标记为新鲜,而缓存内容则由筛选器提供,无需保存。
提升缓存命中率
如果某个虚拟主机有许多不同的服务端的别名,确保 UseCanonicalName 设置为 On 可以显著提高缓存命中的比率。这是因为提供内容的虚拟主机,其主机名在缓存键中被使用。设置为 On,有多个服务端名字或别名的虚拟主机不会生成不同的缓存条目,缓存内容是基于标准主机名而生成的。
新鲜度的使用期
新鲜度的使用期,以下简称使用期。
拟缓存的格式良好的内容应该显式声明其使用期,可使用 Cache-Control 标头中的 max-age 或 s-maxage 字段,也可以包含一个 Expires 标头。
同时,当客户端在请求中提供了自己的 Cache-Control 标头时,源服务端定义的使用期可以被客户端覆盖。这种情况下,在请求和响应中,最短的使用期会获胜。
如果请求或响应中使用期缺失,则使用默认的使用期,即一小时。可以用 CacheDefaultExpire指令来修改。
如果响应中不包含 Expires 标头,但包含 Last-Modified 标头,mod_cache 会自己推算一个使用期,其基于的启发式算法可以由 CacheLastModifiedFactor 指令控制。
对于本地内容,或没有定义其 Expires 标头的远程内容,可以使用 mod_expires 来调整使用期,具体方法是添加 max-age 和 Expires 。
最大的使用期也用 CacheMaxExpire 来控制。
条件请求
当缓存中的内容过期时,httpd 并不会把直接传递源请求,而是会把它并成条件请求。
- 如果源缓存响应中有
Etag标头,mod_cache会向针对源服务端的请求中添加一个If-None-Match标头。 - 如果源缓存响应中有
Last-Modified标头,mod_cache会向针对源服务端的请求中添加一个If-Modified-Since标头。
执行上述任一操作将使请求变成条件请求。
如何应对条件请求
当源服务端接收到一个条件请求时,它应该检查 Etag 或 Last-Modified 参数是否发生了改变:
- 如果没变,源服务端应该响应 “304 Not Modifed”。这会给缓存一个信号,告知其过期的内容依然新鲜,应该继续为后续的请求服务,直到其使用期再次结束。
- 如果内容发生了改变,则当作一开始不是条件请求来发送内容。
条件请求带来两个好处:
- 对服务端进行这样的请求时,如果源内容与缓存中内容匹配,可以轻松判断,无需传输全部源内容。
- 好的服务端设计就当如此,条件请求的开销比完整响应要小的多。
对于静态文件来说,通常只需要调用 stat() 或类似的系统调用,就可以知道文件大小和时间是否发生了改变。因此,即使是本地内容,如果没有改变,也仍然可以用缓存更快地传送。
源服务端应尽全力来支持条件请求,这是切实可行的,但是,如果条件请求不被支持,源服务端会将该请求看作非条件请求,缓存会认为内容已过期,然后把新内容保存到缓存中。这种情况下,缓存会按简易的双状态缓存方式工作,即内容要么新鲜,要么被删除。
哪些内容可以缓存
- 由
CacheEnable指令设定的 URL。 - 响应必须包含 HTTP 状态码 200,203,300,301,410。
- 必须是 HTTP GET 请求。
- 如果响应包含 “授权” 标头,则在
Cache-Control:标头中也必须包含s-maxage,must-revalidate或public选项,否则不会被缓存。 - 如果 URL 包含一个查询字符串,则不会被缓存,除非响应中用
Expires标头显式指定了有效期,或在Cache-Control:标头中指定了max-age或s-maxage指令。 - 如果响应状态码为 200,即 OK,该响应也应该至少包含
Etag,Last-Modified,Expires标头中的一个,或Cache-Control:标头中的max-age或s-maxage,除非使用了CacheIgnoreNoLastMod指令。 - 如果响应的
Cache-Control:标头中包含private选项,不会被缓存,除非使用了CacheStorePrivate指令。 - 类似地,如果响应的
Cache-Control:标头中包含no-store选项,不会被缓存,除非使用了CacheStoreNoStore指令。 - 如果响应的
Vary:标头中包含*,也不会缓存。
哪些不应该缓存
对时间敏感的内容,或者针对特定请求会有所不同的内容都不应该被缓存。这些内容应该用 Cache-Control 标头来自己声明不可缓存。
如果内容改变频繁,使用期只有几分钟或几秒钟,内容也仍然可以缓存。然而,建议让源服务端支持条件请求,以保证无需经常生成完整的响应。
针对客户端请求标头而有所区别的内容,可以通过智能使用 Vary 响应标头来进行缓存。
可变内容
如果源服务端会基于请求中不同的标头来响应不同的内容,如为同一个 URL 提供多种语言,HTTP 的缓存机制可以实现对同一 URL,缓存同一页面的多个变体。
这是由源服务端增加一个 Vary 标头来实现的,在判断两个变体是否相同时,用来表示哪些标头需要被缓存。
如,收到如下包含 Vary 标头的响应:
Vary: negotiate,accept-language,accept-charset
mod_cache 只会把那些 accept-language 及 accept-charset 匹配的缓存内容发给请求方。
内容的多个变体可以并排保存,mod_cache 用 Vary 标头和由它指定的请求标头的对应值来决定给客户端返回哪个变体。
缓存设置范例
| 相关模块 | 相关指令 |
|---|---|
mod_cachemod_cache_diskmod_cache_socachemod_socache_memcache |
CacheEnableCacheRootCacheDirLevelsCacheDirLengthCacheSocache |
缓存到磁盘
mod_cache 模块依赖于特定的后端存储部署来管理缓存,用 mod_cache_disk 来实现缓存到磁盘。
通常该模块按如下配置:
CacheRoot "/var/cache/apache/"
CacheEnable disk /
CacheDirLevels 2
CacheDirLength 1
很重要的一点,因为缓存文件是保存在本地,操作系统的内存缓存也会应用到对它们的访问上。因此,虽然这些文件保存在磁盘中,如果被频繁访问,操作系统也会将其放到内存。
缓存是如何保存的
为了在缓存中保存数据,mod_cache_disk 会为请求的 URL 创建一个 22 字符的哈希,这个哈希把主机名、协议、端口、路径及所有 CGI 参数都合并到 URL 中,也包括 Vary 标头所定义的元素,以确保多个 URL 不会互相混淆。
共有 64 个不同的字符备选,每个字符都是其中的一个,也就意味着总共有 6422 种可能的哈希值,混淆的机率相当小。
例如,某个 URL 可能被哈希为 xyTGxSMO2b68mBCykqkp1w,该哈希值做为前缀,为缓存中专属于该 URL 的文件来命名,不过,先要依据 CacheDireLevels 和 CacheDirLength 分割成目录名。
CacheDirLevels 用于指定需要几层子目录,CacheDirLength 用于指定每层目录使用几个字符。按上行范例,该哈希值会被转换成如下的文件名前缀:
/var/cache/apache/x/y/TGxSMO2b68mBCykqkp1w
该技术的根本目的是想减少子目录或文件的数量,因为该数量的增加会导致文件系统变慢。把 CacheDirLength 设定为 1 的话,每一层就可以最多有 64 个子目录。如果设置成 2,则会有 64*64 个子目录,等等。通常建议设定为 1。
对 CacheDirLevels 的设定取决于在缓存中要保存多少文件,如果设定为 2,总共可以创建 4096 个子目录。如果缓存了一百万个文件,每个目录大约有 245 个缓存的 URL。
每个 URL 在缓存中至少要使用两个文件,通常一个是 .header 文件,其中包含 URL 相关的元信息,如何时过期;另一个是 .data 文件,即缓存内容。
如果通过 Vary 标头协商得到了内容,会为该 URL 生成一个 .vary 目录,该目录中会包含多个 .data 文件,对应不同的协商内容。
磁盘缓存的维护
mod_cache_disk 模块不会尝试对缓存使用的磁盘空间量进行调节,但是如果发生了任何磁盘错误,它就会优雅地退出,好像从未有过缓存一样。
httpd 内建的工具 htcacheclean 可以周期性地清理缓存。它有两种操作模式,可以做为服务运行,也可以借助 cron 来周期运行。几十个 G 的缓存,htcacheclean 要花费一个小时或更久来处理。如果用 cron 来运行,建议把时间适当拉长,以避免同时运行多个实例。
同时还建议为 htcacheclean 选择一个合适的 nice 值,以避免它占用过多的磁盘 I/O。
因为 mod_cache_disk 自己不会留意缓存占用了多少磁盘空间,因此要确保正确配置 htcacheclean,以为缓存的生长留出足够的空间。
缓存到 memcached
通过使用 mod_cache_socache 模块,mod_cache 可以缓存来自多个提供程序的数据。例如,使用 mod_socache_memcache 模块,可以设定把 memcached 做为后端的存储机制。
通常如下设置模块:
CacheEnable socache /
CacheSocache memcache:memcd.example.com:11211
通过把服务端追加在 CacheSocache memcache: 的行尾,可以指定额外的 memcached 服务端,用逗号分隔:
CacheEnable socache /
CacheSocache memcache:mem1.example.com:11211,mem2.example.com:11212
这种格式也可用在其它的各种 mod_cache_socache 提供程序上:
CacheEnable socache /
CacheSocache shmcb:/path/to/datafile(512000)
CacheEnable socache /
CacheSocache dbm:/path/to/datafile
普通的双状态键/值共享对象缓存
| 相关模块 | 相关指令 |
|---|---|
mod_authn_socachemod_socache_dbmmod_socache_dcmod_socache_memcachemod_socache_shmcbmod_ssl |
AuthnCacheSOCacheSSLSessionCacheSSLStaplingCache |
Apache HTTP Server 拥有一个低阶共享对象缓存,用于在 socache 接口中缓存如 SSL 会话、或身份验证凭据等信息。
缓存身份验证凭据
通过 mod_authn_socache 模块,可以缓存验证凭据,以减轻后端的验证负载。
缓存 SSL 会话
mod_ssl 模块使用 socache 接口来提供会话缓存和 stapling 缓存。
缓存专用文件
在有些平台上,文件系统比较慢,或文件句柄比较宝贵,可以考虑在系统启动时把文件预加载到内存中。
如果某些系统中打开文件比较慢,可以在启动时就打开文件,并缓存文件句柄。
缓存文件句柄
打开一个文件的行为本身就是延迟的来源之一,尤其是网络文件系统。针对常用的文件,httpd 会为其文件描述符维护一个缓存,以避免这类延迟。
CacheFile
httpd 支持的最基本的缓存形式是由 mod_file_cache 实现的文件句柄缓存。与缓存文件内容不同,句柄缓存会为打开文件维护一个描述符表。需要用这种方式缓存的文件会在配置文件中用 CacheFile 指令来指定。
CacheFile 指令用于告知 httpd 在启动时打开特定文件,然后为之后对该文件的访问重用该文件句柄。
CacheFile /usr/local/apache2/htdocs/index.html
如果希望用此方式缓存一大批文件,必须确保提前正确设置操作系统对打开文件总数的限制。
虽然使用 CacheFile 不让文件内容永远在缓存中保留,不过如果原始文件发生了改变,缓存不会跟着更新。
如果原始文件被删除,缓存中的打开文件描述符依然存在,原始文件在文件系统中所占用的空间直到 httpd 停止运行才会被收回。
内存中缓存
从系统内存中直接读取肯定是提供内容服务最快的办法。从磁盘控制器中读取文件,甚至,从远程网络读取,速度就更慢。磁盘控制器通常会受物理进程的影响,而网络访问则受限于可用带宽。而访问内存仅需几纳秒。
不过系统内存并不便宜,必须有效率地使用。如果把文件缓存在内存中,会降低系统可用内存的数量。对于文件系统的缓存来说,不会有什么问题。但当使用 httpd 自己的内存缓存时,必须要注意绝对不能占用太多内存,否则就会被交换出内存,性能会明显降低。
缓存操作系统
几乎所有现代操作系统都会把文件数据缓存到内存中,由内核直接管理。这是一个强大的功能。只要有多余的内存,就可以在缓存中保存更多的文件内容,非常有效,不用额外配置 httpd。
另外,因为如果文件被删除或修改,操作系统会很清楚,需要时可以从缓存中自动删除文件内容,比起 httpd 的内存缓存来说,这是一个非常大的优势,因为 httpd 无从知道文件是否被修改了。
尽管实现了自动操作系统缓存的性能和优点,但在某些情况下,httpd 仍然可以更好地执行内存缓存。
MMapFile 缓存
mod_file_cache 的 MMapFile 指令,可以让 httpd 在启动时,使用 mmap 系统调用,把一个静态文件的内容映射到内存中。之后,httpd 会使用内存中的内容来为后来的访问提供服务。
MMapFile /usr/local/apache2/htdocs/index.html
这些文件的任何改变,httpd 都不会知道。
MMapFile 指令不会关注它占用了多少内存,因此,要注意不能过度使用这个指令。每个 httpd 的子进程都会复制这块内存,因此一定要确保映射的文件不至于过大,否则会导致系统开始交换内存。
安全考量
身份验证及访问控制
在 CacheQuickHandler 设置为 On 的默认状态下使用 mod_cache,相当于把启用缓存的反向代理连接到服务器前端。请求将由缓存模块来处理,这会极大地改变 httpd 的安全模型。
遍历文件系统层次结构来检查潜在的 .htaccess 文件将是一个非常昂贵的操作,mod_cache 无法决定某个缓存条目是否被授权访问。
如果配置了某个 IP 地址允许访问某个资源,一定要确保该内容不会被缓存。可以用 CacheDisable 或 mod_expires 指令来设置。否则,mod_cache 就像一个反向代理一样,会把内容放到缓存,任何地址都可以访问了。
本地漏洞
因为来自终端用户的请求可以用缓存来应对,缓存本身就成了一个靶子。向缓存中写入内容的身份必须是运行 httpd 的那个用户。
如果 Apache 用户被攻破,如通过 CGI 脚本的漏洞,缓存就有可能被坏人盯上。如果使用了 mod_cache_disk,要想向缓存插入内容或修改内容就变得相对容易了。
平时的维护中,如果 httpd 因为安全更新有了升级,一定要及时升级。平时运行 CGI 脚本时不要使用 Apache 的用户,如果有可能尽量使用 suEXEC。
缓存中投毒
如果把 httpd 作为一个缓存代理服务运行,也存在缓存被投毒的风险。缓存投毒是一个宽泛的术语,是指在攻击时,攻击者会让代理服务器从源服务器获取错误的内容,通常是不需要的内容。
例如,如果运行 httpd 的系统中也运行了 DNS 服务,如果该服务容易遭受 DNS 缓存投毒,当从源服务器请求内容时,攻击者有可能控制把 httpd 连接到哪里。另一个例子是 HTTP 伪装请求攻击。
攻击者可能会生成一系列请求,利用源服务器的漏洞来控制代理服务器获得的内容。
拒绝服务
借助 Vary 机制,允许把同一个 URL 的不同变体并排保存在缓存中。根据客户端提供的标头值,缓存将选择正确的变体返回给客户端。如果知道某个标头中含有很多个可能值,该机制就会成为一个问题,比如 User-Agent 标头。视乎网站的火热程度,同一个 URL 可能会有成千上万个重复的缓存条目,让其它条目无法生存。
在其它情况下,有可能会为每个请求修改某个资源的 URL,通常是在后面追加一个称为 cachebuster 的字符串。如果该内容声称自己可被缓存,并可保鲜很长时间,这些条目就可以把缓存中合法的条目排挤出去。然而 mod_cache 还有一个 CacheIgnoreURLSessionIdentifiers 指令,要小心使用,以确保下游的代理或浏览器缓存不会成为拒绝服务攻击的目标。