Skip to content
Sanyam Jain
Go back

Nginx as a Reverse Proxy Nginx 作为反向代理

Your app runs on port 3000.你的应用运行在 3000 端口。

node server.js
# listening on 0.0.0.0:3000

You open http://localhost:3000 and it works.打开 http://localhost:3000 就能工作。

Then it has to go live. A real domain. HTTPS on port 443. The same app, now reachable from the internet.然后它需要上线。一个真实的域名。HTTPS 在 443 端口。同一个应用,现在可以从互联网访问。

The first instinct is to make the app listen on 443 directly. That works for about a day, until the app also needs to serve static files, handle TLS certificates, survive restarts without dropping traffic, limit how fast a single client can hit it, and eventually run as more than one process behind one address.第一反应是让应用直接监听 443 端口。这大约能工作一天,直到应用还需要提供静态文件、处理 TLS 证书、在不中断流量情况下重启、限制单个客户端的访问速率,并最终在同一个地址后面以多个进程运行。

A single application process is not a good front door.单个应用进程不是一个好的前门。

Table of contents目录

Open Table of contents打开目录

The Problem Nginx Is Solving

An application process is good at running application logic. It is not built to be the thing the public internet talks to directly.应用进程擅长运行应用逻辑。它不适合直接面对公共互联网。

When the app is the only layer, it has to handle:当应用是唯一一层时,它必须处理:

Every one of these is work that has nothing to do with the actual business logic.每一项都是与实际业务逻辑无关的工作。

Nginx takes that work and sits in front. The app moves behind it and goes back to doing one job.Nginx 承担了这些工作并位于前端。应用移到它后面,重新专注于一项工作。

The Big Idea

Nginx becomes the single front door.Nginx 成为唯一的前门。

The public talks to Nginx. Nginx talks to the app over a private connection.公众与 Nginx 通信。Nginx 通过私有连接与应用通信。

client -> nginx (443, TLS) -> app (3000, plain http)

The app no longer faces the internet. It listens on a local port that only Nginx reaches. Nginx handles the parts that face the outside world: the certificate, the public port, the timeouts, the limits.应用不再直接面对互联网。它监听一个只有 Nginx 能访问的本地端口。Nginx 处理面向外部的部分:证书、公共端口、超时、限制。

This is what “reverse proxy” means here. A normal proxy sits in front of clients and talks to many servers on their behalf. A reverse proxy sits in front of servers and takes requests from many clients on their behalf.这就是“反向代理”的含义。普通代理位于客户端前面,代表它们与许多服务器通信。反向代理位于服务器前面,代表它们接收来自许多客户端的请求。

How Nginx Is Built

Nginx runs as one master process and a set of worker processes.Nginx 运行一个主进程和一组工作进程。

master process
   ├── worker process
   ├── worker process
   └── worker process

The master process reads the config, binds the ports, and manages the workers. It does not handle a single request itself. When you reload the config, the master is what starts new workers and retires old ones.主进程读取配置、绑定端口并管理工作进程。它本身不处理任何请求。当你重新加载配置时,主进程启动新工作进程并淘汰旧工作进程。

The workers do the actual work. Each worker is a single process running an event loop.工作进程执行实际工作。每个工作进程是一个运行事件循环的单一进程。

The older model, used by servers like Apache in its default setup, gave each connection its own thread or process. A thousand idle clients meant a thousand threads sitting around, each using memory and forcing the kernel to switch between them.旧模型(如 Apache 默认设置)为每个连接分配一个线程或进程。一千个空闲客户端意味着有一千个线程闲置,每个都占用内存并迫使内核在它们之间切换。

A single Nginx worker takes a different approach. It keeps thousands of connections open at once and only touches a connection when something actually happens on it, using the kernel’s event notification (epoll on Linux). When a connection is waiting on the network, the worker is not blocked on it. It moves on and services another.单个 Nginx 工作进程采用不同方法。它同时保持数千个连接打开,并且仅在连接上实际发生事件时才处理它,使用内核的事件通知机制(Linux 上的 epoll)。当连接等待网络时,工作进程不会阻塞。它继续处理其他连接。

This is why a slow client holding a connection open is cheap for Nginx and expensive for a thread-per-connection app. It is also the reason Nginx is placed in front of the app: it absorbs slow and idle connections so the app only deals with complete, ready requests.这就是为什么慢客户端保持连接打开对 Nginx 来说很便宜,而对每个连接一个线程的应用来说很昂贵。这也是 Nginx 放在应用前面的原因:它吸收慢速和空闲连接,使应用只处理完整、准备好的请求。

