Rocky Linux 10 搭建企业级 NGINX:动静分离、反向代理、负载均衡、健康监测、集群与 Docker 部署

NGINX 不只是一个静态 Web 服务器。它还可以承担:

  • 静态资源服务器;
  • HTTPS 终止;
  • HTTP/HTTPS 反向代理;
  • WebSocket 和 gRPC 代理;
  • HTTP、TCP、UDP 负载均衡;
  • 内容缓存;
  • 限流与连接控制;
  • 多域名虚拟主机;
  • 内部统一入口;
  • 应用节点故障摘除;
  • 高可用入口集群。

在单机环境中,启动一个 NGINX 并不困难:

1
2
dnf install -y nginx
systemctl enable --now nginx

真正困难的是把它变成一个可持续运行的生产基础设施:

1
2
3
4
5
6
7
配置可验证
-> 发布可回滚
-> 上游故障可降级
-> NGINX 进程可自愈
-> 入口节点可漂移
-> 日志和指标可观测
-> 容量和内核参数经过验证

本文基于 Rocky Linux 10,使用系统仓库中的 NGINX 1.26.3 作为原生部署基线;Docker 部分使用 NGINX 官方稳定镜像,并固定到明确版本。内容覆盖动静分离、反向代理、负载均衡、健康监测、Keepalived 集群、调优和容器化部署。

截至 2026 年 7 月 31 日,Rocky Linux 10 AppStream 仓库中的 NGINX 基线为 1.26.3;NGINX 官方当前稳定分支为 1.30.x。系统 RPM 与 Docker 镜像不必强行使用相同版本,但都必须固定并经过测试,不要在生产环境长期追踪 latest

一、整体架构

本文示例环境:

角色 主机名 IP 作用
NGINX 1 nginx01.example.com 192.168.9.10 主入口节点
NGINX 2 nginx02.example.com 192.168.9.11 备用入口节点
VIP app.example.com 192.168.9.100 客户端访问地址
Backend 1 app01.example.com 192.168.9.31:8080 Java/业务服务
Backend 2 app02.example.com 192.168.9.32:8080 Java/业务服务
Backend 3 app03.example.com 192.168.9.33:8080 备用业务服务
flowchart LR
    Client[浏览器/客户端] -->|HTTPS 443| VIP[VIP 192.168.9.100]

    VIP --> N1[NGINX 01]
    VIP -.故障切换.-> N2[NGINX 02]

    N1 --> Static1[本地静态资源]
    N2 --> Static2[本地静态资源]

    N1 --> B1[Backend 1]
    N1 --> B2[Backend 2]
    N1 --> B3[Backend 3 Backup]

    N2 --> B1
    N2 --> B2
    N2 --> B3

    K1[Keepalived] --- N1
    K2[Keepalived] --- N2

这里需要区分两层“负载均衡”:

1
2
3
4
5
6
Keepalived:
负责 NGINX 入口节点高可用
默认是 Active/Standby,不会同时分担两台 NGINX 的流量

NGINX upstream:
负责多个后端应用之间的请求分发

需要两台 NGINX 同时处理流量时,应在它们前面使用:

  • 云负载均衡;
  • 硬件负载均衡;
  • LVS/IPVS;
  • BGP/ECMP;
  • Anycast;
  • DNS 多地址与健康探测。

二、安装前的系统准备

检查系统:

1
2
3
4
cat /etc/rocky-release
uname -r
uname -m
hostnamectl

设置主机名:

1
hostnamectl set-hostname nginx01.example.com

同步时间:

1
2
3
timedatectl set-timezone Asia/Shanghai
systemctl enable --now chronyd
chronyc tracking

更新系统:

1
2
3
dnf clean all
dnf makecache
dnf upgrade -y

安装基础工具:

1
2
3
4
5
6
7
8
9
10
11
12
dnf install -y \
vim \
curl \
wget \
jq \
tar \
unzip \
bind-utils \
lsof \
tcpdump \
policycoreutils-python-utils \
firewalld

确认 SELinux:

1
2
getenforce
sestatus

生产环境不要为了省事直接关闭 SELinux。

三、安装 NGINX

3.1 使用 Rocky Linux 10 官方仓库

查看包:

1
2
dnf info nginx
dnf list --showduplicates nginx

安装:

1
dnf install -y nginx

如果需要 TCP/UDP Stream 代理:

1
dnf install -y nginx-mod-stream

查看版本和编译参数:

1
2
3
nginx -v
nginx -V
rpm -q nginx nginx-core nginx-mod-stream

检查模块:

1
2
3
nginx -V 2>&1 | tr ' ' '\n' | sort
ls -lah /usr/share/nginx/modules
ls -lah /usr/share/nginx/modules-enabled 2>/dev/null || true

3.2 启动服务

1
systemctl enable --now nginx

检查:

1
2
3
4
5
6
systemctl status nginx --no-pager -l
systemctl is-enabled nginx
systemctl is-active nginx

ss -lntp | grep nginx
curl -I http://127.0.0.1/

查看 systemd Unit:

1
systemctl cat nginx

3.3 firewalld

1
2
3
4
5
6
7
systemctl enable --now firewalld

firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload

firewall-cmd --list-all

3.4 主要目录

