Skip to main content
我最早以为 Nginx 就是个静态服务器,配过生产环境的转发和负载后才明白,它其实卡在整个请求链路的关键位置上。

Q1: Nginx 的历史?为什么要有它?

1

1995-2000: Apache 称霸与同步阻塞模型

Apache HTTP Server 在 1995 年发布,迅速成为 Web 服务器的事实标准。它采用进程/线程驱动模型——每来一个连接就分配一个进程或线程。在连接数不多的年代,这个模型简单有效,稳定可靠。
2

2000-2004: C10K 问题爆发

随着互联网用户激增,单台服务器需要同时处理上万个连接(C10K = 10,000 connections)。Apache 的进程模型在高并发下崩溃——每个进程消耗 2-10MB 内存,1 万个连接就需要 20-100GB 内存。服务器频繁出现拒绝服务和内存溢出。[1]
3

2004年: Igor Sysoev 开发 Nginx

俄罗斯工程师 Igor Sysoev 为解决 Rambler 搜索引擎的高并发访问问题,用 C 语言开发了 Nginx。核心创新是事件驱动、异步非阻塞架构——一个工作进程通过 epoll/kqueue 可以同时处理数千个连接,内存开销仅为 Apache 的 1/10。
4

2004-2010: 从 Web 服务器到反向代理

Nginx 最初作为静态文件服务器和反向代理快速普及。随着架构演进,Nginx 逐渐承担起负载均衡、SSL 终止、缓存、限流等更多职责,成为现代 Web 架构中不可或缺的”流量入口”。
5

2010-至今: 成为云原生基础设施标配

Nginx 被 F5 Networks 收购后演进为 Nginx Plus(商业版),开源版继续活跃。如今几乎每一台生产服务器都运行着 Nginx,它是 Docker 容器、Kubernetes Ingress、Serverless 网关的底层基石。全球超过 34% 的网站使用 Nginx 作为 Web 服务器。
Nginx 解决的核心问题:
Nginx 用事件驱动模型解决了 Apache 无法应对的 C10K 高并发问题,成为现代 Web 架构的流量入口。

🚫 没有 Nginx

Apache 每接一个请求就起一个进程,像餐厅里每来一位客人就雇一个专属服务员。当 1 万名客人同时涌入,餐厅(服务器)人满为患,最终崩溃。

✅ 引入 Nginx

Nginx 像一个超级服务员,用”事件通知”机制同时照看数千桌客人。客人举手(事件触发)时才过去服务,其余时间在柜台待命,内存消耗极低。
回答时先说 C10K 问题(为什么需要新方案),再对比 Apache 和 Nginx 的架构差异(进程模型 vs 事件驱动),最后落到 Nginx 在现代架构中的角色。想听的是你对”为什么要发明 Nginx”的理解,而不是背诵发布时间。

Q2: Nginx 反向代理原理

Nginx 反向代理的本质是流量转发网关——客户端以为自己在和 Nginx 通信,实际上 Nginx 在背后将请求转发给真正的后端服务器。核心逻辑只有 3 步:
正向代理 vs 反向代理:
正向代理隐藏的是客户端身份(服务器不知道谁在访问),反向代理隐藏的是服务器身份(客户端不知道请求被转发给了谁)。两者方向相反,配置方式完全不同。
正向代理像”外卖代购”——你让跑腿小哥帮你买某家店的东西,店家不知道是你买的。反向代理像”餐厅前台”——你走进餐厅跟前台点菜(以为前台就是厨师),前台把菜单转给后厨,你不需要知道后厨有几口锅、几个厨师。Nginx 就是那个前台。

Nginx 反向代理配置详解

proxy_set_header 的作用

proxy_set_header 决定了 Nginx 转发请求时携带哪些信息。后端服务依赖这些 Header 获取客户端的真实 IP、协议和域名:

Node.js 后端如何获取真实 IP


Q3: Nginx 负载均衡

当单台服务器无法承载所有流量时,需要将请求分发到多台服务器上。Nginx 的负载均衡模块就是这个”流量分配器”。

负载均衡策略对比

负载均衡配置示例

健康检查机制

Nginx 的健康检查分为被动检查(开源版支持)和主动检查(Nginx Plus 商业版支持):
被动健康检查只有在请求失败后才会发现服务器故障,存在一定延迟。在生产环境中,建议结合外部监控(如 Consul、Prometheus)实现主动健康检查,确保故障服务器被快速摘除。
负载均衡就像医院的分诊台。轮询是按挂号顺序叫号;加权轮询是专家号看 3 个、普通号看 1 个;IP Hash 是你的病历固定在某个医生手上,每次复诊都找同一个;最少连接是谁手头病人少就给谁分配新病人。

Q4: Nginx 静态资源服务

