NGINX.COM
Web Server Load Balancing with NGINX Plus

我们都知道,应用和网站的性能关系着企业的成败。但是对于如何使您的应用或网站性能更好,并没有明确的答案。代码质量和基础架构固然很重要,但很多时候您还可以通过使用一些非常基本的应用交付技术来大幅改善最终用户的应用体验,在应用堆栈中实施和优化缓存就是一个典型的例子。本博文介绍了一些利用 NGINX 和 NGINX Plus 的内容缓存功能提升性能的技术,以供新手和高级用户参考。

 

概述

内容缓存位于客户端和“源服务器”之间,保存了所有可见内容的副本。如果客户端请求缓存存储的内容,它将直接返回内容而无需与源服务器通信。由于内容缓存离客户端更近,这样可以提高性能,加之不用每次请求都重新执行页面生成任务,应用服务器也会得到更高效的利用。

Web 浏览器和应用服务器之间可能存在多种缓存:客户端浏览器缓存、中间缓存、内容交付网络 (CDN) 以及位于应用服务器前面的负载均衡器或反向代理。即使是反向代理/负载均衡器层面的缓存,也可以大大提高性能。

举例来说,去年我接手了一项任务,对一个加载缓慢的网站进行性能优化。首先引起我注意的是,这个网站花费超过 1 秒钟才生成了主页。经过一系列调试,我发现原因在于该页面被标记为不可缓存,每次响应请求都要动态生成一次。其实该页面本身并不经常变化,也没有个性化内容,因此这样做并没有必要。我尝试性地将主页标记为由负载均衡器每 5 秒钟缓存一次,仅仅是这样一个调整,就能明显感受到性能的提升。第一个字节到达的时间缩短到几毫秒,页面加载速度也肉眼可见地快了。

NGINX 通常在应用堆栈中部署为反向代理或负载均衡器,并且具有完整的缓存功能。下一节将讨论如何配置 NGINX 的基础缓存功能。

 

如何设置和配置基础缓存功能

我们只需要两个指令就可以启用基础缓存:proxy_cache_pathproxy_cacheproxy_cache_path 指令用于设置缓存的路径和配置,proxy_cache 指令用于启用缓存。

proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g 
                 inactive=60m use_temp_path=off;

server {
    # ...
    location / {
        proxy_cache my_cache;
        proxy_pass http://my_upstream;
    }
}

proxy_cache_path 指令的参数定义了以下设置:

  • 用于缓存的本地磁盘目录是 /path/to/cache/
  • levels/path/to/cache/ 下设置了一个两级目录结构。将大量文件放到一个目录下会导致文件访问缓慢,因此我们建议对大多数部署使用两级目录结构。如果没有添加 levels 参数,NGINX 会将所有文件放到同一个目录中。
  • keys_zone 设置了一个共享内存区,用于存储缓存键 (key) 和元数据(比如使用计时器)。通过将键的副本放在内存中,NGINX 可以在不检索磁盘的情况下快速决定一个请求是命中(HIT)还是未命中缓存(MISS),从而显著提高检索速度。鉴于 1MB 内存可以存储大约 8000 个键的数据,那么上例中配置的 10MB 内存可以存储大约 80000 个键的数据。
  • max_size 设置了缓存的存储空间上限(在上面的例子中是 10G)这是一个可选项;不指定具体值就代表允许缓存不断增长,直到占用所有可用的磁盘空间。当缓存达到上限时,cache manager 进程会删除近期最少使用的文件,把缓存空间降低到这个限值之下。
  • inactive 指定了项目在不被访问的情况下可以在缓存中保留多长时间。在上面的例子中,如果一个文件在 60 分钟内没有被请求,那么无论是否过期,Cache Manager 进程都会自动将其从缓存中删除。该参数默认值为 10 分钟 (10m)。注意,非活动内容与过期的内容不同。NGINX 不会自动删除由缓存控制头定义的过期内容(例如 Cache-Control:max-age=120)。过期内容(旧内容)只有在 inactive 指定时间内没有被访问的情况下才会被删除。如果用户访问过期内容,那么 NGINX 就会从源服务器上刷新,并重置 inactive 计时器。
  • NGINX 会先把要缓存的文件放到临时存储区,use_temp_path=off 指令再指示 NGINX 将它们写入要被缓存的同一目录。我们建议您将此参数设置为 off,以免在文件系统之间进行不必要的数据复制。use_temp_path 是在 NGINX 版本 1.7.10 和 NGINX Plus R6 中引入的。

