代码编程 · 2022年12月27日

如何使用 Nginx 配置反向代理缓存

缓存是一种存储给定资源副本以便快速检索的技术。这种技术可以在 Web 服务的任何级别找到,无论是在后端应用程序内部、在代理服务器上,还是在客户端 Web 浏览器中。目的是减少服务器负载并提高响应能力。

Web 应用程序通常在反向代理后面运行。它将传入请求转发到后端服务器并执行其他任务:TLS 加密、负载平衡、缓存、压缩等。Nginx是一个免费开源的反向代理,实现了所有这些功能。

本文介绍如何使用 Nginx 配置反向代理缓存、通过提供陈旧的响应来提高可用性、通过锁定限制请求并发以及规范化请求以提高缓存效率。先决条件是 HTTP 的基本知识和之前使用 Nginx 的经验。

介绍

Nginx 文档应该是您的首选参考,尤其是以下条目:

  • 模块 ngx_http_proxy_module:缓存指令的文档。
  • 按字母顺序排列的索引变量列表:配置中定义的变量。
  • 配置文件测量单位:时间和空间说明符列表。

本文假设 Nginx 配置有一个虚拟主机来响应http://app.example.com/. 该应用程序中有一些静态文件, /srv/http/app后端服务器正在侦听http://127.0.0.1:3000

upstream app {
	server 127.0.0.1:3000;
}

server {
	listen 80;
	server_name app.example.com;

	root /srv/http/app;

	# Matches all requests.
	location / {
		# Try to serve a file from the root if any, or route the request to
		# @app.
		try_files $uri @app;
	}

	location @app {
		proxy_pass http://app;
	}
}

在重新加载服务之前,请使用命令检查是否存在任何配置错误nginx -t§ 结论中提供了应用本文中的课程后的完整配置。

定义缓存区域

proxy_cache_path指令定义了一个命名的缓存位置,您可以使用该proxy_cache指令启用它。

参数

本节介绍最重要的参数proxy_cache_path

path(强制的)

例如,该path参数指示缓存条目的目录/var/cache/nginx/app(请注意,不会自动创建父目录)。

keys_zone(强制的)

keys_zone=name:size指定保存密钥的共享内存区域的name和。size大小1M对应于 1 MB 的共享内存,可以容纳 8000 个密钥。

levels

代理缓存的工作方式类似于哈希表,其密钥派生自 proxy_cache_key,默认设置为$scheme$proxy_host$uri$is_args$args。在请求处理过程中,这些变量被它们的值所取代。缓存文件位置取决于 MD5 散列proxy_cache_key和选项的参数levels,指定如何将摘要拆分为段。目的是防止由于单个目录中的条目太多而导致速度下降。

例如,levels=1:2组织具有两个目录级别的存储层次结构。散列键对应的路径 b1946ac92492d2347c6235b4d2611184为 /var/cache/nginx/app/b/19/46ac92492d2347c6235b4d2611184

  • 一级目录以密钥的第一个字节(b)命名。
  • 二级目录以下两个( )命名19

默认情况下,所有条目都存储在path.

max_size

您可能想要设置max_size以指定磁盘缓存的最大大小(默认情况下,它是无界的)。

manager_*

当缓存超过max_size时,缓存管理器驱逐:

  • 最多manager_files条目(默认情况下,100),
  • 在小于manager_threshold毫秒的迭代中(默认情况下,200 毫秒),
  • 间隔manager_sleep几毫秒(默认为 50 毫秒)。

Nginx 启动时索引缓存的加载进程具有等效参数。

inactive

在指定的时间内未使用的条目inactive将从缓存中逐出,无论它们是否过期。默认情况下,inactive 设置为10m

配置

proxy_cache_path定义缓存的共享内存区域、位置和属性:

http {
	proxy_cache_path /var/cache/nginx/app
		levels=1:2 keys_zone=app:1M max_size=200M;
}
激活

要激活缓存,您需要设置proxy_cache共享内存区域的名称:

http|server|location {
	proxy_cache app;
}

它现在处于活动状态,但您仍然必须指定缓存哪些响应。

控制缓存

要将响应保存到缓存,您可以匹配请求或响应属性,例如 HTTP 方法、状态代码或标头。您可以在 Nginx 配置中声明这些规则或从后端应用程序控制缓存。

配置

