尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Nginx反向代理、负载均衡与高并发调优实战指南

Nginx反向代理、负载均衡与高并发调优实战指南 前阵子线上有个服务一到高峰期就卡死后面接的 Tomcat 线程全部被占满用户请求排队排到超时。当时第一反应是加机器结果加完发现瓶颈根本不在 CPU 和内存而是连接全阻塞在上游响应上加再多的机器也只是把问题往后挪。后来把 Nginx 架在最前面做反向代理静态资源直接由 Nginx 返回动态请求才转发给 Tomcat整个系统的并发能力直接翻了几倍那之后我才算真正体会到 Nginx 的价值。这篇文章把我在生产环境里用过、踩过坑、最后沉淀下来的 Nginx 核心功能讲清楚包括反向代理、负载均衡、静态资源服务、高并发调优、平滑升级、常见报错排查这些。内容会尽量贴合实际运维场景适合刚接触 Nginx 的运维、后端开发也适合准备面试时想快速梳理知识体系的朋友。1. 为什么大家都在用 Nginx它到底解决了什么问题先放下那些高大上的概念用大白话说Nginx 就是一个非常能扛并发、功能又很灵活的 Web 服务器和反向代理服务器。它最初就是为高并发而生的和传统的 Apache 那种“一个连接一个进程”的模式完全不同Nginx 用的是事件驱动模型用少量进程就能处理海量连接。这也是为什么很多高流量网站前面都会架一层 Nginx。1.1 反向代理和正向代理的区别很多新手容易把反向代理和正向代理搞混。正向代理是替客户端去访问服务器比如你在浏览器里配置代理访问被限制的网站那个代理就是正向代理服务器不知道真实的客户端是谁。反向代理则正好反过来它替服务器接收客户端的请求客户端不知道真实响应它的服务器是哪台只知道自己在访问 Nginx 的地址。放到实际架构里看你的服务端有多台 Tomcat、多台 Node.js 实例客户端想要访问这些服务不可能把每台机器的 IP 都暴露给用户这时候 Nginx 作为统一入口接收请求再按规则转发到内部的某个节点这个“统一入口 内部转发”就是反向代理最常见的用法。1.2 一个请求在 Nginx 里走什么样的流程理解了代理方向之后我们看下完整请求链路。用户发起一个 HTTP 请求先到 Nginx 的监听端口Nginx 会做一系列处理先根据域名匹配对应的 server 块再根据 URL 匹配 location 块如果命中了静态资源规则就直接返回本地文件如果命中了反向代理规则就把请求转发给 upstream 或 proxy_pass 指定的后端地址。这一步理解清楚后很多配置就好懂了。Nginx 所有配置都在解决一个问题什么样的请求走什么样的处理方式。可以是返回静态文件、转发给后端、重定向到另一个域名、返回错误码甚至做限流和鉴权。核心就是 server 和 location 这两层匹配逻辑。2. 核心功能拆解反向代理、负载均衡和静态资源服务2.1 反向代理配置的核心逻辑反向代理最常见的配置就是 proxy_pass 指令简单到几行就能搞定server { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }上面这个配置把所有访问 api.example.com 的请求都转发到本机 8080 端口。关键点在于 proxy_set_header因为后端服务很多时候需要判断客户端真实 IP如果不在 Nginx 层把原始 IP 传过去后端拿到的一律都是 Nginx 的 IP日志排障和用户定位都会很痛苦。配置 proxy_pass 时有个容易踩的坑proxy_pass 后面带不带 URI路径会导致完全不同的转发行为。比如 proxy_pass http://127.0.0.1:8080; 表示原来请求什么路径就转发什么路径而 proxy_pass http://127.0.0.1:8080/api; 则会把匹配到 location 的路径替换掉。举个例子location /user/ 匹配到 /user/abc转发时如果 proxy_pass 没带路径就还是 /user/abc如果带了 /api 就变成 /api/abc。这块我是吃过亏的建议拿到新环境先做一次带路径的请求测试再上线。server { listen 80; server_name example.com; location /api/ { proxy_pass http://backend_server:8080; 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_connect_timeout 5s; proxy_read_timeout 10s; } }2.2 负载均衡的几种策略如果后端有多台机器Nginx 可以通过 upstream 定义一组后端节点然后在 location 里用 proxy_pass http://upstream_name 引用这组节点请求会被分摊到不同机器上。upstream backend_cluster { server 192.168.1.10:8080 weight3; server 192.168.1.11:8080 weight1; server 192.168.1.12:8080 backup; } server { listen 80; server_name app.example.com; location / { proxy_pass http://backend_cluster; proxy_set_header Host $host; } }这里用了 weight 参数weight3 的机器会被分配更多请求适合后端机器配置不一致的场景。backup 表示备用节点只有前面两台都不可用时才接管请求。除了默认的轮询策略还有最少连接、IP 哈希等策略策略指令适用场景轮询默认后端机器配置基本一致权重轮询weight机器性能不同按比例分流量最少连接least_conn长连接、耗时差异大的请求IP 哈希ip_hash需要把同一用户固定到同一后端同一个用户固定到同一台后端这种需求常见于会话没有做统一缓存的老系统。如果用了 IP 哈希就不要再叠 weight 做动态调整了否则哈希结果会被打乱。2.3 静态资源服务与 autoindex 在线文件浏览Nginx 处理静态文件的能力比后端应用服务器强很多因为它在读取本地文件时直接用 sendfile 系统调用数据从磁盘到网卡基本是零拷贝CPU 占用非常低。所以像前端打包后的 dist 目录、图片资源、文件下载目录都建议直接交给 Nginx。server { listen 80; server_name static.example.com; root /data/wwwroot; index index.html; location / { try_files $uri $uri/ /index.html; } location /download/ { alias /data/files/; autoindex on; autoindex_exact_size off; autoindex_localtime on; charset utf-8; } }这里重点说下 autoindex它可以把某个目录列表直接展示出来做成一个简单的在线文件浏览页面。autoindex_exact_size off 表示文件大小显示为可读格式KB、MB 这样autoindex_localtime on 表示用本地时间显示文件修改时间。我经常用这个功能在公司内网搭一个共享文件的下载站点比搭 FTP 省事多了。需要注意中文文件名在目录列表中可能出现乱码所以一定要加上 charset utf-8。还有一个高频场景是前端路由的 history 模式。如果直接用 Nginx 托管 React/Vue 打包后的页面刷新一个 /user/123 之类的路由时 Nginx 会去找对应的物理文件结果返回 404。所以中间加了 try_files $uri $uri/ /index.html意思是先找真实文件找不到就回退到 index.html让前端路由自己去解析。3. 高并发能力与性能调优经验3.1 Nginx 为什么能扛高并发Nginx 的高并发本质上是靠事件驱动 异步非阻塞 IO它不像传统服务那样每来一个连接就起一个线程。Nginx 的 worker 进程数量一般和 CPU 核心数一致每个 worker 用 epoll 这样的机制同时盯着成千上万个连接有事件才处理没有事件就挂起等待所以少量的 worker 就能吃掉极大的并发连接数。这种方式和我们平时处理事情的逻辑很像一个人同时接很多电话不打电话的时候就干别的而不是每来一个电话就雇一个新人。这样既省资源又不会因为线程切换导致性能损耗。3.2 核心参数调优先看几个直接影响并发能力的参数worker_processes auto; worker_connections 65535; keepalive_timeout 65; keepalive_requests 1000; gzip on; gzip_min_length 1k; gzip_types text/plain application/json application/javascript text/css;worker_processes auto 会根据 CPU 核心数自动设置进程数。worker_connections 表示每个 worker 进程能同时打开的最大连接数默认值通常比较保守生产环境一般会调高到 10240 或者更高同时要同步调整系统层面的 ulimit否则连接数限制会卡在操作系统这里。一个 Nginx 服务能承载的最大并发连接数可以简单估算worker_processes × worker_connections。比如 4 核机器、worker_connections 设为 10240理论最大并发约 40960但这只是理论值实际还要看带宽、请求大小、后端响应时间。响应越慢连接被占用越久真实吞吐量越低所以 Nginx 层要配合超时时间和 gzip 压缩一起优化。gzip 压缩对带宽的节省非常明显。我记得之前有个纯 JSON 接口没开 gzip 时一个响应大概 80KB开完之后直接降到 8KB整体访问耗时下降了一大截。但 gzip 对 CPU 有消耗所以一般只压缩文本类类型图片和视频这种已经压缩过的就不用再压了。3.3 上游长连接和 keepalive 配置高并发场景下还容易忽略一个问题Nginx 和后端之间的连接复用。如果每个请求都重新建立 TCP 连接握手开销会消耗大量时间和资源。用 upstream keepalive 可以维护 Nginx 和后端之间的长连接池。upstream backend_cluster { server 192.168.1.10:8080; keepalive 32; } server { listen 80; location / { proxy_pass http://backend_cluster; proxy_http_version 1.1; proxy_set_header Connection ; } }两个关键点不能漏proxy_http_version 必须设为 1.1因为 HTTP/1.0 不支持长连接proxy_set_header Connection 用来清掉请求头里的 Connection 字段让 Nginx 能复用和后端之间的连接。不这么配的话 keepalive 参数写了也是白写后端还是每次请求都建连。3.4 关于 QUIC 和 HTTP/3很多人在关注 Nginx 对 QUIC 的支持毕竟 HTTP/3 在弱网环境下的表现确实好一些。新版本 Nginx 已经支持 HTTP/3配置也不算太复杂可以单独开一个 443 端口监听 HTTP/3 流量。但说实话目前国内普通业务场景下 HTTP/3 的普及度还没有那么高中间网络设备、客户端兼容性都需要考虑建议先在测试环境做压测验证不要直接在生产环境大规模切过去。做压测时可以用 nghttp3 这类工具模拟 HTTP/3 请求看下 UDP 443 端口的转发是否正常同时关注 UDP 丢包和防火墙限制这两个典型的坑。4. 从安装到上线一份完整的实操记录4.1 Linux 环境下安装 Nginx 的几种方式Yum/Apt 安装如果用 CentOS、Rocky Linux 或 AlmaLinux最省事的方式是直接通过包管理器安装。以 AlmaLinux 9 为例sudo dnf install -y nginx sudo systemctl enable --now nginx但有两点要注意官网的 Nginx 版本往往比发行版自带的版本更新新版会包含更多安全修复和功能比如 HTTP/3另一个是默认仓库的 Nginx 有些模块没有编译进去如果你需要特定第三方模块推荐用编译安装。如果你是生产环境用户建议先配官网提供的 nginx.repo再装官方版本这样后续可以用 yum/dnf 持续升级。编译安装编译安装的优势是可控性强可以选择自己需要的模块。步骤大致是wget http://nginx.org/download/nginx-1.26.2.tar.gz tar -zxvf nginx-1.26.2.tar.gz cd nginx-1.26.2 ./configure --prefix/usr/local/nginx \ --with-http_ssl_module \ --with-http_v2_module \ --with-http_stub_status_module \ --with-http_gzip_static_module make -j$(nproc) make install编译前需要确保系统装了 gcc、pcre-devel、zlib-devel、openssl-devel 这些依赖。编译报错九成是缺依赖缺哪个装哪个就行。编译安装完没有 systemd 管理脚本需要自己写一个或者直接用 nginx 自带的启动脚本。我的习惯是用编译安装的方式部署生产环境因为可控性最高后续加模块也知道自己的 Nginx 是哪些组件拼起来的。国内镜像下载如果服务器在国外源拉取很慢或者网络环境受限可以用国内镜像站下载 Nginx 源码包。有多个镜像源都提供同步直接替换 URL 里的域名即可注意选择和自己系统匹配的版本。AArch64 纯内网离线安装有的内网环境是 ARM 架构服务器也就是 aarch64而且完全隔离不能连外网只能把 RPM 包或源码包传到内网装。离线安装的核心思路是在一台能联网的同架构机器上下载好所有 RPM然后拷贝到内网用 rpm -Uvh 或 dnf localinstall 安装。如果是源码方式需要把 gcc、make、openssl-devel、pcre-devel、zlib-devel 一并拷贝进去离线编译时同样会用到这些依赖。我记得有个经验离线安装最费时间的不是在目标机器上安装而是找齐所有依赖的 RPM建议用 yumdownloader --resolve 这种方式一次性拉完依赖省得来回折腾。装完之后顺便要把 worker 进程数和系统 ulimit 一起调了否则内网高并发压测一样会出问题。4.2 Windows 安装 Nginx 和踩坑记录Windows 下安装 Nginx 相对简单去官网下载 Windows 版压缩包解压到没有中文和空格的目录双击 nginx.exe 或者命令行运行即可。有一个非常典型的问题如果解压路径带中文比如 D:\测试\nignx运行时会报各种路径相关的错误如果目录名有空格某些配置中的绝对路径也可能解析异常。建议直接用 D:\nginx老老实实别折腾。Windows 下还有个容易踩的坑启动时报错[emerg] CreateFile() D:/phpstudy_pro/www/admin2.com/.nginx.htaccess failed这个本质上是 Nginx 启动了但找不到对应的文件或权限不足。常见原因包括配置文件里定义的路径不存在或者该路径在 Windows 下没有访问权限。排查思路很简单看 error.log根据路径去检查目录是否存在然后确认 Nginx 是否有该目录的读写权限。很多人在 Windows 上做本地开发会用 phpStudy里面集成的 Nginx 经常出现这个问题解决方法通常就是手动创建缺失的路径或修改配置里的 root 地址。4.3 Nginx Proxy Manager如果不想手写 Nginx 配置可以考虑 Nginx Proxy Manager简称 NPM。它是一个带 Web UI 的 Nginx 管理工具通过 Docker 一键部署用界面配置反向代理、SSL 证书、访问控制生成后会自动维护一套 Nginx 配置。适合小团队或个人项目用可以极大降低配置成本。它的部署方式不复杂需要 Docker 环境或者用项目自带的 Docker Compose 文件把管理端口映射出来之后就能访问 Web 界面。不过 NPM 的配置能力相比手写 Nginx 还是有限的复杂的自定义配置落地难度比较大需要用它的高级自定义配置功能来补充。4.4 一份能直接用的反向代理和域名配置主域名和二级域名共存生产场景中一台 Nginx 同时托管主域名和多个二级域名非常常见。比如 example.com 是公司官网api.example.com 是对接接口files.example.com 放静态资源。核心就是为每个域名配置独立的 server 块server { listen 80; server_name example.com www.example.com; root /data/wwwroot/example; index index.html; }server { listen 80; server_name api.example.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里的关键是理解 server_name 匹配规则Nginx 收到一个请求后会用请求的 Host 头去匹配配置文件里的 server_name匹配到哪个就交给哪个 server 块处理。如果一个请求的域名没有任何 server 块匹配会落到 listen 80 的默认 server 块可能是第一个加载的配置也可能是 default_server 指定的配置生产环境最好显式声明一个默认返回 444 的 server避免非法域名打进来还打到业务上。server { listen 80 default_server; server_name _; return 444; }Nginx 支持 JSP 吗经常有新手问“Nginx 支持 JSP 吗”答案是 Nginx 本身不支持直接执行 JSP。Nginx 是 Web 服务器和反向代理它只能处理静态文件和转发请求像 JSP 这种需要容器解析的动态语言必须交给 Tomcat、Jetty 这样的 Servlet 容器去处理。Nginx 在里面扮演的角色是统一入口请求进入 Nginx如果你的 URL 是 /app/* 这种动态路径就用 proxy_pass 转发给后端的 Tomcat如果是 /static/* 这种资源路径Nginx 自己直接返回。很多经典架构就是前端 Nginx 后端 TomcatNginx 负责静态资源和负载均衡Tomcat 负责跑 JSP。4.5 配置修改后的加载与重启改配置文件最稳妥的验证流程是这样的nginx -t nginx -s reloadnginx -t 会检查所有配置文件语法只有没问题了再用 nginx -s reload 平滑重载配置。reload 不会中断正在处理的请求Nginx 会重新读取配置、创建新的 worker 进程然后优雅地停掉旧 worker所以生产环境改配置后用 reload 而不是 restart。只有在修改了监听端口、需要加载新模块这种级别的大改动时才需要重新 start 或 restart。5. 平滑升级在不中断服务的情况下换版本5.1 为什么要平滑升级生产环境运行的 Nginx 版本如果出现安全漏洞比如之前 F5 Nginx 的 CVE-2022-41742 缓冲区错误漏洞影响范围很大攻击者可能利用它导致进程异常。这时升级就是必须的但直接停服务再换二进制线上业务会中断。好在 Nginx 提供了平滑升级能力整个过程几乎不影响在线请求。5.2 平滑升级的具体步骤我的操作流程大致是这样的先准备新版本源码并编译到新的安装路径或者直接准备好新版本二进制文件。查看当前 Nginx 的编译配置参数用nginx -V输出。新的版本编译参数要和原来保持一致否则升级后某些模块行为会不一致。确认当前运行的 master 进程 PID通常可从 /usr/local/nginx/logs/nginx.pid 读取。向旧 master 进程发送 USR2 信号它会启动新的 master 和新的 worker 进程新旧版本会短暂并行。向旧 master 进程发送 WINCH 信号它会优雅地关闭旧 worker 进程保留旧 master方便回滚。验证新版本运行正常后向旧 master 发送 QUIT 信号彻底退出旧 master。这套流程本质是利用 Nginx 的信号机制做二进制替换整个过程只需要几个 kill 命令。如果升级后发现新版本有问题只要旧 master 还没退出就可以再向旧 master 发送 HUP 信号重新拉起旧 worker实现回滚。这是 Nginx 非常经典的一个运维操作值得花时间演练两遍。5.3 配置热加载的原理顺带说一下为什么 nginx -s reload 不会中断连接reload 的过程是重新解析配置文件并启动新的 worker 进程然后把新请求交给新 worker旧的 worker 在处理完它手里正在服务的请求之后才退出。所以正在进行的下载、长轮询请求不会因为配置重载而被强制断开。这也是 Nginx 设计上一个很优秀的点。6. 高频问题的排查经验记录6.1 访问返回 403最常踩的三个坑Nginx 返回 403 是出现频率非常高的一个问题我在不同环境里遇到过三种原因第一是索引文件缺失。如果你访问的目录下没有 index.html 或 index.htm而配置里没有开启 autoindexNginx 会禁止列出目录内容直接返回 403。这种情况下要么把 autoindex 打开要么确保目录里有默认首页文件。第二是权限不足。Nginx 的 worker 进程通常以 nginx 用户运行它需要对该用户有路径的执行权限。比如 root 目录下的网站目录权限是 700Nginx 用户进不去就一定会 403。解决方法是确认目录链路每一级目录都要有执行权限用ls -ld /data /data/wwwroot /data/wwwroot/site逐级检查。第三是 SELinux 拦截。这个在 CentOS、Rocky Linux、AlmaLinux 上尤其常见配置和权限都看着没问题但 Nginx 就是无法访问目录。排查办法很简单先执行getenforce如果是 Enforcing可以看下 AVC 日志确认是否被拦截或者临时用setenforce 0验证验证完再恢复。很多时候 403 不是配置写错而是文件系统权限和 SELinux 策略在背后搞事把这些链路排查一遍基本就能定位。6.2 upstream keepalive 相关的坑配了 upstream keepalive 之后反而不稳定了报错里看到upstream prematurely closed connection这类信息大多数情况是后端服务不支持 HTTP/1.1 长连接或者后端在连接空闲后主动断开但 Nginx 不知道还在复用这条连接。解决思路先确认后端是否支持 HTTP/1.1 和 keepalive如果后端是短连接模型就不要在 Nginx 层硬开 keepalive同时配合调短后端空闲超时时间让两边保持在相近的节奏上。6.3 静态资源中文文件名乱码Nginx 默认按 UTF-8 输出页面但有的系统文件名字符集不是 UTF-8就会乱码。前面提到的 autoindex 场景需要加 charset utf-8另外建议文件系统层面统一用 UTF-8服务器和客户端编码一致才能避免各种显示问题。6.4 压测时高并发连接数上不去明明 Nginx 参数都调高了压测还是发现大量请求失败先用ab -n 10000 -c 1000这样的工具压一遍再看系统层面的连接限制。核心检查三件事ulimit -n是否够大/etc/security/limits.conf是否限制了用户的最大打开文件数Nginx 的 worker_connections 和 worker_rlimit_nofile 参数是否合理Linux 下每个 TCP 连接都对应一个文件描述符如果文件描述符上限不够连接根本建立不起来这时候加再多的 worker 也没用。生产服务器上我一般把 nofile 设为 65535 或更高同时把 worker_rlimit_nofile 同步调上去。6.5 配置了反向代理但总拿到空白页空白页一般是后端返回了 502 或 504但 Nginx 没有把错误页面显示出来或者 Nginx 和后端之间通信失败。建议看 error.log确认是连不上后端、超时还是后端本身崩溃。排查时可以在服务器上用 curl 直接访问后端的地址绕开 Nginx 测试后端是否正常再回到 Nginx 层面检查 proxy_pass 配置、防火墙、DNS 解析这几个环节。7. 最后再分享两个我实际使用中的小技巧第一个技巧是给每个 server 块记录独立的访问日志。项目多了以后所有日志混在一起很难排查给每个域名一个独立的 access_log 和 error_log日志归档和分析都会舒服很多比如 access_log /data/logs/nginx/api.example.com.access.log。第二个技巧是优先用 nginx -t 检查配置再操作。我在生产环境几乎不碰 restart都是改完配置先nginx -t通过之后nginx -s reload整个过程对线上服务零影响。学会这个习惯之后会减少非常多没必要的故障。Nginx 看起来配置量大但核心其实就那几个模型server 匹配域名、location 匹配路径、upstream 定义后端集群、proxy_pass 做转发。把这几个理解透了剩下的模块都是在这些骨架上补充能力。我自己的学习路径是先把反向代理和静态资源吃透再进阶负载均衡和高并发调优最后再去接触平滑升级、HTTP/3、Nginx Proxy Manager 这些上层工具一步一个脚印比拿到文档硬啃效率高很多。
返回列表