How A Request Flows

A request to https://example.com/api/users goes through a few steps.对 https://example.com/api/users 的请求经历几个步骤。

  1. The client opens a TLS connection to Nginx on port 443.客户端在 443 端口上向 Nginx 打开 TLS 连接。
  2. Nginx terminates TLS and now has a plain HTTP request.Nginx 终止 TLS,现在有一个明文 HTTP 请求。
  3. Nginx matches the request against its server and location rules.Nginx 根据服务器和位置规则匹配请求。
  4. Nginx opens a connection to the app on 127.0.0.1:3000.Nginx 在 127.0.0.1:3000 上打开到应用的连接。
  5. The app responds to Nginx.应用响应 Nginx。
  6. Nginx sends the response back to the client over the encrypted connection.Nginx 通过加密连接将响应发送回客户端。

The app sees a plain HTTP request coming from Nginx on the local machine. It never sees the TLS handshake and never sees the client directly.应用看到来自本地机器上 Nginx 的明文 HTTP 请求。它从未看到 TLS 握手,也从未直接看到客户端。

Server Blocks

A server block tells Nginx how to handle traffic for a given name and port.服务器块告诉 Nginx 如何处理特定名称和端口的流量。

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
    }
}

listen 443 ssl means this block handles HTTPS.listen 443 ssl 表示此块处理 HTTPS。

server_name decides which block answers when a request arrives. One Nginx instance can hold many server blocks for many domains on the same port, and the Host header picks the right one.server_name 决定请求到达时哪个块响应。一个 Nginx 实例可以在同一端口上为多个域持有许多服务器块,Host 头部选择正确的块。

The ssl_certificate lines point at the certificate and private key. Nginx uses them to terminate TLS.ssl_certificate 行指向证书和私钥。Nginx 使用它们终止 TLS。

Location Blocks

A location block decides what happens for a path.位置块决定路径的处理方式。

location /api/ {
    proxy_pass http://127.0.0.1:3000;
}

location /static/ {
    root /var/www/app;
}

Requests to /api/... go to the application.对 /api/... 的请求转到应用程序。

Requests to /static/... are served as files straight from disk. Nginx reads the file and sends it. The application is never involved, which keeps the app process free for requests that actually need it.对 /static/... 的请求直接从磁盘作为文件提供。Nginx 读取文件并发送。应用程序从不参与,这使应用进程可以处理真正需要的请求。

This split is the practical reason to put Nginx in front. Static files go out fast from disk, and dynamic requests go to the app.这种分离是将 Nginx 放在前面的实际原因。静态文件从磁盘快速输出,动态请求转到应用。

proxy_pass And The Trailing Slash

proxy_pass is the line that forwards a request to the backend. It has one behavior that is easy to miss the first time.proxy_pass 是将请求转发到后端的指令。它有一个容易第一次忽略的行为。

The trailing slash changes how the path is rewritten.尾部斜杠改变了路径重写的方式。

location /api/ {
    proxy_pass http://127.0.0.1:3000/;
}

With the trailing slash on proxy_pass, a request to /api/users reaches the app as /users. Nginx strips the matched /api/ prefix.当 proxy_pass 有尾部斜杠时,对 /api/users 的请求到达应用时为 /users。Nginx 剥离了匹配的 /api/ 前缀。

location /api/ {
    proxy_pass http://127.0.0.1:3000;
}

Without it, the same request reaches the app as /api/users. The full path is passed through.没有尾部斜杠时,相同的请求到达应用时为 /api/users。完整路径被传递。

Passing The Real Client Information

The app sees the request coming from Nginx, so by default it thinks every client is 127.0.0.1.应用看到请求来自 Nginx,因此默认认为每个客户端都是 127.0.0.1。

That breaks logging, rate limiting, and anything that depends on the client address or the original protocol.这会破坏日志记录、速率限制以及任何依赖客户端地址或原始协议的功能。

Nginx has to pass that information forward as headers.Nginx 必须将这些信息作为头部传递。

location / {
    proxy_pass http://127.0.0.1:3000;

    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;
}

Host carries the domain the client actually asked for. Without this line, the backend receives the proxy_pass target as its host, which is something like 127.0.0.1:3000. Any logic that depends on the requested domain breaks.Host 携带客户端实际请求的域名。没有这一行,后端接收到的 Host 是 proxy_pass 目标,类似于 127.0.0.1:3000。任何依赖请求域名的逻辑都会出错。

X-Real-IP carries the client address as a single value.X-Real-IP 以单个值携带客户端地址。