proxy_cache_valid告诉 Nginx 在指定的持续时间内缓存具有匹配状态代码的响应。以下示例将状态代码为 200、301 和 302(未给出时的默认值)的响应缓存 1 分钟,将所有其他响应(包括错误)缓存 10 秒:

http|server|location {
	proxy_cache_valid 1m;
	proxy_cache_valid any 10s;
}

虽然proxy_cache_valid包含基于状态代码的响应,但您可以使用以下指令排除资源:

  • proxy_cache_bypass: 总是查询上游服务器而不查看缓存,而是将响应保存到缓存中。
  • proxy_no_cache: 始终查询上游服务器而不查看缓存,但不将响应保存到缓存中。

这两个指令接受字符串参数,包括变量。如果其中一个参数不为空且不同于0,则该请求将绕过或禁用缓存。例如:

  • proxy_cache_bypass: 1: 总是绕过缓存。
  • proxy_cache_bypass: $http_pragma:当请求包含标头时绕过缓存Pragma
  • proxy_no_cache $cookie_sessionid: 为具有 sessionidcookie 的用户禁用缓存(但未经身份验证的用户访问同一 URL 将获得公共缓存响应)。

您可以将缓存指令应用于特定位置块,但对于更复杂的场景,您可以使用自定义变量和条件,如9 Tips for Improving WordPress Performance 中的 示例所示:

# Enable caching by default.
set $skip_cache 0;

# Do not cache POST requests.
if ($request_method = POST) {
	set $skip_cache 1;
}

# Do not cache URLs with query strings.
if ($query_string != "") {
	set $skip_cache 1;
}

# Do not cache URLs containing the following segments.
if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
	set $skip_cache 1;
}

# Do not use the cache for logged-in users or recent commenters.
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
	set $skip_cache 1;
}

proxy_cache_bypass $skip_cache;

因为proxy_cache_key包含$args,所以缓存可能充满许多重复的条目,除非响应以某种方式取决于查询参数。用于proxy_cache_min_uses设置在关联响应保存到缓存之前所需的最小请求数:

http|server|location {
	proxy_cache_min_uses 3;
}

配置语法有点深奥,有一些 陷阱,所以可以从后端设置缓存策略。

标头

您可以使用以下优先于以下响应标头覆盖 Nginx 配置中设置的默认缓存策略proxy_cache_valid

  • X-Accel-Expires: 表示响应缓存持续时间(0 表示完全禁用缓存)。
  • Vary:将列出的请求标头添加到缓存键。
  • Set-Cookie:如果响应中存在,则禁用缓存。

小心Vary标头,因为它会使缓存变得无用。例如,基于整个Cookie标头的键是没有意义的,因为每个登录用户的键可能不同,因此您最终会得到与用户一样多的条目。请参阅使用 Vary 标头的最佳实践

如果X-Accel-Expires响应中不存在,Nginx 会查找以下标头:

  • Expires: 相同,X-Accel-Expires但格式更复杂。
  • Cache-Control: 设置缓存策略的现代方法。

由于最后两个标头也会影响客户端上的缓存,因此更喜欢 X-Accel-Expires显式控制反向代理缓存:

  • X-Accel-Expires: 0禁用缓存。
  • X-Accel-Expires: 3600将响应缓存一个小时。
在实践中

很难准确解释如何配置缓存,因为它高度依赖于应用程序,更不用说如何Cache-Control影响客户端 Web 浏览器了。不过,这里有一个通用的方法:

  1. 对于静态资源,设置expires为较长的​​持续时间,并在其名称中添加指纹以进行缓存清除。
  2. 为所有共享资源定义默认缓存策略proxy_cache_valid,确保它对私有资源(如用户会话)禁用。
  3. 始终指示来自后端的私有资源,并将响应标头 Cache-Control设置为privateno-cacheno-store,以防止任何可能妨碍安全的配置不匹配(您真的不想缓存私有网页并将它们提供给所有人)。
  4. X-Accel-Expires当不可能在配置中这样做时,用于覆盖以前的策略。

要测试配置,您可以使用变量检测缓存 $upstream_cache_status。例如,您可以将缓存状态(命中、未命中)添加到扩展标头中的响应X-Cache-Status

http|server|location {
	add_header X-Cache-Status $upstream_cache_status;
}

如果反向代理缓存难以应用于某些资源,那么您可能不得不在后端实现缓存(我将其作为练习留给读者)。

提高可用性

