Skip to content

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.11Docker 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
# 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 - 轻量级镜像,仅 ~40MB
  • daemon off; - 前台运行,适配容器环境
  • dist/ 在本地 Docker 容器内构建(保证构建环境一致,避免服务器 OOM)

第三步:配置 docker-compose.yml

yaml
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 - 容器异常退出时自动重启

第四步:运行容器

bash
# 构建并启动容器(生产环境)
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 自动部署 的方式:

  1. 本地构建pnpm deploy:local - 使用开发镜像在临时容器内执行 pnpm build
  2. 提交推送:dist/ 提交到 Git 并 push
  3. 自动触发:GitHub Actions 检测到 dist/ 变化时自动触发
  4. 打包上传:打包部署文件并 scp 上传到服务器
  5. 服务器部署:docker compose up -d --build(使用 Dockerfile.prod)

关键点:

  • 开发和构建复用同一镜像(bush-dev:latest),节省空间
  • 构建在本地容器内进行,避免服务器 OOM(2GB 内存)
  • 服务器仅 COPY dist/,无需安装依赖和构建

关于完整部署流程和 CI/CD 配置,请参考:Docker - 实战案例:Bush & Jungle

第五步:验证服务

部署完成后,可以通过以下方式验证:

在服务器上验证:

bash
# SSH 到服务器后执行

# 1. 测试健康检查
curl http://localhost/health
# 应返回: ok

# 2. 检查容器状态
docker compose ps

# 3. 查看 Nginx 日志
docker compose logs bush

从外部访问验证:

bash
# 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 的核心配置单元,用于定义一个虚拟主机。

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 直接提供。

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 会怎样?

nginx
# ❌ 错误配置(缺少 try_files)
location / {
    root /usr/share/nginx/html;
}

问题:

  • ✅ 访问 / → 正常显示
  • ✅ 访问 /assets/app.js → 正常加载
  • ❌ 刷新 /user/123404 Not Found
  • ❌ 直接访问 /user/123404 Not Found

3. 反向代理 - API 请求转发

将前端的 API 请求代理到后端服务,实现前后端分离。

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

代理配置要点:

  1. resolver - Docker 环境必须配置,用于解析容器名
  2. 变量方式 - 避免 Nginx 启动时检查后端是否可用
  3. proxy_set_header - 让后端能获取客户端真实信息
  4. rewrite - URL 重写,前后端路径不一致时使用

4. 静态资源缓存

为前端资源文件(JS/CSS/图片)设置长期缓存,提升加载速度。

nginx
# 匹配特定文件类型的资源
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. 用户上传文件服务

处理用户上传的静态文件(图片、文档等)。

nginx
# 处理上传的静态文件
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 的区别:

nginx
# 使用 root(追加路径)
location /static/ {
    root /app;
}
# 请求 /static/image.jpg -> 查找 /app/static/image.jpg

# 使用 alias(替换路径)
location /static/ {
    alias /app/uploads/;
}
# 请求 /static/image.jpg -> 查找 /app/uploads/image.jpg

6. 健康检查接口

提供一个简单的健康检查端点,用于监控服务状态。

nginx
location /health {
    # 不记录访问日志,避免大量监控请求刷屏
    access_log off;
    
    # 直接返回 200 状态码和 "ok" 文本
    return 200 "ok";
}

使用场景:

  • 容器编排工具(Docker/K8s)的健康检查
  • 负载均衡器的后端状态检测
  • 监控系统的存活探测

7. 日志配置

记录访问日志和错误日志,方便问题排查。

nginx
server {
    # ... 其他配置
    
    # 访问日志:记录所有请求
    access_log /var/log/nginx/access.log;
    
    # 错误日志:记录错误和警告信息
    error_log /var/log/nginx/error.log;
}

查看日志:

bash
# 实时查看访问日志
tail -f /var/log/nginx/access.log

# 实时查看错误日志
tail -f /var/log/nginx/error.log

8. 完整配置示例

Bush 项目的完整 Nginx 配置:

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 优化与安全配置(按需选择):

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)

什么是负载均衡?

负载均衡是反向代理的扩展应用,将请求分发到多个相同的后端实例。

nginx
# 基础反向代理(单实例)
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!

javascript
// 典型 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 操作:

javascript
// CPU 密集型操作(消耗 CPU)
- JSON 序列化/反序列化
- 加密解密(bcrypt 加密密码)
- 图片压缩、视频转码
- 复杂的数据计算

// 示例:加密密码
const hash = await bcrypt.hash(password, 10);  // CPU 工作 50ms

特点:

  • ✅ 速度极快(纳秒级)
  • ❌ 数量有限(1 核、2 核、4 核等)

2. 内存(RAM)- 临时工作台

