跳到主要内容

Nginx 核心概念与架构

Nginx 大概是运维日常接触频率最高的中间件:前面挡流量,后面接应用,中间顺手做 HTTPS、压缩、缓存和限流。它配置看着简单,但「为什么这么写」「为什么不生效」的问题,多半要回到架构和配置层级上找答案。这篇先把地基打牢:它到底解决什么问题、进程怎么跑、配置怎么分层,以及 upstream、proxy_pass、location 这几个绕不开的核心概念。

为什么用 Nginx​

一句话概括:它用一套配置同时干好了三件事——托管静态文件、做反向代理、做负载均衡。绝大多数站点从单机到集群,都是在这三件事上渐进演化的。

角色解决的问题典型配置
静态资源服务器图片、JS、CSS、前端打包产物直接由 Nginx 吐出去,不占用应用进程root、try_files、expires
反向代理对外只暴露 80/443,把请求转给本机或内网的应用端口,顺带做 TLS 终结proxy_pass、proxy_set_header
负载均衡一个域名后面挂多台应用实例,按策略分流并做故障摘除upstream、server、max_fails

除了这三件主业,还有几件「顺手就做了」的事,也是生产环境常见的用法:

  • TLS 终结:证书统一在 Nginx 上管,后端应用只跑明文 HTTP,证书续期只动一处。
  • 压缩与缓存:gzip 压文本资源,proxy_cache 把后端响应缓存到本地磁盘,扛住热点。
  • 限流与鉴权:limit_req 按 IP 限速,auth_basic 做一层简单的访问控制。
  • 灰度与重定向:按 $http_*、Cookie、来源 IP 分流到不同的 upstream,或者用 return 做整站跳转。
什么时候不该用 Nginx 扛

Nginx 擅长的是「连接管理 + 转发」,不是「业务逻辑」。把复杂的鉴权、路由规则、请求改写全塞进 Nginx 配置,最后会得到一份没人敢改的巨型配置文件。这类需求放到应用层或 API 网关(Kong、APISIX、Envoy)更合适。真要在 Nginx 里写逻辑,那是 OpenResty 的领域,属于另一个话题。

进程模型​

Nginx 启动后不是一个进程在干活,而是一组:一个 master 进程 + N 个 worker 进程,另外可能还有 cache manager 和 cache loader 两个辅助进程(只有开了 proxy_cache 才有)。

                    ┌─────────────────────────────┐
│ master process │
│ 读配置 / 绑定端口 / 管信号 │
└───────────┬─────────────────┘
│ fork
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ worker 1 │ │ worker 2 │ ... │ worker N │
│ 事件循环 │ │ 事件循环 │ │ 事件循环 │
│ epoll │ │ epoll │ │ epoll │
└─────────────┘ └─────────────┘ └─────────────┘

两种进程的分工非常清晰:

  • master:以 root 启动,负责读配置、绑定 80/443 端口、派生 worker、接收信号(reload、quit、reopen 都发给它)。它自己不处理任何请求。
  • worker:真正干活的进程,以 user 指令指定的低权限用户运行(Ubuntu 上是 www-data),每个 worker 内部是一个单线程事件循环。

单线程 + epoll 为什么快​

传统 Apache 的 prefork 模型是「一个连接一个进程」,1000 个并发连接就是 1000 个进程,内存和上下文切换开销都吃不消。Nginx 走的是另一条路:

  • 一个 worker 只用一个线程,但用 epoll 同时监听成千上万个 socket。
  • 没有请求处理时就阻塞在 epoll_wait 上,内核说哪个 fd 可读了才去读,不做任何轮询和等待。
  • 请求处理全程非阻塞:读文件、连上游、写响应都靠事件驱动,不会因为一个慢请求把整个 worker 卡住。

所以 Nginx 的内存占用大致是「固定基线 + 每连接少量缓冲」,几万并发在几十 MB 内存里跑完,这不是魔法,是模型决定的。

单线程的前提是「不阻塞」