X-Forwarded-For carries the chain of client addresses the request passed through. The value $proxy_add_x_forwarded_for takes any existing X-Forwarded-For header and appends the current client’s address to it. With one proxy this is just the client IP. With several proxies in front, each hop adds its caller, so the app can read the original client at the start of the list.X-Forwarded-For 携带请求经过的客户端地址链。$proxy_add_x_forwarded_for 的值会获取任何现有的 X-Forwarded-For 头部,并将当前客户端地址附加到它后面。只有一个代理时,这只是客户端 IP。前面有多个代理时,每一跳都会添加其调用者,因此应用可以读取列表开头的原始客户端。

X-Forwarded-Proto tells the app whether the original request was http or https. The app needs this because, after TLS termination, the request arriving at the app is plain HTTP. Without this header, an app trying to build absolute https:// URLs may build http:// ones instead.X-Forwarded-Proto 告诉应用原始请求是 http 还是 https。应用需要这个,因为在 TLS 终止后,到达应用的请求是明文 HTTP。没有这个头部,尝试构建绝对 https:// URL 的应用可能会构建 http:// 的 URL。

WebSockets Need An Upgrade

A plain proxy_pass does not carry a WebSocket connection. The connection starts as HTTP and asks to upgrade, and Nginx has to forward that upgrade.普通的 proxy_pass 不会携带 WebSocket 连接。连接以 HTTP 开始并请求升级,Nginx 必须转发该升级。

location /ws/ {
    proxy_pass http://127.0.0.1:3000;

    proxy_http_version 1.1;
    proxy_set_header Upgrade    $http_upgrade;
    proxy_set_header Connection "upgrade";
}

Without these lines, the WebSocket handshake fails and the connection keeps falling back or dropping.没有这些行,WebSocket 握手失败,连接会不断回退或断开。

Buffering And Timeouts

When the backend sends a response, Nginx does not have to pass it through byte by byte. By default it reads the whole response from the app into a buffer as fast as the app can produce it, then feeds it out to the client at whatever pace the client can take.当后端发送响应时,Nginx 不必逐字节传递。默认情况下,它尽可能快地从应用读取整个响应到缓冲区,然后以客户端能接受的速度输出给客户端。

This matters because clients are often slow and apps are often not built to wait. Without buffering, a phone on a weak connection would hold an app worker busy for the entire download. With buffering, the app hands the response to Nginx quickly and moves on, and Nginx deals with the slow client.这很重要,因为客户端通常很慢,而应用通常不设计为等待。没有缓冲,弱连接上的手机会在整个下载期间占用应用工作进程。有了缓冲,应用快速将响应交给 Nginx 并继续处理,Nginx 处理慢客户端。

location / {
    proxy_pass http://app;

    proxy_buffering on;
    proxy_buffers 8 16k;
    proxy_buffer_size 16k;
}

proxy_buffering is the master switch for this response-side behavior, and it is on by default. proxy_buffers sets how many buffers and how large. If a response does not fit in the memory buffers, Nginx spills it to a temporary file on disk, which is slower, so the sizes are worth matching to typical response sizes.proxy_buffering 是响应端行为的主开关,默认开启。proxy_buffers 设置缓冲区的数量和大小。如果响应不适合内存缓冲区,Nginx 会将其溢出到磁盘上的临时文件,这较慢,因此大小值得与典型响应大小匹配。

Turning buffering off makes sense for streaming, where the client should receive bytes as they are produced rather than after the whole response is ready. Server-sent events and long-polling are the common cases.关闭缓冲适用于流式传输,其中客户端应在字节生成时接收,而不是在整个响应准备好之后。服务器发送事件和长轮询是常见情况。

location /events/ {
    proxy_pass http://app;
    proxy_buffering off;
}

Buffering also works in the other direction. By default Nginx reads the whole request body from the client before it opens the connection to the backend. This is proxy_request_buffering, and it is on by default too.缓冲也适用于另一个方向。默认情况下,Nginx 在打开到后端的连接之前从客户端读取整个请求体。这是 proxy_request_buffering,默认也是开启的。

location /upload/ {
    proxy_pass http://app;
    proxy_request_buffering off;
}

With request buffering on, a slow upload is the proxy’s problem, not the app’s. Nginx collects the full body and hands the backend a complete request, so a backend worker is not tied up for the length of a slow upload. Turning it off streams the body to the backend as it arrives, which avoids buffering very large uploads in Nginx but holds a backend connection open for the whole transfer.开启请求缓冲时,慢速上传是代理的问题,而不是应用的问题。Nginx 收集完整的请求体,并将完整请求交给后端,因此后端工作进程不会因慢速上传而被占用。关闭它会在请求体到达时流式传输到后端,这避免了在 Nginx 中缓冲非常大的上传,但会在整个传输过程中保持后端连接打开。