Nginx 处理静态资源的速度远超 Node.js——它用 C 语言实现了零拷贝(sendfile)和内存映射(mmap),是前端项目生产部署的最佳选择。

静态文件配置与缓存策略

gzip 压缩配置

HTTPS + HTTP/2 配置

Chrome 106+ 已移除 HTTP/2 Server Push 支持,原因是实现复杂且效果不如预加载(103 Early Hints)。生产环境中应使用 <link rel="preload"> 或 103 Early Hints 替代。
生产环境部署前端项目时,让 Nginx 直接服务静态资源(而非代理给 Node.js),性能提升可达 10-50 倍。Node.js 只负责处理 API 请求。带 hash 的文件名设为长期缓存(1 年 + immutable),HTML 设为协商缓存(no-cache)。

Q5: Nginx 反向代理消除跨域

浏览器的同源策略(Same-Origin Policy)限制了跨域请求。前端开发中,反向代理是最常用的跨域解决方案——它不改变浏览器行为,而是让请求”看起来”是同源的。

跨域产生的原因

开发环境: webpack-dev-server proxy 配置

生产环境: Nginx 反向代理配置

❌/✅ 代码对比:错误与正确的跨域处理方式


Q6: BFF 架构详解

什么是 BFF?

BFF(Backend For Frontend)是为前端专门定制的后端服务层。它不是通用的后端 API,而是针对特定前端(Web、移动端、小程序)的”适配层”——聚合多个后端服务的接口,裁剪数据格式,处理鉴权和缓存。

为什么需要 BFF?

在没有 BFF 的架构中,前端直接对接多个后端微服务,会面临三个痛点: 痛点一:接口聚合。 一个页面需要调用 5 个微服务接口,前端发 5 次请求,页面加载慢,代码复杂。 痛点二:数据裁剪。 后端返回的通用数据结构包含大量前端不需要的字段,浪费带宽。Web 端和移动端需要的数据字段不同。 痛点三:逻辑下沉。 鉴权、数据格式转换、错误处理等逻辑散落在每个前端项目中,重复代码难以维护。

BFF 的典型场景

Node.js BFF 实现示例

BFF 的优缺点

BFF 就像一位翻译官。你在法国旅游(前端),不懂法语(后端微服务的各种接口协议)。你告诉翻译官”我想点一份牛排和一杯红酒”(一次请求),翻译官分别跟厨师和酒保沟通(聚合多个后端接口),最后把你要的东西一起端上来(裁剪后的数据)。不同国家的游客(Web/移动端)有不同的翻译官(不同的 BFF),各自处理不同的需求。

Q7: 高频面试题

基础梳理

专家视角深度剖析

很多人能说出 Nginx 是反向代理、负载均衡,但实际更在意的是:你的前端项目在生产环境到底是怎么部署的?请求链路是怎样的? 一个合格的前端工程师应该能清晰描述:浏览器发起请求 → CDN 缓存 → Nginx 入口 → 路由分发(静态资源直返 / API 转发)→ BFF 聚合层 → 后端微服务。Nginx 在这条链路中承担了 SSL 终止、负载均衡、静态资源服务、反向代理、gzip 压缩等多项职责。理解这条完整的请求链路,才是区分”会用脚手架”和”懂工程化”的关键标准。[2]

🔍 底层源码:Nginx 事件驱动是如何实现的?

Nginx 的事件循环基于操作系统的事件通知机制(Linux 上是 epoll,macOS 上是 kqueue),核心架构是一个 master-worker 多进程模型(详见 Nginx 源码 src/event/ngx_event.c):
这段代码做了 4 件事:通过 epoll_wait 监听所有连接的 I/O 事件;将就绪事件逐个分发给对应的 handler;一个 worker 进程可以同时管理数千个连接;定时器驱动超时检测和健康检查。[1] Nginx 使用 Linux 的 sendfile() 系统调用实现零拷贝(详见 Nginx 源码 src/os/unix/ngx_linux_sendfile_chain.c):
传统方式需要 4 次上下文切换和 4 次数据拷贝,sendfile 只需要 2 次上下文切换和 2 次拷贝(DMA 拷贝到网卡),在大文件传输场景下性能提升显著。[1]

十、及格回答 vs 满分回答

问题:“你们前端项目的生产部署架构是怎样的?Nginx 在其中扮演什么角色?”