最后,proxy_cache 指令启动缓存与父 location 代码块的 URL 相匹配的所有内容(本例中为  /)。您也可以将 proxy_cache 指令添加到 server 代码块,它适用于自身没有 proxy_cache 指令的服务器的所有 location 代码块。

 

在源服务器宕机时交付缓存的内容

NGINX 内容缓存有一个强大的功能:当无法从源服务器获取最新内容时,NGINX 可以分发缓存中的旧内容。这种情况一般发生在关联缓存内容的所有源服务器宕机或繁忙时。NGINX 不是给客户端传递错误消息,而是从缓存中发送旧文件。这种方式为 NGINX 代理的服务器提供了额外的容错能力,并且在出现服务器故障或流量高峰时也能保证正常运行。要启用此功能,请添加 proxy_cache_use_stale 指令:

location / {
    # ...
    proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
}

按照上述示例的配置,如果 NGINX 收到了源服务器返回的 errortimeout 或指定的任何 5xx 错误,并且其缓存中有旧版本的请求文件,那么它不会向客户端传递错误消息,而是会发送旧文件。

 

缓存调优和性能优化

NGINX 具有丰富的缓存调优设置选项。以下示例就运用了其中几项配置:

proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g 
                 inactive=60m use_temp_path=off;

server {
    # ...
    location / {
        proxy_cache my_cache;
        proxy_cache_revalidate on;
        proxy_cache_min_uses 3;
        proxy_cache_use_stale error timeout updating http_500 http_502
                              http_503 http_504;
        proxy_cache_background_update on;
        proxy_cache_lock on;

        proxy_pass http://my_upstream;
    }
}

这些指令配置了以下行为:

  • proxy_cache_revalidate 指示 NGINX 在刷新来自源服务器的内容时使用有条件的 GET 请求。如果客户端的请求项已经被缓存过了,但是在缓存控制头中定义为过期,那么 NGINX 就会在发送给源服务器的 GET 请求头中添加 If-Modified-Since 字段。这可以节省带宽,因为服务器只会发送在 NGINX 一开始缓存时附加在文件上的 Last-Modified 请求头记录的时间之后进行修改的完整项目。
  • proxy_cache_min_uses 设置了一个项目必须被请求多少次才会在 NGINX 中进行缓存。如果缓存不断被填充,这项设置将十分有用,因为它可以确保仅将访问最频繁的项目添加到缓存中。proxy_cache_min_uses 的默认值为 1。
  • 在组合使用 proxy_cache_background_update 指令时,proxy_cache_use_stale 指令的 updating 参数将指示 NGINX 在客户端请求的项目已经过期或正在源服务器端进行更新时发送旧内容。所有更新都将在后台完成。NGINX 将为所有请求返回旧文件,直到更新的文件完全下载下来为止。
  • 启用 proxy_cache_lock 后,如果多个客户端请求当前不在缓存中的文件 (MISS),那么只允许这些请求中的第一个被发送到源服务器。其余请求将等待该请求得到响应,然后从缓存中提取文件。如果不启用 proxy_cache_lock,则所有在缓存中找不到文件的请求都会直接与源服务器通信。

 

跨多个硬盘分割缓存

如果您有多个硬盘,可以使用 NGINX 在它们之间分割缓存。下面的例子会根据请求 URI 将客户端请求平均分配给两个硬盘:

proxy_cache_path /path/to/hdd1 levels=1:2 keys_zone=my_cache_hdd1:10m
                 max_size=10g inactive=60m use_temp_path=off;
proxy_cache_path /path/to/hdd2 levels=1:2 keys_zone=my_cache_hdd2:10m
                 max_size=10g inactive=60m use_temp_path=off;

split_clients $request_uri $my_cache {
              50%          “my_cache_hdd1”;
              50%          “my_cache_hdd2”;
}

server {
    # ...
    location / {
        proxy_cache $my_cache;
        proxy_pass http://my_upstream;
    }
}

两个 proxy_cache_path 指令在两个硬盘上定义了两个缓存(my_cache_hdd1my_cache_hdd2)。split_clients 配置块指定将一半的请求结果 (50%) 缓存到 my_cache_hdd1,另一半缓存到 my_cache_hdd2。基于 $request_uri 变量(请求 URI)的哈希值决定了每个请求使用哪个缓存,这样做的结果是特定 URI 的请求总是会缓存在同一个缓存去中。

