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_cachemod_cache_disk

相关指令 :CacheEnableCacheDisableUseCanonicalNameCacheNegotiatedDocs

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-ages-maxage 字段,也可以包含一个 Expires 标头。

同时,当客户端在请求中提供了自己的 Cache-Control 标头时,源服务端定义的使用期可以被客户端覆盖。这种情况下,在请求和响应中,最短的使用期会获胜。

如果请求或响应中使用期缺失,则使用默认的使用期,即一小时。可以用 CacheDefaultExpire指令来修改。

如果响应中不包含 Expires 标头,但包含 Last-Modified 标头,mod_cache 会自己推算一个使用期,其基于的启发式算法可以由 CacheLastModifiedFactor 指令控制。

对于本地内容,或没有定义其 Expires 标头的远程内容,可以使用 mod_expires 来调整使用期,具体方法是添加 max-ageExpires

最大的使用期也用 CacheMaxExpire 来控制。

条件请求

当缓存中的内容过期时,httpd 并不会把直接传递源请求,而是会把它并成条件请求。

  • 如果源缓存响应中有 Etag 标头,mod_cache 会向针对源服务端的请求中添加一个 If-None-Match 标头。
  • 如果源缓存响应中有 Last-Modified 标头,mod_cache 会向针对源服务端的请求中添加一个 If-Modified-Since 标头。

执行上述任一操作将使请求变成条件请求。

如何应对条件请求

当源服务端接收到一个条件请求时,它应该检查 EtagLast-Modified 参数是否发生了改变:

  • 如果没变,源服务端应该响应 “304 Not Modifed”。这会给缓存一个信号,告知其过期的内容依然新鲜,应该继续为后续的请求服务,直到其使用期再次结束。
  • 如果内容发生了改变,则当作一开始不是条件请求来发送内容。
条件请求带来两个好处:
  • 对服务端进行这样的请求时,如果源内容与缓存中内容匹配,可以轻松判断,无需传输全部源内容。
  • 好的服务端设计就当如此,条件请求的开销比完整响应要小的多。

对于静态文件来说,通常只需要调用 stat() 或类似的系统调用,就可以知道文件大小和时间是否发生了改变。因此,即使是本地内容,如果没有改变,也仍然可以用缓存更快地传送。

源服务端应尽全力来支持条件请求,这是切实可行的,但是,如果条件请求不被支持,源服务端会将该请求看作非条件请求,缓存会认为内容已过期,然后把新内容保存到缓存中。这种情况下,缓存会按简易的双状态缓存方式工作,即内容要么新鲜,要么被删除。

哪些内容可以缓存

  1. CacheEnable 指令设定的 URL。
  2. 响应必须包含 HTTP 状态码 200,203,300,301,410。
  3. 必须是 HTTP GET 请求。
  4. 如果响应包含 “授权” 标头,则在 Cache-Control: 标头中也必须包含 s-maxagemust-revalidatepublic 选项,否则不会被缓存。
  5. 如果 URL 包含一个查询字符串,则不会被缓存,除非响应中用 Expires 标头显式指定了有效期,或在 Cache-Control: 标头中指定了 max-ages-maxage 指令。
  6. 如果响应状态码为 200,即 OK,该响应也应该至少包含 EtagLast-ModifiedExpires 标头中的一个,或 Cache-Control: 标头中的 max-ages-maxage,除非使用了 CacheIgnoreNoLastMod 指令。
  7. 如果响应的 Cache-Control: 标头中包含 private 选项,不会被缓存,除非使用了 CacheStorePrivate 指令。
  8. 类似地,如果响应的 Cache-Control: 标头中包含 no-store 选项,不会被缓存,除非使用了 CacheStoreNoStore 指令。
  9. 如果响应的 Vary: 标头中包含 *,也不会缓存。

哪些不应该缓存

对时间敏感的内容,或者针对特定请求会有所不同的内容都不应该被缓存。这些内容应该用 Cache-Control 标头来自己声明不可缓存。

如果内容改变频繁,使用期只有几分钟或几秒钟,内容也仍然可以缓存。然而,建议让源服务端支持条件请求,以保证无需经常生成完整的响应。

针对客户端请求标头而有所区别的内容,可以通过智能使用 Vary 响应标头来进行缓存。

可变内容

如果源服务端会基于请求中不同的标头来响应不同的内容,如为同一个 URL 提供多种语言,HTTP 的缓存机制可以实现对同一 URL,缓存同一页面的多个变体。

这是由源服务端增加一个 Vary 标头来实现的,在判断两个变体是否相同时,用来表示哪些标头需要被缓存。

如,收到如下包含 Vary 标头的响应:

Vary: negotiate,accept-language,accept-charset

mod_cache 只会把那些 accept-languageaccept-charset 匹配的缓存内容发给请求方。

内容的多个变体可以并排保存,mod_cacheVary 标头和由它指定的请求标头的对应值来决定给客户端返回哪个变体。

缓存设置范例

相关模块 相关指令
mod_cache
mod_cache_disk
mod_cache_socache
mod_socache_memcache
CacheEnable
CacheRoot
CacheDirLevels
CacheDirLength
CacheSocache

缓存到磁盘

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 的文件来命名,不过,先要依据 CacheDireLevelsCacheDirLength 分割成目录名。

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_socache
mod_socache_dbm
mod_socache_dc
mod_socache_memcache
mod_socache_shmcb
mod_ssl
AuthnCacheSOCache
SSLSessionCache
SSLStaplingCache

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_cacheMMapFile 指令,可以让 httpd 在启动时,使用 mmap 系统调用,把一个静态文件的内容映射到内存中。之后,httpd 会使用内存中的内容来为后来的访问提供服务。

MMapFile /usr/local/apache2/htdocs/index.html

这些文件的任何改变,httpd 都不会知道。

MMapFile 指令不会关注它占用了多少内存,因此,要注意不能过度使用这个指令。每个 httpd 的子进程都会复制这块内存,因此一定要确保映射的文件不至于过大,否则会导致系统开始交换内存。

安全考量

身份验证及访问控制

CacheQuickHandler 设置为 On 的默认状态下使用 mod_cache,相当于把启用缓存的反向代理连接到服务器前端。请求将由缓存模块来处理,这会极大地改变 httpd 的安全模型。

遍历文件系统层次结构来检查潜在的 .htaccess 文件将是一个非常昂贵的操作,mod_cache 无法决定某个缓存条目是否被授权访问。

如果配置了某个 IP 地址允许访问某个资源,一定要确保该内容不会被缓存。可以用 CacheDisablemod_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 指令,要小心使用,以确保下游的代理或浏览器缓存不会成为拒绝服务攻击的目标。