作用: 临时存储正在运行的程序和数据

形象比喻: 内存就像厨师的工作台,台面越大,能同时准备的食材越多

2GB 内存:小工作台,只能同时处理几个订单
8GB 内存:大工作台,可以同时处理很多订单

Web 应用中的内存使用:

javascript
// 内存占用示例
- 每个 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 操作:

javascript
// 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 倍!)   │
│ 原理:轮流工作,没有闲置时间            │
└────────────────────────────────────────┘

关键理解:

  1. 为什么 3 个厨师更快?

    • 不是因为厨师做菜更快(CPU 速度没变)
    • 而是因为一个厨师等待时,其他厨师在工作
    • 充分利用了等待时间!
  2. 为什么服务器配置低也能多实例?

    单实例:1 核 CPU,使用率 20%(80% 时间等待 I/O)
    3 实例:1 核 CPU,使用率 60%(充分利用 CPU)
    
    内存:500MB × 3 = 1.5GB(2GB 服务器完全够用)
  3. 什么时候多实例无效?

    • 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 请求:

javascript
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%500MB10 req/sCPU 闲置,浪费资源
3 实例60%1.5GB30 req/s✅ 充分利用 CPU

结论:

  • I/O 密集型应用(Web API、数据库操作)→ 多实例提升并发
  • CPU 密集型应用(图像处理、视频转码)→ 多实例无效

完整配置示例

nginx
# 定义后端服务器组
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 配置示例:

yaml
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
# 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 人)
  • 功能简单

组合使用:微服务 + 负载均衡

终极形态:每个微服务根据流量独立扩展实例数

继续用电商系统举例:

nginx
# 商品服务 - 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 QPS100-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.ymlNginx 服务预构建 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

nginx
# ❌ 错误写法(启动时立即解析)
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

nginx
location / {
    try_files $uri $uri/ /index.html;
}

Q3: API 代理不工作,提示跨域错误

检查清单:

  1. 确认 Nginx 配置中 proxy_pass 正确
  2. 确认 Docker 网络配置,容器在同一网络
  3. 确认 proxy_set_header 正确传递请求头
  4. 检查后端 CORS 配置
nginx
# 完整的代理配置
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: 静态资源缓存不生效

检查点:

  1. 确认响应头中有 Cache-Controlcurl -I http://localhost/assets/app.js
  2. 检查 location 规则优先级是否正确
  3. 注意 if 语句可能导致缓存规则被跳过
nginx
# 正确配置
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=31536000

Q5: 容器间通信失败

检查步骤:

bash
# 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 error

Q6: 日志文件权限问题

问题: Nginx 无法写入日志文件

解决方案:

bash
# 确保宿主机日志目录权限正确
chmod 755 logs/
chown -R 1000:1000 logs/

# 或在 docker-compose.yml 中设置用户
services:
  bush:
    user: "1000:1000"  # 与宿主机用户一致

Q7: 部署后前端显示旧版本

原因: 浏览器缓存或 CDN 缓存

解决方案:

  1. 确保构建工具添加了文件 hash(Webpack/Vite 默认支持)
  2. 强制刷新浏览器缓存:Ctrl + Shift + R
  3. 检查 index.html 是否也被缓存
nginx
# 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 压缩

nginx
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

nginx
server {
    listen 443 ssl http2;
    # ... SSL 配置
}

3. 静态资源CDN化

将静态资源上传到 CDN,减轻服务器压力:

nginx
# 前端代码中引用 CDN 地址
<script src="https://cdn.example.com/assets/app.js"></script>

4. 配置连接池

nginx
upstream backend {
    server jungle_app:3000;
    keepalive 32;  # 保持连接池
}

location /api/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}

监控与日志

查看实时访问日志

bash
# 实时查看访问日志
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

配置自定义日志格式

nginx
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 承担了:

  1. 静态文件服务 - 提供前端构建产物
  2. 反向代理 - 转发 API 请求到后端
  3. 路径重写 - 移除 /api 前缀
  4. 缓存优化 - 静态资源长期缓存
  5. 健康检查 - 监控服务状态
  6. 日志记录 - 便于问题排查

核心优势:

  • ✅ 轻量高效(Alpine 镜像 40MB+)
  • ✅ 配置简单(一个配置文件搞定)
  • ✅ 容器友好(与 Docker 完美集成)
  • ✅ 生产级稳定性

扩展能力:

  • 🚀 负载均衡 - 水平扩展,提高并发
  • 🏗️ API 网关 - 微服务架构支持
  • 🔒 SSL/TLS - HTTPS 安全连接
  • 📦 缓存加速 - 性能优化

掌握 Nginx 的配置和使用,是构建高性能 Web 应用的必备技能。

Released under the MIT License.