请注意,此方法不能替代 RAID 硬盘设置。如果发生硬盘故障,这可能会导致不可预测的系统行为,例如,对于被导向到故障硬盘的请求,用户可能会收到 500 响应码。适当的 RAID 硬盘设置可以处理硬盘故障。

 

常见问题解答 (FAQ)

本节回答了一些有关 NGINX 内容缓存的常见问题。

可以检测 NGINX 缓存状态吗?

可以,需要使用 add_header 指令:

add_header X-Cache-Status $upstream_cache_status;

该示例在对客户端的响应中添加了一个 X-Cache-Status HTTP 请求头。下面是 $upstream_cache_status 可能的值:

  • MISS —— 响应在缓存中找不到,需要从源服务器获取。这个响应之后可能会被存入缓存。
  • BYPASS —— 响应从源服务器获取,而非由缓存提供,因为请求匹配了一个 proxy_cache_bypass 指令(见下方的“可以在缓存中‘打洞’ (Punch a Hole) 吗?”)这个响应之后可能会被存入缓存。
  • EXPIRED —— 缓存中的条目过期了,响应包含来自源服务器的最新内容。
  • STALE —— 因源服务器不能正确响应而导致内容过期,并且配置了 proxy_cache_use_stale
  • UPDATING —— 内容过期了,因为当前正在更新条目以响应之前的请求,并且配置了 proxy_cache_use_stale updating
  • REVALIDATED —— 启用了 proxy_cache_revalidate 指令,并且 NGINX 验证发现当前的缓存内容依然有效(If-Modified-SinceIf-None-Match)。
  • HIT —— 响应中包含直接来自缓存的最新有效内容。

NGINX 如何确定是否缓存?

只有当源服务器包含 Expires 请求头(含未来的日期和时间)或 Cache-Control 请求头(其中 max-age 指令设置为非零值)时,NGINX 才会缓存响应。

默认情况下,NGINX 会考虑 Cache-Control 请求头中的其他指令:当请求头中包含 PrivateNo-CacheNo-Store 指令时,NGINX 不缓存响应。NGINX 也不缓存包含 Set-Cookie 请求头的响应,只缓存对 GETHEAD 请求的响应。您可以参照下面的答案覆盖这些默认值。

如果 proxy_buffering 设置为 off,NGINX 不缓存响应。默认情况下该参数为 on

可以忽略 Cache-Control 请求头吗?

可以,需要使用 proxy_ignore_headers 指令。例如,使用如下配置:

location /images/ {
    proxy_cache my_cache;
    proxy_ignore_headers Cache-Control;
    proxy_cache_valid any 30m;
    # ...
}

NGINX 会忽略 /images/ 下的所有 Cache-Control 请求头。proxy_cache_valid 指令规定了缓存数据的有效期,如果忽略 Cache-Control 请求头,则必须设置该指令。NGINX 不会缓存没有过期时间的文件。

NGINX 可以缓存请求头中含 Set-Cookie 的内容吗?

可以,需要使用 proxy_ignore_headers 指令,请参见上述回答。

NGINX 可以缓存 POST 请求吗?

可以,需要使用 proxy_cache_methods 指令:

proxy_cache_methods GET HEAD POST;

该示例启用了对 POST 请求的缓存。

NGINX 可以缓存动态内容吗?

可以,只要 Cache-Control 请求头允许便可。即使短暂缓存动态内容也可以减少源服务器和数据库的负载 —— 由于不必再为每个请求重新生成页面,这可以缩短获取第一个字节的时间。

可以在缓存中“打洞” (Punch a Hole) 吗?

可以,需要使用 proxy_cache_bypass 指令:

location / {
    proxy_cache_bypass $cookie_nocache $arg_nocache;
    # ...
}

该指令定义了 NGINX 会立即为哪些类型的请求从源服务器获取内容,而不是先尝试在缓存中查询。这一现象有时又称为在内存中“打洞”(Punch a Hole)。在此示例中,NGINX 会为包含 nocache cookie 或参数的请求执行这一操作,例如 http://www.example.com/?nocache=true。NGINX 仍然可以为以后那些没有被绕过的请求缓存生成的响应。

NGINX 使用什么缓存键 (Key)?