worker 是单线程的,任何一次阻塞都会拖住这个 worker 上的所有连接。第三方模块里做同步磁盘 IO、同步 DNS 解析、调用外部脚本,都是典型的踩坑来源。这也是为什么 Nginx 的模块生态强调「必须非阻塞」。

几个和进程相关的参数​

# worker 数量:auto 等于 CPU 核数,生产基本都用 auto
worker_processes auto;

# 每个 worker 能同时打开的文件描述符数,包括连接和本地文件
worker_rlimit_nofile 65535;

events {
# 单个 worker 允许的最大并发连接数
worker_connections 10240;
# Linux 下默认就是 epoll,一般不用显式写
use epoll;
}

三个数字的关系值得记一下:理论最大并发连接数 ≈ worker_processes × worker_connections。但要注意,反向代理场景下一个客户端请求会占用两个连接(客户端到 Nginx、Nginx 到上游),所以实际能扛的客户端数是这个乘积的一半左右。这个值还必须小于系统的 ulimit -n,否则日志里会看到 too many open files,排障细节放在第 6 篇。

架构图​

从请求进来到响应出去,Nginx 内部的分层大致是这样:

Nginx 架构

对照上图理解三个关键点:

  • master/worker 分离:配置变更、二进制升级只需要动 master,worker 可以平滑替换,服务不中断。
  • 事件驱动核心:所有连接都由 epoll 统一管理,worker 之间互不通信,靠共享内存(zone)交换限流、缓存这类状态。
  • 模块化处理链:一个请求依次经过 access、content、log 各阶段,location 决定由哪个 content 模块处理——是读本地文件,还是转发给 upstream。

配置结构​

Nginx 的配置是树形嵌套的,理解层级比背指令更重要。安装后的目录大致长这样:

/etc/nginx/
├── nginx.conf # 主配置,一切的入口
├── conf.d/ # 站点配置,被主配置 include 进来
│ ├── default.conf
│ └── api.conf
├── sites-available/ # Ubuntu 包管理的约定,存放可用站点
├── sites-enabled/ # 软链到 sites-available,只加载这里面的
├── mime.types # 扩展名到 Content-Type 的映射
├── fastcgi_params # FastCGI / PHP 相关参数
└── snippets/ # 可复用的配置片段

/var/www/html/ # 默认站点根目录
/var/log/nginx/ # access.log / error.log,源码编译时在 prefix 下

