Chapter 10

部署实战

从本地开发到生产上线的完整路径:容器化、编排、反向代理、零停机发布,以及生产环境的调优与监控。

为什么需要容器化部署?

Swift 是一门以 Apple 平台闻名的语言,但服务端几乎永远跑在 Linux 上。这带来了一个独特的挑战:开发者在 macOS 上写代码、本地测试,但生产环境是 Ubuntu 或 Debian。如果直接把 macOS 编译的二进制文件上传到服务器,它根本无法运行——两个平台的 ABI、系统库都不一样。

Docker 解决了这个问题,同时还带来了额外的好处:

跨平台一致性
在 macOS 上构建 Docker 镜像时,构建过程发生在 Linux 容器内部(使用官方 swift:6.0 镜像),生成的是原生 Linux 二进制文件。无论开发机是 macOS、Windows 还是 Linux,最终产物完全相同。
依赖隔离
Vapor 应用依赖 libssl、libcrypto、glibc 等系统库。容器把所有运行时依赖打包在一起,避免"在我机器上能跑"的经典问题。不同服务使用不同版本的库也不会互相干扰。
可重复构建
Dockerfile 是构建过程的声明式文档。任何人、任何时间、任何机器执行相同的 Dockerfile,得到的镜像行为完全一致。这对 CI/CD 流水线至关重要。
水平扩展
容器可以在编排系统(docker-compose、Kubernetes)中轻松复制多份。流量增加时,启动更多容器实例;流量降低时,缩减实例数量。整个过程无需修改应用代码。
快速回滚
每次发布产生一个带版本标签的镜像(如 myapp:v1.2.3)。发现问题时,只需将运行中的容器替换为上一版本的镜像,回滚时间以秒计,无需重新编译。
Swift on Linux 的注意事项 Swift 标准库在 Linux 上可用,但部分 Foundation 功能(如 DateFormatter 的区域设置、URLSession 的某些行为)与 Apple 平台有细微差异。确保在 Linux 容器中运行完整的测试套件,不要只在 macOS 上测试后直接上线。

多阶段 Dockerfile

最直接的想法是:用 swift:6.0 镜像编译,然后把整个镜像作为生产镜像推送。问题是 swift:6.0 镜像体积超过 1.5 GB——里面包含了编译器、调试工具、头文件、包管理器等大量只在构建时需要的东西。生产环境运行一个二进制文件,完全不需要这些。

多阶段构建(Multi-stage Build)是解决方案:用一个"构建镜像"编译,只把编译产物复制到一个极小的"运行时镜像"中。最终推送和运行的只是运行时镜像。

多阶段构建流程: ┌─────────────────────────────────────┐ │ 构建阶段 (builder) │ │ 基础镜像: swift:6.0 (~1.5 GB) │ │ │ │ 1. 复制 Package.swift │ │ 2. 解析并缓存依赖 │ │ 3. 复制源代码 │ │ 4. swift build -c release │ │ 5. 输出: .build/release/App │ └──────────────┬──────────────────────┘ │ COPY --from=builder │ (只复制二进制文件) ▼ ┌─────────────────────────────────────┐ │ 运行时阶段 (runtime) │ │ 基础镜像: ubuntu:24.04 (~80 MB) │ │ │ │ 1. 安装运行时依赖 (libssl 等) │ │ 2. 复制编译好的二进制 │ │ 3. 设置入口点 │ │ 最终镜像大小: ~120 MB │ └─────────────────────────────────────┘ 节省: ~1.4 GB (约 92% 的体积减少)
# ─── 阶段一:构建 ────────────────────────────────────────────────────────────
FROM swift:6.0-jammy AS builder

# 工作目录
WORKDIR /build

# 先只复制 Package 描述文件,利用 Docker 层缓存
# 如果源码变了但依赖没变,这一层可以直接复用缓存
COPY Package.swift Package.resolved ./
RUN swift package resolve

# 再复制源代码
COPY Sources ./Sources
COPY Resources ./Resources