路径 用途
/etc/nginx/nginx.conf 主配置
/etc/nginx/conf.d/*.conf 虚拟主机和业务配置
/etc/nginx/default.d/*.conf 默认 Server 扩展配置
/usr/share/nginx/html 默认静态目录
/var/log/nginx/access.log 访问日志
/var/log/nginx/error.log 错误日志
/run/nginx.pid Master PID
/usr/share/nginx/modules/*.conf 动态模块加载配置

推荐将业务配置拆分:

1
2
3
4
5
6
7
8
9
10
11
/etc/nginx/
├── nginx.conf
├── conf.d/
│ ├── 00-upstreams.conf
│ ├── 10-status.conf
│ ├── app.example.com.conf
│ └── stream.conf
└── snippets/
├── proxy-common.conf
├── ssl-common.conf
└── security-headers.conf

不要把所有域名、upstream、TLS 和缓存规则堆进一个几千行的 nginx.conf

四、配置测试与平滑发布

每次修改后先执行:

1
nginx -t

查看完整展开配置:

1
nginx -T

只有测试通过后才 Reload:

1
systemctl reload nginx

或者:

1
nginx -s reload

推荐使用 systemd:

1
nginx -t && systemctl reload nginx

Reload 的流程:

sequenceDiagram
    participant Admin as 运维
    participant Master as NGINX Master
    participant Old as Old Workers
    participant New as New Workers

    Admin->>Master: reload
    Master->>Master: 重新读取并校验配置
    Master->>New: 启动新 Worker
    Master->>Old: 请求优雅退出
    Old->>Old: 完成现有连接
    Old-->>Master: 退出

Reload 不等于重启:

  • 新请求由新 Worker 处理;
  • 旧 Worker 尽量完成已有请求;
  • 长连接可能让旧 Worker 保留较长时间;
  • 配置语法错误时 Reload 不会应用新配置。

生产发布流程至少应是:

1
2
3
4
nginx -t
cp -a /etc/nginx "/backup/nginx-$(date +%F-%H%M%S)"
systemctl reload nginx
curl -fsS http://127.0.0.1:8088/healthz

五、推荐的主配置

备份:

1
2
3
cp -a \
/etc/nginx/nginx.conf \
"/etc/nginx/nginx.conf.bak.$(date +%F-%H%M%S)"

编辑 /etc/nginx/nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
user nginx;

worker_processes auto;
worker_rlimit_nofile 131072;

error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;

include /usr/share/nginx/modules/*.conf;

events {
worker_connections 8192;
}

http {
log_format main
'$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'request_time=$request_time '
'upstream_addr=$upstream_addr '
'upstream_status=$upstream_status '
'upstream_connect_time=$upstream_connect_time '
'upstream_header_time=$upstream_header_time '
'upstream_response_time=$upstream_response_time '
'request_id=$request_id';

access_log /var/log/nginx/access.log main;

include /etc/nginx/mime.types;
default_type application/octet-stream;

sendfile on;
tcp_nopush on;

keepalive_timeout 65;
keepalive_requests 10000;

server_tokens off;

client_max_body_size 50m;
client_body_timeout 30s;
client_header_timeout 15s;
send_timeout 60s;

types_hash_max_size 4096;

open_file_cache max=10000 inactive=60s;
open_file_cache_valid 120s;
open_file_cache_min_uses 2;
open_file_cache_errors on;

gzip on;
gzip_vary on;
gzip_comp_level 5;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
application/json
application/javascript
application/xml
application/rss+xml
image/svg+xml;

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

limit_req_zone $binary_remote_addr
zone=api_rate:20m
rate=20r/s;

limit_conn_zone $binary_remote_addr
zone=per_ip_conn:20m;

include /etc/nginx/conf.d/*.conf;
}

5.1 worker_processes

1
worker_processes auto;

通常会按可用 CPU 数量创建 Worker。

不要直接写:

1
worker_processes 64;

如果机器只有 8 核,额外 Worker 只会增加调度和内存开销。

5.2 worker_connections

理论客户端连接上限并不简单等于:

1
worker_processes × worker_connections

反向代理请求通常同时占用:

1
2
1 个客户端连接
+ 1 个上游连接

还受到:

  • 文件句柄;
  • upstream keepalive;
  • HTTP/2 多路复用;
  • 长连接;
  • WebSocket;
  • 系统端口;
  • 内存;
  • 上游容量;

共同限制。

5.3 gzip

适合压缩:

1
2
3
4
5
6
HTML
CSS
JavaScript
JSON
XML
SVG

不建议重复压缩:

1
2
3
4
5
6
7
8
JPEG
PNG
WebP
MP4
ZIP
Gzip
Brotli
RPM

已经压缩的文件再压一遍,通常只会多烧 CPU,文件大小却像拒绝加班的员工一样基本不动。

六、静态资源服务器

6.1 创建目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mkdir -p /srv/www/app/releases/20260731
mkdir -p /srv/www/app/shared/assets

cat > /srv/www/app/releases/20260731/index.html <<'EOF'
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Example Application</title>
</head>
<body>
<h1>Rocky Linux 10 + NGINX</h1>
</body>
</html>
EOF

ln -sfn \
/srv/www/app/releases/20260731 \
/srv/www/app/current

权限:

1
2
3
chown -R root:nginx /srv/www/app
find /srv/www/app -type d -exec chmod 0755 {} \;
find /srv/www/app -type f -exec chmod 0644 {} \;

6.2 SELinux Context

自定义静态目录要设置持久化 Label:

1
2
3
4
5
6
semanage fcontext \
-a \
-t httpd_sys_content_t \
'/srv/www/app(/.*)?'

restorecon -Rv /srv/www/app

检查:

1
2
ls -ldZ /srv/www/app
ls -lZ /srv/www/app/current

不要长期使用:

1
chcon -R ...

chcon 的修改可能在 restorecon 或重新标记后丢失。

6.3 静态站点配置

创建 /etc/nginx/conf.d/app.example.com.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
server {
listen 80;
server_name app.example.com;

root /srv/www/app/current;
index index.html;

location = /nginx-health {
access_log off;
default_type text/plain;
return 200 "ok\n";
}

location /assets/ {
alias /srv/www/app/shared/assets/;

try_files $uri =404;

expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";

access_log off;
}

location / {
try_files $uri $uri/ /index.html;
}

location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}

验证:

1
2
3
4
5
6
7
8
nginx -t
systemctl reload nginx

curl -H 'Host: app.example.com' \
http://127.0.0.1/

curl -H 'Host: app.example.com' \
http://127.0.0.1/nginx-health

6.4 root 与 alias 的区别

root 会把完整 URI 拼到目录后:

1
2
3
location /assets/ {
root /srv/www/app;
}

请求:

1
/assets/logo.svg

实际文件:

1
/srv/www/app/assets/logo.svg

alias 会用指定目录替换匹配到的 Location:

1
2
3
location /assets/ {
alias /srv/www/app/shared/assets/;
}

请求:

1
/assets/logo.svg

实际文件:

1
/srv/www/app/shared/assets/logo.svg

使用 alias 时,Location 和目录末尾的斜杠要保持一致,否则很容易制造出“文件明明存在但 NGINX 就是说 404”的玄学现场。

七、前后端动静分离

动静分离的典型职责:

1
2
3
4
5
6
7
8
/index.html、/assets/*:
由 NGINX 本地直接返回

/api/*:
代理到 Java、Go、Node.js 等后端

/ws/*:
代理 WebSocket
flowchart TD
    Request[客户端请求] --> N[NGINX]
    N -->|/ /assets| Static[静态文件]
    N -->|/api| Backend[业务服务]
    N -->|/ws| WS[WebSocket 服务]

7.1 配置 upstream

创建 /etc/nginx/conf.d/00-upstreams.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
upstream app_backend {
zone app_backend 64k;

least_conn;

server 192.168.9.31:8080
weight=5
max_conns=200
max_fails=3
fail_timeout=10s;

server 192.168.9.32:8080
weight=5
max_conns=200
max_fails=3
fail_timeout=10s;

server 192.168.9.33:8080
backup
max_fails=2
fail_timeout=10s;

keepalive 64;
keepalive_requests 1000;
keepalive_timeout 60s;
}

7.2 代理公共参数

创建目录:

1
mkdir -p /etc/nginx/snippets

创建 /etc/nginx/snippets/proxy-common.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
proxy_http_version 1.1;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Request-ID $request_id;

proxy_set_header Connection "";

proxy_connect_timeout 3s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;

proxy_next_upstream
error
timeout
invalid_header
http_502
http_503
http_504;

proxy_next_upstream_tries 3;
proxy_next_upstream_timeout 10s;

proxy_buffering on;

7.3 完整动静分离配置

更新 /etc/nginx/conf.d/app.example.com.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
server {
listen 80;
server_name app.example.com;

root /srv/www/app/current;
index index.html;

location = /nginx-health {
access_log off;
default_type text/plain;
return 200 "ok\n";
}

location /assets/ {
alias /srv/www/app/shared/assets/;
try_files $uri =404;

expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";

access_log off;
}

location /api/ {
limit_req zone=api_rate burst=40 nodelay;
limit_conn per_ip_conn 50;

include /etc/nginx/snippets/proxy-common.conf;

proxy_pass http://app_backend;
}

location /ws/ {
proxy_http_version 1.1;

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

proxy_connect_timeout 3s;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;

proxy_pass http://app_backend;
}

location / {
try_files $uri $uri/ /index.html;
}

location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}

7.4 proxy_pass 末尾斜杠

以下配置:

1
2
3
location /api/ {
proxy_pass http://app_backend;
}

请求:

1
/api/users

上游收到:

1
/api/users

以下配置:

1
2
3
location /api/ {
proxy_pass http://app_backend/;
}

上游收到:

1
/users

一个斜杠就能决定后端收到什么 URI。它个头不大,但制造 404 的能力相当成熟。

7.5 上传和流式接口

大文件上传:

1
2
3
4
5
6
7
8
location /api/upload/ {
include /etc/nginx/snippets/proxy-common.conf;

client_max_body_size 2g;
proxy_request_buffering off;

proxy_pass http://app_backend;
}

关闭 Request Buffering 后,NGINX 会更早将请求体发送给上游,但一旦开始发送,请求通常不能安全切换到下一个上游。

SSE 或流式响应:

1
2
3
4
5
6
7
8
9
location /api/stream/ {
include /etc/nginx/snippets/proxy-common.conf;

proxy_buffering off;
proxy_cache off;
proxy_read_timeout 1h;

proxy_pass http://app_backend;
}

不要对所有接口无脑关闭 Buffering。普通 JSON 接口开启 Buffering 往往能降低慢客户端对后端连接的长期占用。

八、反向代理 HTTPS 上游

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
location /partner-api/ {
proxy_http_version 1.1;

proxy_set_header Host partner.internal.example.com;
proxy_set_header Connection "";

proxy_ssl_server_name on;
proxy_ssl_name partner.internal.example.com;

proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/pki/ca-trust/source/anchors/company-root-ca.crt;
proxy_ssl_verify_depth 3;

proxy_connect_timeout 3s;
proxy_read_timeout 60s;

proxy_pass https://partner.internal.example.com/;
}

不要使用:

1
proxy_ssl_verify off;

长期绕过上游证书校验。修好证书链、SAN、DNS 和内部 CA 才是正路。

九、gRPC 反向代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
upstream grpc_backend {
server 192.168.9.31:9090 max_fails=3 fail_timeout=10s;
server 192.168.9.32:9090 max_fails=3 fail_timeout=10s;
}

server {
listen 443 ssl;
http2 on;

server_name grpc.example.com;

ssl_certificate /etc/nginx/ssl/grpc.example.com.crt;
ssl_certificate_key /etc/nginx/ssl/grpc.example.com.key;

location / {
grpc_set_header Host $host;
grpc_set_header X-Real-IP $remote_addr;
grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

grpc_connect_timeout 3s;
grpc_read_timeout 60s;
grpc_send_timeout 60s;

grpc_pass grpc://grpc_backend;
}
}

后端启用 TLS 时使用:

1
grpc_pass grpcs://grpc_backend;

十、负载均衡算法

10.1 Round Robin

默认算法,不需要额外指令:

1
2
3
4
upstream backend {
server 192.168.9.31:8080;
server 192.168.9.32:8080;
}

适合请求耗时比较均匀的无状态服务。

10.2 Weight

1
2
3
4
upstream backend {
server 192.168.9.31:8080 weight=5;
server 192.168.9.32:8080 weight=2;
}

适合节点规格不同。

10.3 Least Connections

1
2
3
4
5
6
upstream backend {
least_conn;

server 192.168.9.31:8080;
server 192.168.9.32:8080;
}

适合请求耗时差异较大、长连接较多的服务。

10.4 IP Hash

1
2
3
4
5
6
upstream backend {
ip_hash;

server 192.168.9.31:8080;
server 192.168.9.32:8080;
}

可提供有限的客户端亲和性,但存在问题:

  • 大量用户经过同一个 NAT;
  • CDN 或代理隐藏真实地址;
  • 节点上下线造成映射变化;
  • 后端故障后会重新分配;
  • 不是可靠的 Session 高可用方案。

更推荐将 Session 放入 Redis、数据库或 Token 中,让业务服务真正无状态。

10.5 Consistent Hash

1
2
3
4
5
6
upstream backend {
hash $cookie_tenant_id consistent;

server 192.168.9.31:8080;
server 192.168.9.32:8080;
}

适合按租户、缓存 Key 或业务标识进行稳定分片。

10.6 Random Two Least Connections

1
2
3
4
5
6
7
8
upstream backend {
random two least_conn;

server 192.168.9.31:8080;
server 192.168.9.32:8080;
server 192.168.9.33:8080;
server 192.168.9.34:8080;
}

适合较大上游集群,先随机选择两个节点,再从中选择连接较少的节点。

10.7 Backup

1
2
3
4
5
upstream backend {
server 192.168.9.31:8080;
server 192.168.9.32:8080;
server 192.168.9.33:8080 backup;
}

Backup 只在普通节点都不可用时使用,不参与正常负载分配。

十一、上游连接复用

1
2
3
4
5
6
7
8
upstream app_backend {
server 192.168.9.31:8080;
server 192.168.9.32:8080;

keepalive 64;
keepalive_requests 1000;
keepalive_timeout 60s;
}

Rocky Linux 10 的 NGINX 1.26.3 中,还需要明确:

1
2
proxy_http_version 1.1;
proxy_set_header Connection "";

keepalive 64 表示每个 Worker 保存的空闲上游连接上限,不是上游总连接上限,也不会阻止 Worker 创建更多活跃连接。

配置过大可能导致:

  • 上游保持大量空闲连接;
  • 新客户端无法建立连接;
  • 后端文件句柄耗尽;
  • 每个 NGINX Worker 都保存一份连接池。

应结合后端线程池、连接上限和 NGINX Worker 数量计算。

十二、健康检查的能力边界

NGINX Open Source 原生提供的是被动健康检查

1
2
3
4
真实请求访问上游
-> 连接失败、超时或返回指定错误
-> NGINX 记录失败
-> 在 fail_timeout 内暂时避免选择该节点

配置:

1
2
3
4
5
6
7
8
9
upstream app_backend {
server 192.168.9.31:8080
max_fails=3
fail_timeout=10s;

server 192.168.9.32:8080
max_fails=3
fail_timeout=10s;
}

含义:

  • fail_timeout=10s 窗口内累计 3 次失败;
  • 节点在接下来约 10 秒内被视为不可用;
  • 后续会重新尝试该节点。

需要配合:

1
2
3
4
5
6
7
proxy_next_upstream
error
timeout
invalid_header
http_502
http_503
http_504;

12.1 单节点 upstream 的陷阱

如果 upstream 中只有一个 Server:

1
2
3
upstream backend {
server 192.168.9.31:8080 max_fails=3 fail_timeout=10s;
}

max_failsfail_timeout 不会像多节点场景那样把唯一节点摘除,因为摘除后没有任何节点可选。

12.2 开源版没有原生周期性主动健康检查

以下指令:

1
health_check;

属于 NGINX Plus 主动健康检查能力,不是 Rocky Linux 仓库中 NGINX Open Source 的标准能力。

不要把网上的 Plus 配置直接复制到开源版,否则:

1
nginx: [emerg] unknown directive "health_check"

开源版可以通过以下方式补齐不同层级的检查:

检查对象 推荐实现
NGINX 进程崩溃 systemd Restart=on-failure
NGINX 本地 HTTP 可用性 systemd Timer + curl
NGINX 集群节点 Keepalived vrrp_script
后端业务实例 应用编排平台、服务发现、外部监控或 NGINX Plus
整条用户链路 Prometheus Blackbox、Zabbix、Nagios、云监控等外部探测

本机健康检查不能代替外部监控。本机认为自己很健康,并不代表 DNS、VIP、交换机、防火墙和公网都同意。

十三、配置本地健康与状态端点

创建 /etc/nginx/conf.d/10-status.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server {
listen 127.0.0.1:8088;
server_name localhost;

access_log off;

location = /healthz {
default_type text/plain;
return 200 "ok\n";
}

location = /nginx-status {
stub_status;
allow 127.0.0.1;
deny all;
}
}

验证模块:

1
nginx -V 2>&1 | grep -o -- '--with-http_stub_status_module'

应用:

1
2
nginx -t
systemctl reload nginx

检查:

1
2
curl -fsS http://127.0.0.1:8088/healthz
curl -fsS http://127.0.0.1:8088/nginx-status

stub_status 输出示例:

1
2
3
4
Active connections: 12
server accepts handled requests
10567 10567 20331
Reading: 0 Writing: 2 Waiting: 10

指标含义:

指标 说明
Active 当前活跃客户端连接
accepts 已接受的连接总数
handled 已处理连接总数
requests 已处理请求总数
Reading 正在读取请求头
Writing 正在向客户端返回响应
Waiting Keepalive 空闲连接

如果:

1
accepts > handled

可能存在连接处理失败或资源限制。

十四、systemd 进程自愈

Rocky Linux RPM 已提供 nginx.service,不要直接修改 /usr/lib/systemd/system/nginx.service,升级包时会被覆盖。

创建 Override:

1
systemctl edit nginx

写入:

1
2
3
4
5
6
7
8
9
[Unit]
StartLimitIntervalSec=30
StartLimitBurst=5

[Service]
Restart=on-failure
RestartSec=2s
LimitNOFILE=131072
TasksMax=65536

应用:

1
2
systemctl daemon-reload
systemctl restart nginx

检查:

1
2
3
4
5
6
systemctl cat nginx
systemctl show nginx \
-p Restart \
-p RestartUSec \
-p LimitNOFILE \
-p TasksMax

14.1 为什么不是 Restart=always

Restart=always 会在管理员执行正常停止后仍尝试拉起服务,可能干扰维护。

推荐:

1
Restart=on-failure

它主要处理:

  • Master 进程异常退出;
  • OOM 或信号终止;
  • 未预期崩溃。

它无法判断:

  • NGINX 进程活着但 HTTP 无响应;
  • 配置代理到了错误后端;
  • TLS 证书过期;
  • VIP 不在本机;
  • 后端全部返回业务错误。

因此还需要应用层检查。

十五、使用 systemd Service + Timer 监测 NGINX

15.1 健康检查脚本

创建 /usr/local/sbin/nginx-healthcheck.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
cat > /usr/local/sbin/nginx-healthcheck.sh <<'SCRIPT'
#!/usr/bin/env bash

set -Eeuo pipefail

LOCK_FILE="/run/nginx-healthcheck.lock"
FAIL_FILE="/run/nginx-healthcheck.failures"

HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:8088/healthz}"
MAX_FAILURES="${MAX_FAILURES:-3}"

exec 9>"${LOCK_FILE}"

if ! flock -n 9; then
exit 0
fi

current_failures=0

if [[ -r "${FAIL_FILE}" ]]; then
read -r current_failures < "${FAIL_FILE}" || current_failures=0
fi

is_healthy() {
systemctl is-active --quiet nginx \
&& curl \
--fail \
--silent \
--show-error \
--max-time 2 \
"${HEALTH_URL}" \
| grep -qx 'ok'
}

if is_healthy; then
printf '0\n' > "${FAIL_FILE}"
exit 0
fi

current_failures=$((current_failures + 1))
printf '%s\n' "${current_failures}" > "${FAIL_FILE}"

logger \
-t nginx-healthcheck \
"NGINX health check failed: ${current_failures}/${MAX_FAILURES}"

if (( current_failures < MAX_FAILURES )); then
exit 0
fi

if ! /usr/sbin/nginx -t; then
logger \
-p daemon.err \
-t nginx-healthcheck \
"NGINX configuration is invalid; refusing automatic restart"
exit 1
fi

logger \
-p daemon.warning \
-t nginx-healthcheck \
"Restarting NGINX after ${current_failures} consecutive failures"

systemctl restart nginx

sleep 2

if is_healthy; then
printf '0\n' > "${FAIL_FILE}"
logger \
-t nginx-healthcheck \
"NGINX recovered after restart"
exit 0
fi

logger \
-p daemon.err \
-t nginx-healthcheck \
"NGINX is still unhealthy after restart"

exit 1
SCRIPT

chmod 0750 /usr/local/sbin/nginx-healthcheck.sh
chown root:root /usr/local/sbin/nginx-healthcheck.sh

该脚本连续失败 3 次才重启,避免一次瞬时抖动就触发“重启疗法”。

15.2 systemd Service

创建 /etc/systemd/system/nginx-healthcheck.service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Unit]
Description=NGINX application-level health check
After=network-online.target nginx.service
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/nginx-healthcheck.sh

NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict

ReadWritePaths=/run

15.3 systemd Timer

创建 /etc/systemd/system/nginx-healthcheck.timer

1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=Run NGINX health check periodically

[Timer]
OnBootSec=30s
OnUnitActiveSec=30s
AccuracySec=5s
RandomizedDelaySec=3s
Unit=nginx-healthcheck.service

[Install]
WantedBy=timers.target

启用:

1
2
systemctl daemon-reload
systemctl enable --now nginx-healthcheck.timer

查看:

1
2
3
4
5
systemctl list-timers nginx-healthcheck.timer

systemctl status nginx-healthcheck.timer
journalctl -u nginx-healthcheck.service -n 100 --no-pager
journalctl -t nginx-healthcheck -n 100 --no-pager

手工执行:

1
systemctl start nginx-healthcheck.service

15.4 自动重启的边界

自动重启应当谨慎:

  • 只能在配置通过 nginx -t 时执行;
  • 必须限制重启频率;
  • 必须产生告警;
  • 不能掩盖持续性故障;
  • 不能用来修复错误业务配置;
  • 不能代替外部探测。

不停重启并不叫高可用,那叫服务在原地做开合跳。

十六、TLS 配置

创建 /etc/nginx/snippets/ssl-common.conf

1
2
3
4
5
6
7
ssl_protocols TLSv1.2 TLSv1.3;

ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;

ssl_prefer_server_ciphers off;

创建 /etc/nginx/snippets/security-headers.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
add_header Strict-Transport-Security
"max-age=31536000; includeSubDomains"
always;

add_header X-Content-Type-Options
"nosniff"
always;

add_header Referrer-Policy
"strict-origin-when-cross-origin"
always;

add_header Permissions-Policy
"camera=(), microphone=(), geolocation=()"
always;

HTTPS Server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
server {
listen 80;
server_name app.example.com;

return 301 https://$host$request_uri;
}

server {
listen 443 ssl;
http2 on;

server_name app.example.com;

ssl_certificate
/etc/nginx/ssl/app.example.com.fullchain.crt;

ssl_certificate_key
/etc/nginx/ssl/app.example.com.key;

include /etc/nginx/snippets/ssl-common.conf;
include /etc/nginx/snippets/security-headers.conf;

root /srv/www/app/current;

location = /nginx-health {
access_log off;
default_type text/plain;
return 200 "ok\n";
}

location /api/ {
include /etc/nginx/snippets/proxy-common.conf;
proxy_pass http://app_backend;
}

location / {
try_files $uri $uri/ /index.html;
}
}

证书权限:

1
2
3
4
5
6
chown -R root:nginx /etc/nginx/ssl
find /etc/nginx/ssl -type d -exec chmod 0750 {} \;
find /etc/nginx/ssl -type f -name '*.key' -exec chmod 0640 {} \;
find /etc/nginx/ssl -type f ! -name '*.key' -exec chmod 0644 {} \;

restorecon -Rv /etc/nginx/ssl

测试:

1
2
3
4
5
6
7
nginx -t
systemctl reload nginx

openssl s_client \
-connect app.example.com:443 \
-servername app.example.com \
-showcerts

HSTS 一旦带 includeSubDomains 发布,所有子域名都必须长期支持 HTTPS。不要在没有盘点子域名时盲目开启。

十七、SELinux 配置

17.1 允许反向代理连接网络

1
setsebool -P httpd_can_network_connect 1

查看:

1
getsebool httpd_can_network_connect

否则可能出现:

1
connect() failed (13: Permission denied) while connecting to upstream

17.2 自定义监听端口

如果 NGINX 要监听 8443:

1
2
3
4
5
semanage port \
-a \
-t http_port_t \
-p tcp \
8443

如果已经存在:

1
2
3
4
5
semanage port \
-m \
-t http_port_t \
-p tcp \
8443

查看:

1
semanage port -l | grep http_port_t

17.3 自定义缓存目录

1
2
3
4
5
6
7
8
mkdir -p /var/cache/nginx/proxy

semanage fcontext \
-a \
-t httpd_cache_t \
'/var/cache/nginx(/.*)?'

restorecon -Rv /var/cache/nginx

17.4 排查 AVC

1
2
ausearch -m AVC -ts recent
journalctl -t setroubleshoot --since today

不要看到 Permission Denied 就先执行:

1
setenforce 0

那会把真正的问题藏起来,并给未来留下一颗“这台机器为什么和别人不一样”的定时炸弹。

十八、代理缓存

定义缓存区,在 http 中加入:

1
2
3
4
5
6
7
proxy_cache_path \
/var/cache/nginx/proxy \
levels=1:2 \
keys_zone=api_cache:100m \
inactive=60m \
max_size=10g \
use_temp_path=off;

缓存公开 GET 接口:

1
2
3
4
5
6
7
8
9
map $http_authorization $skip_auth_cache {
default 1;
"" 0;
}

map $cookie_session $skip_session_cache {
default 1;
"" 0;
}

Location:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
location /public-api/ {
include /etc/nginx/snippets/proxy-common.conf;

proxy_cache api_cache;
proxy_cache_methods GET HEAD;
proxy_cache_key "$scheme$request_method$host$request_uri";

proxy_cache_valid 200 5m;
proxy_cache_valid 301 302 1m;
proxy_cache_valid 404 30s;

proxy_cache_bypass
$skip_auth_cache
$skip_session_cache;

proxy_no_cache
$skip_auth_cache
$skip_session_cache
$upstream_http_set_cookie;

proxy_cache_lock on;

proxy_cache_use_stale
error
timeout
invalid_header
updating
http_500
http_502
http_503
http_504;

add_header X-Proxy-Cache $upstream_cache_status always;

proxy_pass http://app_backend;
}

不要缓存:

  • 登录接口;
  • 带 Authorization 的私有请求;
  • 用户个人数据;
  • 支付和订单写接口;
  • 未明确 Cache Key 的多租户接口;
  • 会返回 Set-Cookie 的动态页面。

缓存做错以后,性能可能很好,数据隔离也可能好到大家都能看到彼此的数据。

十九、限流与连接控制

19.1 请求速率

主配置:

1
2
3
4
limit_req_zone \
$binary_remote_addr \
zone=api_rate:20m \
rate=20r/s;

业务配置:

1
2
3
4
5
6
location /api/ {
limit_req zone=api_rate burst=40 nodelay;

include /etc/nginx/snippets/proxy-common.conf;
proxy_pass http://app_backend;
}

19.2 并发连接

主配置:

1
2
3
limit_conn_zone \
$binary_remote_addr \
zone=per_ip_conn:20m;

业务配置:

1
limit_conn per_ip_conn 50;

19.3 登录接口

1
2
3
4
5
6
7
8
9
10
11
limit_req_zone \
$binary_remote_addr \
zone=login_rate:10m \
rate=1r/s;

location = /api/login {
limit_req zone=login_rate burst=5 nodelay;

include /etc/nginx/snippets/proxy-common.conf;
proxy_pass http://app_backend;
}

IP 限流不能代替:

  • 账号级失败次数;
  • 验证码;
  • MFA;
  • WAF;
  • 风险识别;
  • 应用层幂等和熔断。

大量用户经过同一 NAT 时,IP 限流可能误伤整个办公室。

二十、日志调优

推荐记录:

1
2
3
4
5
6
7
request_time
upstream_addr
upstream_status
upstream_connect_time
upstream_header_time
upstream_response_time
request_id

判断瓶颈:

表现 可能原因
request_time 大,upstream_response_time 客户端慢、响应体大、下行网络慢
upstream_connect_time 上游连接拥塞、网络、端口或 SYN 问题
upstream_header_time 应用开始处理慢
upstream_response_time 应用整体耗时大
多个 upstream_addr 请求发生了重试
upstream_status502, 200 第一个节点失败,第二个节点成功

20.1 日志轮转

检查:

1
cat /etc/logrotate.d/nginx

手工重新打开日志:

1
nginx -s reopen

不要直接:

1
rm -f /var/log/nginx/access.log

进程可能仍持有已删除文件的 FD,磁盘空间不会立即释放。

查看:

1
lsof +L1 | grep nginx

20.2 高流量日志

高 QPS 场景可以:

  • 使用 buffer= 缓冲访问日志;
  • 将日志输出到本地高速盘;
  • 异步采集到日志系统;
  • 对健康检查关闭 Access Log;
  • 对静态资源按需关闭日志;
  • 不在同步远程日志挂载上直接写入。

示例:

1
2
3
4
5
access_log \
/var/log/nginx/access.log \
main \
buffer=256k \
flush=1s;

二十一、系统与 NGINX 调优

21.1 文件句柄

systemd:

1
LimitNOFILE=131072

NGINX:

1
worker_rlimit_nofile 131072;

检查:

1
2
3
4
systemctl show nginx -p LimitNOFILE

NGINX_MASTER_PID="$(cat /run/nginx.pid)"
cat "/proc/${NGINX_MASTER_PID}/limits"

21.2 内核参数基线

创建 /etc/sysctl.d/99-nginx.conf

1
2
3
4
5
6
fs.file-max = 2097152

net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 16384

net.ipv4.ip_local_port_range = 10240 65535

应用:

1
sysctl --system

这些是起点,不是所有服务器的标准答案。

修改前查看:

1
2
3
4
sysctl fs.file-max
sysctl net.core.somaxconn
sysctl net.ipv4.tcp_max_syn_backlog
sysctl net.ipv4.ip_local_port_range

21.3 出口临时端口

反向代理到上游时,NGINX 作为客户端使用临时端口。

如果没有 Keepalive、请求量很高或上游连接很短,可能出现:

1
cannot assign requested address

检查:

1
2
3
ss -s
ss -tan state time-wait | wc -l
cat /proc/sys/net/ipv4/ip_local_port_range

优先顺序:

  1. 启用合理 upstream keepalive;
  2. 检查上游是否主动关闭连接;
  3. 扩大临时端口范围;
  4. 增加 NGINX 出口 IP;
  5. 优化连接生命周期;
  6. 再评估其他 TCP 参数。

不要先复制 tcp_tw_recycle。这个参数早已退出历史舞台,而且它当年也没少给 NAT 用户制造惊喜。

21.4 backlog

NGINX Listen:

1
listen 443 ssl backlog=8192;

系统:

1
2
net.core.somaxconn
net.ipv4.tcp_max_syn_backlog

三者应结合压测调整。只改其中一个,不会凭空得到无限连接能力。

21.5 multi_accept

multi_accept on 会让 Worker 一次接受尽可能多的新连接。

它不总是更快:

  • 突发流量可能被一个 Worker 大量拿走;
  • 增加单次事件循环处理时间;
  • 当前默认行为通常已经足够。

除非压测证明 Accept 是瓶颈,否则保留默认。

21.6 sendfile

1
2
sendfile on;
tcp_nopush on;

适合本地文件系统上的静态资源。

如果静态目录位于:

  • NFS;
  • FUSE;
  • 某些分布式文件系统;
  • 特殊加密挂载;

需要验证 sendfile 的兼容性和一致性。

21.7 CPU 与 Worker Affinity

一般使用:

1
worker_processes auto;

不必手工配置 worker_cpu_affinity

只有在以下场景才评估绑核:

  • 高 QPS;
  • NUMA;
  • IRQ 和网卡队列经过设计;
  • 有稳定压测;
  • 已确认调度迁移是瓶颈。

21.8 Buffer 调优

不要把网上的巨大 Buffer 配置直接搬过来。

总内存近似受到:

1
2
3
并发请求
× 每请求 Buffer
× Worker

影响。

先看:

1
2
3
4
5
响应头大小
响应体大小
是否需要落临时文件
上游延迟
客户端速度

再调整:

1
2
3
4
proxy_buffer_size
proxy_buffers
proxy_busy_buffers_size
proxy_max_temp_file_size

二十二、性能验证

22.1 基础工具

1
2
3
4
5
6
dnf install -y \
httpd-tools \
wrk \
sysstat \
iotop \
perf

如果仓库没有 wrk,可使用受控的压测主机或容器。

22.2 ab

1
2
3
4
ab \
-n 10000 \
-c 200 \
http://app.example.com/nginx-health

22.3 wrk

1
2
3
4
5
6
wrk \
-t8 \
-c500 \
-d60s \
--latency \
http://app.example.com/api/ping

不要在生产高峰期直接压测。

22.4 观察指标

1
2
3
4
5
6
7
8
watch -n 1 'curl -s http://127.0.0.1:8088/nginx-status'

pidstat -p "$(cat /run/nginx.pid)" 1
sar -n DEV 1
sar -n TCP,ETCP 1
ss -s
iostat -xz 1
vmstat 1

测试应同时观察:

  • QPS;
  • P50、P95、P99;
  • 错误率;
  • CPU;
  • SoftIRQ;
  • 网络吞吐;
  • 文件句柄;
  • 上游连接;
  • 临时端口;
  • 后端延迟;
  • 日志 I/O。

只看平均响应时间,通常会把最痛苦的那批用户平均没了。

二十三、使用 Keepalived 搭建 NGINX 高可用集群

23.1 工作方式

两台 NGINX 使用 Keepalived 运行 VRRP 选举,共享一个 VIP:

sequenceDiagram
    participant C as Client
    participant VIP as VIP 192.168.9.100
    participant N1 as NGINX 01 Priority 150
    participant N2 as NGINX 02 Priority 100

    C->>VIP: HTTPS Request
    VIP->>N1: 正常由 NGINX 01 处理

    N1--xN1: NGINX 健康检查失败
    N1-->>N2: VRRP 优先级下降
    N2->>N2: 获得 VIP
    N2-->>C: 后续请求由 NGINX 02 处理

它解决的是:

1
2
3
单台 NGINX 进程故障
单台服务器故障
入口节点网络故障

它不自动解决:

1
2
3
4
5
6
交换机故障
整个网段故障
VIP 所在二层网络故障
后端全部故障
错误配置被同步到两台节点
证书同时过期

23.2 网络前提

Keepalived VIP 通常要求:

  • 两台节点位于同一个二层网络;
  • 网卡允许配置额外 IP;
  • 网络允许 VRRP 或配置 Unicast VRRP;
  • 上游交换机能够学习 VIP 对应的新 MAC;
  • 安全组和防火墙允许 VRRP;
  • 云平台允许 Floating IP 或辅助私有 IP 漂移。

很多公有云不支持传统 VRRP VIP。公有云中优先使用:

  • 云负载均衡;
  • Floating IP API;
  • 辅助私网 IP 漂移;
  • 云路由切换;
  • DNS 健康检查。

23.3 安装 Keepalived

两台节点执行:

1
dnf install -y keepalived

检查:

1
2
3
keepalived --version
rpm -q keepalived
systemctl cat keepalived

23.4 NGINX 节点健康脚本

创建 /usr/local/sbin/check-nginx-vip.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cat > /usr/local/sbin/check-nginx-vip.sh <<'SCRIPT'
#!/usr/bin/env bash

set -Eeuo pipefail

systemctl is-active --quiet nginx

curl \
--fail \
--silent \
--max-time 1 \
http://127.0.0.1:8088/healthz \
| grep -qx 'ok'
SCRIPT

chown root:root /usr/local/sbin/check-nginx-vip.sh
chmod 0755 /usr/local/sbin/check-nginx-vip.sh

测试:

1
2
/usr/local/sbin/check-nginx-vip.sh
echo $?

不要让脚本本身做复杂修复。Keepalived 的职责是判断该节点是否还应持有 VIP,而不是把所有故障处理逻辑塞进一段 Shell。

23.5 NGINX 01 配置

确认网卡:

1
2
ip -br link
ip -br address

假设接口为:

1
ens160

创建 /etc/keepalived/keepalived.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
global_defs {
router_id NGINX_01

enable_script_security
script_user root
}

vrrp_script chk_nginx {
script "/usr/local/sbin/check-nginx-vip.sh"

interval 2
timeout 2

fall 3
rise 2

weight -60

user root
}

vrrp_instance VI_NGINX {
state BACKUP

interface ens160
virtual_router_id 51

priority 150
advert_int 1

unicast_src_ip 192.168.9.10

unicast_peer {
192.168.9.11
}

authentication {
auth_type PASS
auth_pass NgxVr51
}

virtual_ipaddress {
192.168.9.100/24 dev ens160
}

track_interface {
ens160
}

track_script {
chk_nginx
}
}

23.6 NGINX 02 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
global_defs {
router_id NGINX_02

enable_script_security
script_user root
}

vrrp_script chk_nginx {
script "/usr/local/sbin/check-nginx-vip.sh"

interval 2
timeout 2

fall 3
rise 2

weight -60

user root
}

vrrp_instance VI_NGINX {
state BACKUP

interface ens160
virtual_router_id 51

priority 100
advert_int 1

unicast_src_ip 192.168.9.11

unicast_peer {
192.168.9.10
}

authentication {
auth_type PASS
auth_pass NgxVr51
}

virtual_ipaddress {
192.168.9.100/24 dev ens160
}

track_interface {
ens160
}

track_script {
chk_nginx
}
}

两台都使用:

1
state BACKUP

由 Priority 决定初始 Master,可以减少节点重启时某些强制 Master 行为带来的意外。

auth_pass 不是现代加密认证机制,只能作为基础识别。VRRP 网络仍需要通过隔离、ACL 和防火墙保护。

23.7 权重计算

正常:

1
2
NGINX 01:150
NGINX 02:100

NGINX 01 检查失败:

1
150 - 60 = 90

此时低于 NGINX 02 的 100,VIP 漂移到 NGINX 02。

如果 Weight 只配置为:

1
-10

失败后的优先级仍然是 140,VIP 不会切换。

23.8 firewalld 放行 VRRP

NGINX 01:

1
2
3
4
5
firewall-cmd \
--permanent \
--add-rich-rule='rule family="ipv4" source address="192.168.9.11/32" protocol value="vrrp" accept'

firewall-cmd --reload

NGINX 02:

1
2
3
4
5
firewall-cmd \
--permanent \
--add-rich-rule='rule family="ipv4" source address="192.168.9.10/32" protocol value="vrrp" accept'

firewall-cmd --reload

检查:

1
firewall-cmd --list-rich-rules

如果 firewalld 版本不识别 vrrp 名称,可根据当前系统语法允许 IP Protocol 112。

23.9 启动 Keepalived

1
systemctl enable --now keepalived

查看:

1
2
systemctl status keepalived --no-pager -l
journalctl -u keepalived -n 200 --no-pager

检查 VIP:

1
ip address show dev ens160

在当前 Master 上应看到:

1
192.168.9.100/24

23.10 故障切换测试

在 NGINX 01:

1
systemctl stop nginx

观察:

1
watch -n 0.5 'ip -br address show ens160'

在 NGINX 02:

1
journalctl -u keepalived -f

客户端:

1
2
3
4
5
6
7
8
while true; do
date
curl -sk \
-o /dev/null \
-w '%{http_code} %{remote_ip} %{time_total}\n' \
https://app.example.com/nginx-health
sleep 1
done

恢复:

1
systemctl start nginx

23.11 是否自动抢回 VIP

当前配置中,NGINX 01 恢复后因为 Priority 更高,会重新成为 Master。

如果不希望恢复后立刻抢回,避免短时间内二次切换,可以使用 nopreempt 设计,但必须理解:

  • 初始状态应使用 BACKUP;
  • 高优先级节点恢复后不主动抢回;
  • 只有当前 Master 失效才切换;
  • 运维需要明确主节点长期可能不是 Priority 最高节点。

是否自动回切取决于业务对稳定性和主节点固定性的要求。

二十四、避免 Keepalived Split-Brain

如果两台节点互相收不到 VRRP 通告,可能同时认为自己是 Master,并同时持有 VIP。

典型原因:

  • 防火墙阻断 VRRP;
  • Unicast Peer 写错;
  • VRID 不一致;
  • 网卡配置错误;
  • 网络单向中断;
  • 交换机 ACL;
  • 安全组不支持 VRRP。

检查两台节点:

1
ip address show dev ens160 | grep 192.168.9.100

VIP 不应同时存在于两台节点。

抓包:

1
2
tcpdump -ni ens160 \
'ip proto 112'

Unicast 模式:

1
2
tcpdump -ni ens160 \
'host 192.168.9.10 or host 192.168.9.11'

监控系统应告警:

1
2
3
4
VIP 同时存在于两个节点
VIP 在所有节点都不存在
Keepalived 进入 FAULT
VRRP 报文长时间中断

二十五、集群配置同步

NGINX 集群最危险的状态之一:

1
2
两台机器都叫 NGINX
但配置、证书、静态资源和模块版本并不一样

推荐使用:

  • Git;
  • Ansible;
  • SaltStack;
  • Puppet;
  • RPM 包;
  • CI/CD 配置发布;
  • 制品仓库中的版本化配置包。

25.1 发布原则

  1. 先更新备用节点;
  2. 执行 nginx -t
  3. Reload;
  4. 本机健康检查;
  5. 通过 VIP 或直连节点验证;
  6. 再更新当前主节点;
  7. 保留上一版本配置和静态资源;
  8. 证书在两台节点保持一致;
  9. 记录发布 Commit 与操作人。

25.2 简单同步脚本

在配置源机器创建 /usr/local/sbin/deploy-nginx-config.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env bash

set -Eeuo pipefail

TARGET_HOST="${1:?target host is required}"
RELEASE_ID="$(date +%Y%m%d%H%M%S)"

rsync \
-aH \
--delete \
--exclude='ssl/*.key' \
/etc/nginx/ \
"${TARGET_HOST}:/tmp/nginx-${RELEASE_ID}/"

ssh "${TARGET_HOST}" \
"nginx -t -c /tmp/nginx-${RELEASE_ID}/nginx.conf"

ssh "${TARGET_HOST}" \
"sudo rsync -aH --delete /tmp/nginx-${RELEASE_ID}/ /etc/nginx/ \
&& sudo nginx -t \
&& sudo systemctl reload nginx \
&& curl -fsS http://127.0.0.1:8088/healthz"

这只是基础示例。生产环境还应处理:

  • Sudo 权限;
  • 原子目录切换;
  • 私钥分发;
  • 回滚;
  • 审批;
  • Hash 校验;
  • 并发锁;
  • 审计日志。

不要使用:

1
rsync --delete /etc/nginx/

直接同时推到两台生产节点。配置错误也会高可用——高可用地一起坏。

二十六、Active/Active NGINX 集群

Keepalived 默认是 Active/Standby。

需要两台 NGINX 同时承担请求时:

flowchart LR
    User[Client] --> LB[External L4 Load Balancer]
    LB --> N1[NGINX 01]
    LB --> N2[NGINX 02]
    N1 --> Backend[Backend Cluster]
    N2 --> Backend

可选方案:

26.1 云负载均衡

最省运维:

1
2
3
Cloud LB
-> NGINX 01
-> NGINX 02

云 LB 负责:

  • 节点主动健康检查;
  • 多可用区;
  • VIP;
  • 流量分配;
  • DDoS 基础能力;
  • 节点摘除。

26.2 LVS/IPVS

适合自建机房高吞吐四层负载均衡。

26.3 DNS 轮询

1
2
app.example.com -> 192.168.9.10
app.example.com -> 192.168.9.11

局限:

  • 客户端和递归 DNS 有缓存;
  • 故障摘除不够实时;
  • 客户端可能长期命中故障地址;
  • 不能只靠低 TTL 解决全部问题。

26.4 BGP/ECMP/Anycast

适合大规模数据中心或多地域入口,需要成熟网络团队。

二十七、Keepalived 不适合的场景

以下场景应优先使用外部负载均衡:

  • NGINX 跨不同二层网络;
  • 公有云禁止 VRRP;
  • 需要多可用区;
  • 需要 Active/Active;
  • 需要应用层主动健康检查;
  • 需要全球流量调度;
  • 需要 DDoS 防护;
  • 入口吞吐超过单节点能力;
  • VIP 漂移会受到网络安全策略限制。

二十八、TCP/UDP 代理

安装:

1
dnf install -y nginx-mod-stream

检查是否加载:

1
nginx -T 2>&1 | grep -i stream

创建 /etc/nginx/conf.d/stream.conf 是否生效取决于主配置如何 Include。更清晰的方式是在 /etc/nginx/nginx.conf 顶层、http 块之外加入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
stream {
upstream mysql_backend {
least_conn;

server 192.168.9.41:3306
max_fails=3
fail_timeout=10s;

server 192.168.9.42:3306
backup;
}

server {
listen 3307;

proxy_connect_timeout 3s;
proxy_timeout 1h;

proxy_pass mysql_backend;
}
}

SELinux 自定义端口:

1
2
3
4
5
semanage port \
-a \
-t http_port_t \
-p tcp \
3307

但代理数据库需要谨慎:

  • NGINX 不理解 SQL;
  • 无法识别主从角色;
  • 无法保证事务一致性;
  • 不会自动进行数据库故障切换;
  • TCP 连接一旦建立不会在中途无损迁移。

数据库高可用应由数据库专用代理或集群组件处理。

二十九、备份和回滚

备份:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mkdir -p /backup/nginx

tar \
--xattrs \
--acls \
-czf "/backup/nginx/nginx-$(date +%F-%H%M%S).tar.gz" \
/etc/nginx \
/srv/www/app \
/etc/systemd/system/nginx.service.d \
/etc/systemd/system/nginx-healthcheck.service \
/etc/systemd/system/nginx-healthcheck.timer \
/etc/keepalived \
/usr/local/sbin/check-nginx-vip.sh \
/usr/local/sbin/nginx-healthcheck.sh \
2>/dev/null || true

回滚配置:

1
2
3
4
5
6
7
8
9
10
11
12
tar \
-xzf /backup/nginx/nginx-时间.tar.gz \
-C /

restorecon -Rv /etc/nginx /srv/www/app /etc/keepalived

systemctl daemon-reload

nginx -t
systemctl reload nginx

curl -fsS http://127.0.0.1:8088/healthz

静态前端建议使用 Release 目录和软链接:

1
2
3
/srv/www/app/releases/20260731-01
/srv/www/app/releases/20260731-02
/srv/www/app/current -> releases/20260731-02

回滚:

1
2
3
4
5
ln -sfn \
/srv/www/app/releases/20260731-01 \
/srv/www/app/current

nginx -t && systemctl reload nginx

纯静态目录切换通常不需要 Reload,但执行健康验证仍然必要。

三十、基于 Docker 搭建 NGINX

Docker 方案分为两种:

1
2
3
4
5
6
7
8
单机实验:
Docker Compose 启动 Edge NGINX 和两个 Backend
用于验证反向代理、负载均衡和健康检查

多主机生产:
每台 Rocky Linux 10 运行一个 NGINX 容器
宿主机 Keepalived 漂移 VIP
或前置云 LB/硬件 LB

不推荐在一个 Compose 项目里强行容器化 Keepalived,并通过:

1
2
3
network_mode: host
CAP_NET_ADMIN
CAP_NET_RAW

控制宿主机 VIP。这样做可以运行,但权限高、网络行为隐晦、排障复杂。Keepalived 更适合直接运行在 Rocky Linux 宿主机。

三十一、安装 Docker Engine

卸载可能冲突的软件包:

1
2
3
4
5
6
7
8
9
10
dnf remove -y \
docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine \
podman-docker

安装仓库管理工具:

1
dnf install -y dnf-plugins-core

添加 Docker 官方 RHEL 仓库:

1
2
3
dnf config-manager \
--add-repo \
https://download.docker.com/linux/rhel/docker-ce.repo

安装:

1
2
3
4
5
6
dnf install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin

启动:

1
systemctl enable --now docker

验证:

1
2
3
docker version
docker compose version
docker info

不要开放未认证的:

1
tcp://0.0.0.0:2375

它几乎等价于将宿主机 root 权限交给网络上的访问者。

三十二、Docker Compose 实验项目

32.1 目录结构

1
2
3
4
5
6
7
8
mkdir -p /opt/nginx-docker/edge
mkdir -p /opt/nginx-docker/backend-a
mkdir -p /opt/nginx-docker/backend-b
mkdir -p /opt/nginx-docker/static
mkdir -p /opt/nginx-docker/certs
mkdir -p /opt/nginx-docker/cache

cd /opt/nginx-docker

结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/opt/nginx-docker/
├── compose.yaml
├── edge/
│ ├── nginx.conf
│ └── default.conf
├── backend-a/
│ └── index.html
├── backend-b/
│ └── index.html
├── static/
│ ├── index.html
│ └── assets/
├── certs/
└── cache/

32.2 Backend 页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cat > backend-a/index.html <<'EOF'
backend-a
EOF

cat > backend-b/index.html <<'EOF'
backend-b
EOF

cat > static/index.html <<'EOF'
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Docker NGINX</title>
</head>
<body>
<h1>Docker NGINX Static Page</h1>
</body>
</html>
EOF

32.3 Edge 主配置

创建 edge/nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
user nginx;

worker_processes auto;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
worker_connections 4096;
}

http {
log_format main
'$remote_addr [$time_local] "$request" '
'$status $body_bytes_sent '
'request_time=$request_time '
'upstream_addr=$upstream_addr '
'upstream_status=$upstream_status '
'upstream_response_time=$upstream_response_time';

access_log /var/log/nginx/access.log main;

include /etc/nginx/mime.types;
default_type application/octet-stream;

sendfile on;
tcp_nopush on;

keepalive_timeout 65;
keepalive_requests 10000;

server_tokens off;

resolver 127.0.0.11
valid=10s
ipv6=off;

include /etc/nginx/conf.d/*.conf;
}

Docker 内置 DNS 为:

1
127.0.0.11

NGINX 1.27.3 之后,开源版本的 upstream resolve 可动态更新后端域名对应的 IP。本文 Docker 使用稳定版 1.30.4,因此可以使用该能力。

Rocky Linux 10 原生 NGINX 1.26.3 不具备同样的开源动态 upstream Resolve 能力;原生部署的固定后端建议使用固定 IP、服务发现生成配置并 Reload,或升级到经过验证的新版本。

32.4 Edge 业务配置

创建 edge/default.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
upstream docker_backends {
zone docker_backends 64k;

least_conn;

server backend-a:80
resolve
max_fails=3
fail_timeout=10s;

server backend-b:80
resolve
max_fails=3
fail_timeout=10s;

keepalive 32;
keepalive_requests 1000;
keepalive_timeout 60s;
}

server {
listen 80;
server_name _;

root /usr/share/nginx/html;

location = /healthz {
access_log off;
default_type text/plain;
return 200 "ok\n";
}

location = /nginx-status {
access_log off;
stub_status;

allow 127.0.0.1;
allow 172.16.0.0/12;
deny all;
}

location /assets/ {
try_files $uri =404;

expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
}

location /api/ {
proxy_http_version 1.1;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Request-ID $request_id;
proxy_set_header Connection "";

proxy_connect_timeout 3s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;

proxy_next_upstream
error
timeout
invalid_header
http_502
http_503
http_504;

proxy_next_upstream_tries 2;

proxy_pass http://docker_backends/;
}

location / {
try_files $uri $uri/ /index.html;
}
}

32.5 compose.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
services:
backend-a:
image: nginx:1.30.4-alpine
container_name: nginx-backend-a
restart: unless-stopped

volumes:
- ./backend-a:/usr/share/nginx/html:ro,Z

networks:
- nginx-backend

healthcheck:
test:
- CMD-SHELL
- wget -qO- http://127.0.0.1/ | grep -q backend-a
interval: 10s
timeout: 3s
retries: 3

read_only: true

tmpfs:
- /var/cache/nginx
- /var/run

security_opt:
- no-new-privileges:true

mem_limit: 128m
cpus: 0.5

backend-b:
image: nginx:1.30.4-alpine
container_name: nginx-backend-b
restart: unless-stopped

volumes:
- ./backend-b:/usr/share/nginx/html:ro,Z

networks:
- nginx-backend

healthcheck:
test:
- CMD-SHELL
- wget -qO- http://127.0.0.1/ | grep -q backend-b
interval: 10s
timeout: 3s
retries: 3

read_only: true

tmpfs:
- /var/cache/nginx
- /var/run

security_opt:
- no-new-privileges:true

mem_limit: 128m
cpus: 0.5

edge:
image: nginx:1.30.4-alpine
container_name: nginx-edge
restart: unless-stopped

depends_on:
backend-a:
condition: service_healthy
backend-b:
condition: service_healthy

ports:
- "80:80"

volumes:
- ./edge/nginx.conf:/etc/nginx/nginx.conf:ro,Z
- ./edge/default.conf:/etc/nginx/conf.d/default.conf:ro,Z
- ./static:/usr/share/nginx/html:ro,Z
- ./cache:/var/cache/nginx:Z

networks:
- nginx-frontend
- nginx-backend

healthcheck:
test:
- CMD-SHELL
- nginx -t && wget -qO- http://127.0.0.1/healthz | grep -q ok
interval: 15s
timeout: 5s
retries: 3
start_period: 10s

security_opt:
- no-new-privileges:true

ulimits:
nofile:
soft: 65536
hard: 65536

mem_limit: 512m
cpus: 2.0

networks:
nginx-frontend:
name: nginx-frontend

nginx-backend:
name: nginx-backend
internal: true

32.6 启动

1
2
3
docker compose config
docker compose pull
docker compose up -d

查看:

1
2
docker compose ps
docker compose logs -f edge

验证静态资源:

1
curl http://127.0.0.1/

验证负载均衡:

1
2
3
4
for i in {1..10}; do
curl -s http://127.0.0.1/api/
echo
done

预期交替或根据连接数返回:

1
2
backend-a
backend-b

停止一个后端:

1
docker compose stop backend-a

再次请求:

1
2
3
4
for i in {1..5}; do
curl -sS http://127.0.0.1/api/
echo
done

恢复:

1
docker compose start backend-a

三十三、Docker 生产部署建议

33.1 固定镜像

不要长期使用:

1
image: nginx:latest

应固定:

1
image: nginx:1.30.4-alpine

更严格的环境固定 Digest:

1
image: nginx@sha256:<digest>

33.2 配置进入镜像还是挂载

配置稳定、随版本发布:

1
2
3
4
5
FROM nginx:1.30.4-alpine

COPY nginx.conf /etc/nginx/nginx.conf
COPY conf.d/ /etc/nginx/conf.d/
COPY static/ /usr/share/nginx/html/

优势:

  • 镜像不可变;
  • 可测试;
  • 可回滚;
  • 配置和版本绑定;
  • 不依赖宿主机文件。

动态证书、环境差异和 Secret 可以挂载。

33.3 Dockerfile

1
2
3
4
5
6
7
FROM nginx:1.30.4-alpine

COPY nginx.conf /etc/nginx/nginx.conf
COPY conf.d/ /etc/nginx/conf.d/
COPY static/ /usr/share/nginx/html/

RUN nginx -t

构建:

1
2
3
docker build \
-t registry.example.com/infra/nginx-edge:2026.07.31 \
.

推送:

1
2
docker push \
registry.example.com/infra/nginx-edge:2026.07.31

33.4 容器日志

官方镜像默认将 Access 和 Error Log 链接到 stdout/stderr,适合 Docker 日志采集。

配置 Docker 日志轮转:

1
2
3
4
5
6
7
8
9
10
11
cat > /etc/docker/daemon.json <<'EOF'
{
"log-driver": "local",
"log-opts": {
"max-size": "50m",
"max-file": "5"
}
}
EOF

systemctl restart docker

33.5 Read-only

官方镜像以只读文件系统运行时,需要给以下目录写权限:

1
2
/var/cache/nginx
/var/run

Compose:

1
2
3
4
5
read_only: true

tmpfs:
- /var/cache/nginx
- /var/run

如果使用磁盘 Proxy Cache,应将缓存目录挂载为持久卷,而不是 Tmpfs。

33.6 非 Root

可以使用 NGINX Unprivileged 镜像,或将容器监听端口改为 8080,再映射:

1
2
ports:
- "80:8080"

以任意 UID 运行时,要同步调整:

1
2
3
4
5
6
7
pid /tmp/nginx.pid;

client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;

不要一边声明 user: 10001,一边让 NGINX 写 Root 所属目录,然后用“容器有问题”总结故障。

三十四、Docker 多主机 NGINX 集群

推荐架构:

flowchart LR
    Client[Client] --> VIP[Keepalived VIP]
    VIP --> H1[Rocky Host 1]
    VIP -.Failover.-> H2[Rocky Host 2]

    H1 --> C1[NGINX Container]
    H2 --> C2[NGINX Container]

    C1 --> Backend[Backend Cluster]
    C2 --> Backend

每台宿主机:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
services:
nginx:
image: registry.example.com/infra/nginx-edge:2026.07.31
container_name: nginx-edge
restart: unless-stopped

network_mode: host

volumes:
- /etc/nginx/ssl:/etc/nginx/ssl:ro,Z

healthcheck:
test:
- CMD-SHELL
- curl -fsS http://127.0.0.1:8088/healthz >/dev/null
interval: 15s
timeout: 3s
retries: 3

ulimits:
nofile:
soft: 131072
hard: 131072

宿主机 Keepalived 脚本改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env bash

set -Eeuo pipefail

docker inspect \
--format '{{.State.Running}} {{if .State.Health}}{{.State.Health.Status}}{{end}}' \
nginx-edge \
| grep -q '^true healthy$'

curl \
--fail \
--silent \
--max-time 1 \
http://127.0.0.1:8088/healthz \
| grep -qx 'ok'

生产多主机更推荐前置云 LB 或硬件 LB,以获得主动检查和 Active/Active。

三十五、常见故障排查

35.1 nginx -t 失败

1
2
3
nginx -t
nginx -T
journalctl -u nginx -n 200 --no-pager

常见问题:

  • 指令放错 Context;
  • 缺少分号;
  • Include 路径错误;
  • 重复 Listen;
  • 证书文件不存在;
  • 动态模块未安装;
  • Plus 指令被写到 Open Source;
  • 配置使用了高版本才支持的参数。

35.2 403 Forbidden

逐层检查:

1
2
3
4
5
6
7
8
9
namei -om /srv/www/app/current/index.html

ls -ldZ /srv/www/app
ls -lZ /srv/www/app/current/index.html

sudo -u nginx \
test -r /srv/www/app/current/index.html

ausearch -m AVC -ts recent

常见原因:

  • 父目录没有执行权限;
  • 文件不可读;
  • SELinux Context 错误;
  • rootalias 配错;
  • Index 不存在;
  • Location 被更高优先级规则匹配。

35.3 404 但文件存在

检查:

1
nginx -T

重点关注:

  • rootalias
  • 末尾斜杠;
  • 正则 Location;
  • try_files
  • SPA Fallback;
  • 实际 Server Name;
  • 请求是否进入了默认 Server。

35.4 502 Bad Gateway

1
2
3
4
5
6
7
curl -v http://192.168.9.31:8080/health
nc -vz 192.168.9.31 8080

grep -n 'upstream' /var/log/nginx/error.log | tail -100

getsebool httpd_can_network_connect
ausearch -m AVC -ts recent

常见原因:

  • 后端没启动;
  • IP 或端口错误;
  • firewalld;
  • SELinux;
  • DNS;
  • 上游主动关闭;
  • 协议不匹配;
  • HTTP 代理到了 HTTPS 端口;
  • TLS SNI 错误;
  • 容器 DNS 变化。

35.5 504 Gateway Timeout

检查:

1
2
3
upstream_connect_time
upstream_header_time
upstream_response_time

不要只调大:

1
proxy_read_timeout 600s;

还要确认:

  • SQL;
  • 线程池;
  • 下游 API;
  • 锁;
  • GC;
  • 网络;
  • 应用是否真的允许长请求。

超时时间调大只是让用户等更久,不会让慢 SQL突然产生羞耻心然后自行优化。

35.6 WebSocket 立即断开

必须:

1
2
3
4
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;

还要检查:

  • 后端路径;
  • Origin;
  • TLS;
  • 空闲超时;
  • 外层 LB;
  • 应用心跳。

35.7 静态文件更新后仍是旧版本

检查:

1
curl -I https://app.example.com/assets/app.js

如果配置:

1
immutable

文件名必须带内容 Hash:

1
app.8f31a2.js

不要给固定文件名:

1
app.js

设置一年 Immutable,然后每次发布都覆盖它。

35.8 upstream 没有摘除故障节点

检查是否只有一个节点。

查看日志:

1
2
3
4
grep -E \
'upstream timed out|connect\(\) failed|no live upstreams' \
/var/log/nginx/error.log \
| tail -100

确认:

1
2
max_fails=3 fail_timeout=10s;
proxy_next_upstream error timeout http_502 http_503 http_504;

被动健康检查需要真实请求触发,不会主动定时访问 /health

35.9 systemd 不自动重启

1
2
3
systemctl cat nginx
systemctl show nginx -p Restart -p NRestarts
journalctl -u nginx

如果配置错误,Restart 不会神奇修复配置。

35.10 VIP 不漂移

1
2
3
4
5
6
7
8
9
systemctl status keepalived
journalctl -u keepalived -n 300 --no-pager

/usr/local/sbin/check-nginx-vip.sh
echo $?

ip address show ens160

tcpdump -ni ens160 'ip proto 112'

检查:

  • Priority;
  • Weight;
  • Interface;
  • Unicast Peer;
  • VRID;
  • 防火墙;
  • 健康脚本权限;
  • fallrise
  • VIP 掩码;
  • 云平台限制。

35.11 两台节点同时持有 VIP

这是 Split-Brain。

立即检查 VRRP 通信,不要先重启两台机器。重启能暂时改变选举结果,但修不好被防火墙吃掉的协议 112。

35.12 Docker 容器循环重启

1
2
3
4
5
6
7
8
9
10
11
docker inspect nginx-edge \
--format '{{json .State}}' \
| jq

docker logs --tail 300 nginx-edge

docker run --rm \
-v "$PWD/edge/nginx.conf:/etc/nginx/nginx.conf:ro" \
-v "$PWD/edge/default.conf:/etc/nginx/conf.d/default.conf:ro" \
nginx:1.30.4-alpine \
nginx -t

常见原因:

  • 配置错误;
  • 挂载文件不存在;
  • Read-only 但临时目录不可写;
  • 端口冲突;
  • SELinux;
  • 证书权限;
  • 上游域名无法解析;
  • Healthcheck 工具不存在。

35.13 Docker 后端重建后 NGINX 仍使用旧 IP

Rocky 原生 NGINX 1.26.3 解析 upstream 域名通常发生在加载配置时。

Docker 使用 1.30.4 时,可配置:

1
2
3
4
5
6
resolver 127.0.0.11 valid=10s;

upstream backend {
zone backend 64k;
server backend-a:80 resolve;
}

如果没有使用动态 Resolve,重建后执行:

1
docker compose restart edge

或:

1
docker exec nginx-edge nginx -s reload

三十六、生产上线检查清单

安装和配置

  • 使用 Rocky 仓库或官方固定版本;
  • nginx -t 通过;
  • 配置已进入 Git;
  • 有上一版本备份;
  • Worker 和文件句柄匹配;
  • 静态目录权限正确;
  • SELinux 保持 Enforcing;
  • firewalld 只开放必要端口;
  • 自定义端口已标记为 http_port_t

反向代理

  • 传递真实客户端 IP;
  • 配置 Request ID;
  • Connect、Read、Send Timeout 合理;
  • upstream keepalive 合理;
  • 上传接口单独配置;
  • SSE 和 WebSocket 单独配置;
  • HTTPS 上游启用证书验证;
  • 没有错误重试非幂等请求。

负载均衡

  • 后端服务无状态或 Session 外置;
  • Weight 与节点规格匹配;
  • 配置被动健康检查;
  • 有 Backup 或足够普通节点;
  • 单节点时理解 max_fails 的限制;
  • 后端连接上限和 NGINX Keepalive 匹配;
  • 监控每个 Upstream 的状态与延迟。

健康和监控

  • systemd 配置 Restart=on-failure
  • 本机有 /healthz
  • systemd Timer 检查已启用;
  • 外部链路探测已启用;
  • stub_status 只对可信地址开放;
  • 证书过期告警;
  • 5xx、P95、P99 和上游错误告警;
  • 自动重启会产生告警。

集群

  • 两台 NGINX 配置一致;
  • 模块和版本一致;
  • 证书一致;
  • VIP 故障切换已演练;
  • 防止 Split-Brain;
  • VRRP 仅在可信网络;
  • 已确认平台支持 VIP 漂移;
  • 理解 Keepalived 是 Active/Standby;
  • 需要 Active/Active 时有外部 LB。

Docker

  • 镜像版本或 Digest 已固定;
  • 配置进入镜像或受控挂载;
  • 容器有 Healthcheck;
  • Read-only 和临时目录匹配;
  • 日志已轮转;
  • SELinux Volume Label 正确;
  • 没有开放 Docker 2375;
  • 没有将 Keepalived 高权限容器化后却无人理解;
  • 容器重建和回滚已验证。

三十七、总结

在 Rocky Linux 10 上搭建生产级 NGINX,需要同时处理四个层次:

1
2
3
4
5
6
7
8
9
10
11
第一层:NGINX 自身
静态资源、反向代理、TLS、缓存、限流

第二层:后端服务
负载均衡、连接复用、超时、被动健康检查

第三层:操作系统
systemd、SELinux、firewalld、文件句柄、TCP 和日志

第四层:入口高可用
Keepalived VIP、外部负载均衡、配置同步和故障演练

核心原则:

  1. 每次变更先执行 nginx -t,再平滑 Reload;
  2. 静态资源使用版本化文件名和明确缓存策略;
  3. 普通 API 保持 Buffering,流式和上传接口单独处理;
  4. upstream keepalive 要与后端容量一起计算;
  5. NGINX Open Source 默认是被动健康检查;
  6. systemd 负责进程自愈,Timer 负责本机 HTTP 检测;
  7. Keepalived 负责 VIP 漂移,不负责 Active/Active 流量分担;
  8. 反向代理必须正确配置 SELinux;
  9. 不要通过无限增大 Timeout 和 Buffer 掩盖后端问题;
  10. Docker 配置应不可变、可扫描、可回滚;
  11. 本机健康不等于用户链路健康,必须有外部探测;
  12. 高可用不是“有两台机器”,而是故障真的被检测、隔离、切换并演练过。

NGINX 性能问题通常不是靠一份“百万并发配置”解决的。先找到限制发生在 CPU、连接、端口、网络、磁盘、日志还是后端,再改对应参数。否则配置文件会越来越像法术卷轴,服务器却依旧在 502 面前保持朴素和诚实。

参考资料


Rocky Linux 10 搭建企业级 NGINX:动静分离、反向代理、负载均衡、健康监测、集群与 Docker 部署
https://allendericdalexander.github.io/2026/07/31/devops/linux/rocky/rocky-linux-10-nginx-server-cluster-docker/
作者
AtLuoFu
发布于
2026年7月31日
许可协议