ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Nginx核心功能详解:反向代理、负载均衡与性能调优实践

Nginx核心功能详解:反向代理、负载均衡与性能调优实践 做了这么多年后端和运维我越来越觉得Nginx就是一套行走的架构课。不管是刚入门的新人还是带过线上集群的老手最终都会绕回同一件事把 Nginx 的核心功能吃透。它不只是“一个 Web 服务器”更是静态资源托管、反向代理、负载均衡、网关限流、HTTPS 接入的枢纽。这篇文章不打算照着官方文档念一遍我会从实际场景出发把 Nginx 真正值钱的核心功能拆开讲清楚包括每一段配置背后的原理以及我踩过的坑。如果你是刚准备用 Nginx 的读者或者已经在生产环境维护它但有些概念还模糊这篇都可以当作一份能直接落地的参考。1. 先搞清楚 Nginx 到底解决了什么问题1.1 高并发能力来自哪里事件驱动模型Nginx 之所以能扛住高并发根源在于它的进程模型和事件处理机制。传统服务器比如 Apache 的 prefork 模式每个连接通常对应一个进程或线程并发一高内存和上下文切换开销立刻爆炸。Nginx 采用 master 进程加多个 worker 进程的结构每个 worker 内部用事件驱动方式同时处理成千上万个连接谁有数据来了就处理谁没事件就闲着并不需要为每个连接单独开线程。你可以把它想象成一个餐厅服务员Apache 像一个客人配一个专人服务员客人多了就得雇一堆人Nginx 则是一个服务员同时照看许多桌哪桌举手就过去没举手的不用管。这种模型下单个 worker 进程能承担的并发连接数远高于线程模型机器配置不变吞吐量却上了一个量级。实际部署时worker 进程数一般设为 CPU 核心数或稍多一些。我习惯用auto让 Nginx 自己探测然后通过worker_cpu_affinity把进程绑定到物理核上能减少进程切换带来的损耗。这些细节看着不起眼但在 4 核以下的小机器上差异很明显。在压测场景里同配置的机器用 Nginx 做静态文件服务QPS 经常是其他方案的好几倍靠的正是这个底子。1.2 与 Apache、Tomcat 的分工差异很多刚接触的人会问Nginx 能代替 Tomcat 吗答案是不能它们压根不在一个层面。Nginx 擅长处理静态请求、转发、负载均衡而 Tomcat 是 Java Servlet 容器本质上是把动态业务跑起来的应用服务器。常见的组合是 Nginx 在最前面接流量静态资源自己直接吐给客户端动态请求用proxy_pass转发给后端的 Tomcat再由 Tomcat 返回结果。Nginx 与 Apache 相比内存占用更低、并发能力强、配置更灵活而且 Nginx 的异步非阻塞模型在长连接、反向代理场景优势更明显。Apache 的.htaccess目录级配置虽然方便但每次请求都要检查目录规则性能代价不小Nginx 不支持.htaccess所有配置集中管理反而更利于规范团队配置和维护。现在很多新项目直接跳过 Apache 选 Nginx我并不意外。1.3 一张图看清 Nginx 的六个核心功能在深入配置之前先把 Nginx 的能力边界画清楚。它不只是处理 HTTP 请求还可以静态资源服务直接返回 HTML、CSS、JS、图片等文件支持autoindex目录列表。反向代理接收客户端请求转发给后端应用服务器并把响应返回给客户端。负载均衡在多个上游服务器之间按策略分发请求提高整体吞吐和可用性。HTTP 层高级处理URL 重写、重定向、跨域、缓存、压缩、限流。SSL/TLS 终结统一处理 HTTPS 证书解密后的明文请求再转发给后端后端不用各自处理证书。四层负载均衡Stream不只看 HTTP 协议还能直接代理 TCP/UDP 流量。后面几个部分我会围绕这些能力展开每个场景都会给出可直接套用的配置和取舍理由。2. 静态资源服务与目录浏览初始配置里的大学问2.1 root 和 alias 的经典区别配置静态站点时root和alias是最容易用错的一组指令。root会完整拼接 URL 路径和 root 指定的目录而alias则是把 URL 中的匹配部分直接替换成指定路径。看两个例子server { listen 80; server_name example.com; # 访问 /static/logo.png 时实际查找文件/data/public/static/logo.png location /static/ { root /data/public; } # 访问 /img/logo.png 时实际查找文件/data/icons/logo.png location /img/ { alias /data/icons/; } }我遇到过一个项目开发人员把alias写成了root导致图片路径里多了一层目录页面上的资源全部 404。排查方法也很简单打开 Nginx 的 error.log看到open() /xxx/xxx/static/static/logo.png failed这种路径重复拼接的报错基本就是root和alias用混了。结论很简单location是前缀匹配、且希望文件目录和 URL 路径不一致时用alias大多数整站静态托管场景直接用root语义更好理解。2.2 autoindex 在文件分享场景的正确姿势Nginx 自带目录列表功能autoindex on打开后访问目录 URL 时会生成一个简单的文件索引页面。很多团队拿它当临时文件分享工具用比起搭建专门的网盘系统轻量且零成本。我的常用配置是这样location /files/ { alias /data/share_files/; autoindex on; autoindex_exact_size off; # 文件大小显示为可读格式K、M、G autoindex_localtime on; # 显示服务器本地时间方便跨时区同事判断 charset utf-8; # 中文文件名不乱码 add_header Cache-Control no-cache; }有几个坑需要提醒第一autoindex只对以/结尾的目录请求生效直接访问目录不带斜杠会 301 跳转由 Nginx 自动完成但如果你自定义了return规则可能把这个隐式跳转覆盖掉第二中文文件名必须配charset utf-8否则浏览器里看到的是一堆乱码第三文件分享目录不要放在 Web 根目录之外还需要特殊权限的地方尽量统一权限管理避免权限错乱引起的 403。不过autoindex的页面样式比较朴素功能也有限没有搜索和上传能力。如果是团队正式使用的共享盘还是建议用专门的工具处理。但“快速临时共享一个包”这种需求Nginx 的 autoindex 完全够用。2.3 动静分离到底怎么设计动态请求和静态资源的处理路径不同性能表现差异很大。动静分离的核心思路是静态资源直接由 Nginx 读取磁盘或内存缓存返回动态请求才转发给应用服务器。这样既能缓解后端压力又能利用 Nginx 的高并发能力加速静态资源访问。我的典型配置长这样server { listen 80; server_name www.example.com; # 静态资源直接返回并带上传过期时间 location ~* \.(html|css|js|png|jpg|jpeg|gif|ico|svg|webp|woff2?)$ { root /data/www; expires 30d; access_log off; add_header Cache-Control public, max-age2592000; } # 动态请求反代给后端 location / { proxy_pass http://backend_server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有一个值得说的细节静态资源的expires缓存时间不是越长越好如果项目发布频繁而文件名不带 hash浏览器强缓存了旧资源用户就会看到旧页面。常见的做法是文件名带上版本号或内容 hash例如app.20250101.js这种情况下可以把缓存时间设得长一些。如果文件名固定不变缓存建议控制在 5 到 10 分钟避免上线后需要用户手动强刷。3. 反向代理与负载均衡日常使用率最高的功能3.1 proxy_pass 的路径拼接规则反向代理是 Nginx 日常存在感最强的功能但也是配置歧义最多的地方。proxy_pass后面的 URI 写不写结果完全不同。规则只有一条proxy_pass不带 URI路径Nginx 会将完整请求 URI 原样传给后端带 URI匹配location的部分会被替换。举两个例子# 场景一不带 URI访问 /api/user 转发到后端同样是 /api/user location /api/ { proxy_pass http://192.168.1.10:8080; } # 场景二带 URI访问 /api/user 会转发到 http://192.168.1.10:8080/user location /api/ { proxy_pass http://192.168.1.10:8080/; }场景二里proxy_pass的结尾有一个/整条路径被替换成后端根路径。因为后端服务通常没有/api这个前缀需要把前缀剥掉所以很多人会这么用。但如果你理解错了这个规则把该带/的地方漏掉后端接口全部 404不该带的时候加了/路由又对不上。排查这类问题最快的办法是看后端的 access log确认实际收到的 URI 是什么。还需要注意的是location的匹配方式会影响替换行为。location /api/ {}是前缀匹配proxy_pass带 URI 时匹配到的那一截会被替换。如果用了正则形式的location ~ ^/api/(.*)$想保留部分路径就需要用$1手工拼接麻烦得多。能用前缀匹配解决的问题不要轻易上正则。3.2 负载均衡策略与权重分配Nginx 的 upstream 模块提供了多种负载均衡算法默认是轮询。下面是我常写的一个后端集群配置upstream backend_servers { server 192.168.1.11:8080 weight3; server 192.168.1.12:8080 weight2; server 192.168.1.13:8080 backup; keepalive 32; } server { listen 80; server_name api.example.com; location / { proxy_pass http://backend_servers; proxy_http_version 1.1; proxy_set_header Connection ; } }这个配置里第一台和第二台按 3:2 的比例接收请求第三台作为备份机平时不接流量前两台挂了才顶上。backup的语义在做容灾时很有用它不会参与正常负载但能兜底。关于不同策略的使用场景我整理了一张表策略指令适用场景注意事项轮询默认后端无状态、配置相近请求平均分配不考虑机器性能差异加权轮询weight机器配置不同权重按处理能力设置别凭感觉IP 哈希ip_hash;需要会话保持的旧系统同一 IP 固定到同一后端但可能分布不均最少连接least_conn;请求耗时不稳定的场景把新请求交给当前连接数最少的后端一致性哈希hash $request_uri;缓存类、粘性路由通过consistent参数减少节点变化影响如果后端有登录态且依赖 Session 而不用分布式会话ip_hash或hash能避免用户频繁掉线。但线上系统尽量还是把会话集中到 Redis让负载均衡算法保持简单后续扩缩容才灵活。3.3 开启 keepalive 的收益与必须注意的坑upstream里那段keepalive 32很多人不理解容易漏配。它表示 Nginx 和后端之间保持 32 个空闲的长连接反复复用而不是每次请求都重新建立 TCP 连接。HTTP 短连接每次都要经历 TCP 三次握手高并发下时间开销和资源开销都不小。但要真正生效还需要两个配套条件proxy_http_version 1.1和proxy_set_header Connection 。因为 HTTP/1.0 默认不支持 keepalive而 Nginx 向后端发请求默认用的是 HTTP/1.0。如果不设置这两行keepalive配置就是空中楼阁连接根本复用了。代价是每个 worker 进程会占住一定数量的空闲连接keepalive数值不宜过大。后端是 Tomcat 这类 Java 应用时连接数本身受容器线程池限制keepalive设得太高会让后端连接堆积建议控制在 16 到 64 之间。这些参数我在压力测试环境下对比过业务高峰期的 P99 延迟可以下降 20% 左右值得重视。4. 性能调优让 Nginx 在最省资源的情况下跑得更快4.1 worker 进程、连接数和文件描述符的关系很多人以为worker_processes越大越好其实不是。每个 worker 都是独立进程进程多了上下文切换和内存占用都会上升。常规建议是设为 CPU 核心数例如worker_processes autoNginx 会自动探测。如果服务器还承担其他业务可以适当减少不要让 Nginx 独占所有核。worker_connections决定单个 worker 能同时打开的最大连接数这个值受系统最大文件描述符限制。Linux 默认单进程能打开的文件数通常是 1024如果不调Nginx 的并发能力会被死死卡住。需要同时调整几个地方Nginx 配置里设置worker_rlimit_nofile 65535;修改/etc/security/limits.conf中的nofile使用 systemd 管理时还要检查服务的LimitNOFILE理论上最大并发连接数约等于worker_processes * worker_connections但实际受内存、带宽、后端处理能力制约。这些参数没有万能配置需要结合压测调整。4.2 gzip、缓存和 HTTP/2 能带来的真实提升Nginx 开启 gzip 压缩对文本类资源效果非常显著JS、CSS、HTML 这类文件体积能压缩到原来的三分之一。我常用的压缩配置gzip on; gzip_comp_level 5; # 1-9越高越耗 CPU gzip_min_length 1k; # 小于 1KB 的文件不压缩压了反而更大 gzip_types text/plain text/css application/javascript application/json application/xml image/svgxml; gzip_vary on;有几个细节不能漏第一图片和视频不要开 gzip它们本身已经压缩过再压浪费时间第二gzip_comp_level不是越高越好超过 6 以后 CPU 开销上升明显而体积下降有限第三如果前面还有一层 CDN需要在 CDN 上同步开启 gzip 相关配置否则可能返回双重编码的乱码。HTTP/2 开启很简单一行listen 443 ssl http2;它能通过多路复用避免浏览器对同域名的并发连接限制静态资源加载速度会有明显改善。开启后可以直接用 Chrome 的开发者工具查看加载瀑布图原本排队等待的连接会变成并行传输。4.3 高并发下的基本攻击防护与限流Nginx 本身可以作为第一层安全屏障。限制请求速率是保护后端应用的重要手段# 定义限流区域每个 IP 每秒 5 个请求突发允许 10 个 limit_req_zone $binary_remote_addr zoneapi_limit:10m rate5r/s; server { location /api/ { limit_req zoneapi_limit burst10 nodelay; proxy_pass http://backend_servers; } }burst10允许短时突发超过速率限制nodelay表示突发请求不排队直接放行但如果连突发量也超过Nginx 会直接返回 503。个人经验是限流值不能拍脑袋定需要结合压测和后端实际处理能力。限得太紧正常用户的请求被误伤限得太松又起不到保护作用。除了限流还可以在 http 块层面限制请求体大小、设置超时时间降低恶意请求对后端的压力。5. HTTPS 接入与安全加固实践5.1 证书配置与 HTTP 强制跳转HTTPS 是现在线上服务的底线没有证书的网站基本见不了人。Nginx 接入 SSL 的配置很成熟server { listen 443 ssl; server_name www.example.com; ssl_certificate /etc/nginx/ssl/example.com.pem; ssl_certificate_key /etc/nginx/ssl/example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; } # HTTP 全部跳转到 HTTPS server { listen 80; server_name www.example.com; return 301 https://$host$request_uri; }这里有一个常见坑某些安全扫描工具会报 SSL 证书链不完整问题往往出在只配置了域名证书而没配置中间证书。正确的做法是把中间证书和域名证书合并在一个.pem文件里顺序是域名证书在前、中间证书在后。另外HTTP 跳转用301还是308有些讲究。301会把 POST 请求改成 GET可能造成接口调用异常如果业务中存在表单提交且需要保留请求方法应使用308 Permanent Redirect。我一般看业务性质纯前端展示站用301没问题涉及接口回调用308更稳。5.2 隐藏版本号与基础安全响应头Nginx 默认会在错误页面和响应头里暴露版本号例如Server: nginx/1.24.0。攻击者拿到精确版本后可以直接匹配已知漏洞进行定向攻击。隐藏版本号很简单server_tokens off;除了隐藏版本号我建议在 server 块统一加上几个安全响应头add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options SAMEORIGIN always; add_header Referrer-Policy strict-origin-when-cross-origin always;X-Frame-Options可以防止页面被恶意网站用 iframe 嵌套降低点击劫持风险。add_header指令有个继承坑需要注意如果在某个location里重新定义了add_header它会覆盖整个 server 块里定义的所有add_header而不会合并。所以要么把安全头集中在每个需要的 location 里要么确认 location 中没有单独设置add_header。5.3 常见漏洞的防护思路最近几年 Nginx 相关的漏洞通报不少比如某些版本存在请求走私风险、越界读取漏洞等修复方式通常聚焦在两点升级版本和调整配置。如果你用的是发行版自带源里的 Nginx建议关注官方仓库的安全公告如果是编译安装的版本更要留意版本号是否过旧。还有一个经常被忽视的点Nginx 后面的业务接口如果没有做权限校验任何人都可以直接访问内网接口。代理层能做的是用allow和deny控制来源 IPlocation /admin/ { allow 10.0.0.0/8; allow 192.168.1.0/24; deny all; proxy_pass http://backend_servers; }这种基于来源 IP 的访问控制不能替代业务鉴权但它能把管理后台的暴露面缩小到可信网段安全性高一个层次。6. Rewrite 重写、多站点与四层代理6.1 Rewrite 的常见使用场景URL 重写是迁移网站、整理链接结构时的高频需求。rewrite指令放在server块和location块里的行为不同放在server块会在进入location匹配前执行放在location块则在该块内部匹配后执行。例如旧接口路径迁移server { listen 80; server_name old.example.com; rewrite ^/api/(.*)$ http://new.example.com/api/$1 permanent; }这里用permanent返回 301对搜索引擎友好会把旧链接的权重传导到新链接。如果是临时迁移则用redirect返回 302。但我不建议在 Nginx 层做太复杂的重写逻辑。重写规则一多可读性和可维护性都会直线下降。能用应用层完成的跳转尽量交给应用Nginx 只负责那些非做不可的路径规则、协议跳转和泛域名支持。6.2 一个服务器部署多个网站利用server_name区分域名Nginx 可以在同一台服务器、同一个 80/443 端口上托管多个站点。下面是一个典型的多站点结构server { listen 80; server_name a.example.com; root /data/www/a; } server { listen 80; server_name b.example.com; root /data/www/b; }如果还想要c.example.com等任意子域名都指向同一个入口可以用通配符server { listen 80; server_name ~^(?sub.)\.example\.com$; root /data/www/$sub; }这种配置很适合快速搭建多租户演示环境。需要注意通配符server_name的正则支持有限并且多个 server 块匹配时Nginx 会优先选择精确匹配、再选择通配符和正则。生产环境如果站点规模大建议把每个站点的server块拆成独立配置文件放在/etc/nginx/conf.d/下并统一加前缀区分比全部堆在主配置里好维护得多。6.3 基于 Stream 的 TCP/UDP 四层代理Nginx 不只是七层代理它的stream模块可以直接代理 TCP 和 UDP 流量。比如把 MySQL 3306 或 Redis 6379 端口转发到后端或者做数据库连接的负载均衡。stream { upstream mysql_backends { server 192.168.1.21:3306; server 192.168.1.22:3306; } server { listen 3306; proxy_pass mysql_backends; proxy_timeout 60s; } }使用stream时要注意它和http模块是平级的不能写在http块里面。常见的错误是把stream配置塞到http块然后 Nginx 直接报错无法启动。不过四层代理只做流量转发看不到 HTTP 层的信息没法做基于域名的路由或请求头修改。如果业务需要精细控制还是得用七层反向代理。四层代理更适合底层数据库、消息队列等组件的流量分发。7. 日常运维与高频排障7.1 编译安装还是包管理器安装新手最容易纠结的问题用包管理器装还是源码编译装。我的建议是能用系统源安装就用系统源发行版会持续推送安全更新省心。但如果你需要特定模块例如第三方模块或新版特性系统源版本不够新就要考虑编译安装。编译安装的完整流程不算复杂但有一个容易失败的点依赖库不完整。常见的报错是缺pcre、zlib、openssl开发包所以编译前先把依赖装齐yum install -y gcc pcre-devel zlib-devel openssl-devel # 或 apt install -y build-essential libpcre3-dev zlib1g-dev libssl-dev然后下载源码并编译记得带上需要的模块。--prefix指定安装目录--with-http_ssl_module开启 SSL 核心模块。如果一开始忘了加 SSL 模块后面再想升级版本时可以直接重新编译或者用动态模块加载方式补上。7.2 平滑升级与优雅重启Nginx 最被人称道的一点就是可以在不中断服务的情况下完成升级或配置重载。修改配置文件后执行nginx -t # 先检查配置语法 nginx -s reload # 平滑重载reload的过程是 master 进程重新读取配置并启动新的 worker处理完当前请求的旧 worker 会逐渐退出不会出现连接中断。升级二进制文件时也能做到平滑整体思路是把新二进制文件替换后发送USR2信号让 master 启动新进程再逐步退出旧进程。不过这个过程细节较多我建议在测试环境先演练一遍不要直接在生产上临场操作。日常排查时最常用的命令就这么几条nginx -t # 语法检查 nginx -s stop / quit # 立即停止 / 优雅停止 nginx -V # 查看编译参数和模块信息 ps -ef | grep nginx # 查看 master 和 worker 进程7.3 高频报错的经验速查表把我在群里和工作中遇到最多的 Nginx 报错整理成一张表覆盖原因和方向排查时对照着来效率很高报错或现象常见原因排查方向403 Forbidden目录权限不足或索引文件不存在检查root目录权限、index指令配置404 Not Found 但文件存在root/alias 路径拼接错误查看 error.log 中实际 open 的路径502 Bad Gateway后端服务未启动或防火墙拦截检查后端端口连通性504 Gateway Timeout后端处理太慢超过 proxy_read_timeout调大proxy_read_timeout或优化后端worker_connections are not enough连接数超过配置调大worker_connections和系统文件描述符upstream prematurely closed connection后端主动断开连接检查后端日志排查超时或线程池满[emerg] bind() to 0.0.0.0:80 failed端口被占用检查是否有其他进程占用端口to upstream ... while reading response header后端响应头超时结合upstream日志定位慢业务出现 502 时我的排查顺序是先确认后端端口通不通再确认 Nginx 能否解析 upstream 域名最后看后端的健康检查接口是否正常。很多 502 不是 Nginx 的问题而是后端进程没起来。关于日志我习惯在http块里开启 access_log 和 error_log并按天切割。定位问题时日志里的request_time、upstream_response_time这两个字段最有价值前者能看出整体耗时后者能看出后端处理耗时两者差值就是 Nginx 层自身和网络开销。把日志格式加两个字段很多问题会变得一眼就能定位。7.4 自动检查配置和守护进程的小技巧最后分享一个我在多台服务器上养成的习惯每次修改完配置写一个简单的检查脚本流程把nginx -t放在提交之前强制跑一遍。尤其是在多管理员协作环境下一个人改了配置忘记检查reload 时搞挂所有站点的情况并不罕见。还可以用 crontab 写一个最简易的健康检查定时 curl 本机健康检查接口连续失败则触发重启或告警。不要把重启写得太激进至少连续失败三次以上才动作避免后端偶发抖动导致 Nginx 被反复重启。根据我个人经验Nginx 的很多问题不是配置不会写而是对每一行指令背后的语义理解不够透彻。proxy_pass带不带斜杠、add_header的覆盖规则、keepalive是否真正生效这些细节测试环境里可能看不出来线上流量一大就全部暴露。所以我会建议每个团队维护一份属于自己的 Nginx 配置清单和 check-list新同学入职先过一遍能少踩太多坑。Nginx 的能力边界远不止上面这些比如mirror流量镜像、js模块做复杂路由、njs脚本扩展等都是更高阶的玩法。但核心功能就这七块静态服务、反向代理、负载均衡、性能调优、HTTPS、重写与多站点、四层代理。把这些搞透线上绝大多数问题都能应对。后续如果你想深入我会再单独拆一下 Nginx 的模块化体系和 Lua 扩展。
返回列表