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