yegor-usoltsev/nginx-upstream-keepalive

GitHub: yegor-usoltsev/nginx-upstream-keepalive

演示如何逐步配置 NGINX 反向代理的上游 HTTP keep-alive 连接复用以提升性能的实践教程项目。

Stars: 12 | Forks: 1

# nginx-upstream-keepalive 本仓库演示了在使用 [NGINX](https://github.com/nginx/nginx) 作为反向代理时,为上游服务器启用 HTTP keep-alive 的正确配置。在此设置中启用 HTTP keep-alive 可以通过以下方式显著提升性能: - **减少 CPU 负载**:通过最小化所需的新连接数量来降低上游服务器的 CPU 负载。 - **改善请求延迟**:通过重用连接来改善请求延迟,同时增强处理高请求量的能力。 创建本仓库是为了解答在[以下 Pull Request](https://github.com/antonputra/tutorials/pull/334) 中提出的问题。 ## TL;DR 要将 NGINX 最佳地配置为支持 HTTP keep-alive 的反向代理,请使用以下配置: ``` server { location / { # Reference to "upstream" block with the name "backend" (see below) proxy_pass http://backend; # Use HTTP/1.1 instead of HTTP/1.0 for upstream connections proxy_http_version 1.1; # Remove any "Connection: close" header and handle WebSockets (see "map" below) proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } } # 如果 "Upgrade" header 存在且不为空,则转发 "Connection: Upgrade"。 # 否则,不转发 "Connection" header。 map $http_upgrade $connection_upgrade { default upgrade; "" ""; } upstream backend { # 127.0.0.1:8080 is an example upstream server server 127.0.0.1:8080; # Maintain 2 idle keep-alive connections to upstream servers from each worker process keepalive 2; } ``` ## 概述 本仓库包含以下文件: - **`main.go`**:一个用 Go 编写的简单 HTTP 服务器,作为 NGINX 的上游。它监听 **8080** 端口并记录请求,方便检查 HTTP keep-alive 是否处于活动状态。 - **`nginx.conf`**:一个包含 4 个 `server` 块的最小化 NGINX 配置文件: - 第 1 个块(端口 **8081**)仅使用标准的 `proxy_pass`。 - 第 2 个块(端口 **8082**)添加了 `proxy_http_version 1.1`。 - 第 3 个块(端口 **8083**)添加了 `proxy_set_header Connection ""`。 - 第 4 个块(端口 **8084**)包含一个启用了 `keepalive` 的 `upstream` 块。 - **`docker-compose.yaml` & `Dockerfile`**:一个 Docker Compose 设置,用于将 Go 服务器与作为反向代理的 NGINX 一起运行。 ### 前置条件 要运行此示例,您需要 **Docker Compose** 以及作为客户端的 **curl**。 ### 运行 要启动 Go 服务器和 NGINX 代理,请运行: ``` docker compose up -d --build ``` 要查看 NGINX 和 Go 服务器的日志,请使用: ``` docker compose logs -f ``` 完成后,您可以使用以下命令停止应用程序: ``` docker compose down ``` ## 结果 ### 步骤 0:验证 Go 服务器支持 HTTP Keep-Alive 首先,通过使用 `curl` 直接向端口 **8080** 发送 3 个连续请求,确保 Go 服务器支持 HTTP keep-alive。检查连接是否被重用: ``` curl -sv http://localhost:8080 http://localhost:8080 http://localhost:8080 ``` 在 `curl` 的输出中,您应该会看到: ``` * Connection #0 to host localhost left intact ... * Re-using existing connection with host localhost ``` 这表明 `curl` 为第一个请求打开了一个连接,并将其重用于接下来的两个请求。此外,在 Go 服务器日志中,您应该会看到: ``` Received request from 192.168.107.1:55694 | Protocol: HTTP/1.1 | Will be closed: false ... Received request from 192.168.107.1:55694 | Protocol: HTTP/1.1 | Will be closed: false ... Received request from 192.168.107.1:55694 | Protocol: HTTP/1.1 | Will be closed: false Request headers: User-Agent: curl/8.7.1 Accept: */* ``` 每个请求都使用相同的端口(`55694`),确认**连接已被重用**。 ### 步骤 1:使用标准 `proxy_pass` 的 NGINX 接下来,让我们测试仅带有 `proxy_pass` 指令的 NGINX: ``` server { listen 8081; location / { proxy_pass http://golang:8080; } } ``` 向端口 **8081** 发送 3 个请求: ``` curl -sv http://localhost:8081 http://localhost:8081 http://localhost:8081 ``` ``` Received request from 192.168.107.3:42110 | Protocol: HTTP/1.0 | Will be closed: true ... Received request from 192.168.107.3:42122 | Protocol: HTTP/1.0 | Will be closed: true ... Received request from 192.168.107.3:42136 | Protocol: HTTP/1.0 | Will be closed: true Request headers: Connection: close User-Agent: curl/8.7.1 Accept: */* ``` 在 Go 服务器日志中,您会看到每个请求都来自不同的端口(`42110`、`42122`、`42136`),这表明**连接没有被重用**。发生这种情况是因为 NGINX 默认为上游连接使用 `HTTP/1.0`,而该版本缺乏连接重用机制。 ### 步骤 2:将 NGINX 升级到 HTTP/1.1 通过添加 `proxy_http_version 1.1` 来启用 HTTP/1.1: ``` server { - listen 8081; + listen 8082; location / { proxy_pass http://golang:8080; + proxy_http_version 1.1; } } ``` 向端口 **8082** 发送 3 个请求: ``` curl -sv http://localhost:8082 http://localhost:8082 http://localhost:8082 ``` ``` Received request from 192.168.107.3:60914 | Protocol: HTTP/1.1 | Will be closed: true ... Received request from 192.168.107.3:60918 | Protocol: HTTP/1.1 | Will be closed: true ... Received request from 192.168.107.3:60926 | Protocol: HTTP/1.1 | Will be closed: true Request headers: User-Agent: curl/8.7.1 Accept: */* Connection: close ``` Go 服务器日志显示请求来自不同的端口(`60914`、`60918`、`60926`),这意味着**连接没有被重用**。发生这种情况是因为 NGINX 默认添加了 `Connection: close` 标头,这会指示上游服务器在每次请求后关闭连接。 ### 步骤 3:不带 `Connection: close` 标头的 NGINX 通过添加 `proxy_set_header Connection "";` 来移除 `Connection: close` 标头: ``` server { - listen 8082; + listen 8083; location / { proxy_pass http://golang:8080; proxy_http_version 1.1; + proxy_set_header Connection ""; } } ``` 向端口 **8083** 发送 3 个请求: ``` curl -sv http://localhost:8083 http://localhost:8083 http://localhost:8083 ``` ``` Received request from 192.168.107.3:49260 | Protocol: HTTP/1.1 | Will be closed: false ... Received request from 192.168.107.3:49270 | Protocol: HTTP/1.1 | Will be closed: false ... Received request from 192.168.107.3:49274 | Protocol: HTTP/1.1 | Will be closed: false Request headers: User-Agent: curl/8.7.1 Accept: */* ``` 尽管移除了 `Connection: close`,NGINX 仍然**没有重用连接**,而是在每次请求后自动关闭它们。 ### 步骤 3.1:修复 WebSocket 支持 细心的读者可能会注意到,`Connection` 标头对于 WebSocket 连接也是必不可少的。在建立 WebSocket 连接时(例如,在 JavaScript 中:`let ws = new WebSocket("ws://localhost:8080")`),客户端会发送以下标头: ``` Connection: Upgrade Upgrade: websocket ``` 然而,我们当前的配置移除了 `Connection` 标头,这破坏了 WebSocket 连接。让我们使用以下方法修复此问题: - 如果 `Upgrade` 标头存在且不为空,则转发 `Connection: Upgrade`。 - 否则,不转发 `Connection` 标头。 为此,我们使用了 `map` 指令。NGINX 中的 `map` 指令允许我们在变量值(在此例中为 `$http_upgrade`)与分配给另一个变量(此处为 `$connection_upgrade`)的输出值之间创建映射。这对于根据请求属性动态设置配置值非常有用。 以下是正确处理 WebSocket 连接的更新配置: ``` server { listen 8083; location / { proxy_pass http://golang:8080; proxy_http_version 1.1; - proxy_set_header Connection ""; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; } } +map $http_upgrade $connection_upgrade { + default upgrade; + "" ""; +} ``` ### 步骤 4:带有 `keepalive` 的 NGINX 要启用连接重用,请定义一个带有 `keepalive` 的 `upstream` 块(参见 [NGINX 文档:ngx_http_upstream_module](https://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive))。这指定了每个 worker 进程的空闲 keep-alive 连接数量。以下是最终配置: ``` server { - listen 8083; + listen 8084; location / { - proxy_pass http://golang:8080; + proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } } map $http_upgrade $connection_upgrade { default upgrade; "" ""; } +upstream backend { + server golang:8080; + keepalive 2; +} ``` 向端口 **8084** 发送 3 个请求: ``` curl -sv http://localhost:8084 http://localhost:8084 http://localhost:8084 ``` ``` Received request from 192.168.107.3:55980 | Protocol: HTTP/1.1 | Will be closed: false ... Received request from 192.168.107.3:55980 | Protocol: HTTP/1.1 | Will be closed: false ... Received request from 192.168.107.3:55980 | Protocol: HTTP/1.1 | Will be closed: false Request headers: User-Agent: curl/8.7.1 Accept: */* ``` 终于成功了!在 Go 服务器日志中,所有请求都来自同一个端口(`55980`),确认**连接已被重用**。 ## 参考资料 - [NGINX 博客:避免十大 NGINX 配置错误](https://www.f5.com/company/blog/nginx/avoiding-top-10-nginx-configuration-mistakes#no-keepalives)(错误 3:未启用与上游服务器的 Keepalive 连接) - [NGINX 博客:提升 10 倍应用性能的 10 个技巧](https://www.f5.com/company/blog/nginx/10-tips-for-10x-application-performance#web-server-tuning)(技巧 9 – 调优 Web 服务器以提升性能) - [NGINX 博客:HTTP Keepalive 连接与 Web 性能](https://www.f5.com/company/blog/nginx/http-keepalives-and-web-performance) - [NGINX 文档:ngx_http_upstream_module](https://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive) ## 额外内容:将 NGINX 与其他反向代理进行比较 我还测试了几个通常用作反向代理的流行开源项目,每个项目均使用其默认配置: - [Apache HTTP Server](https://github.com/apache/httpd)(端口 **9090**) - [Caddy](https://github.com/caddyserver/caddy)(端口 **9091**) - [Envoy](https://github.com/envoyproxy/envoy)(端口 **9092**) - [HAProxy](https://github.com/haproxy/haproxy)(端口 **9093**) - [Traefik](https://github.com/traefik/traefik)(端口 **9094**) 这些代理的所有配置都可以在 **[bonus](bonus)** 目录中找到。 结果:**所有这些代理默认都使用 HTTP keep-alive**,而不像 NGINX 那样需要额外的设置。 要自行运行这些测试,请使用 `bonus` profile 启动 Docker Compose: ``` docker compose --profile bonus up -d --build ``` 要观察所有应用程序的日志,请使用: ``` docker compose --profile bonus logs -f ``` 完成后,使用以下命令停止所有应用程序: ``` docker compose --profile bonus down ``` 要向每个代理发送一系列 HTTP 请求: ``` for PORT in 9090 9091 9092 9093 9094; do curl -sv http://localhost:$PORT http://localhost:$PORT http://localhost:$PORT done ``` ## 贡献 欢迎提交 Pull Request。对于重大更改,请先[创建一个 issue](https://github.com/yegor-usoltsev/nginx-upstream-keepalive/issues/new) 以讨论您想要更改的内容。 ## 许可证 [MIT](https://github.com/yegor-usoltsev/nginx-upstream-keepalive/blob/main/LICENSE)
标签:EVTX分析, Go, HTTP Keep-Alive, Nginx, Ruby工具, 反向代理, 性能优化, 日志审计, 检测绕过, 版权保护, 系统配置, 请求拦截, 负责任AI