Timeouts decide how long Nginx waits at each stage. They come in two groups: timers for the connection to the backend, and timers for the connection to the client.超时决定 Nginx 在每个阶段等待的时间。它们分为两组:与后端连接的计时器,以及与客户端连接的计时器。

# backend side
proxy_connect_timeout 5s;
proxy_send_timeout    60s;
proxy_read_timeout    60s;

# client side
client_header_timeout 10s;
client_body_timeout   10s;
keepalive_timeout     65s;

proxy_connect_timeout is how long to wait to open the connection to the backend. A low value here fails fast when a backend is down instead of hanging.proxy_connect_timeout 是等待打开到后端连接的时间。较低的值在后端宕机时快速失败,而不是挂起。

proxy_read_timeout is how long to wait between reads once the backend is responding. This is the one behind a 504: the backend went quiet for longer than the timeout, so Nginx gave up. Raising it buys time for genuinely slow work, but a request that needs a 120-second read timeout is usually a request that should be doing its work in the background instead.proxy_read_timeout 是后端响应后两次读取之间的等待时间。这是 504 错误背后的原因:后端静默时间超过超时,因此 Nginx 放弃。提高它可以为真正慢的工作争取时间,但需要 120 秒读取超时的请求通常应该改为在后台执行工作。

client_header_timeout and client_body_timeout cap how long a client can take to send its headers and body. They cut off clients that open a connection and then dribble bytes slowly to hold a worker open, which is the shape of a slowloris attack.client_header_timeout 和 client_body_timeout 限制客户端发送头部和请求体的时间。它们切断打开连接然后缓慢发送字节以占用工作进程的客户端,这是慢速攻击的形式。

keepalive_timeout is how long an idle client connection stays open for reuse before Nginx closes it.keepalive_timeout 是空闲客户端连接在 Nginx 关闭之前保持打开以供重用的时间。

These are the timers worth knowing first. There are more, and the exact rules for when each one resets matter once you start tuning, so read the proxy module and core module docs before changing them in production.这些是首先值得了解的计时器。还有更多,每个计时器何时重置的确切规则在开始调优时很重要,因此在生产环境中更改它们之前,请阅读代理模块和核心模块文档。

HTTP/2 vs HTTP/1.1

Under HTTP/1.1, a connection carries one request at a time. The next request on that connection waits for the previous response to finish. Browsers work around this by opening several connections to the same host, usually around six, and spreading requests across them. Each connection costs a TLS handshake and its own memory.在 HTTP/1.1 下,一个连接一次携带一个请求。该连接上的下一个请求等待前一个响应完成。浏览器通过打开到同一主机的多个连接(通常约六个)并在它们之间分配请求来解决这个问题。每个连接都需要 TLS 握手和自身的内存。

HTTP/2 changes the shape of the connection. One connection carries many requests at once as independent streams, so a page with dozens of small assets does not need a pile of parallel connections. It also compresses headers and uses a binary framing instead of plain text.HTTP/2 改变了连接的形式。一个连接作为独立流同时携带许多请求,因此包含数十个小资产的页面不需要一堆并行连接。它还压缩头部并使用二进制帧而不是纯文本。

Turning it on in Nginx is one line.在 Nginx 中启用它只需一行。

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;
}

Older Nginx versions wrote this as listen 443 ssl http2;. Either way, browsers only use HTTP/2 over TLS, so it goes hand in hand with HTTPS.较旧的 Nginx 版本将其写为 listen 443 ssl http2;。无论哪种方式,浏览器只通过 TLS 使用 HTTP/2,因此它与 HTTPS 相辅相成。

Nginx speaks HTTP/2 to the client, but it usually speaks HTTP/1.1 to the backend app. The multiplexing benefit is on the public side of Nginx, between the browser and the proxy, not between Nginx and your app.Nginx 与客户端使用 HTTP/2 通信,但通常与后端应用使用 HTTP/1.1 通信。多路复用的好处在 Nginx 的公共侧,即浏览器和代理之间,而不是 Nginx 和你的应用之间。

Compressing Responses

