Nginx
Nginx 是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。
HTTP 服务器 - 处理 HTTP 请求并返回响应的 Web 服务器,可以提供静态文件(HTML、CSS、JS、图片等)
反向代理服务器 - 代理后端服务器接收客户端请求,客户端以为自己在访问代理服务器,实际请求被转发到后端真实服务器
- 正向代理:代理客户端(如 VPN)
- 反向代理:代理服务器(客户端不知道后端真实 IP)
IMAP/POP3/SMTP - 邮件协议
- IMAP (Internet Message Access Protocol) - 邮件接收协议,支持在服务器管理邮件
- POP3 (Post Office Protocol 3) - 邮件接收协议,下载到本地后从服务器删除
- SMTP (Simple Mail Transfer Protocol) - 邮件发送协议
Overview
Nginx 以其高性能、稳定性、丰富的功能集、简单的配置和低资源消耗而闻名。常用于:
- 静态文件服务 - 高效提供前端构建后的 HTML、CSS、JS 文件
- 反向代理 - 将 API 请求转发到后端服务,解决跨域问题
- 负载均衡 - 在多个后端服务器之间分配流量
- HTTP 缓存 - 缓存静态资源,减轻后端压力
在 Bush 项目中,Nginx 主要用作静态文件服务器和反向代理,具有轻量(Alpine 镜像仅 40MB+)、高性能(支持高并发、Gzip 压缩)等优势。
如何在项目中集成和运行 Nginx
以下内容基于真实项目 Bush 的生产环境实践。
💡 完整容器化架构
本章节重点讲解 Nginx 配置和使用。如需了解 Bush & Jungle 的完整容器化架构(包括开发环境、生产环境、网络通信、健康检查等),请参考:Docker - 实战案例
1. 项目架构
Bush 是一个完全容器化的前后端分离应用,生产环境架构如下:
┌─────────────────────────────────────────────────────────┐
│ Docker 网络:jungle_default │
│ │
│ 客户端浏览器 │
│ ↓ │
│ ┌─────────────────────────┐ │
│ │ Bush 容器 (Nginx) │ │
│ │ bush_app │ │
│ │ 端口: 80 │ │
│ │ │ │
│ │ 功能: │ │
│ │ 1. 静态文件服务 │ │
│ │ / → React 构建产物 │ │
│ │ │ │
│ │ 2. API 反向代理 │ │
│ │ /api/* → jungle_app:3000 │
│ │ (路径重写: /api/users → /users) │
│ │ │ │
│ │ 3. 用户文件服务 │ │
│ │ /static/* → 共享目录 │ │
│ └────────────┬────────────┘ │
│ │ │
│ │ HTTP 请求(容器名解析) │
│ │ │
│ ┌────────────▼────────────┐ │
│ │ Jungle 容器 (NestJS) │ │
│ │ jungle_app │ │
│ │ 端口: 3000 (内部) │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘关键特点:
- Nginx 通过 Docker 内置 DNS(
resolver 127.0.0.11)解析容器名jungle_app /api/*请求被代理到后端,并通过rewrite移除前缀- 用户上传文件通过共享卷实现前后端共享
2. 如何在项目中运行 Nginx
以 Bush 项目为例,说明如何在生产环境通过容器运行 Nginx。
⚠️ 注意
开发环境(pnpm c:up)使用容器化的 UmiJS Dev Server,不使用 Nginx。
本节介绍的是生产环境的 Nginx 容器化部署。
第一步:准备 nginx.conf 配置文件
在 Bush 项目根目录创建 nginx.conf,配置包含以下核心模块:
必备配置模块:
| 模块 | 配置项 | 说明 |
|---|---|---|
| 基础配置 | listen 80 | 监听 HTTP 端口 |
server_name 1px.club | 域名匹配 | |
root /usr/share/nginx/html | 静态文件根目录 | |
| SPA 路由 | try_files $uri $uri/ /index.html | 所有路由返回 index.html |
| API 代理 | resolver 127.0.0.11 | Docker DNS 解析 |
proxy_pass http://jungle_app:3000 | 转发到后端 | |
rewrite ^/api/(.*)$ /$1 break | 移除 /api 前缀 | |
| 上传文件 | location ^~ /static/ | 用户上传文件访问 |
alias /app/shared_uploads/ | 共享目录映射 | |
| 健康检查 | location /health | 监控端点 |
| 缓存优化 | expires 1y | 静态资源长期缓存 |
📖 完整配置示例
详细的配置代码和注释请查看:完整配置示例
第二步:创建 Dockerfile.prod
Bush 生产环境使用轻量的 Nginx Alpine 镜像:
# Dockerfile.prod
FROM nginx:alpine
# 复制预构建的前端产物
COPY dist/ /usr/share/nginx/html/
# 复制 Nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 暴露 80 端口
EXPOSE 80
# 启动 Nginx(前台运行)
CMD ["nginx", "-g", "daemon off;"]关键点:
nginx:alpine- 轻量级镜像,仅 ~40MBdaemon off;- 前台运行,适配容器环境dist/在本地 Docker 容器内构建(保证构建环境一致,避免服务器 OOM)
第三步:配置 docker-compose.yml
version: '3.8'
services:
bush:
container_name: bush_app
build:
context: .
dockerfile: Dockerfile.prod
ports:
- '80:80'
restart: always
volumes:
- ./logs:/var/log/nginx # 日志持久化
- /root/jungle_uploads:/app/shared_uploads # 共享上传目录
networks:
- jungle_default # 加入后端网络
networks:
jungle_default:
external: true # 使用后端已创建的网络关键配置:
volumes- 日志和上传文件持久化到宿主机networks- 加入后端网络,实现容器间通信restart: always- 容器异常退出时自动重启
第四步:运行容器
# 构建并启动容器(生产环境)
docker compose up -d --build
# 查看容器状态
docker compose ps
# 查看实时日志
docker compose logs -f bush
# 查看 Nginx 访问日志
tail -f logs/access.log
# 重启容器
docker compose restart bush💡 关于部署流程
Bush 采用 本地容器构建 + GitHub Actions 自动部署 的方式:
- 本地构建:
pnpm deploy:local- 使用开发镜像在临时容器内执行pnpm build- 提交推送:dist/ 提交到 Git 并 push
- 自动触发:GitHub Actions 检测到 dist/ 变化时自动触发
- 打包上传:打包部署文件并 scp 上传到服务器
- 服务器部署:docker compose up -d --build(使用 Dockerfile.prod)
关键点:
- 开发和构建复用同一镜像(
bush-dev:latest),节省空间- 构建在本地容器内进行,避免服务器 OOM(2GB 内存)
- 服务器仅 COPY dist/,无需安装依赖和构建
关于完整部署流程和 CI/CD 配置,请参考:Docker - 实战案例:Bush & Jungle
第五步:验证服务
部署完成后,可以通过以下方式验证:
在服务器上验证:
# SSH 到服务器后执行
# 1. 测试健康检查
curl http://localhost/health
# 应返回: ok
# 2. 检查容器状态
docker compose ps
# 3. 查看 Nginx 日志
docker compose logs bush从外部访问验证:
# 1. 测试健康检查
curl http://1px.club/health
# 2. 访问前端页面
curl -I http://1px.club
# 3. 测试 API 代理
curl http://1px.club/api/health如果一切正常,浏览器访问 http://1px.club 即可看到前端应用 🎉
Nginx 配置详解
以下详细讲解 Nginx 配置文件的各个模块。
1. Server Block - 虚拟主机配置
Server Block 是 Nginx 的核心配置单元,用于定义一个虚拟主机。
server {
# 监听端口(默认 80 端口用于 HTTP)
listen 80;
# 域名匹配,当请求的 Host 头为 1px.club 时使用此配置
server_name 1px.club;
}关键配置说明:
listen 80- 监听 80 端口,处理 HTTP 请求server_name- 域名匹配规则,支持通配符(如*.example.com)
2. 静态文件服务与 SPA 路由
前端项目(React/Vue)构建后的静态文件由 Nginx 直接提供。
server {
listen 80;
server_name 1px.club;
# 前端静态文件根目录(dist/ 构建产物)
root /usr/share/nginx/html;
# 默认首页
index index.html;
# 处理所有请求
location / {
try_files $uri $uri/ /index.html;
}
}为什么需要 try_files?理解 SPA 路由问题
问题场景:
1. 用户访问 http://1px.club
→ Nginx 返回 /usr/share/nginx/html/index.html ✅
2. 用户在前端点击链接,跳转到 http://1px.club/user/profile
→ 前端路由(React Router)处理,显示用户页面 ✅
→ 此时浏览器地址栏:http://1px.club/user/profile
3. 用户在此页面刷新浏览器(F5 或点击刷新按钮)
→ 浏览器向服务器发送请求:GET http://1px.club/user/profile
→ Nginx 在 /usr/share/nginx/html/ 目录查找 user/profile 文件
→ 找不到文件 → 返回 404 Not Found ❌核心原因: SPA 应用的路由是前端路由,在服务器上并不存在对应的物理文件。
try_files 工作原理
语法: try_files $uri $uri/ /index.html;
执行流程:
当请求 /user/profile 时:
第 1 步:尝试 $uri
→ 查找文件 /usr/share/nginx/html/user/profile
→ 不存在 ❌
第 2 步:尝试 $uri/
→ 查找目录 /usr/share/nginx/html/user/profile/
→ 然后查找目录下的 index.html
→ 不存在 ❌
第 3 步:返回 /index.html(最后的兜底选项)
→ 返回 /usr/share/nginx/html/index.html ✅
→ 前端路由接管,显示 /user/profile 页面 ✅实际例子对比
场景 1:访问静态资源(图片、CSS、JS)
请求:GET /assets/logo.png
try_files 执行:
① $uri → 查找 /usr/share/nginx/html/assets/logo.png
✅ 找到!直接返回图片
结果:正常显示图片场景 2:访问前端路由
请求:GET /user/123
try_files 执行:
① $uri → 查找 /usr/share/nginx/html/user/123
❌ 找不到
② $uri/ → 查找 /usr/share/nginx/html/user/123/index.html
❌ 找不到
③ /index.html → 返回 /usr/share/nginx/html/index.html
✅ 返回 index.html
结果:
→ 浏览器加载 index.html
→ React Router 看到 URL 是 /user/123
→ 渲染用户详情页面场景 3:访问根路径
请求:GET /
try_files 执行:
① $uri → 查找 /usr/share/nginx/html/
✅ 是目录
② 因为配置了 index index.html;
→ 自动返回 /usr/share/nginx/html/index.html
结果:正常显示首页如果不配置 try_files 会怎样?
# ❌ 错误配置(缺少 try_files)
location / {
root /usr/share/nginx/html;
}问题:
- ✅ 访问
/→ 正常显示 - ✅ 访问
/assets/app.js→ 正常加载 - ❌ 刷新
/user/123→ 404 Not Found - ❌ 直接访问
/user/123→ 404 Not Found
3. 反向代理 - API 请求转发
将前端的 API 请求代理到后端服务,实现前后端分离。
# API 代理到后端 Jungle 服务
location /api/ {
# Docker 内部 DNS 解析器
# 127.0.0.11 是 Docker 内置的 DNS 服务器地址
# valid=30s 表示 DNS 缓存 30 秒
resolver 127.0.0.11 valid=30s;
# 使用变量存储后端服务地址
# jungle_app 是 Docker Compose 中定义的容器名称
# 使用变量可以避免启动时后端服务未就绪导致 Nginx 启动失败
set $jungle_service http://jungle_app:3000;
# 将请求代理到后端服务
proxy_pass $jungle_service;
# 传递原始请求的 Host 头
proxy_set_header Host $host;
# 传递客户端真实 IP
proxy_set_header X-Real-IP $remote_addr;
# 传递代理链路中的所有 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 传递原始协议(http/https)
proxy_set_header X-Forwarded-Proto $scheme;
# 重写 URL:移除 /api 前缀
# 前端请求: http://1px.club/api/users
# 转发给后端: http://jungle_app:3000/users
rewrite ^/api/(.*)$ /$1 break;
# 开启详细的错误日志,方便调试
error_log /var/log/nginx/api_error.log debug;
}代理配置要点:
- resolver - Docker 环境必须配置,用于解析容器名
- 变量方式 - 避免 Nginx 启动时检查后端是否可用
- proxy_set_header - 让后端能获取客户端真实信息
- rewrite - URL 重写,前后端路径不一致时使用
4. 静态资源缓存
为前端资源文件(JS/CSS/图片)设置长期缓存,提升加载速度。
# 匹配特定文件类型的资源
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
# 排除 /static/ 路径,避免规则冲突
if ($request_uri ~* "^/static/") {
break;
}
# 缓存 1 年
expires 1y;
# 设置缓存控制头
# public: 允许中间代理缓存
# max-age=31536000: 缓存时长(秒),1年 = 365 * 24 * 60 * 60
add_header Cache-Control "public, max-age=31536000";
}缓存策略说明:
~*- 正则匹配,不区分大小写expires 1y- 简写方式,自动计算并添加 Expires 和 Cache-Control 头- 构建工具(Webpack/Vite)会为文件添加 hash,文件内容变化时 hash 变化,避免缓存问题
5. 用户上传文件服务
处理用户上传的静态文件(图片、文档等)。
# 处理上传的静态文件
location ^~ /static/ {
# alias 指向容器内的共享上传目录
# alias 会直接替换掉 URL 中的 /static/ 部分
# 请求: http://1px.club/static/avatar.jpg
# 实际: /app/shared_uploads/avatar.jpg
alias /app/shared_uploads/;
# 用户上传的文件缓存 1 年
expires 1y;
add_header Cache-Control "public, max-age=31536000";
}alias vs root 的区别:
# 使用 root(追加路径)
location /static/ {
root /app;
}
# 请求 /static/image.jpg -> 查找 /app/static/image.jpg
# 使用 alias(替换路径)
location /static/ {
alias /app/uploads/;
}
# 请求 /static/image.jpg -> 查找 /app/uploads/image.jpg6. 健康检查接口
提供一个简单的健康检查端点,用于监控服务状态。
location /health {
# 不记录访问日志,避免大量监控请求刷屏
access_log off;
# 直接返回 200 状态码和 "ok" 文本
return 200 "ok";
}使用场景:
- 容器编排工具(Docker/K8s)的健康检查
- 负载均衡器的后端状态检测
- 监控系统的存活探测
7. 日志配置
记录访问日志和错误日志,方便问题排查。
server {
# ... 其他配置
# 访问日志:记录所有请求
access_log /var/log/nginx/access.log;
# 错误日志:记录错误和警告信息
error_log /var/log/nginx/error.log;
}查看日志:
# 实时查看访问日志
tail -f /var/log/nginx/access.log
# 实时查看错误日志
tail -f /var/log/nginx/error.log8. 完整配置示例
Bush 项目的完整 Nginx 配置:
# 网关架构
# 本 Nginx 作为 API 网关,负责统一入口和路由分发
#
# Bush 服务(主站 1px.club):
# - 静态文件服务(前端资源)
# - SPA 路由处理
# - API 反向代理(转发到 Jungle)
# - 上传文件静态访问(/static/)
#
# Jungle 服务(后端 API):
# - 路径转发:/api/* → jungle_app:3000
# - 业务逻辑、数据库操作、文件上传处理
#
# Blog 服务(子域名 blog.1px.club):
# - 域名转发:blog.1px.club → blog_app:80
# - 静态文件服务、SPA 路由
# Bush & Jungle
server {
# 端口监听
listen 80;
# 域名匹配
server_name 1px.club;
# 静态文件根目录
root /usr/share/nginx/html;
# 默认首页
index index.html;
# 处理所有路由请求
# 返回真实文件(如JS/CSS/图片)
# 路由请求则统一返回 index.html,前端路由接管
location / {
try_files $uri $uri/ /index.html;
}
# 反向代理
location /api/ {
# Docker 内部 DNS 解析器
# 127.0.0.11 是 Docker 内置的 DNS 服务器地址
# valid=30s 表示 DNS 缓存 30 秒
resolver 127.0.0.11 valid=30s;
# 使用变量存储后端服务地址
# jungle_app 是 Docker Compose 中定义的容器名称,DNS 需要动态解析
# 使用变量时 Nginx 不立即解析,仅保存变量,可以避免启动时后端服务未就绪导致 Nginx 启动失败
set $jungle_service http://jungle_app:3000;
# 将请求转发到后端服务
proxy_pass $jungle_service;
# 传递原始请求的 Host 头
proxy_set_header Host $host;
# 传递客户端真实 IP
proxy_set_header X-Real-IP $remote_addr;
# 传递代理链路中的所有 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 传递原始协议(http/https)
proxy_set_header X-Forwarded-Proto $scheme;
# 重写 URL:移除 /api 前缀
# 前端请求: http://1px.club/api/users
# 转发给后端: http://jungle_app:3000/users
rewrite ^/api/(.*)$ /$1 break;
# 错误日志
error_log /var/log/nginx/api_error.log debug;
}
# 用户文件上传 - 处理用户上传的静态文件(图片、文档等)
location ^~ /static/ {
# 'alias' 指向我们在容器内的共享上传目录
# 'alias' 会用 /app/shared_uploads/ 直接替换掉 URL 里的 /static/ 部分
# 请求 http://1px.club/static/image.png -> Nginx 会去容器内找 /app/shared_uploads/image.png
alias /app/shared_uploads/;
# 可选:为这些文件开启浏览器缓存
expires 1y;
add_header Cache-Control "public, max-age=31536000";
}
# 静态资源缓存设置
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
# 排除 /static/ 路径,避免与上面的规则冲突
if ($request_uri ~* "^/static/") {
break;
}
expires 1y;
add_header Cache-Control "public, max-age=31536000";
}
# 健康检查接口
location /health {
access_log off;
return 200 "ok";
}
# 日志配置
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
}
# Blog
server {
listen 80;
server_name blog.1px.club;
location / {
resolver 127.0.0.11 valid=30s;
set $blog_service http://blog_app:80;
proxy_pass $blog_service;
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;
}
access_log /var/log/nginx/blog_access.log;
error_log /var/log/nginx/blog_error.log;
}9. 性能与安全常用配置
常用的 Nginx 优化与安全配置(按需选择):
# 压缩(Brotli 需额外模块)
gzip on;
gzip_comp_level 5;
gzip_min_length 1k;
gzip_types text/plain text/css application/json application/javascript application/xml+rss application/xml application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml;
# 上传大小限制(根据业务调整)
client_max_body_size 10m;
# 反向代理超时(根据后端平均响应时间调整)
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 连接与发送优化
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
# 安全响应头(按需开启)
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# 可选:API 限流(需要在 http 块定义共享内存区)
# limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
# 在 /api/ 中启用:
# limit_req zone=api_limit burst=20 nodelay;扩展:从反向代理到负载均衡与微服务
Nginx 的反向代理功能是架构演进的基础。随着应用规模增长,可以基于反向代理实现更复杂的架构。
架构演进概览
基础:反向代理
↓
扩展 1:负载均衡(多个相同实例)
↓
扩展 2:微服务(多个不同服务)
↓
扩展 3:微服务 + 负载均衡负载均衡(Load Balancing)
什么是负载均衡?
负载均衡是反向代理的扩展应用,将请求分发到多个相同的后端实例。
# 基础反向代理(单实例)
location /api/ {
proxy_pass http://jungle_app:3000;
}
# 负载均衡(多实例)
upstream jungle_backend {
server jungle_app_1:3000;
server jungle_app_2:3000;
server jungle_app_3:3000;
}
location /api/ {
proxy_pass http://jungle_backend; # Nginx 自动分配请求
}前置知识:服务器资源基础概念
快速理解:
| 资源 | 作用 | 类比 | Web 应用特点 |
|---|---|---|---|
| CPU | 执行计算 | 厨师做菜 | 速度快但大部分时间等待 I/O |
| 内存 | 临时存储 | 工作台 | Node.js 进程 ~500MB |
| I/O | 读写数据 | 等配送 | 数据库查询 10-100ms,最耗时 |
| 带宽 | 数据传输 | 门的宽度 | 影响并发用户数 |
| 硬盘 | 永久存储 | 仓库 | 存代码、日志、上传文件 |
核心结论:Web 应用 90% 的时间在等待 I/O!
// 典型 API 请求:总耗时 62ms
async function getUser(userId) {
// CPU 处理:2ms (3%)
const cached = await redis.get(`user:${userId}`); // I/O: 5ms
const user = await db.users.findOne({ id: userId }); // I/O: 50ms
await redis.set(`user:${userId}`, user); // I/O: 5ms
// I/O 等待:60ms (97%) ← CPU 大部分时间闲置!
}为什么多实例能提高并发?
- ✅ 一个实例等待 I/O 时,其他实例在工作
- ✅ 充分利用 CPU(从 20% 提升到 60%)
- ✅ 即使单核 CPU 也有效(内存够用即可)
💡 简单类比: 单个厨师等食材时闲着(低效) vs 3 个厨师轮流工作(高效 3 倍)
📚 点击展开:详细解析(CPU、内存、I/O、带宽、硬盘、餐厅类比)
在理解负载均衡之前,先详细了解服务器的核心资源:
1. CPU(中央处理器)- 大脑
作用: 执行计算和逻辑运算
形象比喻: CPU 就像厨师,负责做菜(处理任务)
单核 CPU:一个厨师
双核 CPU:两个厨师,可以同时做两道菜Web 应用中的 CPU 操作:
// CPU 密集型操作(消耗 CPU)
- JSON 序列化/反序列化
- 加密解密(bcrypt 加密密码)
- 图片压缩、视频转码
- 复杂的数据计算
// 示例:加密密码
const hash = await bcrypt.hash(password, 10); // CPU 工作 50ms特点:
- ✅ 速度极快(纳秒级)
- ❌ 数量有限(1 核、2 核、4 核等)
2. 内存(RAM)- 临时工作台
作用: 临时存储正在运行的程序和数据
形象比喻: 内存就像厨师的工作台,台面越大,能同时准备的食材越多
2GB 内存:小工作台,只能同时处理几个订单
8GB 内存:大工作台,可以同时处理很多订单Web 应用中的内存使用:
// 内存占用示例
- 每个 Node.js 进程:~500MB
- 每个请求的数据:~1-10KB
- 数据库查询结果缓存:10MB-100MB
// 示例:查询大量数据
const posts = await db.posts.find(); // 加载到内存:50MB特点:
- ✅ 速度快(纳秒级)
- ✅ 断电数据丢失(临时存储)
- ❌ 容量有限且昂贵
3. I/O(输入/输出)- 等待时间
作用: 与外部设备交互(硬盘、网络、数据库)
形象比喻: I/O 就像等待食材配送,厨师(CPU)在等待时闲着
厨师做菜的真实流程:
1. 切菜(CPU 工作 10s)
2. 等水烧开(I/O 等待 5 分钟)← CPU 闲置!
3. 炒菜(CPU 工作 20s)
4. 等配送员送食材(I/O 等待 10 分钟)← CPU 闲置!Web 应用中的 I/O 操作:
// I/O 密集型操作(等待外部响应)
- 数据库查询:等待 10-100ms
- Redis 读写:等待 1-10ms
- 文件读写:等待 5-50ms
- HTTP 请求:等待 100-1000ms
// 示例:API 请求处理
async function getUser(userId) {
// CPU 处理:1ms
const cacheKey = `user:${userId}`;
// I/O 等待:5ms(等待 Redis 响应)
const cached = await redis.get(cacheKey);
if (cached) return cached;
// I/O 等待:50ms(等待数据库响应)
const user = await db.users.findOne({ id: userId });
// I/O 等待:5ms(等待 Redis 写入)
await redis.set(cacheKey, user);
// CPU 处理:1ms
return user;
}
// 总耗时:62ms
// CPU 实际工作:2ms (3%) ← CPU 大部分时间在等待!
// I/O 等待:60ms (97%)I/O 类型速度对比:
内存访问: 100 纳秒 (基准)
SSD 读取: 100 微秒 (慢 1,000 倍)
网络请求: 1-10 毫秒 (慢 10,000-100,000 倍)
数据库查询: 10-100 毫秒特点:
- ❌ 速度慢(毫秒级)
- ❌ 等待时间长
- ✅ 等待时 CPU 可以做其他事
4. 带宽(网络传输速度)- 管道大小
作用: 数据传输的速度
形象比喻: 带宽就像水管,管子越粗,水流越快
1Mbps 带宽:小水管,每秒传输 125KB
10Mbps 带宽:中水管,每秒传输 1.25MB
100Mbps 带宽:粗水管,每秒传输 12.5MB实际场景:
下载一张图片(1MB):
- 1Mbps 带宽:需要 8 秒
- 10Mbps 带宽:需要 0.8 秒
- 100Mbps 带宽:需要 0.08 秒
同时 100 个用户访问网站:
- 每个用户下载 500KB 页面
- 总共需要传输:50MB
- 10Mbps 带宽:需要 40 秒(拥堵)
- 100Mbps 带宽:需要 4 秒特点:
- ❌ 云服务商通常限制带宽
- ❌ 带宽成本高
- ✅ 可以升级
5. 硬盘/存储 - 永久仓库
作用: 永久存储数据
形象比喻: 硬盘就像仓库,存放所有食材
HDD(机械硬盘):老式仓库,存取慢但便宜
SSD(固态硬盘):现代仓库,存取快但贵特点:
- ✅ 容量大且便宜
- ✅ 断电数据保留
- ❌ 速度慢(比内存慢 1000 倍)
资源对比总结
| 资源 | 速度 | 容量 | 价格 | 作用 |
|---|---|---|---|---|
| CPU | 极快 | N/A | 中 | 计算 |
| 内存 | 快 | 小(GB) | 高 | 临时存储 |
| 硬盘 | 慢 | 大(TB) | 低 | 永久存储 |
| 带宽 | 中 | N/A | 高 | 网络传输 |
Web 应用的真相:I/O 密集型
90% 的 Web 应用都是 I/O 密集型!
一个典型的 API 请求:
┌────────────────────────────────────────┐
│ 总耗时:100ms │
├────────────────────────────────────────┤
│ CPU 计算:10ms (10%) │
│ ├─ 参数校验:2ms │
│ ├─ 业务逻辑:5ms │
│ └─ JSON 序列化:3ms │
├────────────────────────────────────────┤
│ I/O 等待:90ms (90%) ← 关键! │
│ ├─ 数据库查询:60ms │
│ ├─ Redis 读取:15ms │
│ ├─ 外部 API 调用:10ms │
│ └─ 文件读取:5ms │
└────────────────────────────────────────┘
结论:CPU 90% 的时间在等待 I/O!终极类比:餐厅运营
把服务器比作餐厅,帮助你理解资源关系:
┌─────────────────────────────────────────────────────────────┐
│ 餐厅 = 服务器 │
├─────────────────────────────────────────────────────────────┤
│ 厨师 = CPU │
│ ├─ 数量:1 个厨师 = 单核,2 个厨师 = 双核 │
│ └─ 作用:做菜(计算)、切菜(处理数据) │
├─────────────────────────────────────────────────────────────┤
│ 工作台 = 内存 │
│ ├─ 大小:2 平米 = 2GB,8 平米 = 8GB │
│ └─ 作用:摆放正在处理的食材(运行中的程序和数据) │
├─────────────────────────────────────────────────────────────┤
│ 仓库 = 硬盘 │
│ ├─ 大小:100 平米 = 100GB,1000 平米 = 1TB │
│ └─ 作用:存放所有食材(永久存储) │
├─────────────────────────────────────────────────────────────┤
│ 配送员 = I/O │
│ ├─ 速度:从仓库拿食材到工作台需要时间 │
│ └─ 问题:配送慢,厨师等待时闲着 │
├─────────────────────────────────────────────────────────────┤
│ 前门大小 = 带宽 │
│ ├─ 宽度:1 米 = 10Mbps,10 米 = 100Mbps │
│ └─ 作用:客户进出的速度(数据传输) │
└─────────────────────────────────────────────────────────────┘实际场景对比:
场景 1:单个厨师的餐厅(单实例)
┌────────────────────────────────────────┐
│ 1 个厨师(1 核 CPU) │
│ 2 平米工作台(2GB 内存) │
│ │
│ 客户 A 点单:炒饭 │
│ ├─ 厨师切菜:10 秒(CPU 工作) │
│ ├─ 等配送员送肉:5 分钟(I/O 等待) │ ← 厨师闲着!
│ └─ 厨师炒饭:20 秒(CPU 工作) │
│ │
│ 客户 B 点单:面条(必须等 A 完成) │
│ ├─ 等待 A 的订单:5 分 30 秒 │
│ ├─ 厨师煮面:1 分钟 │
│ └─ 等配送员送菜:3 分钟 │
│ │
│ 总时间:约 10 分钟处理 2 个订单 │
│ 吞吐量:12 个订单/小时 │
│ 问题:厨师等待时闲着,效率低 │
└────────────────────────────────────────┘
场景 2:3 个厨师的餐厅(3 个实例负载均衡)
┌────────────────────────────────────────┐
│ 3 个厨师(同样的 1 核 CPU,但有 3 个实例)│
│ 6 平米工作台(6GB 内存,每个 2GB) │
│ │
│ 客户 A 点单:厨师 1 处理 │
│ 客户 B 点单:厨师 2 处理(同时进行) │
│ 客户 C 点单:厨师 3 处理(同时进行) │
│ │
│ 厨师 1 等食材时 → 厨师 2、3 在工作 │
│ 厨师 2 等食材时 → 厨师 1、3 在工作 │
│ 厨师 3 等食材时 → 厨师 1、2 在工作 │
│ │
│ 总时间:约 5 分钟处理 3 个订单 │
│ 吞吐量:36 个订单/小时(提升 3 倍!) │
│ 原理:轮流工作,没有闲置时间 │
└────────────────────────────────────────┘关键理解:
为什么 3 个厨师更快?
- 不是因为厨师做菜更快(CPU 速度没变)
- 而是因为一个厨师等待时,其他厨师在工作
- 充分利用了等待时间!
为什么服务器配置低也能多实例?
单实例:1 核 CPU,使用率 20%(80% 时间等待 I/O) 3 实例:1 核 CPU,使用率 60%(充分利用 CPU) 内存:500MB × 3 = 1.5GB(2GB 服务器完全够用)什么时候多实例无效?
- CPU 密集型:厨师一直在做菜,没有等待时间
- 内存不足:工作台太小,放不下 3 个厨师的食材
- 带宽不足:门太小,客户进不来
这就是为什么多实例能提高并发的核心原因!
为什么多实例能提高并发?
现在你知道了:Web 应用是 I/O 密集型,CPU 大部分时间在等待!
单实例的瓶颈:
请求处理流程(总耗时 100ms):
├─ CPU 处理:10ms (10%)
└─ I/O 等待:90ms (90%) ← 等待数据库、Redis、文件读写
这段时间 CPU 在闲着!
单实例串行处理:
请求1 [CPU 10ms][I/O 90ms]
请求2 [CPU 10ms][I/O 90ms]
请求3 [CPU 10ms][I/O 90ms]
总时间:300ms,处理 3 个请求
QPS:10 请求/秒多实例的优势:
3 个实例并行处理:
实例1: 请求1 [CPU 10ms][I/O 90ms]
实例2: 请求2 [CPU 10ms][I/O 90ms]
实例3: 请求3 [CPU 10ms][I/O 90ms]
总时间:100ms,处理 3 个请求
QPS:30 请求/秒(提升 3 倍!)典型的 Jungle API 请求:
async function getUserPosts(userId) {
// 1. 查询用户(等待数据库 50ms)
const user = await db.users.findOne(userId); // CPU: 5ms, I/O: 45ms
// 2. 查询文章(等待数据库 80ms)
const posts = await db.posts.find({ userId }); // CPU: 10ms, I/O: 70ms
// 3. 处理数据(CPU 5ms)
return format(posts);
}
// 总耗时:135ms
// CPU 工作:20ms (15%) ← CPU 大部分时间闲置
// I/O 等待:115ms (85%) ← 可以处理其他请求!资源使用对比(2GB 内存、1 核 CPU 服务器):
| 配置 | CPU 使用率 | 内存使用 | 并发能力 | 说明 |
|---|---|---|---|---|
| 单实例 | 20% | 500MB | 10 req/s | CPU 闲置,浪费资源 |
| 3 实例 | 60% | 1.5GB | 30 req/s | ✅ 充分利用 CPU |
结论:
- ✅ I/O 密集型应用(Web API、数据库操作)→ 多实例提升并发
- ❌ CPU 密集型应用(图像处理、视频转码)→ 多实例无效
完整配置示例
# 定义后端服务器组
upstream jungle_backend {
# 默认:轮询(round-robin)
server jungle_app_1:3000;
server jungle_app_2:3000;
server jungle_app_3:3000;
# 其他负载均衡算法:
# ip_hash; # 根据客户端 IP 分配(保持会话)
# least_conn; # 分配到连接数最少的服务器
# server ... weight=3; # 权重分配
}
location /api/ {
proxy_pass http://jungle_backend;
proxy_set_header Host $host;
}架构图:
Nginx (bush_app:80)
|
+----------------+----------------+
↓ ↓ ↓
jungle_app_1 jungle_app_2 jungle_app_3
(相同代码) (相同代码) (相同代码)
↓ ↓ ↓
同一个 PostgreSQL 数据库Docker Compose 配置示例:
version: '3.8'
services:
# 后端服务 - 3 个实例
jungle_1:
image: jungle:latest
container_name: jungle_app_1
environment:
- NODE_ENV=production
networks:
- jungle_default
jungle_2:
image: jungle:latest
container_name: jungle_app_2
environment:
- NODE_ENV=production
networks:
- jungle_default
jungle_3:
image: jungle:latest
container_name: jungle_app_3
environment:
- NODE_ENV=production
networks:
- jungle_default
# 数据库 - 共享
postgres:
image: postgres:latest
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- jungle_default
networks:
jungle_default:
external: true
volumes:
postgres_data:优势:
- ✅ 提高并发处理能力(3 倍)
- ✅ 单个实例故障不影响整体
- ✅ 无需修改代码,只需增加实例
- ✅ 可根据负载动态调整实例数
适用场景:
- 流量突然增大
- 需要处理更多并发请求
- 单实例 CPU/内存不足
微服务架构(Microservices)
什么是微服务?
微服务是反向代理的另一种扩展,将单体应用拆分为多个不同职责的独立服务。
单体应用 vs 微服务:
单体应用(Jungle):
┌─────────────────────────────────┐
│ Jungle 后端(一个服务) │
│ ├─ 用户模块 │
│ ├─ 文章模块 │
│ ├─ 评论模块 │
│ ├─ 分类模块 │
│ └─ 认证模块 │
│ │
│ 所有功能在一个代码库 │
│ 共享一个数据库 │
└─────────────────────────────────┘
微服务架构:
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│用户服务 │ │文章服务 │ │评论服务 │ │认证服务 │
│:3001 │ │:3002 │ │:3003 │ │:3004 │
│独立代码 │ │独立代码 │ │独立代码 │ │独立代码 │
│独立DB │ │独立DB │ │独立DB │ │独立DB │
└─────────┘ └─────────┘ └─────────┘ └─────────┘真实场景:电商系统微服务拆分
以电商平台为例(类似 Florist 花店系统),展示典型的微服务架构:
业务模块拆分:
# Nginx 作为 API 网关,路由到不同的微服务
# 1. 用户服务(User Service - 端口 3001)
upstream user_service {
server user_service:3001;
}
# 2. 商品服务(Product Service - 端口 3002)
upstream product_service {
server product_service:3002;
}
# 3. 订单服务(Order Service - 端口 3003)
upstream order_service {
server order_service:3003;
}
# 4. 库存服务(Inventory Service - 端口 3004)
upstream inventory_service {
server inventory_service:3004;
}
# 5. 营销服务(Marketing Service - 端口 3005)
upstream marketing_service {
server marketing_service:3005;
}
# API 网关路由配置
# 用户相关:注册、登录、个人信息
location /api/users/ {
proxy_pass http://user_service;
rewrite ^/api/users/(.*)$ /$1 break;
}
# 商品相关:商品列表、详情、分类
location /api/products/ {
proxy_pass http://product_service;
rewrite ^/api/products/(.*)$ /$1 break;
}
# 订单相关:下单、订单查询、订单状态
location /api/orders/ {
proxy_pass http://order_service;
rewrite ^/api/orders/(.*)$ /$1 break;
}
# 库存相关:库存查询、库存扣减
location /api/inventory/ {
proxy_pass http://inventory_service;
rewrite ^/api/inventory/(.*)$ /$1 break;
}
# 营销相关:优惠券、活动、积分
location /api/marketing/ {
proxy_pass http://marketing_service;
rewrite ^/api/marketing/(.*)$ /$1 break;
}架构图:
Nginx (API 网关)
|
+----------+----------+----------+----------+
↓ ↓ ↓ ↓ ↓
用户服务 商品服务 订单服务 库存服务 营销服务
:3001 :3002 :3003 :3004 :3005
注册登录 商品管理 下单支付 库存管理 优惠券
个人信息 分类搜索 订单查询 库存扣减 活动管理
权限管理 商品详情 物流跟踪 库存预警 积分系统
User DB Product DB Order DB Inventory Marketing
DB DB实际业务流程示例:用户下单
1. 用户浏览商品
GET /api/products/123
→ Nginx → 商品服务 → 返回商品信息
2. 检查库存
GET /api/inventory/check?productId=123
→ Nginx → 库存服务 → 返回库存数量
3. 下单
POST /api/orders/create
→ Nginx → 订单服务
→ 订单服务内部调用:
- 库存服务:扣减库存
- 营销服务:使用优惠券
- 用户服务:扣减积分
4. 每个服务独立运行,互不影响为什么要拆分微服务?
场景 1:团队协作
单体应用的问题:
- 10 个开发人员修改同一个代码库
- 代码冲突频繁
- 一个人的错误影响整个系统
- 部署时必须整体部署
微服务的优势:
- 用户团队:3 人负责用户服务
- 商品团队:2 人负责商品服务
- 订单团队:3 人负责订单服务
- 各团队独立开发、独立部署
- 互不干扰场景 2:独立扩展
电商系统的流量特点:
- 商品服务:流量大(用户浏览)→ 需要 5 个实例
- 订单服务:流量中等 → 需要 2 个实例
- 库存服务:流量小 → 1 个实例即可
单体应用:必须整体扩展,浪费资源
微服务:每个服务独立扩展,精准投入场景 3:技术选型
不同服务可以用不同技术:
- 用户服务:Node.js + PostgreSQL
- 商品服务:Java + MySQL
- 订单服务:Python + MongoDB
- 库存服务:Go + Redis
各取所长,灵活组合微服务的挑战
不是所有项目都适合微服务!
| 优势 | 劣势 |
|---|---|
| ✅ 独立开发部署 | ❌ 架构复杂度高 |
| ✅ 团队协作并行 | ❌ 服务间调用开销 |
| ✅ 独立扩展 | ❌ 数据一致性难 |
| ✅ 技术栈灵活 | ❌ 运维成本高 |
适用场景:
- 应用功能复杂(10+ 模块)
- 团队规模大(10+ 人)
- 需要独立迭代不同模块
- 不同模块负载差异大
不适用场景:
- 小型项目(Bush/Jungle 当前规模)
- 小团队(1-3 人)
- 功能简单
组合使用:微服务 + 负载均衡
终极形态:每个微服务根据流量独立扩展实例数
继续用电商系统举例:
# 商品服务 - 5 个实例(流量最大,用户频繁浏览)
upstream product_service {
server product_service_1:3002;
server product_service_2:3002;
server product_service_3:3002;
server product_service_4:3002;
server product_service_5:3002;
}
# 订单服务 - 2 个实例(流量中等)
upstream order_service {
server order_service_1:3003;
server order_service_2:3003;
}
# 库存服务 - 1 个实例(流量小,但很重要)
upstream inventory_service {
server inventory_service_1:3004;
}
# 营销服务 - 2 个实例(活动时流量大)
upstream marketing_service {
server marketing_service_1:3005;
server marketing_service_2:3005;
}
# API 网关路由
location /api/products/ {
proxy_pass http://product_service; # 5 个实例负载均衡
}
location /api/orders/ {
proxy_pass http://order_service; # 2 个实例负载均衡
}
location /api/inventory/ {
proxy_pass http://inventory_service; # 1 个实例
}
location /api/marketing/ {
proxy_pass http://marketing_service; # 2 个实例负载均衡
}资源分配示意图:
服务器资源分配(总共 8GB 内存):
┌─────────────────────────────────────────────────────────┐
│ 商品服务 (5 实例) ████████████████ 40% (3.2GB) │
│ 订单服务 (2 实例) ██████ 15% (1.2GB) │
│ 库存服务 (1 实例) ███ 10% (0.8GB) │
│ 营销服务 (2 实例) ██████ 15% (1.2GB) │
│ 用户服务 (2 实例) ██████ 15% (1.2GB) │
│ 数据库 + Redis ███ 5% (0.4GB) │
└─────────────────────────────────────────────────────────┘
根据业务特点精准分配资源架构对比:
| 架构类型 | 实例特点 | 数据库 | 适用场景 |
|---|---|---|---|
| 单体应用 | 1 个服务 | 1 个数据库 | 小型项目 |
| 单体 + 负载均衡 | 多个相同实例 | 共享数据库 | 流量增大 |
| 微服务 | 多个不同服务 | 独立数据库 | 功能复杂 |
| 微服务 + 负载均衡 | 每个服务多实例 | 各自独立 | 大型系统 |
架构演进路径总结
Bush/Jungle 项目演进
阶段 1:单体应用(当前)
适用场景:小型项目、1-3 人团队
资源要求:2GB 内存、1 核 CPU
Nginx (bush_app:80)
└─ /api/ → Jungle 单体应用 (1 实例)
├─ 用户模块
├─ 文章模块
├─ 评论模块
├─ 分类模块
└─ 上传模块阶段 2:负载均衡(流量增长)
适用场景:流量突增、并发需求高
资源要求:4-6GB 内存、2 核 CPU
Nginx (bush_app:80)
└─ /api/ → Jungle (3 个相同实例)
├─ jungle_app_1
├─ jungle_app_2
└─ jungle_app_3
优势:并发能力提升 3 倍,高可用
劣势:内存占用增加,数据库可能成为瓶颈阶段 3:微服务拆分(功能复杂)
适用场景:功能模块多、团队扩大
资源要求:8GB+ 内存、4 核 CPU
Nginx (API 网关)
├─ /api/auth/ → 认证服务 (JWT、权限)
├─ /api/users/ → 用户服务 (个人信息、关注)
├─ /api/posts/ → 文章服务 (发布、编辑、搜索)
├─ /api/comments/ → 评论服务 (评论、点赞)
├─ /api/categories/ → 分类服务 (分类管理)
└─ /api/upload/ → 文件服务 (上传、存储)
优势:独立开发部署、团队并行、技术栈灵活
劣势:架构复杂、服务间调用、数据一致性阶段 4:微服务 + 负载均衡(大型系统)
适用场景:大型应用、高并发、高可用
资源要求:16GB+ 内存、8 核 CPU
Nginx (API 网关)
├─ /api/users/ → 用户服务 (2 实例)
├─ /api/posts/ → 文章服务 (3 实例,流量大)
├─ /api/comments/ → 评论服务 (2 实例)
└─ /api/upload/ → 文件服务 (1 实例)
优势:功能解耦 + 高并发 + 高可用
劣势:运维成本高、监控复杂电商系统(Florist)演进参考
单体应用:
适合创业初期、MVP 验证
Florist 后端 (单体)
├─ 用户模块
├─ 商品模块
├─ 订单模块
├─ 库存模块
└─ 营销模块微服务拆分后:
适合业务成熟、团队扩大
Nginx (API 网关)
├─ /api/users/ → 用户服务 (2 实例)
├─ /api/products/ → 商品服务 (5 实例,浏览多)
├─ /api/orders/ → 订单服务 (2 实例)
├─ /api/inventory/ → 库存服务 (1 实例,关键)
└─ /api/marketing/ → 营销服务 (2 实例,活动时)
每个服务:
- 独立代码库
- 独立数据库
- 独立团队
- 独立部署如何判断何时演进?
| 指标 | 单体应用 | 负载均衡 | 微服务 |
|---|---|---|---|
| 团队规模 | 1-3 人 | 3-5 人 | 10+ 人 |
| 并发量 | < 100 QPS | 100-500 QPS | > 500 QPS |
| 功能模块 | < 5 个 | 5-10 个 | 10+ 个 |
| 代码规模 | < 5 万行 | 5-10 万行 | 10+ 万行 |
| 迭代速度 | 每月 1-2 次 | 每周 1 次 | 每天多次 |
建议:
- ✅ 先单体,验证业务可行性
- ✅ 流量大时,加负载均衡
- ✅ 功能复杂时,考虑微服务
- ❌ 不要过早优化架构
Bush 项目实战总结
基于真实项目 Bush 的 Nginx 使用经验总结。
部署架构
┌─────────────────────────────────────────────────────────────┐
│ 生产环境部署流程 │
└─────────────────────────────────────────────────────────────┘
1. 本地开发完成
↓
2. pnpm deploy:local (scripts/deploy-local.sh)
• 检查工作区状态(未提交变更提示)
• 构建/复用开发镜像(bush-dev:latest)
• 创建临时容器执行构建:
- docker run --rm bush-dev:latest
- 环境变量:CI=true
- pnpm config set store-dir /tmp/pnpm
- pnpm install --frozen-lockfile
- pnpm run build
• 生成 dist/ 到宿主机(volume 挂载)
• git add dist/ && git commit && git push
↓
3. GitHub Actions 自动触发 (.github/workflows/ci-cd.yml)
• 触发条件:push main 且 dist/** 变化
• 检查 dist/ 目录存在且非空
• 打包部署文件:
- Dockerfile.prod、docker-compose.yml
- nginx.conf、dist/
- scripts/deploy-remote.sh
• scp 上传 bush.tar.gz 到服务器
• SSH 执行远程部署脚本
↓
4. 服务器自动部署 (scripts/deploy-remote.sh)
• 解压 bush.tar.gz
• docker compose down(停止旧容器)
• docker compose up -d --build
• 使用 Dockerfile.prod(FROM nginx:alpine + COPY dist)
• 健康检查:curl http://localhost/health关键设计决策
| 决策点 | 方案 | 原因 |
|---|---|---|
| 构建位置 | 本地容器内构建 | 服务器配置低(2GB),避免 OOM |
| 构建镜像 | 复用 bush-dev:latest | 开发和构建环境一致,节省空间 |
| 生产镜像 | nginx:alpine | 轻量(40MB),性能好 |
| 开发环境 | Dockerfile.dev + docker-compose.dev.yml | 容器化 UmiJS Dev Server(端口 8000) |
| 生产环境 | Dockerfile.prod + docker-compose.yml | Nginx 服务预构建 dist/(端口 80) |
| 网络通信 | Docker 网络 + DNS | 容器名解析,无需暴露后端端口 |
| 静态文件 | Nginx 直接服务 | 高性能,支持缓存和压缩 |
| API 代理 | Nginx 反向代理 | 统一入口,解决跨域 |
| 日志管理 | Volume 挂载 | 持久化到宿主机,方便查看 |
| 上传文件 | 共享 Volume | 前后端共享存储(/root/jungle_uploads) |
| 部署触发 | dist/ 变化时 | 只监听 dist/**,节省 CI/CD 资源 |
文件结构
bush/
├── dist/ # 前端构建产物(本地容器构建,提交到 Git)
├── nginx.conf # Nginx 配置文件(生产环境用)
├── Dockerfile.dev # 开发环境镜像(UmiJS Dev Server,也用于构建)
├── Dockerfile.prod # 生产环境镜像(Nginx + 预构建 dist/)
├── docker-compose.dev.yml # 开发环境编排(bush_app_dev:8000)
├── docker-compose.yml # 生产环境编排(bush_app:80)
├── .github/workflows/
│ └── ci-cd.yml # GitHub Actions 自动部署(监听 dist/** 变化)
├── scripts/
│ ├── deploy-local.sh # 本地部署脚本(容器内构建 + Git 提交)
│ ├── upload.sh # GitHub Actions 调用(打包上传到服务器)
│ └── deploy-remote.sh # 服务器端部署脚本(docker compose 启动)
├── logs/ # Nginx 日志目录(Volume 挂载到宿主机)
└── .gitignore # 忽略 .pnpm-store 等常见问题与解决方案
Q1: Nginx 启动失败,提示 "host not found in upstream"
原因: Nginx 启动时尝试解析后端容器名,但后端容器还未启动。
解决方案: 使用变量方式配置 proxy_pass
# ❌ 错误写法(启动时立即解析)
location /api/ {
proxy_pass http://jungle_app:3000;
}
# ✅ 正确写法(延迟解析)
location /api/ {
resolver 127.0.0.11 valid=30s;
set $jungle_service http://jungle_app:3000;
proxy_pass $jungle_service;
}Q2: 前端 SPA 路由刷新后 404
原因: Nginx 找不到对应的物理文件。
解决方案: 使用 try_files 将请求交给 index.html
location / {
try_files $uri $uri/ /index.html;
}Q3: API 代理不工作,提示跨域错误
检查清单:
- 确认 Nginx 配置中
proxy_pass正确 - 确认 Docker 网络配置,容器在同一网络
- 确认
proxy_set_header正确传递请求头 - 检查后端 CORS 配置
# 完整的代理配置
location /api/ {
resolver 127.0.0.11 valid=30s;
set $backend http://jungle_app:3000;
proxy_pass $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;
proxy_set_header X-Forwarded-Proto $scheme;
# URL 重写(如需要)
rewrite ^/api/(.*)$ /$1 break;
}Q4: 静态资源缓存不生效
检查点:
- 确认响应头中有
Cache-Control:curl -I http://localhost/assets/app.js - 检查
location规则优先级是否正确 - 注意
if语句可能导致缓存规则被跳过
# 正确配置
location ~* \.(js|css|png|jpg)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000";
}
# 验证
curl -I http://localhost/assets/app.js | grep Cache-Control
# 应输出: Cache-Control: public, max-age=31536000Q5: 容器间通信失败
检查步骤:
# 1. 确认容器在同一网络
docker network inspect jungle_default
# 2. 在 bush 容器内测试连通性
docker exec -it bush_app sh
ping jungle_app
curl http://jungle_app:3000/health
# 3. 检查后端容器是否正常运行
docker compose ps
# 4. 查看 Nginx 错误日志
docker compose logs bush | grep errorQ6: 日志文件权限问题
问题: Nginx 无法写入日志文件
解决方案:
# 确保宿主机日志目录权限正确
chmod 755 logs/
chown -R 1000:1000 logs/
# 或在 docker-compose.yml 中设置用户
services:
bush:
user: "1000:1000" # 与宿主机用户一致Q7: 部署后前端显示旧版本
原因: 浏览器缓存或 CDN 缓存
解决方案:
- 确保构建工具添加了文件 hash(Webpack/Vite 默认支持)
- 强制刷新浏览器缓存:
Ctrl + Shift + R - 检查 index.html 是否也被缓存
# index.html 不应该被缓存
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires 0;
}性能优化建议
1. 启用 Gzip 压缩
http {
gzip on;
gzip_comp_level 5;
gzip_min_length 1k;
gzip_types text/plain text/css application/json application/javascript application/xml;
gzip_vary on;
}2. 使用 HTTP/2
server {
listen 443 ssl http2;
# ... SSL 配置
}3. 静态资源CDN化
将静态资源上传到 CDN,减轻服务器压力:
# 前端代码中引用 CDN 地址
<script src="https://cdn.example.com/assets/app.js"></script>4. 配置连接池
upstream backend {
server jungle_app:3000;
keepalive 32; # 保持连接池
}
location /api/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}监控与日志
查看实时访问日志
# 实时查看访问日志
docker compose logs -f bush
# 统计访问量最高的 URL
cat logs/access.log | awk '{print $7}' | sort | uniq -c | sort -rn | head -10
# 统计响应时间
cat logs/access.log | awk '{print $NF}' | sort -n | tail -100配置自定义日志格式
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time';
access_log /var/log/nginx/access.log main;
}总结
Nginx 作为高性能的 Web 服务器和反向代理,是现代 Web 应用的核心组件。在 Bush 项目中,Nginx 承担了:
- 静态文件服务 - 提供前端构建产物
- 反向代理 - 转发 API 请求到后端
- 路径重写 - 移除 /api 前缀
- 缓存优化 - 静态资源长期缓存
- 健康检查 - 监控服务状态
- 日志记录 - 便于问题排查
核心优势:
- ✅ 轻量高效(Alpine 镜像 40MB+)
- ✅ 配置简单(一个配置文件搞定)
- ✅ 容器友好(与 Docker 完美集成)
- ✅ 生产级稳定性
扩展能力:
- 🚀 负载均衡 - 水平扩展,提高并发
- 🏗️ API 网关 - 微服务架构支持
- 🔒 SSL/TLS - HTTPS 安全连接
- 📦 缓存加速 - 性能优化
掌握 Nginx 的配置和使用,是构建高性能 Web 应用的必备技能。