及格回答(60-70分):
“我们用 Nginx 做反向代理,把 API 请求转发给后端。静态资源直接由 Nginx 托管,前端打包后放到服务器上。”
问题出在:知道 Nginx 能做代理,但不清楚完整的请求链路和配置细节。 满分回答(90-100分):
“我们的请求链路是这样的:用户浏览器 → CDN(静态资源缓存)→ Nginx 入口层。Nginx 承担了四项职责:第一,SSL 终止——在 Nginx 层完成 TLS 握手,后端服务之间用 HTTP 通信,降低后端的 CPU 压力。第二,静态资源服务——带 hash 的 JS/CSS 文件设置 1 年长期缓存(Cache-Control: public, immutable),HTML 文件设置协商缓存(no-cache),这样每次发版只有变更的 chunk 需要重新下载。Nginx 开启了 sendfile 零拷贝和 gzip 压缩,静态资源的传输性能是 Node.js 的 10 倍以上。[1]
第三,API 反向代理——/api/ 路径的请求通过 proxy_pass 转发到 Node.js BFF 服务。Nginx 设置了 proxy_set_header 把客户端真实 IP、协议等信息传递给后端。对于 WebSocket 长连接,配置了 proxy_http_version 1.1 和 Upgrade 头。第四,负载均衡——BFF 服务部署了 3 个实例,用 upstream + ip_hash 策略保证同一用户的请求打到同一台服务器,解决了 Session 一致性问题。健康检查配置了 max_fails=3 fail_timeout=30s。[2]
踩过的坑:有一次上线后用户反馈接口 502,排查发现是 Nginx 默认的 proxy_read_timeout 只有 60 秒,而一个复杂报表接口的后端处理超过了 60 秒,Nginx 主动断开了连接。后来对长耗时接口单独配置了 300 秒超时。还有一个坑是 proxy_pass 末尾有没有 / 的区别——proxy_pass http://backend 会把 /api/users 转发为 /api/users,而 proxy_pass http://backend/ 会转发为 /users,少一个 / 就多一层路径。[3]
我们的 BFF 层用 Node.js 实现,主要做三件事:接口聚合(首页需要用户信息 + 推荐列表 + 通知数,BFF 并行请求三个微服务后合并返回)、数据裁剪(移动端只需要商品的 10 个字段,而不是后端返回的 50 个)、鉴权前置(在 BFF 层统一校验 Token,后端服务只处理业务逻辑)。“

常见追问