Text responses like HTML, CSS, JavaScript, and JSON compress well. Sending them compressed cuts transfer size, which is the slowest part of a request for a user on a phone or a far-away network. Nginx can compress on the way out with gzip.文本响应如 HTML、CSS、JavaScript 和 JSON 压缩效果好。压缩它们可以减少传输大小,这对于手机或远距离网络上的用户来说是最慢的部分。Nginx 可以在输出时使用 gzip 进行压缩。

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

gzip_types lists what to compress. HTML is always included, so it does not appear in the list. Images and video in their normal formats are already compressed, so running gzip over them burns CPU for no gain.gzip_types 列出要压缩的内容。HTML 始终包含在内,因此不会出现在列表中。常见格式的图像和视频已经压缩,因此对它们运行 gzip 会消耗 CPU 而没有任何收益。

gzip_min_length skips tiny responses, where the compression overhead is larger than the saving.gzip_min_length 跳过非常小的响应,其中压缩开销大于节省。

gzip_comp_level trades CPU for size. Higher levels compress a little more but cost more CPU per response, and the gain past the middle of the range is small. A level around 5 is a reasonable balance for most sites.gzip_comp_level 用 CPU 换取大小。更高的级别压缩更多,但每个响应消耗更多 CPU,并且超过中间范围的增益很小。对于大多数网站,级别 5 左右是合理的平衡。

The cost is real: compression uses CPU on every response it touches. For a busy site serving large text payloads, that CPU is well spent. For static assets that never change, compressing them once ahead of time and serving the precompressed file avoids paying for it on every request.成本是真实的:压缩会消耗每个响应的 CPU。对于提供大型文本负载的繁忙网站,这些 CPU 是值得的。对于从不更改的静态资产,提前压缩一次并提供预压缩文件可以避免每次请求都支付成本。

Tuning Workers To The Server

worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 16384;
}

worker_processes auto starts one worker per CPU core. Each core runs one event loop, so the workers run in parallel instead of fighting over the same core. Setting this far above the core count does not add capacity, it just adds processes competing for the same CPUs.worker_processes auto 为每个 CPU 核心启动一个工作进程。每个核心运行一个事件循环,因此工作进程并行运行,而不是争夺同一个核心。将此值设置得远高于核心数不会增加容量,只会增加竞争同一 CPU 的进程。

worker_connections is how many connections one worker can hold at once. Multiplying it by the worker count gives the number people quote as the limit:worker_connections 是一个工作进程可以同时保持的连接数。将其乘以工作进程数得到人们引用的上限:

max connections = worker_processes * worker_connections

That is an upper bound, not the real one. The actual ceiling is whichever resource runs out first: file descriptors, memory, CPU, or network. The formula only tells you the most connections Nginx will let itself track, not the most the machine can carry.这是一个上限,而不是实际值。实际上限是哪个资源先用完:文件描述符、内存、CPU 或网络。该公式只告诉你 Nginx 允许自己跟踪的最大连接数,而不是机器可以承载的最大连接数。

As a reverse proxy, Nginx usually keeps a connection to the client and a separate one to the backend, so its client capacity tends to be lower than that number. The relationship is not a clean one-to-one, though. A keep-alive connection serves many requests over its life, response buffering can release the backend connection early, a cached response may need no backend at all, and HTTP/2 carries many requests over a single client connection. The takeaway is that practical capacity sits below the theoretical maximum, not at a fixed fraction of it.作为反向代理,Nginx 通常保持一个到客户端的连接和一个到后端的单独连接,因此其客户端容量往往低于该数字。然而,关系并非一对一的。保持活动连接在其生命周期内服务许多请求,响应缓冲可以提前释放后端连接,缓存响应可能根本不需要后端,HTTP/2 在单个客户端连接上携带许多请求。结论是实际容量低于理论最大值,而不是其固定比例。

worker_rlimit_nofile is the part people forget. Every connection needs a file descriptor, and the operating system caps how many files a process can open. If worker_connections is high but the file descriptor limit is low, Nginx hits its real ceiling first and the error log fills with:worker_rlimit_nofile 是人们忘记的部分。每个连接需要一个文件描述符,操作系统限制一个进程可以打开的文件数。如果 worker_connections 很高但文件描述符限制很低,Nginx 会先达到实际上限,错误日志中会充满:

socket() failed (24: Too many open files)