当大量请求同时到达且缓存不包含有效条目时,所有请求都会在更新缓存的同时转发到后端服务器。陈旧的响应和缓存锁定的组合可以防止对后端的这些突发请求。

陈旧的回应

启用proxy_cache_background_update在后台更新缓存,并proxy_cache_use_stale使用参数updating在更新条目时返回陈旧的响应:

http|server|location {
	proxy_cache_background_update on;
	proxy_cache_use_stale updating;
}

该条目在后台更新。更新期间到达的任何请求都会收到过时的响应。proxy_cache_use_stale当后端由于错误或超时不可用时,也可以返回陈旧的响应:

http|server|location {
	proxy_cache_background_update on;
	proxy_cache_use_stale error timeout updating
		http_500 http_502 http_503 http_504;
}

过时的响应限制了对上游服务器的并发请求数。但是,如果找不到缓存条目,则所有请求都会通过,直到它被填充。

缓存锁

Nginx 提供proxy_cache_lock防止并发请求同时更新缓存。只有第一个请求向上游传递以更新缓存,而其他请求则等待其完成:

http|server|location {
	proxy_cache_lock on;
}

缺点是等待缓存的请求至少会被阻塞 500 毫秒,并且会达到由proxy_cache_lock_timeout(默认为 5 秒)设置的持续时间。这 500 毫秒来自 Nginx 唤醒阻塞请求任务以检查条目是否存在的不可配置间隔。要克服此问题,请使用上一小节中的技术在后台更新条目时返回陈旧的响应:

http|server|location {
	proxy_cache_lock on;
	proxy_cache_background_update on;
	proxy_cache_use_stale error timeout updating
		http_500 http_502 http_503 http_504;
}

不幸的是,proxy_cache_lock很容易被误用。如果资源被配置为缓存(例如, with proxy_cache_valid),但后端总是阻止它(例如, withX-Accel-Expires: 0或任何其他缓存控制头),那么该条目将始终丢失并且 Nginx 将永远无法在期间返回陈旧的响应更新。假设后端需要 100 毫秒来响应,代理每秒只能发出 10 个后台请求来尝试更新缓存。

同时,客户等待proxy_cache_lock_timeout到期。然后,他们的请求都被转发到后端。这不仅会增加延迟,还会导致对上游服务器的请求激增。因此,当 Nginx 从缓存中排除私有资源时,请确保保持与后端同步。您可以禁用基于虚拟主机、位置块或身份验证 cookie 的缓存。作为解决方法,您还可以减少proxy_cache_lock_timeout.

规范化请求属性

响应压缩减少了带宽使用和加载时间。客户端在请求标头中公布他们支持的算法。不同的值可能对应相同的响应编码,导致缓存为相同的响应存储多个条目。规范化是将这些不同的值映射到一组减少的条目的过程,从而提高缓存效率。

压缩

压缩可以大大减少静态资源(如 JavaScript 或 CSS)的大小。你可以用更高的比率预压缩资产,而不是现场做,并配置 Nginx 自动返回适当的响应:

location / {
	try_files $uri @app;
	brotli_static on;
	gzip_static on;
}

要发送压缩响应,服务器必须根据标头的值选择客户端接受的最佳编码Accept-Encoding。客户端通常支持多种压缩算法,以逗号分隔的列表表示,例如Accept-Encoding: br, gzip, deflate.

HTML 格式的内容也很适合压缩。您可以使用 Nginx 启用实时响应压缩,但缓存发生在压缩之前,而不是之后。这意味着针对每个请求重新压缩来自缓存的响应。作为一种解决方案,您可以改为在后端实施压缩。

正常化

假设 Nginx 缓存基于 的响应Accept-Encoding,如果客户端使用 发出请求Accept-Encoding: br, gzip, deflate,则 Brotli 压缩响应将保存到由该标头的值标识的第一个条目。如果另一个客户端只指示br, gzip,它会得到相同的 Brotli 压缩响应,但它会被记录到第二个缓存条目中。

为防止多次缓存相同的响应,您可以执行标头规范化。Nginx 的作用是将Accept-Encoding标头映射到一组值,这些值对应于服务器和客户端都支持的最佳压缩方案。例如,如果标头包含br,则应使用 Brotli 压缩响应,如果标头包含gzip,则应使用 Gzip 压缩;否则,它根本不应该被压缩:

server|location {
	set $encoding "";

	if ($http_accept_encoding ~ br) {
			set $encoding "br";
			break;
	}

	if ($http_accept_encoding ~ gzip) {
			set $encoding "gzip";
			break;
	}
}

确保将任何规范化属性传递给后端:

http|server|location {
	proxy_set_header Accept-Encoding $encoding;
}
缓存键

默认情况下,缓存不会查看Accept-Encoding. 由于后端返回编码响应但proxy_cache_key不包含编码,因此不支持 Brotli 的客户端可能会从缓存中接收到先前编码的响应。您可以在响应中插入标头,这与将 的值添加到缓存键Vary: Accept-Encoding具有相同的效果。Accept-Encoding不幸的是,Nginx 使用的是 的原始且不可变的值Accept-Encoding,而不是使用 设置的规范化值 proxy_set_header

作为替代方案,您可以添加$encodingproxy_cache_key. 默认值为$scheme$proxy_host$uri$is_args$args。如果您只是附加$encoding,则当 URL 以br或结尾时可能会发生键冲突gzip。因此, $encoding应该出现在$url任何其他用户控制的字段之前:

http|server|location {
	proxy_cache_key $scheme$proxy_host$encoding$uri$is_args$args;
}

这样,Nginx 可以缓存具有多种编码的响应,而不会出现重复条目​​。与实时再压缩相比,您可以负担得起更高的压缩级别以减少带宽,而不会显着增加服务器负载。

代理链

出于演示目的,可以使用 Nginx 执行所有操作,而无需在后端实施响应压缩,无需修改 proxy_cache_key,并且支持Vary基于标准化标头值。诀窍是链接多个 Nginx“服务器”。

第一个端点是客户端连接到的端点。它规范化请求属性,如Accept-Encoding,并将它们传递到链中的下一个端点。这是解决请求标头不变性所必需的。

server {
	listen 80;
	server_name app.example.com;

	proxy_set_header Accept-Encoding $encoding;

	location / {
		proxy_pass http://127.0.0.1:8080;
	}
}

第二个内部端点缓存响应。如果Vary: Accept-Encoding设置,它将条目与Accept-Encoding前一个端点给出的规范化相关联。

server {
	listen 127.0.0.1:8080;
	server_name localhost;

	proxy_cache_valid 10m;

	location / {
		proxy_cache app;
		proxy_pass http://127.0.0.1:8081;
	}
}

第三个端点将请求代理到后端服务器并执行实时响应压缩,添加Vary: Accept-Encoding. 需要一个单独的服务器,因为如果它们都启用,则压缩发生在缓存之后。

server {
	listen 127.0.0.1:8081;
	server_name localhost;

	brotli on;
	brotli_vary on;

	gzip on;
	gzip_vary on;

	location / {
		proxy_pass http://app;
	}
}

当然,复制数据并打开额外连接的效率低于单个服务器块。

结论

缓存可以显着提高服务器性能,即使在高流量情况下设置为几秒钟也是如此。

使用 Nginx 配置反向代理缓存的最终建议:

  • 缓存静态资产和公共动态网页。
  • 将标头规范化应用于缓存压缩响应。
  • 始终设置Cache-Control为私有资源。
  • 当心 Nginx 和后端之间关于私有资源的任何不匹配。

完整配置:

http {
	proxy_cache_path /var/cache/nginx/app
		levels=1:2 keys_zone=app:1M max_size=200M;

	upstream app {
		server 127.0.0.1:3000;
	}

	server {
		listen 80;
		server_name app.example.com;

		root /srv/http/app;

		location / {
			try_files $uri @app;
			brotli_static on;
			gzip_static on;
		}

		location /admin {
			proxy_pass http://app;
		}

		set $encoding "";

		if ($http_accept_encoding ~ br) {
				set $encoding "br";
				break;
		}

		if ($http_accept_encoding ~ gzip) {
				set $encoding "gzip";
				break;
		}

		proxy_set_header Accept-Encoding $encoding;
		proxy_cache_key $scheme$proxy_host$encoding$uri$is_args$args;

		proxy_cache_valid 1m;
		proxy_cache_valid any 10s;
		proxy_cache_min_uses 3;

		proxy_no_cache $cookie_sessionid;

		proxy_cache_lock on;
		proxy_cache_background_update on;
		proxy_cache_use_stale error timeout updating
			http_500 http_502 http_503 http_504;

		location @app {
			proxy_cache app;
			proxy_pass http://app;
		}
	}
}