题目来源于真实面经(字节/腾讯/阿里前端面试、牛客网、haizlin/fe-interview),按难度递进。每个模块 8 个 Accordion,覆盖 4 个难度层级。
答题要点: 不是越大越好,要匹配 CPU 核心数。高分回答: worker_processes 应设置为 CPU 核心数(或 auto)。Nginx 的每个 worker 进程是一个独立进程,绑定一个 CPU 核心后可以避免上下文切换开销。如果设置为 8 核机器上跑 16 个 worker,反而会因为 CPU 调度竞争导致性能下降。每个 worker 进程通过 epoll 独立处理数千个连接,所以 8 个 worker 就能轻松处理数万并发。可以通过 worker_cpu_affinity 绑定核心,让每个 worker 独占一个 CPU,避免缓存失效(Cache Line Bouncing)。此外,worker_connections 决定每个 worker 的最大连接数,理论最大并发数 = worker_processes x worker_connections。
答题要点: 这是 Nginx 反向代理最经典的坑,加 / 会截断 location 匹配的路径前缀。高分回答: 假设 location /api/ 匹配了请求 /api/users:proxy_pass http://backend 不加斜杠,请求转发为 /api/users(保留完整路径);proxy_pass http://backend/ 加斜杠,请求转发为 /users(截掉了 /api/ 前缀)。这是因为加了 / 后,Nginx 把 http://backend/ 视为 URI,将 location 匹配部分替换掉。如果后端接口路径不包含 /api 前缀,必须加 /;如果后端也需要 /api 前缀,就不能加。最佳实践是用 proxy_pass http://backend/api/ 明确指定转发路径,避免歧义。
答题要点: master 负责管理和监控,worker 负责处理请求。理解这个分工对调优和排障至关重要。高分回答: master 进程以 root 权限运行,负责读取配置文件、管理 worker 进程的生命周期(启动、停止、重启)、处理热重载信号(nginx -s reload)。worker 进程以普通用户(如 www-data)权限运行,每个 worker 是一个独立进程,通过 epoll 事件循环处理实际的客户端连接。热重载的原理是:master 收到 SIGHUP 信号后,根据新配置启动新的 worker 进程,同时向旧 worker 发送优雅退出信号——旧 worker 停止接收新连接,但继续处理已有连接直到完成,然后退出。这种”滚动更新”保证了配置切换期间零中断。可以通过 ps aux | grep nginx 看到一个 master 和多个 worker 进程。
答题要点: Nginx 内置了 limit_req 和 limit_conn 两个模块,分别限制请求速率和并发连接数。高分回答: limit_req_zone 基于漏桶算法(Leaky Bucket)——请求进入桶中,以固定速率流出处理;桶满时拒绝新请求。配置示例:limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; 表示每个 IP 每秒最多 10 个请求。burst=20 参数允许突发 20 个请求排队。limit_conn_zone 基于并发连接数——限制同一 IP 的同时连接数。配置示例:limit_conn_zone $binary_remote_addr zone=addr:10m; limit_conn addr 100; 表示每个 IP 最多 100 个并发连接。这两个模块常用于 API 防刷、防止 DDoS 攻击。注意 limit_req 的 burst 和 nodelay 参数组合可以控制突发请求是排队还是直接拒绝。
答题要点: 这是架构选型题,没有标准答案,需要分场景讨论。高分回答: 取决于团队规模和业务复杂度。小型团队(3-5 人)建议统一一个 BFF——代码少、维护成本低、所有端共用一套聚合逻辑。中大型团队建议按端拆分——Web BFF 和 Mobile BFF 各自独立,因为两端的数据结构差异很大(Web 需要富文本、分页、筛选条件;移动端需要精简数据、懒加载、离线缓存)。如果团队更大(20+ 前端),可以考虑按业务域拆分 BFF——商品 BFF、订单 BFF、用户 BFF,每个 BFF 由对应的前端业务团队维护。还有一种方案是用 GraphQL 代替 BFF——前端通过 GraphQL 查询自己需要的字段,后端提供统一的 GraphQL Schema,省去了 BFF 层。但 GraphQL 需要后端配合,改造成本较高。
答题要点: Nginx 性能远超 Node.js,差距在于语言层面(C vs JS)和系统调用层面(sendfile vs read+write)。高分回答: 基准测试中,Nginx 处理静态文件的 QPS(每秒查询数)是 Node.js 的 10-50 倍。根本原因有三个:第一,Nginx 用 C 语言实现,直接操作内核系统调用,没有 V8 引擎的 GC(垃圾回收)暂停和 JIT(即时编译)开销。第二,Nginx 使用 sendfile() 系统调用实现零拷贝——文件数据从磁盘直接通过 DMA 传到网卡,不经过用户态内存拷贝;Node.js 的 fs.readFile 需要先将文件读入用户态 Buffer,再写入 socket 缓冲区,至少多两次拷贝。第三,Nginx 的 open_file_cache 可以缓存文件描述符和元数据,避免重复的 open() 和 stat() 系统调用。所以生产环境中,静态资源一定交给 Nginx 处理,Node.js 只负责动态 API。
答题要点: 502 代表 Nginx 收到了无效的上游响应,需要从后端服务、网络、超时三个维度排查。高分回答: 502 表示 Nginx 作为网关或代理,从上游服务器收到了无效响应。排查步骤:第一步,看 Nginx 错误日志(/var/log/nginx/error.log),常见信息是 connect() failed (111: Connection refused) ——说明后端服务没有启动或端口不对;upstream timed out (110: Connection timed out) ——说明后端处理超时。第二步,检查后端服务是否存活——curl http://localhost:3000/health 直接请求后端,排除 Nginx 层的问题。第三步,检查 Nginx 配置——proxy_pass 的地址和端口是否正确,upstream 中的服务器是否都可达。第四步,调整超时参数——proxy_connect_timeout(连接超时)、proxy_read_timeout(读取超时)、proxy_send_timeout(发送超时),默认都是 60 秒。第五步,检查系统资源——dmesg 看是否有 OOM(内存溢出),ss -s 看连接数是否达到上限,ulimit -n 看文件描述符限制。一个常见的坑是:后端服务重启期间(约 3-5 秒),所有请求都会 502,需要用 upstream 的 backup 服务器做故障转移。
答题要点: BFF 作为中间层,一旦故障会阻断所有前端请求。需要从部署、降级、监控三个维度保障高可用。高分回答: BFF 高可用的核心策略有四个。第一,多实例部署 + 负载均衡——BFF 至少部署 2 个实例,Nginx 通过 upstream 配置健康检查(max_fails=3 fail_timeout=30s),故障实例自动摘除。第二,优雅降级(Graceful Degradation)——BFF 应该对每个后端请求独立 try-catch,某个微服务超时时返回降级数据(如缓存数据或默认值),而不是整体 502。示例代码:const userInfo = await fetchUser().catch(() => cachedUser || defaultUser)。第三,熔断机制(Circuit Breaker)——当某个后端服务的错误率超过阈值(如 50%),自动”熔断”——后续请求直接返回降级数据,不再打向后端,防止雪崩。可以用 opossum 等 Node.js 熔断库实现。第四,请求超时控制——BFF 的每个请求都要设置超时(如 3 秒),避免一个慢接口拖垮整个 BFF。Promise.race([fetchAPI(), timeout(3000)]) 是最简单的实现方式。监控方面,BFF 需要上报 QPS、P99 延迟、错误率等指标,配合告警及时发现问题。

📚 延伸阅读与引用参考