The fix is to raise the descriptor limit in two places. worker_rlimit_nofile raises it inside Nginx, but the OS has the final say, so the system limits have to move too: ulimit -n for the shell, /etc/security/limits.conf for the user, and LimitNOFILE when Nginx runs under systemd. Setting worker_connections to a large number while the OS still allows 1024 open files does nothing except hide where the real limit is. A worker also needs descriptors for more than client sockets, since log files, upstream sockets, and temporary files each take one, so the limit should sit comfortably above worker_connections rather than exactly at it.解决方法是在两个地方提高描述符限制。worker_rlimit_nofile 在 Nginx 内部提高它,但操作系统有最终决定权,因此系统限制也必须更改:shell 的 ulimit -n,用户的 /etc/security/limits.conf,以及 Nginx 在 systemd 下运行时的 LimitNOFILE。将 worker_connections 设置为大数而操作系统仍允许 1024 个打开文件,除了隐藏实际限制在哪里之外什么也做不了。工作进程还需要比客户端套接字更多的描述符,因为日志文件、上游套接字和临时文件各占一个,因此限制应舒适地高于 worker_connections,而不是恰好等于它。

The tradeoff is plain: these numbers should match what the box can actually back with CPU, memory, and file descriptors. A huge worker_connections on a small server does not create capacity, it just moves the failure from one place to another.权衡很明显:这些数字应与机器实际能支持的 CPU、内存和文件描述符相匹配。在小服务器上设置巨大的 worker_connections 不会创造容量,只会将故障从一个地方转移到另一个地方。

Rate Limiting

Without a limit, one client can send requests as fast as the network allows, and a login endpoint or a search route becomes easy to hammer.没有限制,一个客户端可以以网络允许的速度发送请求,登录端点或搜索路由很容易被攻击。

Nginx rate limiting works in two parts: a zone that tracks clients, and a rule that applies a rate.Nginx 速率限制由两部分组成:跟踪客户端的区域和应用速率的规则。

http {
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
}

server {
    location /api/ {
        limit_req zone=api burst=20 nodelay;
        proxy_pass http://app;
    }
}

limit_req_zone defines the tracking. It keys on $binary_remote_addr, which is the client IP in a compact form, reserves 10 MB of shared memory to hold the counters, and sets a steady rate of 10 requests per second.limit_req_zone 定义跟踪。它基于 $binary_remote_addr(客户端 IP 的紧凑形式),保留 10 MB 共享内存来保存计数器,并设置每秒 10 个请求的稳定速率。

limit_req applies that zone to a location. burst=20 lets a client briefly go over the rate by queueing up to 20 extra requests, which covers normal bursts like a page firing several API calls at once. nodelay serves that burst immediately instead of spacing the requests out to fit the rate.limit_req 将该区域应用于位置。burst=20 允许客户端通过排队最多 20 个额外请求短暂超过速率,这涵盖了正常突发,例如页面同时发出多个 API 调用。nodelay 立即提供该突发,而不是将请求间隔开以符合速率。

Over the limit, Nginx rejects the request. The default status is 503, which you can change to the more accurate 429:超过限制时,Nginx 拒绝请求。默认状态是 503,你可以将其更改为更准确的 429:

limit_req_status 429;

There is one trap that matters in production. The key is the client IP, but if Nginx sits behind a CDN or another load balancer, $remote_addr is that upstream’s IP, not the user’s. Every request then looks like it comes from one client, and the limit applies to the whole world as a single bucket. To rate limit real users in that setup, Nginx has to recover the true client IP from X-Forwarded-For using the real_ip module before the limit is applied.有一个在生产环境中重要的陷阱。键是客户端 IP,但如果 Nginx 位于 CDN 或另一个负载均衡器后面,$remote_addr 是上游的 IP,而不是用户的 IP。然后每个请求看起来来自一个客户端,限制适用于整个世界作为一个桶。要在该设置中对真实用户进行速率限制,Nginx 必须在应用限制之前使用 real_ip 模块从 X-Forwarded-For 恢复真实客户端 IP。

Hiding Server Information

By default, Nginx announces its version in every response and on its error pages.默认情况下,Nginx 在每个响应和错误页面上宣布其版本。

Server: nginx/1.25.3

That version number is a gift to anyone scanning for hosts running a release with a known vulnerability. Turning it off removes the version.该版本号是扫描运行具有已知漏洞版本的任何人的礼物。关闭它会移除版本。

http {
    server_tokens off;
}

The response then says Server: nginx with no version.然后响应显示 Server: nginx,没有版本。

Be precise about what this does. server_tokens off hides the version, not the fact that the server is Nginx. Fully removing or rewriting the Server header needs more than this directive.要精确了解其作用。server_tokens off 隐藏版本,而不是隐藏服务器是 Nginx 的事实。完全移除或重写 Server 头部需要比此指令更多的工作。

Restricting Who Can Embed Your Site