NGINX 生成的键 (Key) 的默认格式类似于 NGINX 变量 $scheme$proxy_host$request_uri 的 MD5 哈希值,但实际使用的算法稍微复杂一些。

proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g
                 inactive=60m use_temp_path=off;

server {
    # ...
    location / {
        proxy_cache my_cache;
        proxy_pass http://my_upstream;
    }
}

在上述示例配置中,http://www.example.org/my_image.jpg 的缓存键被计算为 md5(“http://my_upstream:80/my_image.jpg”)

注意,$proxy_host 变量用于哈希值而非实际的主机名 (www.example.com)。 $proxy_host 被定义为 proxy_pass 指令中指定的代理服务器的主机名和端口。

要改变键的基础变量(或其他项),请使用 proxy_cache_key 指令(另请参见下述答案)。

可以将 cookie 用作缓存键的一部分吗?

可以,缓存键可以配置为任意值,例如:

proxy_cache_key $proxy_host$request_uri$cookie_jessionid;

该示例将 JSESSIONID cookie 的值添加到了缓存键中。具有相同 URI、不同 JSESSIONID 值的项作为唯一项单独进行了缓存。

NGINX 使用 ETag 请求头吗?

在 NGINX 1.7.3 和 NGINX Plus R5 及更高版本中,ETag 请求头与 If-None-Match 一起完全受支持。

NGINX 如何处理字节范围请求?

如果缓存中的文件是最新的,则 NGINX 支持字节范围请求,并且仅将指定字节的项提供给客户端。如果文件未缓存或者过期了,NGINX 将从源服务器下载整个文件。如果请求的是单个字节范围,那么 NGINX 在下载流中一遇到这个范围就发送给客户端。如果请求指定同一文件中的多个字节范围,NGINX 会在下载完成后将整个文件发送给客户端。

下载完成后,NGINX 会把整个资源移动到缓存中,这样无论将来的字节范围请求是单字节还是多字节,NGINX 都可以在缓存中找到内容立即响应。

请注意,upstream 服务器只有支持字节范围请求,NGINX 才能向 upstream 服务器发出字节范围请求。

NGINX 是否支持缓存清除?

NGINX Plus 支持有选择性地清除缓存。当文件已经在源服务器端进行更新,但是在 NGINX Plus 缓存中依然有效时(Cache-Control:max-age 仍然有效,但是 proxy_cache_path 指令的 inactive 参数设置的 timeout 尚未过期),该功能就会派上用场 NGINX Plus 的缓存清除功能可以轻松删除该文件。有关更多信息,请参见“清除缓存内容”。

NGINX 如何处理 Pragma 请求头?

客户端添加 Pragma:no-cache 请求头后,会绕过中间的所有缓存,直接从源服务器获取请求的内容。默认情况下,NGINX 不考虑 Pragma 请求头,但是您可以使用以下 proxy_cache_bypass 指令配置该功能:

location /images/ {
    proxy_cache my_cache;
    proxy_cache_bypass $http_pragma;
    # ...
}

NGINX 支持 Cache-Control 请求头的 stale-while-revalidatestale-if-error 扩展吗?

NGINX Plus R12 和 NGINX 1.11.10 及更高版本均支持。这些扩展的作用:

  • 如果当前正在更新缓存的响应,Cache-Control HTTP 请求头的 stale-while-revalidate 扩展允许使用缓存的旧内容。
  • 当发生错误时,Cache-Control HTTP 请求头的 stale-if-error 扩展允许使用缓存的旧响应。

这些请求头的优先级要低于上文提到的 proxy_cache_use_stale 指令。

NGINX 支持 Vary 请求头吗?

NGINX Plus R5 和 NGINX 1.7.7 及更高版本均支持。如欲详细了解 Vary 请求头,请参见此处。

 

延伸阅读

您可以通过多种方式自定义和调优 NGINX 缓存。如欲了解有关 NGINX 缓存的更多信息,请查看以下资源:

Hero image
免费 O'Reilly 电子书:
《NGINX 完全指南》

更新于 2022 年,一本书了解关于 NGINX 的一切

关于作者

Faisal Memon

软件工程师

关于 F5 NGINX

F5, Inc. 是备受欢迎的开源软件 NGINX 背后的商业公司。我们为现代应用的开发和交付提供一整套技术。我们的联合解决方案弥合了 NetOps 和 DevOps 之间的横沟,提供从代码到用户的多云应用服务。访问 nginx-cn.net 了解更多相关信息。