Debian/Ubuntu 的包默认同时存在 conf.d/*.conf 和 sites-enabled/* 两套 include,二者都会被加载。自己新建站点时选一套用就行,混着写容易「明明没启用却生效了」。

配置本身的嵌套层级是固定的五层:

# main 层:全局唯一,进程级行为
user www-data;
worker_processes auto;

# events 层:连接处理相关
events {
worker_connections 10240;
}

# http 层:所有 HTTP 行为的公共配置
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;

# upstream 定义在 http 层,供 server 引用
upstream app_backend {
server 127.0.0.1:8080;
}

# server 层:一个虚拟主机 = 一组 listen + server_name
server {
listen 80;
server_name example.com;

# location 层:按 URI 匹配,决定怎么处理
location / {
root /var/www/html;
index index.html;
}

location /api/ {
proxy_pass http://app_backend;
}
}
}

层级之间有两条规矩:

  • 指令有上下文限制:worker_processes 只能写在 main 层,server 只能写在 http 里,location 只能写在 server 或 location 里。位置写错,nginx -t 会直接报 "xxx" directive is not allowed here。
  • 指令向下继承:写在 http 的 gzip on 对所有 server 生效;某个 server 里再写一次 gzip off,就只覆盖这个虚拟主机。但继承是「整体覆盖」不是「合并」——父层写了 proxy_set_header Host $host,子层再写一个 proxy_set_header X-Real-IP ...,父层那条就丢了,必须两条一起重写。

核心概念​

upstream:把后端实例编成一组​

upstream 定义一个后端集群,proxy_pass 直接引用这个名字,Nginx 自己负责选实例、重试和摘除:

upstream app_backend {
server 10.0.0.11:8080 weight=3 max_fails=3 fail_timeout=10s;
server 10.0.0.12:8080 weight=1;
server 10.0.0.13:8080 backup; # 前两台都挂了才启用
}

默认是轮询,加上 weight 就变成加权轮询;还有 ip_hash、least_conn 等策略,负载均衡的完整写法在第 3 篇。这里先记住一点:upstream 里的 server 是后端地址,不是对外监听的端口,两者别混。

proxy_pass:转发,以及那个尾斜杠​

proxy_pass 的语义全看末尾有没有 /,这是新手最容易踩的坑:

写法请求 /api/users 实际转发到
proxy_pass http://backend;http://backend/api/users(URI 原样带上)
proxy_pass http://backend/;http://backend/users(location 匹配的部分被替换成 /)
proxy_pass http://backend/v1/;http://backend/v1/users

配合正则 location 时还有一条硬约束:location 里用了正则,proxy_pass 就不能带 URI(不能有 / 结尾的路径),否则 nginx -t 报 "proxy_pass" cannot have URI part in location given by regular expression。要改写路径就用 rewrite ... break; 再转发。

location:匹配优先级才是重点​

location 的匹配不是「从上到下第一个命中」,而是有一套固定优先级。按从高到低排:

优先级语法说明
1location = /path精确匹配,命中就停,不再看别的
2location ^~ /prefix/前缀匹配且不再检查正则,命中就用它
3location ~ pattern正则匹配,区分大小写,按配置里出现的顺序取第一个
4location ~* pattern正则匹配,不区分大小写,同样按顺序
5location /prefix/普通前缀匹配,取最长的那个

举一个能覆盖大部分场景的例子:

server {
listen 80;
server_name example.com;

# 1. 精确命中,优先级最高
location = /health {
return 200 "ok\n";
}

# 2. 前缀 + ^~:命中后不再试正则,适合静态目录
location ^~ /static/ {
root /var/www;
expires 30d;
}

# 3. 正则:大小写敏感,注意顺序
location ~ \.(jpg|png|webp)$ {
root /var/www/images;
expires 7d;
}

# 4. 普通前缀:兜底
location / {
proxy_pass http://app_backend;
}
}

三条实战经验:

  • ^~ 是给「不想被正则抢走」的前缀用的。/static/ 目录下如果有 .php 文件,不加 ^~ 就会被正则 location 截胡。
  • 正则 location 的顺序有意义,写反了就是另一个结果。能用前缀就用前缀,正则只在真需要时才上。
  • = 适合健康检查和固定路径,比如 /health、/favicon.ico,命中即返回,链路最短。
嵌套 location 与 @named

location 可以嵌套,但嵌套后外层的前缀会被剥离再匹配内层,实际项目里用得很少。更常用的是命名 location(location @fallback),配合 try_files ... @fallback 做兜底,SPA 场景的标准写法见第 3 篇。

小结​

  • Nginx 的主业就三件:静态托管、反向代理、负载均衡,TLS 终结、压缩缓存、限流是顺手的附加值。
  • 进程模型是 master 管配置和端口、worker 跑单线程事件循环(epoll),所以并发高、内存省;前提是绝不能阻塞。
  • 配置是五层嵌套:main → events → http → server → location,指令有上下文限制,向下继承但整体覆盖不合并。
  • 三个核心概念:upstream 定义后端集群,proxy_pass 负责转发(尾斜杠决定是否改写 URI),*location 按固定优先级匹配(= > ^~ > ~ > ~ > 普通前缀最长)**。
  • 最大并发 ≈ worker_processes × worker_connections(反向代理要除以 2),且必须小于 ulimit -n。

概念清楚了,下一篇直接动手:安装与快速上手——三种安装方式、目录约定,以及第一个能跑起来的反向代理。