If any other site can load your pages inside an iframe, it can overlay its own controls on top of yours and trick a logged-in user into clicking something they did not intend. Telling browsers who is allowed to frame your site shuts that down.如果任何其他网站可以在 iframe 中加载你的页面,它可以在你的页面之上覆盖自己的控件,并诱使已登录用户点击他们不打算点击的内容。告诉浏览器谁可以嵌入你的网站可以阻止这种情况。

add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;

X-Frame-Options: SAMEORIGIN is the older header, understood everywhere. frame-ancestors 'self' in a Content-Security-Policy is the modern replacement and takes priority in browsers that support it. Sending both covers old and new clients. Use 'none' or DENY if no one, including you, should ever frame the page.X-Frame-Options: SAMEORIGIN 是较旧的头部,被广泛理解。Content-Security-Policy 中的 frame-ancestors 'self' 是现代替代方案,并在支持的浏览器中优先。同时发送两者可以覆盖新旧客户端。如果没有人(包括你)应该嵌入页面,请使用 'none' 或 DENY。

Two Nginx details decide whether these headers actually show up.两个 Nginx 细节决定这些头部是否实际出现。

The always parameter makes Nginx send the header on every response, including errors. Without it, add_header only applies to a set of success and redirect codes, so a 404 or 500 page would go out unprotected.always 参数使 Nginx 在每个响应(包括错误)上发送头部。没有它,add_header 仅适用于一组成功和重定向代码,因此 404 或 500 页面将不受保护地输出。

The sharper gotcha is inheritance. If you set add_header in the server block and then use add_header again inside a location, the location’s headers replace the inherited ones rather than adding to them. A location that sets its own header silently drops the security headers from the server block. The safe habit is to define these headers in one place, or repeat them where you override.更尖锐的陷阱是继承。如果你在服务器块中设置 add_header,然后在位置块中再次使用 add_header,位置块的头部会替换继承的头部,而不是添加到它们。设置自己头部的位置会静默丢弃服务器块中的安全头部。安全的习惯是在一个地方定义这些头部,或者在覆盖的地方重复它们。

Multiple Backends With Upstream

One app process is a single point of failure and a single core’s worth of capacity. Running several and putting Nginx in front of them spreads the load.一个应用进程是单点故障和单个核心的容量。运行多个并将 Nginx 放在它们前面可以分散负载。

An upstream block names a group of backends.upstream 块命名一组后端。

upstream app {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
}

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;

    location / {
        proxy_pass http://app;
    }
}

Nginx spreads requests across the three in round-robin order by default. Other methods exist for when round-robin is not a good fit: least_conn sends each request to the backend with the fewest active connections, and ip_hash pins a client to the same backend by its IP.默认情况下,Nginx 以轮询方式在三个后端之间分配请求。当轮询不适合时,还有其他方法:least_conn 将每个请求发送到活动连接最少的后端,ip_hash 通过 IP 将客户端固定到同一后端。

If one backend stops responding, Nginx marks it as failed for a while and sends traffic to the others. The site stays up on the remaining processes while one is restarting or deploying.如果一个后端停止响应,Nginx 会将其标记为失败一段时间,并将流量发送到其他后端。当其中一个进程重启或部署时,站点在其余进程上保持运行。

This is load balancing, but it is worth being precise about the limit: Nginx is balancing across processes it can reach directly, and it judges health mainly by whether the connection succeeds. It does not know whether the work inside a backend is healthy beyond that.这是负载均衡,但值得精确说明其限制:Nginx 在它可以直接访问的进程之间进行均衡,并且主要通过连接是否成功来判断健康状态。它不知道后端内部的工作是否健康。

Failure Modes

The errors Nginx returns point at where the failure is.Nginx 返回的错误指出了故障位置。

502 Bad Gateway means Nginx reached the backend but could not get a valid response. Usually the app is down, crashed, or listening on a different port than the config expects.502 Bad Gateway 表示 Nginx 到达了后端但无法获得有效响应。通常应用已宕机、崩溃或监听端口与配置预期不同。

curl -I http://127.0.0.1:3000

If that fails from the same host, Nginx will fail too.如果从同一主机失败,Nginx 也会失败。

504 Gateway Timeout means the backend accepted the connection but took too long to respond. The app is alive but slow.504 Gateway Timeout 表示后端接受了连接但响应时间过长。应用存活但缓慢。

proxy_read_timeout 60s;

Raising the timeout hides the symptom. A 504 is usually a signal that a request is doing too much work, not that the timeout is too low.提高超时隐藏了症状。504 通常表示请求做了太多工作,而不是超时设置得太低。