# Release 模式编译:开启优化,去掉调试符号
RUN swift build -c release --product App \
    -Xswiftc -Osize \
    2>&1 | tee /tmp/build.log

# ─── 阶段二:运行时镜像 ──────────────────────────────────────────────────────
FROM ubuntu:24.04

# 安装 Swift 运行时所需的最小系统库
RUN export DEBIAN_FRONTEND=noninteractive \
    && apt-get update -q \
    && apt-get install -yq \
        libssl-dev \
        ca-certificates \
        tzdata \
    && rm -rf /var/lib/apt/lists/*

# 创建非 root 用户(安全最佳实践)
RUN useradd --system --create-home --shell /bin/bash vapor

WORKDIR /app

# 从构建阶段复制二进制文件和资源
COPY --from=builder /build/.build/release/App .
COPY --from=builder /build/Resources ./Resources

# 切换到非 root 用户
USER vapor

# 暴露端口(仅文档用途,实际映射在 docker-compose 中配置)
EXPOSE 8080

# 健康检查(每 30 秒检查一次)
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

# 启动命令:绑定所有接口,使用环境变量配置
ENTRYPOINT ["./App"]
CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
构建缓存技巧 先复制 Package.swiftPackage.resolved,执行 swift package resolve,再复制源代码——这是最重要的 Dockerfile 优化技巧。Docker 按层缓存,只要 Package 文件没变,依赖解析层就会命中缓存,大幅加速后续构建。如果先 COPY . . 再 resolve,任何源文件的修改都会导致依赖重新下载。
Package.resolved 必须提交到 Git Package.resolved 文件记录了所有依赖的精确版本,务必将它提交到版本控制。没有这个文件,每次 Docker 构建可能解析到不同版本的依赖,破坏可重复构建的保证。

docker-compose 编排

实际应用不只是一个进程,还需要数据库、缓存等基础设施。docker-compose 把多个服务的配置集中在一个 YAML 文件中,一条命令启动整个环境。

一个典型的 Vapor 应用栈包含三个服务:Vapor 应用本身、PostgreSQL 数据库、Redis 缓存。它们通过 Docker 内部网络通信,外部只暴露必要的端口。

# docker-compose.yml
version: "3.9"

services:

  # ── Vapor 应用 ──────────────────────────────────────────────────────────
  app:
    build:
      context: .
      dockerfile: Dockerfile
    image: myapp:latest
    restart: unless-stopped
    ports:
      - "8080:8080"   # 仅在需要直接访问时开放;生产中 Nginx 代理后可以不开放
    environment:
      - DATABASE_URL=postgres://vapor:${DB_PASSWORD}@db:5432/vapor_prod
      - REDIS_URL=redis://redis:6379
      - LOG_LEVEL=info
      - APP_ENV=production
    env_file:
      - .env          # 包含 DB_PASSWORD、JWT_SECRET 等敏感变量
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s
    volumes:
      - ./Resources:/app/Resources:ro   # 挂载资源目录(只读)
    networks:
      - backend

  # ── PostgreSQL ──────────────────────────────────────────────────────────
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: vapor
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: vapor_prod
    volumes:
      - pg_data:/var/lib/postgresql/data   # 命名卷,数据持久化
      - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U vapor -d vapor_prod"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend

  # ── Redis ───────────────────────────────────────────────────────────────
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --save 60 1 --loglevel warning
    volumes:
      - redis_data:/data
    networks:
      - backend

  # ── Nginx 反向代理 ───────────────────────────────────────────────────────
  nginx:
    image: nginx:1.27-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/certs:/etc/nginx/certs:ro
    depends_on:
      - app
    networks:
      - backend

# 命名卷:数据在容器删除后依然保留
volumes:
  pg_data:
  redis_data:

# 内部网络:服务间通过服务名互相访问
networks:
  backend:
    driver: bridge
depends_on 与健康检查的区别 depends_on 只保证容器的启动顺序,不保证服务"就绪"。PostgreSQL 容器启动后,数据库进程本身还需要几秒钟初始化。使用 condition: service_healthy 结合 healthcheck 才能真正等待数据库就绪后再启动 Vapor 应用,避免应用启动时连不上数据库而崩溃。

环境变量管理

硬编码密码、密钥、数据库连接串是最常见的安全失误之一。正确的做法是通过环境变量注入所有敏感配置,代码中只读取变量名,不存储具体值。

在 Vapor 项目根目录创建 .env 文件(永远不要提交到 Git):

# .env — 本地开发用,不提交到 Git!
# 在 .gitignore 中添加:.env

DB_PASSWORD=super_secret_password_123
JWT_SECRET=your-256-bit-secret-key-here
REDIS_URL=redis://redis:6379
SMTP_PASSWORD=smtp_api_key
APP_ENV=development

configure.swift 中读取环境变量。Vapor 提供了 Environment 类型来封装这个过程:

// Sources/App/configure.swift
import Vapor
import Fluent
import FluentPostgresDriver
import Redis

public func configure(_ app: Application) async throws {

    // ── 读取环境变量 ──────────────────────────────────────────────────────

    // Environment.get() 读取进程环境变量,找不到时返回 nil
    guard let dbURL = Environment.get("DATABASE_URL") else {
        throw Abort(.internalServerError,
                    reason: "DATABASE_URL environment variable is required")
    }

    guard let jwtSecret = Environment.get("JWT_SECRET") else {
        throw Abort(.internalServerError,
                    reason: "JWT_SECRET environment variable is required")
    }

    // 可选变量:提供合理的默认值
    let logLevel = Environment.get("LOG_LEVEL") ?? "info"
    let maxConnections = Int(Environment.get("DB_MAX_CONNECTIONS") ?? "10") ?? 10

    // ── 数据库配置 ────────────────────────────────────────────────────────
    try app.databases.use(
        .postgres(url: dbURL, maxConnectionsPerEventLoop: maxConnections),
        as: .psql
    )

    // ── Redis 配置 ────────────────────────────────────────────────────────
    if let redisURL = Environment.get("REDIS_URL") {
        try app.redis.use(url: redisURL)
    }

    // ── 日志级别 ──────────────────────────────────────────────────────────
    app.logger.logLevel = .init(rawValue: logLevel) ?? .info

    // ── 在生产环境中,隐藏详细错误信息 ───────────────────────────────────
    if app.environment == .production {
        app.middleware.use(ErrorMiddleware.default(environment: app.environment))
    }

    // ── 注册路由 ──────────────────────────────────────────────────────────
    try routes(app)
}

// 辅助扩展:读取必需环境变量,找不到就抛出明确错误
extension Environment {
    static func require(_ key: String) throws -> String {
        guard let value = get(key), !value.isEmpty else {
            throw Abort(.internalServerError,
                        reason: "Missing required environment variable: \(key)")
        }
        return value
    }
}
绝对禁止的做法 永远不要在代码中硬编码密码、API 密钥或数据库 URL,哪怕是临时测试用的。即使你"之后会删掉",Git 历史会永远记住它。也不要把 .env 文件提交到 Git——在项目初始化时立即将 .env 加入 .gitignore

Nginx 反向代理

Vapor 自带的 HTTP 服务器可以直接对外提供服务,但在生产环境中通常在前面加一层 Nginx。原因有几个:SSL 终止(Nginx 处理 HTTPS,Vapor 只处理 HTTP)、静态文件服务(Nginx 直接返回静态资源,不经过 Vapor)、请求限流(防止单个客户端刷接口)、负载均衡(把请求分发给多个 Vapor 实例)。

# nginx/nginx.conf

user nginx;
worker_processes auto;          # 自动设置为 CPU 核心数
error_log  /var/log/nginx/error.log warn;

events {
    worker_connections 1024;    # 每个 worker 最大并发连接数
    use epoll;                  # Linux 上使用 epoll 事件模型
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    # ── 日志格式(JSON,方便日志聚合系统解析)─────────────────────────────
    log_format json_combined escape=json
        '{"time":"$time_iso8601",'
        '"remote_addr":"$remote_addr",'
        '"method":"$request_method",'
        '"uri":"$request_uri",'
        '"status":$status,'
        '"body_bytes":$body_bytes_sent,'
        '"request_time":$request_time,'
        '"upstream_time":"$upstream_response_time"}';
    access_log /var/log/nginx/access.log json_combined;

    # ── 性能优化 ──────────────────────────────────────────────────────────
    sendfile        on;
    tcp_nopush      on;
    keepalive_timeout 65;

    # ── Gzip 压缩 ─────────────────────────────────────────────────────────
    gzip            on;
    gzip_types      text/plain application/json application/javascript
                    text/css text/xml;
    gzip_min_length 1024;
    gzip_comp_level 5;

    # ── 限流区(防止暴力请求)────────────────────────────────────────────
    limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;

    # ── 上游 Vapor 应用(支持多实例负载均衡)────────────────────────────
    upstream vapor_app {
        server app:8080;                    # docker-compose 服务名
        keepalive 32;                       # 保持 32 个到上游的长连接
        # 多实例时添加:
        # server app_1:8080;
        # server app_2:8080;
        # server app_3:8080;
    }

    # ── HTTP → HTTPS 重定向 ───────────────────────────────────────────────
    server {
        listen 80;
        server_name example.com www.example.com;
        return 301 https://$host$request_uri;
    }

    # ── HTTPS 主服务器 ────────────────────────────────────────────────────
    server {
        listen 443 ssl http2;
        server_name example.com;

        # SSL 证书(推荐用 Let's Encrypt)
        ssl_certificate     /etc/nginx/certs/fullchain.pem;
        ssl_certificate_key /etc/nginx/certs/privkey.pem;
        ssl_protocols       TLSv1.2 TLSv1.3;
        ssl_ciphers         HIGH:!aNULL:!MD5;

        # HSTS:告诉浏览器该域名只能通过 HTTPS 访问
        add_header Strict-Transport-Security "max-age=31536000" always;

        # ── API 路由,带限流 ────────────────────────────────────────────────
        location /api/ {
            limit_req zone=api burst=50 nodelay;
            proxy_pass         http://vapor_app;
            proxy_http_version 1.1;
            proxy_set_header   Upgrade $http_upgrade;
            proxy_set_header   Connection "upgrade";   # WebSocket 支持
            proxy_set_header   Host $host;
            proxy_set_header   X-Real-IP $remote_addr;
            proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Proto $scheme;
            proxy_read_timeout 60s;
        }

        # ── 健康检查端点(不限流,内部监控用)─────────────────────────────
        location /health {
            proxy_pass http://vapor_app;
            access_log off;    # 不记录健康检查日志,避免日志污染
        }

        # ── 静态文件(直接由 Nginx 提供,不经过 Vapor)────────────────────
        location /static/ {
            root      /var/www;
            expires   30d;
            add_header Cache-Control "public, immutable";
        }
    }
}
WebSocket 支持 注意 proxy_set_header UpgradeConnection "upgrade" 这两行。它们是让 Nginx 正确代理 WebSocket 连接的关键。没有这两行,WebSocket 的 HTTP Upgrade 握手会失败,连接会被 Nginx 以普通 HTTP 请求处理然后返回错误。

数据库迁移策略

数据库迁移(Migration)是生产部署中最容易出问题的环节。与代码可以快速回滚不同,数据库 schema 变更涉及数据,回滚成本更高,需要周密的策略。

迁移(Migration)
对数据库 schema 的一次有版本记录的变更操作(创建表、增加列、添加索引等)。Vapor 的 Fluent 用代码描述迁移,并记录哪些迁移已经执行过,避免重复执行。每次迁移包含 prepare(正向变更)和 revert(回滚)两个方向。
幂等性(Idempotency)
迁移必须是幂等的:执行一次和执行多次的效果相同。Fluent 通过记录已执行的迁移来保证这一点,但如果手动修改数据库或迁移记录,可能导致重复执行问题。
向前兼容迁移
在零停机部署场景中,新旧版本代码可能同时运行。迁移必须向前兼容:新增列时设置默认值或允许 NULL,不直接删除旧列(先废弃、下个版本再删除)。

有两种运行迁移的方式,各有适用场景:

方式 A:启动时自动迁移

entrypoint.sh 中先执行迁移再启动应用。适合单实例或开发环境,简单方便。多实例并发启动时可能产生迁移冲突。

#!/bin/sh
# entrypoint.sh
echo "Running migrations..."
./App migrate --yes
echo "Starting server..."
exec ./App serve \
  --env production \
  --hostname 0.0.0.0 \
  --port 8080
方式 B:独立迁移 Job

在 docker-compose 或 Kubernetes 中,用单独的容器/Job 跑迁移,完成后再启动应用容器。适合多实例、CI/CD 流水线,避免并发迁移问题。

# docker-compose.yml 中添加
migrate:
  image: myapp:latest
  command: ["./App", "migrate", "--yes"]
  depends_on:
    db:
      condition: service_healthy
  env_file: .env
  restart: "no"

回滚迁移时要格外谨慎。Fluent 提供了 migrate --revert 命令,但只有当 revert 方法正确实现时才安全:

// Sources/App/Migrations/CreateUserTable.swift
import Fluent

struct CreateUserTable: AsyncMigration {

    // prepare:正向迁移,创建表结构
    func prepare(on database: any Database) async throws {
        try await database.schema("users")
            .id()
            .field("email",      .string,   .required)
            .field("name",       .string,   .required)
            .field("created_at", .datetime)
            .unique(on: "email")
            .create()
    }

    // revert:回滚迁移,删除表
    // 注意:这会丢失所有数据,生产环境谨慎执行
    func revert(on database: any Database) async throws {
        try await database.schema("users").delete()
    }
}

// 向前兼容的新增列迁移(安全)
struct AddUserAvatarColumn: AsyncMigration {
    func prepare(on database: any Database) async throws {
        try await database.schema("users")
            .field("avatar_url", .string)  // 可为 NULL,不破坏旧数据
            .update()
    }

    func revert(on database: any Database) async throws {
        try await database.schema("users")
            .deleteField("avatar_url")
            .update()
    }
}
生产数据库迁移前必做 在生产环境执行迁移前:1) 备份数据库;2) 在预生产环境(staging)完整演练;3) 评估迁移是否可以在线执行(对大表加列/加索引可能锁表,需要用 CREATE INDEX CONCURRENTLY 等在线变更方案);4) 准备好回滚方案和时间窗口。

健康检查端点

健康检查(Health Check)是容器编排系统判断服务是否正常的机制。Docker 和 Kubernetes 都依赖健康检查来决定是否路由流量到某个容器、是否需要重启容器。一个有用的健康检查不只是"进程在运行",还应验证关键依赖(数据库连接、Redis 连接)是否正常。

// Sources/App/Controllers/HealthController.swift
import Vapor
import Fluent

struct HealthController: RouteCollection {

    func boot(routes: any RoutesBuilder) throws {
        routes.get("health", use: check)
    }

    func check(req: Request) async throws -> HealthResponse {
        var checks: [String: CheckResult] = [:]
        var overall: HealthStatus = .healthy

        // ── 数据库健康检查 ──────────────────────────────────────────────────
        do {
            // 执行一个极轻量的查询验证连接
            try await req.db.execute(
                SQLQueryString("SELECT 1"), binds: [], onRow: { _ in }
            )
            checks["database"] = .init(status: .healthy)
        } catch {
            checks["database"] = .init(status: .unhealthy, message: error.localizedDescription)
            overall = .unhealthy
        }

        // ── Redis 健康检查 ──────────────────────────────────────────────────
        do {
            _ = try await req.redis.ping().get()
            checks["redis"] = .init(status: .healthy)
        } catch {
            checks["redis"] = .init(status: .degraded, message: error.localizedDescription)
            // Redis 故障降级为 degraded,不至于完全 unhealthy
        }

        // ── 返回结果 ────────────────────────────────────────────────────────
        let response = HealthResponse(
            status: overall,
            version: Environment.get("APP_VERSION") ?? "unknown",
            checks: checks
        )

        // unhealthy 时返回 503,让负载均衡器停止路由流量到此实例
        if overall == .unhealthy {
            throw Abort(.serviceUnavailable, headers: [:], reason: "Service unhealthy")
        }
        return response
    }
}

// ── 响应类型 ────────────────────────────────────────────────────────────────
enum HealthStatus: String, Codable {
    case healthy, degraded, unhealthy
}

struct CheckResult: Codable {
    let status:  HealthStatus
    let message: String?
    init(status: HealthStatus, message: String? = nil) {
        self.status = status; self.message = message
    }
}

struct HealthResponse: Content {
    let status:  HealthStatus
    let version: String
    let checks:  [String: CheckResult]
}

健康检查返回的 JSON 示例:

# curl -s http://localhost:8080/health | jq
{
  "status": "healthy",
  "version": "1.2.3",
  "checks": {
    "database": { "status": "healthy" },
    "redis":    { "status": "healthy" }
  }
}

零停机部署

传统的部署方式是停止旧版本、部署新版本、重新启动——期间服务不可用,称为"停机部署"。对于需要高可用的服务,必须实现零停机部署(Zero-Downtime Deployment)。

蓝绿部署(Blue-Green Deployment)流程: 部署前(流量全部在蓝环境): 用户请求 ──→ Nginx ──→ [ 蓝环境 v1.0 ] ← 接收流量 [ 绿环境 v1.1 ] ← 新版本部署中 部署步骤: 1. 在绿环境部署并验证 v1.1 2. 绿环境健康检查通过 3. 切换 Nginx upstream → 绿环境 4. 蓝环境处理完已有请求后下线 部署后(流量全部在绿环境): 用户请求 ──→ Nginx ──→ [ 蓝环境 v1.0 ] ← 保留(快速回滚备用) [ 绿环境 v1.1 ] ← 接收流量 回滚(如果 v1.1 有问题): 将 Nginx 切回蓝环境,回滚耗时 < 30 秒

实现零停机的关键在于让 Vapor 应用能够优雅地处理关闭信号(SIGTERM)——当容器收到停止命令时,不要立即中断所有连接,而是等待正在处理的请求完成后再退出:

// Sources/App/entrypoint.swift
import Vapor

// Swift 6 入口点
@main
struct Entrypoint: AsyncEntrypoint {

    static func main() async throws {
        var env = try Environment.detect()
        try LoggingSystem.bootstrap(from: &env)

        let app = try await Application.make(env)

        // 配置优雅关闭超时:最多等待 30 秒让已有请求完成
        app.http.server.configuration.shutdownTimeout = .seconds(30)

        do {
            try await configure(app)
        } catch {
            app.logger.report(error: error)
            try? await app.asyncShutdown()
            throw error
        }

        try await app.execute()
        try await app.asyncShutdown()
    }
}

// 在 configure.swift 中注册应用生命周期回调
struct AppLifecycleHandler: LifecycleHandler {

    func willBoot(_ app: Application) throws {
        app.logger.info("Application is starting up...")
    }

    func didBoot(_ app: Application) throws {
        app.logger.info("Application is ready to accept connections")
    }

    // 收到 SIGTERM 时触发,在此清理资源
    func shutdown(_ app: Application) {
        app.logger.info("Application is shutting down gracefully...")
        // 在这里:关闭后台任务、完成最后的指标上报等
    }
}
docker-compose 滚动更新 使用 docker-compose 进行滚动更新:docker-compose up -d --no-deps --build app。这会重新构建并替换 app 服务,而不重启数据库等其他服务。结合 Nginx 的上游健康检查,可以实现对用户无感知的发布。

日志与监控

生产环境的日志不是给人类实时阅读的,而是被日志聚合系统(ELK Stack、Loki、CloudWatch)收集、索引、查询的。因此,生产日志应该是结构化的 JSON 格式,而不是可读性好的纯文本。

Vapor 的 Logger 遵循 Swift 的 swift-log 标准接口,可以替换后端实现。在代码中使用结构化日志,加入请求上下文信息:

// Package.swift — Swift 6 项目配置
// swift-tools-version:6.0
import PackageDescription

let package = Package(
    name: "MyVaporApp",
    platforms: [.macOS(.v15)],
    swiftLanguageVersions: [.v6],
    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git",               from: "4.99.0"),
        .package(url: "https://github.com/vapor/fluent.git",               from: "4.9.0"),
        .package(url: "https://github.com/vapor/fluent-postgres-driver.git",  from: "2.8.0"),
    ],
    targets: [
        .executableTarget(
            name: "App",
            dependencies: [
                .product(name: "Vapor",               package: "vapor"),
                .product(name: "Fluent",              package: "fluent"),
                .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
            ],
            swiftSettings: [.swiftLanguageVersion(.v6)]
        ),
    ]
)
// 结构化日志:不要拼接字符串,使用元数据
func createOrder(req: Request) async throws -> Order {
    let userID = try req.auth.require(User.self).id

    // 日志中附加业务上下文(会被 JSON 序列化,方便后续过滤查询)
    req.logger.info("Creating order", metadata: [
        "user_id":     .string(userID?.uuidString ?? "unknown"),
        "request_id":  .string(req.id),
        "remote_addr": .string(req.remoteAddress?.description ?? "unknown"),
    ])

    do {
        let order = try await OrderService.create(req: req)
        req.logger.info("Order created successfully", metadata: [
            "order_id": .string(order.id?.uuidString ?? "unknown"),
            "amount":   .stringConvertible(order.total),
        ])
        return order
    } catch {
        req.logger.error("Failed to create order", metadata: [
            "error":   .string(error.localizedDescription),
            "user_id": .string(userID?.uuidString ?? "unknown"),
        ])
        throw error
    }
}

// 自定义中间件:记录每个请求的耗时
struct RequestLoggingMiddleware: AsyncMiddleware {
    func respond(to request: Request, chainingTo next: any AsyncResponder) async throws -> Response {
        let start    = Date()
        let response = try await next.respond(to: request)
        let duration = Date().timeIntervalSince(start)

        request.logger.info("Request completed", metadata: [
            "method":      .string(request.method.string),
            "path":        .string(request.url.path),
            "status":      .stringConvertible(response.status.code),
            "duration_ms": .stringConvertible(Int(duration * 1000)),
        ])
        return response
    }
}
Prometheus 指标集成 生产监控的标准方案是 Prometheus + Grafana。在 Vapor 中通过 swift-metrics 暴露指标:添加 SwiftPrometheus 依赖,在 configure.swift 中注册 PrometheusMetricsFactory,然后暴露 GET /metrics 端点。Prometheus 定期抓取这个端点,Grafana 可视化展示 QPS、延迟分布、错误率等关键指标。

Linux 生产环境调优

默认的 Linux 配置面向通用场景,不是为高并发服务器优化的。在生产环境中,以下几项调优能显著提升 Vapor 的性能上限。

在宿主机(Docker 宿主,不是容器内)修改系统配置:

# /etc/sysctl.conf — 内核参数调优(需要 root 权限)

# ── 文件描述符上限 ──────────────────────────────────────────────────────
# 每个 HTTP 连接占用一个文件描述符
# 默认 1024 对高并发服务器远远不够
fs.file-max = 1048576

# ── TCP 优化 ──────────────────────────────────────────────────────────
# 增大 TCP 接收/发送缓冲区
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728

# TIME_WAIT 状态的 socket 可以被快速复用
net.ipv4.tcp_tw_reuse = 1

# 本地端口范围(允许更多并发出站连接)
net.ipv4.ip_local_port_range = 1024 65535

# SYN 队列长度(防止 SYN flood,也支持更高并发握手)
net.ipv4.tcp_max_syn_backlog = 65536
net.core.somaxconn = 65536

# ── TCP Keepalive ─────────────────────────────────────────────────────
# 60 秒无活动后发送 keepalive 探测包
net.ipv4.tcp_keepalive_time = 60
# 探测间隔 10 秒
net.ipv4.tcp_keepalive_intvl = 10
# 最多探测 6 次无响应后认为连接断开
net.ipv4.tcp_keepalive_probes = 6

# 应用配置:
# sysctl -p /etc/sysctl.conf
# /etc/security/limits.conf — 进程级文件描述符限制

# vapor 用户(运行 Vapor 进程的用户)
vapor soft nofile 65536
vapor hard nofile 65536

# 或者对所有用户生效
*     soft nofile 65536
*     hard nofile 65536

# 验证当前进程的限制:
# ulimit -n
# cat /proc/$(pgrep App)/limits

Swift 运行时本身也有一些可调参数:

# Swift 并发运行时:控制全局并发执行器的线程数
# 默认等于 CPU 核心数,通常不需要修改
export SWIFT_CONCURRENCY_THREAD_COUNT=8

# 禁用调试反射(生产环境,减少内存占用)
export SWIFT_REFLECTION_METADATA_LEVEL=none

# NIO 的 EventLoop 数量(默认等于 CPU 核心数)
# 高 I/O 场景可以适当增加
export VAPOR_WORKER_COUNT=16

# 在 Dockerfile CMD 中传入:
# CMD ["./App", "serve", "--env", "production",
#      "--hostname", "0.0.0.0", "--port", "8080"]
EventLoop(事件循环)
SwiftNIO 的核心抽象,每个 EventLoop 运行在一个线程上,通过 epoll/kqueue 监听 I/O 事件。Vapor 的所有异步操作都在 EventLoop 上调度。线程数通常等于 CPU 核心数——过多会导致上下文切换开销增大,过少会导致 CPU 利用率不足。
文件描述符(File Descriptor)
Linux 中每个打开的文件、网络连接、管道都占用一个文件描述符(整数 ID)。默认上限 1024 意味着进程最多只能同时处理约 1000 个连接(还要减去标准输入/输出/错误等系统描述符)。高并发服务必须提升这个限制。
TCP TIME_WAIT
TCP 连接关闭后,端口进入 TIME_WAIT 状态约 60 秒,防止迟到的数据包被新连接误收。高并发场景下大量 TIME_WAIT 会耗尽端口资源。tcp_tw_reuse=1 允许在安全的情况下复用 TIME_WAIT 状态的连接,缓解端口耗尽问题。
Gzip 压缩
在 Nginx 层开启 Gzip 压缩,可以将 JSON API 响应体积减少 60–80%。压缩发生在 Nginx,Vapor 无需任何改动,客户端(支持 Accept-Encoding: gzip 的浏览器和 HTTP 客户端)自动解压缩。
性能测试先于调优 不要盲目调整参数。先用 wrkheyk6 进行基准测试,找到实际的性能瓶颈(CPU 限制?内存不足?数据库慢查询?连接数耗尽?),然后针对性调优。过早优化往往在错误的方向上浪费时间。
本章小结 至此,你已经掌握了将 Vapor 应用从本地开发带到生产环境的完整工具链:多阶段 Dockerfile 构建出精简的生产镜像;docker-compose 编排应用、数据库和反向代理;环境变量安全管理敏感配置;Nginx 提供 SSL 终止、限流和负载均衡;优雅关闭实现零停机部署;结构化日志为可观测性奠基;生产参数调优释放系统上限。
恭喜完成 Swift 服务端开发 (Vapor) 全课程! 从第 1 章的第一行 swift package init,到今天把应用稳稳地跑在 Linux 生产容器里——你已经走完了一名 Vapor 后端开发者的核心技能路径。路由与中间件、Fluent 数据库 ORM、身份认证、任务队列、WebSocket 实时通信、完整测试策略、最终的容器化部署……每一章都是真实生产代码的缩影。接下来,可以尝试为自己的第一个 Vapor 服务选一个真实的域名,配一张 Let's Encrypt 证书,然后 docker-compose up -d,看着它在互联网上正式运行的那一刻——那才是最好的学习验证。