文章标签: #nginx #反向代理 #负载均衡 #高性能 #epoll #事件驱动 #源码分析 #运维 #面试
- 引言:Nginx的技术本质
- 理论基础:为什么Nginx能支撑10万并发
- 来龙去脉:Web服务器的架构演进
- 核心架构深度解析
- 负载均衡算法底层原理
- 源码深度分析:连接处理全流程
- 工业级配置实战
- Nginx vs Apache vs Caddy:架构对比
- 性能分析与调优
- 常见陷阱与最佳实践
- 面试题与参考答案
Nginx不是"另一个Web服务器",而是基于事件驱动的异步非阻塞I/O架构的高性能网络中间件。
核心认知:
传统Web服务器(Apache Prefork)的本质:
每个连接 = 一个进程/线程
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Client1 │ │ Client2 │ │ Client3 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ Process1│ │ Process2│ │ Process3│
│ 阻塞等待 │ │ 阻塞等待 │ │ 阻塞等待 │
└─────────┘ └─────────┘ └─────────┘
Nginx的本质:
一个Worker进程处理数万个连接
┌─────────┐ ┌─────────┐ ┌─────────┐ ... ┌──────────┐
│ Client1 │ │ Client2 │ │ Client3 │ │ Client10K│
└────┬────┘ └────┬────┘ └────┬────┘ └────┬─────┘
│ │ │ │
└────────────┴────────────┴────────┬────────┘
│
┌─────────▼──────────┐
│ Worker进程 │
│ ┌──────────────┐ │
│ │ 事件循环 │ │
│ │ epoll_wait │ │
│ └──────────────┘ │
└────────────────────┘
关键洞察:Nginx的高性能不取决于"代码写得好",而取决于架构设计彻底摒弃了每个连接一个线程的阻塞模型,将I/O等待从线程占用转化为事件回调。
阻塞I/O模型(Blocking I/O):
应用进程 内核
│ read() │
│────────────────>│
│ │ 数据未就绪
│ 阻塞等待 │
│<────────────────│ 数据就绪,复制到用户态
│ 返回数据 │
│ │
问题:一个线程阻塞在一个连接上,无法处理其他连接
10K连接 = 10K线程 → 上下文切换开销巨大,内存占用爆炸
I/O多路复用模型(select/poll/epoll):
应用进程 内核
│ select(epoll) │
│────────────────>│
│ │ 监控多个fd
│ 阻塞等待 │ 任一fd就绪即返回
│<────────────────│
│ 返回就绪fd列表 │
│ │
│ read(fd1) │ 非阻塞读取
│ read(fd2) │ 非阻塞读取
│ │
突破:一个线程监控数万个连接,只处理就绪的连接
| 特性 | select | poll | epoll |
|---|---|---|---|
| 数据结构 | fd_set(位图) | 链表 | 红黑树 + 就绪链表 |
| 最大fd限制 | 1024(默认) | 无限制 | 无限制 |
| 时间复杂度 | O(n) | O(n) | O(1) |
| 触发模式 | 水平触发 | 水平触发 | 支持边缘触发 |
| 数据拷贝 | 每次调用拷贝fd_set | 每次调用拷贝数组 | 首次epoll_ctl拷贝,后续不拷贝 |
// select 的问题:每次调用都要拷贝整个fd_set到内核
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fd1, &readfds);
FD_SET(fd2, &readfds);
// ... 每次select都要拷贝readfds
select(max_fd + 1, &readfds, NULL, NULL, NULL);
// epoll 的优势:fd只需注册一次
int epoll_fd = epoll_create1(0);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd1;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd1, &ev); // 只注册一次
// 等待事件,返回就绪的fd列表(只返回就绪的,不遍历全部)
struct epoll_event events[MAX_EVENTS];
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);关键理解:
- select/poll 的问题在于每次都要遍历所有fd,连接数增加时性能线性下降
- epoll 使用回调机制:fd就绪时内核通过回调函数将其加入就绪链表,epoll_wait直接返回就绪列表,时间复杂度O(1)
- Nginx在Linux上使用epoll,在FreeBSD/macOS上使用kqueue(类似epoll)
Nginx事件循环(Event Loop)的核心逻辑:
┌─────────────────────────────────────────┐
│ Worker进程启动 │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 1. 初始化:监听端口,注册epoll事件 │
└─────────────────┬───────────────────────┘
│
▼
┌─────────────────┐
│ 事件循环开始 │◄─────────────────────┐
└────────┬────────┘ │
│ │
▼ │
┌─────────────────────────────────────────┐ │
│ 2. epoll_wait(timeout) │ │
│ 阻塞等待I/O事件或定时器到期 │ │
└─────────────────┬───────────────────────┘ │
│ │
▼ │
┌─────────────────────────────────────────┐ │
│ 3. 分发事件(Event Dispatch) │ │
│ - 新连接事件 → accept_handler() │ │
│ - 可读事件 → read_handler() │ │
│ - 可写事件 → write_handler() │ │
│ - 定时器事件 → timer_handler() │ │
└─────────────────┬───────────────────────┘ │
│ │
▼ │
┌─────────────────────────────────────────┐ │
│ 4. 处理事件(非阻塞) │ │
│ - 读取请求 → 解析HTTP │ │
│ - 处理逻辑 → 访问静态文件/反向代理 │ │
│ - 发送响应 → 注册可写事件 │ │
└─────────────────┬───────────────────────┘ │
│ │
▼ │
┌─────────────────┐ │
│ 继续下一轮 │────────────────────┘
└─────────────────┘
关键:没有线程阻塞在I/O等待上,所有等待都在epoll_wait中统一处理
传统文件发送的数据拷贝路径:
磁盘 → 内核PageCache → 用户缓冲区 → 内核Socket缓冲区 → 网卡
(DMA拷贝) (CPU拷贝) (CPU拷贝) (DMA拷贝)
4次上下文切换,4次数据拷贝(2次DMA,2次CPU)
Nginx sendfile零拷贝:
磁盘 → 内核PageCache ──────────────> 网卡
(DMA拷贝) (DMA拷贝,通过sendfile系统调用)
2次上下文切换,2次DMA拷贝,0次CPU拷贝
┌─────────────────────────────────────────┐
│ 用户态 │
│ sendfile(fd_in, fd_out, offset, len) │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 内核态 │
│ 1. DMA从磁盘读取到PageCache │
│ 2. 直接将PageCache引用传递给Socket │
│ 3. DMA从Socket缓冲区发送到网卡 │
│ │
│ 不需要经过用户态缓冲区! │
└─────────────────────────────────────────┘
Nginx配置:
http {
# 开启sendfile零拷贝
sendfile on;
# 配合tcp_nopush,累积数据包减少网络拥塞
tcp_nopush on;
# 快速响应小数据包
tcp_nodelay on;
}Apache Prefork MPM:
┌──────────────────────────────────────┐
│ Apache主进程 │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │子进程1 │ │子进程2 │ │子进程3 │ │
│ │处理请求│ │处理请求│ │处理请求│ │
│ │阻塞I/O │ │阻塞I/O │ │阻塞I/O │ │
│ └────────┘ └────────┘ └────────┘ │
└──────────────────────────────────────┘
优点:稳定性好,一个进程崩溃不影响其他进程
缺点:进程切换开销大,内存占用高,C10K问题
每个进程通常占用2-10MB内存,10K连接 = 20-100GB内存
Apache Worker MPM:
┌──────────────────────────────────────┐
│ Apache主进程 │
│ ┌────────────────────────────────┐ │
│ │ 线程池(每个进程) │ │
│ │ ┌────┐┌────┐┌────┐┌────┐ │ │
│ │ │线程││线程││线程││线程│ │ │
│ │ └────┘└────┘└────┘└────┘ │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘
优点:线程切换比进程切换快,内存共享
缺点:线程仍阻塞在I/O上,并发能力有限
多线程编程复杂,锁竞争严重
Nginx Master-Worker模型:
┌──────────────────────────────────────┐
│ Master进程 │
│ - 读取配置 │
│ - 管理Worker进程(fork/spawn) │
│ - 处理信号(reload/stop/quit) │
│ - 不处理客户端请求 │
└──────────────┬───────────────────────┘
│ fork
┌──────────┼──────────┐
▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐
│Worker1│ │Worker2│ │Worker3│
│epoll │ │epoll │ │epoll │
│事件循环│ │事件循环│ │事件循环│
└───────┘ └───────┘ └───────┘
特点:
1. Worker进程之间相互独立,无共享状态(无锁设计)
2. 每个Worker单线程,通过epoll处理数万连接
3. Master负责管理,Worker负责干活,职责分离
多进程单线程的优势:
1. 无锁编程(Lock-Free)
- 每个Worker独立处理连接,无需锁竞争
- 避免了多线程的复杂同步问题
2. 容错性
- 一个Worker崩溃,Master可以立即fork新的Worker
- 不影响其他Worker和整体服务
3. 利用多核CPU
- 每个Worker绑定到一个CPU核心
- 避免进程迁移带来的缓存失效
4. 简单性
- 单线程代码比多线程代码简单得多
- 没有死锁、竞态条件、线程安全等问题
┌─────────────────────────────────────┐
│ CPU 0 │
│ ┌─────────────────────────────┐ │
│ │ Worker 0(绑定CPU0) │ │
│ │ - 独立内存空间 │ │
│ │ - 独立epoll实例 │ │
│ │ - 无锁,无竞态 │ │
│ └─────────────────────────────┘ │
├─────────────────────────────────────┤
│ CPU 1 │
│ ┌─────────────────────────────┐ │
│ │ Worker 1(绑定CPU1) │ │
│ │ - 独立内存空间 │ │
│ │ - 独立epoll实例 │ │
│ │ - 无锁,无竞态 │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
配置Worker与CPU绑定:
worker_processes auto; # 自动设置为CPU核心数
worker_cpu_affinity auto; # 自动绑定CPUNginx进程家族:
┌──────────────────────────────────────────────┐
│ Master Process (root) │
│ PID: 1234 │
│ ├─ 读取并验证nginx.conf │
│ ├─ 创建监听socket(bind/listen) │
│ ├─ fork Worker进程 │
│ ├─ 处理信号(HUP, USR1, USR2, WINCH, QUIT) │
│ └─ 管理Cache Loader/Cache Manager │
└────────────────────┬─────────────────────────┘
│ fork
┌────────────┼────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Worker1 │ │ Worker2 │ │ Worker3 │
│ (nobody)│ │ (nobody)│ │ (nobody)│
│ PID:1235│ │ PID:1236│ │ PID:1237│
└─────────┘ └─────────┘ └─────────┘
│ │ │
└────────────┴────────────┘
│
监听相同端口
(SO_REUSEPORT)
信号处理:
- HUP : 优雅重载配置(reload)
- USR1 : 重新打开日志文件(日志切割)
- USR2 : 热升级(平滑升级到新版Nginx)
- WINCH: 优雅关闭Worker进程
- QUIT : 优雅退出
- TERM : 快速退出
Nginx将HTTP请求处理划分为11个阶段(phase),每个阶段由不同的模块处理:
HTTP请求处理阶段(ngx_http_phases):
┌─────┬────────────────────────┬──────────────────────────────┐
│ 阶段 │ 名称 │ 典型处理模块 │
├─────┼────────────────────────┼──────────────────────────────┤
│ 1 │ NGX_HTTP_POST_READ │ realip模块(读取真实IP) │
├─────┼────────────────────────┼──────────────────────────────┤
│ 2 │ NGX_HTTP_SERVER_REWRITE │ rewrite模块(server级别重写) │
├─────┼────────────────────────┼──────────────────────────────┤
│ 3 │ NGX_HTTP_FIND_CONFIG │ 核心:根据URI定位location │
├─────┼────────────────────────┼──────────────────────────────┤
│ 4 │ NGX_HTTP_REWRITE │ rewrite模块(location级别) │
├─────┼────────────────────────┼──────────────────────────────┤
│ 5 │ NGX_HTTP_POST_REWRITE │ 检查rewrite次数(防死循环) │
├─────┼────────────────────────┼──────────────────────────────┤
│ 6 │ NGX_HTTP_PREACCESS │ limit_req, limit_conn(限流) │
├─────┼────────────────────────┼──────────────────────────────┤
│ 7 │ NGX_HTTP_ACCESS │ access, auth_basic(认证) │
├─────┼────────────────────────┼──────────────────────────────┤
│ 8 │ NGX_HTTP_POST_ACCESS │ 检查访问权限结果 │
├─────┼────────────────────────┼──────────────────────────────┤
│ 9 │ NGX_HTTP_TRY_FILES │ try_files指令 │
├─────┼────────────────────────┼──────────────────────────────┤
│ 10 │ NGX_HTTP_CONTENT │ proxy, fastcgi, static(内容)│
├─────┼────────────────────────┼──────────────────────────────┤
│ 11 │ NGX_HTTP_LOG │ access_log模块(日志) │
└─────┴────────────────────────┴──────────────────────────────┘
请求处理流程:
Client Request
│
▼
┌──────────────┐
│ POST_READ │ ──> 获取真实IP(X-Forwarded-For)
└──────┬───────┘
│
┌──────▼───────┐
│SERVER_REWRITE│ ──> server块rewrite
└──────┬───────┘
│
┌──────▼───────┐
│FIND_CONFIG │ ──> location匹配(关键阶段!)
└──────┬───────┘
│
┌──────▼───────┐
│ REWRITE │ ──> location块rewrite
└──────┬───────┘
│
...
│
┌──────▼───────┐
│ CONTENT │ ──> 生成响应(proxy_pass / static)
└──────┬───────┘
│
┌──────▼───────┐
│ LOG │ ──> 记录访问日志
└──────────────┘
Nginx使用内存池管理内存,避免频繁的malloc/free:
Nginx内存池结构:
┌─────────────────────────────────────────┐
│ ngx_pool_t(内存池) │
│ ┌─────────────────────────────────┐ │
│ │ 小块内存(<4KB):链表管理 │ │
│ │ ┌────────┐ ┌────────┐ │ │
│ │ │ 4KB块1 │──>│ 4KB块2 │──> ... │ │
│ │ └────────┘ └────────┘ │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ 大块内存(≥4KB):直接malloc │ │
│ │ ┌────────┐ ┌────────┐ │ │
│ │ │ 大块1 │──>│ 大块2 │──> ... │ │
│ │ └────────┘ └────────┘ │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ 清理回调(cleanup):释放资源 │ │
│ │ ┌────────┐ ┌────────┐ │ │
│ │ │ fd关闭 │──>│ 内存释放│──> ... │ │
│ │ └────────┘ └────────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
内存分配策略:
1. 小块内存:从当前block分配,block用完申请新block
2. 大块内存:直接调用malloc,单独管理
3. 内存只在池销毁时统一释放(无单独free)
优势:
- 减少内存碎片
- 避免频繁系统调用
- 请求处理完毕一次性释放,无内存泄漏风险
| 算法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 轮询(Round Robin) | 后端性能均等 | 简单均匀 | 不考虑服务器负载 |
| 加权轮询(Weighted RR) | 后端性能不均 | 按能力分配 | 需要人工配置权重 |
| IP哈希(IP Hash) | 会话保持 | 同一用户固定路由 | 后端变化时大量重哈希 |
| 最少连接(Least Conn) | 长连接场景 | 按实际负载分配 | 需要维护连接计数 |
| 一致性哈希(Consistent Hash) | 缓存场景 | 节点变化影响小 | 实现复杂,可能有倾斜 |
Nginx使用**平滑加权轮询(Smooth Weighted Round Robin)**算法,避免普通加权轮询的"突发"问题:
普通加权轮询的问题:
服务器A权重5,服务器B权重1
分配序列:A A A A A B A A A A A B ...
问题:前5个请求全部打到A,然后才到B,不够平滑
平滑加权轮询(Nginx算法):
初始化:
Server A: weight=5, current_weight=0
Server B: weight=1, current_weight=0
第1轮:
A: current_weight = 0 + 5 = 5
B: current_weight = 0 + 1 = 1
选择A(最大),A.current_weight = 5 - (5+1) = -1
第2轮:
A: current_weight = -1 + 5 = 4
B: current_weight = 1 + 1 = 2
选择A,A.current_weight = 4 - 6 = -2
第3轮:
A: current_weight = -2 + 5 = 3
B: current_weight = 2 + 1 = 3
选择A(相等选第一个),A.current_weight = 3 - 6 = -3
第4轮:
A: current_weight = -3 + 5 = 2
B: current_weight = 3 + 1 = 4
选择B,B.current_weight = 4 - 6 = -2
分配序列:A A A B A A ...
结果:6个请求中A占5个,B占1个,且分布均匀
一致性哈希原理:
普通哈希:hash(key) % N
问题:N变化时,几乎所有key都重新映射
一致性哈希:
将hash空间组织成一个环(0 ~ 2^32-1)
hash环
0 ┌─────────────────┐ 2^32
│ 服务器A │
│ ● │
│ (hash=100) │
│ │
│ 服务器B │
│ ● │
│ (hash=1000) │
│ │
│ 服务器C │
│ ● │
│ (hash=5000) │
└─────────────────┘
Key的分配规则:
hash(key) 顺时针找到的第一个服务器
Key1(hash=50) ──> 服务器A(100)
Key2(hash=800) ──> 服务器B(1000)
Key3(hash=3000)──> 服务器C(5000)
优势:服务器B宕机时,只有B到C之间的key受影响
其他key的映射关系不变!
虚拟节点(解决倾斜问题):
每个物理服务器对应多个虚拟节点
服务器A: A#1, A#2, A#3 ...
使key分布更均匀
Nginx配置:
upstream backend {
# 一致性哈希,基于request_uri
hash $request_uri consistent;
server 192.168.1.1:8080;
server 192.168.1.2:8080;
server 192.168.1.3:8080;
}Nginx处理新连接的事件驱动流程:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 内核TCP层 │ │ Nginx核心 │ │ HTTP模块 │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ 1. SYN包到达 │ │
│──────────────────>│ │
│ │ │
│ 2. 完成三次握手 │ │
│ 触发epoll事件 │ │
│──────────────────>│ │
│ │ │
│ │ 3. accept新连接 │
│ │ ngx_event_accept│
│ │ │
│ │ 4. 创建ngx_connection_t
│ │ 初始化连接结构体 │
│ │ │
│ │ 5. 注册可读事件 │
│ │ epoll_ctl(ADD) │
│ │ │
│ │ 6. 等待HTTP请求 │
│ │ │
│ 7. 客户端发送请求 │ │
│──────────────────>│ │
│ │ │
│ │ 8. 触发read事件 │
│ │ 调用read_handler│
│ │ │
│ │ 9. HTTP状态机解析 │
│ │ 解析请求行/头部 │
│ │ │
│ │ 10. 进入11个phase │
│ │ 处理请求 │
│ │ │
│ │ 11. 生成响应 │
│ │ 注册可写事件 │
│ │ │
│ 12. 发送响应 │ │
│<──────────────────│ │
│ │ │
│ 13. 保持连接或关闭 │ │
│ │ │
关键数据结构:
- ngx_connection_t:连接对象,封装socket fd
- ngx_event_t:事件对象,关联回调函数
- ngx_http_request_t:HTTP请求对象
Nginx HTTP请求解析状态机:
┌──────────────┐
│ sw_start │
└──────┬───────┘
│ 读取第一个字符
▼
┌──────────────┐ ┌──────────────┐
│ sw_method │────>│ sw_spaces │
│ 解析HTTP方法 │ │ 跳过空格 │
└──────────────┘ └──────┬───────┘
│
▼
┌──────────────┐
│ sw_uri │
│ 解析URI │
└──────┬───────┘
│
▼
┌──────────────┐
│ sw_http_H │
│ 等待"HTTP" │
└──────┬───────┘
│
▼
┌──────────────┐
│ sw_major_digit│
│ 解析主版本号 │
└──────┬───────┘
│
▼
┌──────────────┐
│ sw_minor_digit│
│ 解析次版本号 │
└──────┬───────┘
│
▼
┌──────────────┐
│ sw_done │
│ 请求行解析完成│
└──────────────┘
解析完成后进入HTTP头部解析(类似的状态机)
为什么用状态机而非正则?
- 状态机是O(n)线性扫描,性能最高
- 流式处理,不需要等完整请求到达
- 内存占用固定,不受输入大小影响
Nginx Location匹配流程(ngx_http_core_find_location):
1. 精确匹配(=)优先检查
location = /exact {
# 只有URI完全等于/exact才匹配
# O(1)哈希查找
}
2. 前缀字符串匹配(无修饰符)
location /prefix {
# 最长前缀匹配
# 存储在二叉搜索树中,O(logN)
}
3. 优先前缀匹配(^~)
location ^~ /static {
# 如果匹配成功,停止检查正则
# 用于提高性能(跳过正则匹配)
}
4. 正则匹配(~ 和 ~*)
location ~ \.php$ {
# 按配置文件中定义的顺序依次匹配
# 第一个匹配的正则生效
}
location ~* \.(jpg|png)$ {
# ~* 不区分大小写
}
5. 通用匹配(/)
location / {
# 最低优先级,兜底匹配
}
匹配顺序图示:
URI: /api/v1/users
检查顺序:
1. = /api/v1/users ? 否
2. = /api ? 否
3. 最长前缀匹配 ? /api/v1 (长度7)
4. ^~ /api/v1 ? 如果匹配,停止
5. ~ /api/.* ? 按顺序检查正则
6. ~ /.* ? 第一个匹配的正则
7. 无修饰符最长前缀 -> 使用 /api/v1
关键源码逻辑:
static ngx_int_t
ngx_http_core_find_location(ngx_http_request_t *r) {
// 1. 检查精确匹配(哈希表O(1))
rc = ngx_http_core_find_static_location(r, clcf->locations);
if (rc == NGX_OK && r->loc_conf == ...) {
return NGX_OK; // 精确匹配直接返回
}
// 2. 检查前缀匹配(二叉树O(logN))
// 3. 检查正则匹配(顺序遍历)
// 4. 返回最长前缀匹配结果
}
工业级Location配置示例:
server {
listen 80;
server_name api.example.com;
# 1. 精确匹配:最高优先级
location = /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
# 2. 优先前缀匹配:匹配后跳过正则(性能优化)
location ^~ /static/ {
alias /data/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# 3. 正则匹配:区分大小写
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# 4. 正则匹配:不区分大小写(图片缓存)
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ {
root /data/static;
expires 30d;
access_log off;
}
# 5. 通用匹配:兜底
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}高可用架构设计:
┌─────────────┐
│ 客户端 │
└──────┬──────┘
│
┌──────▼──────┐
│ 虚拟IP(VIP) │ 10.0.0.100
│ (浮动IP) │
└──────┬──────┘
│
┌────────────┴────────────┐
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ Nginx主节点 │ │ Nginx备节点 │
│ Keepalived │◄───────►│ Keepalived │
│ MASTER │ VRRP │ BACKUP │
│ 10.0.0.10 │ │ 10.0.0.11 │
└──────┬──────┘ └─────────────┘
│
┌──────┴──────┐
│ │
┌────▼────┐ ┌────▼────┐
│Backend1 │ │Backend2 │
└─────────┘ └─────────┘
Keepalived通过VRRP协议实现VIP漂移:
- 主节点宕机,VIP自动漂移到备节点
- 客户端始终访问VIP,无感知切换
Keepalived配置(/etc/keepalived/keepalived.conf):
# 主节点配置
vrrp_script chk_nginx {
script "/etc/keepalived/check_nginx.sh" # 检查Nginx是否存活
interval 2
weight -20
}
vrrp_instance VI_1 {
state MASTER
interface eth0
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass 1234
}
virtual_ipaddress {
10.0.0.100/24
}
track_script {
chk_nginx
}
}
# 检查脚本
#!/bin/bash
# /etc/keepalived/check_nginx.sh
curl -f http://localhost/health || exit 1# 微服务API网关完整配置
upstream user_service {
least_conn;
server 10.0.1.10:8080 weight=5 max_fails=3 fail_timeout=30s;
server 10.0.1.11:8080 weight=5 max_fails=3 fail_timeout=30s;
server 10.0.1.12:8080 backup;
keepalive 32; # 长连接数
}
upstream order_service {
least_conn;
server 10.0.1.20:8080 weight=5 max_fails=3 fail_timeout=30s;
server 10.0.1.21:8080 weight=5 max_fails=3 fail_timeout=30s;
keepalive 32;
}
# 限流区域定义
limit_req_zone $binary_remote_addr zone=req_limit:10m rate=100r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {
listen 80;
server_name api.example.com;
# 日志格式(JSON,便于ELK收集)
log_format json_analytics escape=json '{'
'"time":"$time_iso8601",'
'"remote_addr":"$remote_addr",'
'"request":"$request",'
'"status":$status,'
'"request_time":$request_time,'
'"upstream_time":"$upstream_response_time",'
'"body_bytes_sent":$body_bytes_sent'
'}';
access_log /var/log/nginx/api-access.log json_analytics;
# 全局限流
limit_req zone=req_limit burst=200 nodelay;
limit_conn conn_limit 50;
# 错误页面
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# 用户服务路由
location /api/users/ {
# 重写去掉/api前缀
rewrite ^/api/users/(.*) /$1 break;
proxy_pass http://user_service;
proxy_http_version 1.1;
proxy_set_header Connection "";
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-Request-ID $request_id;
# 超时配置
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 30s;
# 缓冲区
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
# 订单服务路由
location /api/orders/ {
rewrite ^/api/orders/(.*) /$1 break;
proxy_pass http://order_service;
proxy_http_version 1.1;
proxy_set_header Connection "";
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-Request-ID $request_id;
proxy_connect_timeout 5s;
proxy_send_timeout 10s;
proxy_read_timeout 60s; # 订单处理可能较慢
}
# 静态资源(Swagger UI等)
location /swagger/ {
alias /data/swagger/;
expires 1h;
}
}多级缓存架构设计:
客户端 ──► CDN(边缘缓存)──► Nginx(本地缓存)──► 应用服务器 ──► 数据库
│ │
│ │
静态资源缓存 proxy_cache
(图片/CSS/JS) (动态内容缓存)
缓存层级:
1. 浏览器缓存(Cache-Control)
2. CDN缓存(阿里云/腾讯云/CloudFlare)
3. Nginx代理缓存(proxy_cache)
4. 应用本地缓存(Caffeine/Guava Cache)
5. Redis分布式缓存
6. 数据库
Nginx代理缓存配置:
# 定义缓存区域
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=api_cache:100m
max_size=10g inactive=60m use_temp_path=off;
server {
location /api/ {
proxy_cache api_cache;
proxy_cache_valid 200 302 10m; # 200和302缓存10分钟
proxy_cache_valid 404 1m; # 404缓存1分钟
proxy_cache_use_stale error timeout updating http_500 http_502;
# 缓存Key定义
proxy_cache_key "$scheme$request_method$host$request_uri";
# 添加缓存状态头(调试用)
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://backend;
# 不缓存的条件
proxy_cache_bypass $http_cache_control; # 客户端要求不缓存
proxy_no_cache $http_pragma; # Pragma: no-cache
}
}
# 缓存状态说明:
# HIT - 缓存命中
# MISS - 缓存未命中
# EXPIRED - 缓存过期
# UPDATING - 缓存正在更新
# STALE - 返回过期缓存(后端异常时)server {
listen 443 ssl http2;
server_name www.example.com;
# SSL证书
ssl_certificate /etc/nginx/ssl/example.crt;
ssl_certificate_key /etc/nginx/ssl/example.key;
# SSL安全优化
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# HSTS(强制HTTPS)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# 安全响应头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'" always;
# 防止点击劫持
location / {
# ...
}
}
# 隐藏Nginx版本号
http {
server_tokens off;
}
# 限制请求体大小(防DOS)
client_max_body_size 10m;
# 限制缓冲区(防内存溢出)
client_body_buffer_size 128k;
client_header_buffer_size 1k;
large_client_header_buffers 4 4k;| 维度 | Nginx | Apache | Caddy |
|---|---|---|---|
| 进程模型 | Master-Worker多进程 | Prefork/Worker/Event多进程/多线程 | 单进程多线程 |
| I/O模型 | 事件驱动(epoll/kqueue) | 阻塞(Prefork)/事件(Event MPM) | 事件驱动(Go netpoll) |
| 并发能力 | 10万+ | 几千(Prefork)/几万(Event) | 几万 |
| 内存占用 | 低(连接不占用线程栈) | 高(每个连接一个线程/进程) | 中等 |
| 配置语法 | 专用DSL | 专用DSL(更复杂) | Caddyfile(简洁) |
| 动态模块 | 需编译 | 支持动态加载 | 静态编译 |
| 自动HTTPS | 需手动配置 | 需手动配置 | 内置自动HTTPS |
| 适用场景 | 反向代理、静态资源、高并发 | 动态内容(PHP)、.htaccess | 快速部署、自动HTTPS |
Nginx(事件驱动):
进程:Worker1 ──► epoll_wait ──► 处理就绪连接 ──► 回到epoll_wait
│ │
└──────── 单线程循环 ──────────┘
并发数 = 内存 / 每个连接开销(约几百字节)
10万并发 ≈ 几百MB内存
Apache Prefork(进程模型):
进程:Process1 ──► accept ──► read(阻塞)──► 处理 ──► write(阻塞)──► close
进程:Process2 ──► accept ──► read(阻塞)──► 处理 ──► write(阻塞)──► close
...
并发数 = 内存 / 每个进程开销(几MB)
1万并发 ≈ 几十GB内存
Apache Event MPM(混合模型):
主线程:epoll_wait ──► 新连接 ──► 分配给工作线程
工作线程:read(阻塞)──► 处理 ──► write(阻塞)
并发数比Prefork高,但仍受限于线程数
基准测试场景:1000并发,持续30秒,请求4KB静态文件
┌────────────┬─────────────┬─────────────┬──────────────┐
│ 服务器 │ 请求/秒 │ 平均延迟 │ 内存占用 │
├────────────┼─────────────┼─────────────┼──────────────┤
│ Nginx │ ~120,000 │ ~8ms │ ~50MB │
│ Apache(P) │ ~8,000 │ ~125ms │ ~2GB │
│ Apache(E) │ ~40,000 │ ~25ms │ ~500MB │
│ Caddy │ ~60,000 │ ~16ms │ ~200MB │
└────────────┴─────────────┴─────────────┴──────────────┘
结论:
- Nginx在静态文件和高并发场景优势明显
- Apache适合动态内容(尤其是PHP,mod_php性能极佳)
- Caddy在易用性和自动HTTPS方面最优
# /etc/sysctl.conf
# TCP连接优化
net.ipv4.tcp_max_tw_buckets = 6000 # TIME_WAIT状态连接数上限
net.ipv4.tcp_sack = 1 # 启用选择性确认
net.ipv4.tcp_window_scaling = 1 # 启用窗口缩放
net.ipv4.tcp_rmem = 4096 87380 4194304 # TCP读缓冲区
net.ipv4.tcp_wmem = 4096 16384 4194304 # TCP写缓冲区
net.core.wmem_default = 8388608 # 默认发送缓冲区
net.core.rmem_default = 8388608 # 默认接收缓冲区
net.core.rmem_max = 16777216 # 最大接收缓冲区
net.core.wmem_max = 16777216 # 最大发送缓冲区
# 连接跟踪优化(如果使用iptables)
net.netfilter.nf_conntrack_max = 655350 # 最大连接跟踪数
# 文件描述符
fs.file-max = 2097152 # 系统最大文件句柄数
fs.nr_open = 2097152 # 单进程最大文件句柄数
# 应用后执行
sysctl -pworker_processes auto; # 自动匹配CPU核心数
worker_rlimit_nofile 65535; # Worker进程最大文件句柄数
events {
worker_connections 65535; # 每个Worker最大连接数
use epoll; # Linux使用epoll
multi_accept on; # 尽可能接受更多连接
}
http {
# 高效文件传输
sendfile on;
tcp_nopush on;
tcp_nodelay on;
# 连接保持
keepalive_timeout 65;
keepalive_requests 1000; # 单个连接最大请求数
# 压缩
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json
application/javascript application/rss+xml
application/atom+xml image/svg+xml;
# 打开文件缓存(提升静态文件性能)
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# 上游长连接
upstream backend {
server 127.0.0.1:8080;
keepalive 100; # 保持100个长连接
}
server {
location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://backend;
}
}
}# 使用wrk进行压测
wrk -t12 -c400 -d30s http://localhost/index.html
# 参数说明:
# -t12: 12个线程
# -c400: 400个并发连接
# -d30s: 持续30秒
# 输出示例:
# Running 30s test @ http://localhost/index.html
# 12 threads and 400 connections
# Thread Stats Avg Stdev Max +/- Stdev
# Latency 8.23ms 12.45ms 234.56ms 98.12%
# Req/Sec 4.12k 512.34 5.67k 78.45%
# 1234567 requests in 30.01s, 4.56GB read
# Requests/sec: 41138.12
# Transfer/sec: 155.78MB
# Nginx状态监控模块
server {
location /nginx_status {
stub_status on;
access_log off;
allow 10.0.0.0/8;
deny all;
}
}
# 状态页输出:
# Active connections: 291 # 当前活跃连接数
# server accepts handled requests # 总接受/处理/请求数
# 16630948 16630948 31070465
# Reading: 6 Writing: 125 Waiting: 160 # 读/写/等待状态连接数陷阱:认为location按书写顺序匹配,导致配置不生效。
真相:location匹配有固定优先级,不按书写顺序。
# 错误配置示例:
location /api/ {
proxy_pass http://api_backend;
}
location ~ ^/api/v1/special {
# 用户期望这个匹配 /api/v1/special
# 但实际上 /api/v1/special 已经被上面的前缀匹配了!
proxy_pass http://special_backend;
}
# 正确做法:将更精确的正则放在前面,或使用 ^~
location ~ ^/api/v1/special {
proxy_pass http://special_backend;
}
location /api/ {
proxy_pass http://api_backend;
}
# 或者使用 ^~ 阻止正则匹配
location ^~ /api/ {
proxy_pass http://api_backend;
}陷阱:后端服务获取的IP都是Nginx的IP,无法做限流、审计。
最佳实践:
location / {
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_pass http://backend;
}陷阱:proxy_buffering关闭或缓冲区太小,导致大量后端连接占用。
# 默认proxy_buffering是on,通常应该保持开启
location / {
proxy_buffering on; # 启用缓冲
proxy_buffer_size 4k; # 响应头缓冲区
proxy_buffers 8 4k; # 8个4KB缓冲区
proxy_busy_buffers_size 8k; # 忙缓冲区大小
proxy_temp_file_write_size 64k; # 临时文件写入大小
proxy_pass http://backend;
}
# 大数据流场景(如下载、视频)应关闭缓冲
location /download/ {
proxy_buffering off; # 关闭缓冲,直接流式传输
proxy_pass http://file_server;
}陷阱:后端服务宕机后,Nginx仍向故障节点转发请求。
最佳实践:
upstream backend {
server 192.168.1.1:8080 max_fails=3 fail_timeout=30s;
server 192.168.1.2:8080 max_fails=3 fail_timeout=30s;
server 192.168.1.3:8080 backup; # 备用服务器
# 主动健康检查(需要nginx_upstream_check_module)
check interval=3000 rise=2 fall=3 timeout=1000 type=http;
check_http_send "GET /health HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
}陷阱:用户上传大文件导致Nginx内存占用过高。
# 全局限制
client_max_body_size 50m; # 最大请求体50MB
client_body_buffer_size 128k; # 请求体缓冲区
client_header_buffer_size 1k; # 请求头缓冲区
large_client_header_buffers 4 4k; # 大请求头缓冲区
# 特定location放宽限制
location /upload/ {
client_max_body_size 500m;
proxy_pass http://upload_server;
}# 错误:缺少中间证书
ssl_certificate /etc/nginx/ssl/server.crt; # 只包含服务器证书
# 正确:证书链必须完整
ssl_certificate /etc/nginx/ssl/fullchain.crt; # 服务器证书 + 中间证书
ssl_certificate_key /etc/nginx/ssl/server.key;
# 错误:允许不安全的SSL协议
ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; # SSLv3和TLSv1有漏洞
# 正确:只启用安全协议
ssl_protocols TLSv1.2 TLSv1.3;
# 错误:弱密码套件
ssl_ciphers 'ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP';
# 正确:强密码套件
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers on;参考答案:
根本原因:事件驱动的异步非阻塞I/O架构
1. 多路复用epoll:
- 一个Worker进程通过epoll监控数万个连接
- 只有就绪的连接才进行读写,没有空轮询
- epoll时间复杂度O(1),不受连接数影响
2. 无阻塞设计:
- 没有线程/进程阻塞在I/O等待上
- 所有等待都在epoll_wait中统一处理
- 连接不占用线程栈(一个线程栈通常2-8MB)
3. 零拷贝sendfile:
- 静态文件传输不经过用户态
- 减少2次CPU拷贝和2次上下文切换
4. 内存池机制:
- 减少内存碎片和系统调用
- 请求结束统一释放,无内存泄漏
5. 多进程单线程:
- Worker间无共享状态,无锁竞争
- 利用多核CPU,每个Worker绑定一个核心
对比:Apache Prefork每个连接一个进程(2-10MB)
10万并发需要200GB-1TB内存
Nginx 10万并发只需几百MB内存
参考答案:
Master-Worker模型的优势:
1. 职责分离:
- Master:管理配置、信号处理、Worker生命周期
- Worker:专注处理客户端请求
- 管理逻辑与业务逻辑解耦
2. 热部署能力:
- 修改配置后,Master创建新Worker加载新配置
- 旧Worker处理完现有请求后优雅退出
- 服务不中断
3. 容错性:
- 单个Worker崩溃不影响其他Worker
- Master立即fork新Worker补充
选择多进程单线程的原因:
1. 无锁编程(最重要):
- 每个Worker独立处理连接,无需锁竞争
- 避免了多线程的复杂同步问题
2. 隔离性:
- 进程有独立地址空间,一个进程崩溃不影响其他进程
- 线程共享地址空间,一个线程崩溃可能拖垮整个进程
3. 简单性:
- 单线程代码无需考虑线程安全
- 没有死锁、竞态条件等问题
4. 多核利用:
- 每个Worker绑定一个CPU核心
- 避免线程切换和缓存失效
代价:进程间通信需要通过IPC(如共享内存),比线程间通信复杂
参考答案:
Location匹配优先级(从高到低):
1. = 精确匹配:
location = /exact {
# 只有URI完全等于/exact才匹配
# O(1)哈希查找,性能最高
}
2. ^~ 优先前缀匹配:
location ^~ /static {
# 最长前缀匹配成功,停止检查正则
# 用于性能优化(跳过正则匹配)
}
3. ~ 区分大小写正则匹配:
location ~ \.php$ {
# 按配置文件中的定义顺序匹配
# 第一个匹配的正则生效
}
4. ~* 不区分大小写正则匹配:
location ~* \.(jpg|png)$ {
# 与~同级,按顺序匹配
}
5. 无修饰符前缀匹配(最长前缀):
location /api {
# 最低优先级
}
冲突解决原则:
- 精确匹配 > 优先前缀 > 正则 > 普通前缀
- 同级正则按配置文件书写顺序,第一个匹配的生效
- 建议:将更精确的location放在前面,避免依赖顺序
参考答案:
Nginx支持的负载均衡策略:
1. 轮询(round_robin):默认,按顺序分配
2. 加权轮询(weight):按权重比例分配
3. IP哈希(ip_hash):同一IP固定路由(会话保持)
4. 最少连接(least_conn):分配到当前连接数最少的服务器
5. 一致性哈希(hash $key consistent):缓存友好
平滑加权轮询算法(Nginx SW算法):
每个服务器维护两个权重:
- weight:配置权重(固定)
- current_weight:当前权重(动态变化)
算法步骤:
1. 每个服务器 current_weight += weight
2. 选择 current_weight 最大的服务器
3. 被选中的服务器 current_weight -= total_weight
示例:
服务器A weight=5,服务器B weight=1,total=6
轮次1: A(0+5=5), B(0+1=1) -> 选A -> A=5-6=-1
轮次2: A(-1+5=4), B(1+1=2) -> 选A -> A=4-6=-2
轮次3: A(-2+5=3), B(2+1=3) -> 选A -> A=3-6=-3
轮次4: A(-3+5=2), B(3+1=4) -> 选B -> B=4-6=-2
轮次5: A(2+5=7), B(-2+1=-1) -> 选A -> A=7-6=1
轮次6: A(1+5=6), B(-1+1=0) -> 选A -> A=6-6=0
结果:6轮中A被选5次,B被选1次,分布均匀
参考答案:
Nginx限流模块:
1. limit_conn(连接数限制):
- 限制单个IP的并发连接数
- 基于共享内存计数器
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn addr 10; # 单个IP最多10个并发连接
2. limit_req(请求速率限制):
- 基于漏桶算法(Leaky Bucket)
- 限制请求处理速率
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
limit_req zone=one burst=20 nodelay;
漏桶算法 vs 令牌桶算法:
漏桶算法(Nginx limit_req使用):
- 请求像水一样流入桶,桶以固定速率漏水
- 桶满了,新请求被丢弃(或延迟)
- 特点:强制匀速处理,不允许突发流量
令牌桶算法:
- 系统以固定速率生成令牌放入桶中
- 请求需要获取令牌才能执行
- 桶满了,令牌丢弃(不丢弃请求)
- 特点:允许一定程度的突发流量(桶中有令牌时)
Nginx limit_req参数:
- rate=10r/s:每秒处理10个请求(漏桶漏水速率)
- burst=20:桶容量20,允许突发20个请求排队
- nodelay:突发请求不延迟,立即处理(但会消耗令牌)
参考答案:
Nginx HTTP处理的11个阶段:
1. POST_READ - 读取请求头后(realip模块)
2. SERVER_REWRITE - server块rewrite
3. FIND_CONFIG - 查找location(核心)
4. REWRITE - location块rewrite
5. POST_REWRITE - rewrite后检查(防死循环)
6. PREACCESS - 访问控制前(limit_req/conn)
7. ACCESS - 访问控制(auth_basic, access)
8. POST_ACCESS - 访问控制后检查
9. TRY_FILES - try_files指令
10. CONTENT - 生成内容(proxy_pass, static)
11. LOG - 记录日志
Rewrite指令执行阶段:
- server块中的rewrite:在SERVER_REWRITE阶段(阶段2)
- location块中的rewrite:在REWRITE阶段(阶段4)
- 如果rewrite有break/last标志,可能跳过后续阶段
重要:rewrite可以跳转到其他location,但只在阶段2和4执行
proxy_pass在CONTENT阶段(阶段10)执行
因此rewrite先于proxy_pass执行
参考答案:
Nginx热部署流程(不中断服务升级):
1. 替换Nginx二进制文件:
cp nginx-new /usr/local/nginx/sbin/nginx
2. 发送USR2信号给旧Master:
kill -USR2 $(cat /var/run/nginx.pid)
旧Master响应:
- 重命名pid文件:nginx.pid -> nginx.pid.oldbin
- fork出新Master(使用新二进制)
- 新Master创建新Worker
- 此时有两个Master+多组Worker在运行
3. 发送WINCH信号给旧Master:
kill -WINCH $(cat /var/run/nginx.pid.oldbin)
旧Master响应:
- 优雅关闭旧Worker(不再接受新连接)
- 旧Worker处理完现有请求后退出
- 旧Master保留,用于回滚
4. 验证新Master工作正常:
- 测试新Worker是否正常工作
- 如果异常,可以回滚(发送HUP给旧Master)
5. 发送QUIT信号给旧Master:
kill -QUIT $(cat /var/run/nginx.pid.oldbin)
旧Master退出,完成升级
回滚(如果新Master有问题):
kill -HUP $(cat /var/run/nginx.pid.oldbin) # 重新启动旧Worker
kill -QUIT $(cat /var/run/nginx.pid) # 关闭新Master
整个过程中,旧Worker处理完现有请求才退出,
新连接由新Worker处理,服务不中断。
参考答案:
proxy_buffering机制:
当proxy_buffering on(默认):
1. Nginx接收后端响应,存储在内存缓冲区
2. 如果响应超过缓冲区大小,写入临时文件
3. 响应完全接收后(或缓冲区满),再发送给客户端
优势:
- 后端连接快速释放(不需要等待客户端接收)
- 后端可以处理更多请求
- 支持后端慢、客户端快的场景
劣势:
- 增加内存/磁盘使用
- 客户端看到响应有延迟(等缓冲)
当proxy_buffering off:
- Nginx接收到后端数据立即转发给客户端
- 后端连接保持直到客户端接收完成
- 适合:视频流、大文件下载、实时数据
配置示例:
location / {
proxy_buffering on;
proxy_buffer_size 4k; # 响应头缓冲区
proxy_buffers 8 4k; # 8个4KB缓冲区 = 32KB
proxy_busy_buffers_size 8k; # 忙缓冲区
proxy_max_temp_file_size 1024m; # 临时文件最大1GB
proxy_pass http://backend;
}
location /stream/ {
proxy_buffering off; # 流式传输
proxy_pass http://stream_backend;
}
proxy_cache vs proxy_buffering:
- proxy_buffering:缓解后端压力,不缓存响应
- proxy_cache:缓存响应,供后续请求使用
参考答案:
try_files指令:
- 按顺序尝试多个文件/URI,直到找到存在的
- 如果都不存在,返回最后一个参数(通常是状态码或URI)
语法:try_files file ... uri 或 try_files file ... =code
示例1(前端History模式路由):
location / {
try_files $uri $uri/ /index.html;
# 1. 尝试 $uri(精确文件)
# 2. 尝试 $uri/(目录)
# 3. 返回 /index.html(前端路由处理)
}
示例2(自定义404):
location /images/ {
try_files $uri $uri/ /images/default.jpg;
}
示例3(直接返回状态码):
location /private/ {
try_files $uri =403; # 如果文件不存在,返回403
}
try_files vs rewrite区别:
1. 执行阶段:
- try_files:TRY_FILES阶段(阶段9)
- rewrite:REWRITE阶段(阶段2/4)
2. 行为:
- try_files:按顺序尝试文件存在性,内部重定向
- rewrite:URI重写,可以外部重定向(return 301/302)
3. 性能:
- try_files:内部重定向,客户端无感知
- rewrite外部重定向:增加RTT
4. 使用场景:
- try_files:文件存在性检查、SPA路由、默认页面
- rewrite:URI规范化、伪静态、跳转
参考答案:
499状态码:
- Nginx自定义状态码,非HTTP标准
- 含义:客户端在服务器返回响应前关闭了连接
常见场景:
1. 客户端超时:
- 客户端设置的超时时间 < 后端处理时间
- 客户端主动断开连接
2. 用户取消请求:
- 浏览器中关闭页面
- 移动APP切换页面取消请求
3. 负载均衡器超时:
- LB(如AWS ALB)等待Nginx响应超时
- LB断开连接,Nginx记录499
排查方法:
1. 检查Nginx日志:
log_format 包含 $status 和 $request_time
如果499的请求request_time很大,说明后端处理慢
2. 对比后端日志:
- 如果后端返回200,但Nginx记录499
- 说明后端处理太慢,客户端等不及了
3. 检查客户端超时配置:
- 浏览器fetch/XHR的timeout
- 移动APP的网络请求超时
4. 调整Nginx超时:
proxy_read_timeout 60s; # 增加读取超时
但更好的做法是优化后端性能,而不是延长超时
5. 使用proxy_ignore_client_abort:
location / {
proxy_ignore_client_abort on; # 客户端断开后继续请求后端
proxy_pass http://backend;
}
注意:这会导致后端做无用功,谨慎使用
解决方案:
- 优化后端接口性能
- 前端增加loading状态,避免用户重复点击
- 对于非关键请求,客户端增加重试机制
- 使用异步处理(前端轮询或WebSocket推送)
此文原创,转载请注明出处。