413 Request Entity Too Large means the request body crossed Nginx’s limit before it ever reached the app. File uploads hit this first.413 Request Entity Too Large 表示请求体在到达应用之前超过了 Nginx 的限制。文件上传首先会遇到这个问题。

client_max_body_size 25m;

429 Too Many Requests comes from the rate limit, not the app. The client is sending faster than the configured rate allows.429 Too Many Requests 来自速率限制,而不是应用。客户端发送速度超过了配置的速率。

Too many open files in the error log means the connection load crossed the file descriptor limit, which ties straight back to worker_connections and worker_rlimit_nofile.错误日志中的“Too many open files”表示连接负载超过了文件描述符限制,这直接与 worker_connections 和 worker_rlimit_nofile 相关。

A Practical Config

A setup that pulls the pieces together: worker tuning, HTTP to HTTPS redirect, HTTP/2, gzip, rate limiting, security headers, static files from disk, and everything else proxied to the app.一个整合各部分的设置:工作进程调优、HTTP 到 HTTPS 重定向、HTTP/2、gzip、速率限制、安全头部、从磁盘提供静态文件,以及将所有其他内容代理到应用。

worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 16384;
}

http {
    server_tokens off;

    gzip on;
    gzip_types text/css application/javascript application/json image/svg+xml;
    gzip_min_length 1024;

    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    upstream app {
        server 127.0.0.1:3000;
    }

    server {
        listen 80;
        server_name example.com;
        return 301 https://$host$request_uri;
    }

    server {
        listen 443 ssl;
        http2 on;
        server_name example.com;

        ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

        client_max_body_size 25m;

        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header Content-Security-Policy "frame-ancestors 'self'" always;

        location /static/ {
            root /var/www/app;
            expires 30d;
        }

        location /api/ {
            limit_req zone=api burst=20 nodelay;
            limit_req_status 429;

            proxy_pass http://app;

            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_read_timeout 60s;
        }

        location / {
            proxy_pass http://app;

            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;
        }
    }
}

The port 80 block does nothing but redirect to 443. The port 443 block terminates TLS, speaks HTTP/2, serves static files with a cache header, rate limits the API, and proxies the rest to the app.端口 80 块只做重定向到 443。端口 443 块终止 TLS、使用 HTTP/2、提供带有缓存头部的静态文件、对 API 进行速率限制,并将其余内容代理到应用。

Operating Nginx

Two commands do most of the day-to-day work.两个命令完成大部分日常工作。

Check the config before applying it:在应用之前检查配置:

nginx -t

This catches syntax errors and bad paths. Running it before a reload avoids taking the site down with a broken config.这可以捕获语法错误和错误路径。在重新加载之前运行它可以避免因配置错误导致站点宕机。

Apply a new config without dropping connections:应用新配置而不中断连接:

nginx -s reload

A reload starts new worker processes with the new config and lets the old workers finish their in-flight requests before exiting. Existing requests are not cut off.重新加载会启动具有新配置的新工作进程,并让旧工作进程在退出之前完成正在处理的请求。现有请求不会被中断。

When something is wrong, the logs say where to look.当出现问题时,日志会指出查看位置。

tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log

The access log shows what came in and what status went out. The error log shows why a request failed inside Nginx, including the upstream address it tried to reach. A 502 in the error log usually names the backend that refused the connection, which points straight at the app rather than at Nginx.访问日志显示传入的内容和输出的状态。错误日志显示请求在 Nginx 内部失败的原因,包括它尝试到达的上游地址。错误日志中的 502 通常命名拒绝连接的后端,这直接指向应用而不是 Nginx。

Final Thoughts

Nginx earns its place by separating two jobs that get tangled together when an app faces the internet alone.Nginx 通过分离两个在应用单独面对互联网时纠缠在一起的工作来赢得其位置。

The app runs application logic on a private port.应用在私有端口上运行应用逻辑。

Nginx handles the public side: TLS, HTTP/2, static files, rate limits, security headers, and spreading traffic across backends. Its worker-and-event-loop design is what lets it hold a flood of slow connections cheaply, and the tuning knobs are just that design exposed as numbers you match to the machine.Nginx 处理公共侧:TLS、HTTP/2、静态文件、速率限制、安全头部以及在多个后端之间分配流量。其工作进程和事件循环设计使其能够廉价地处理大量慢速连接,而调优旋钮只是该设计暴露为与机器匹配的数字。


Share this post:

Next Post
Docker Networking ExplainedDocker 网络详解