← 返回笔记索引

项目实践

2C2G 服务器上的生产级 AI Assistant 部署实践:zglab-rag 的运行边界

记录 zglab-rag 在 2 vCPU、2 GB 内存服务器上通过 Nginx、FastAPI、SQLite、systemd 和 HTTPS 长期运行的部署取舍。

2C2G 服务器上的生产级 AI Assistant 部署实践:zglab-rag 的运行边界

目录

部署目标与约束

zglab-rag 需要成为可长期访问的公开服务,而不只是本机 uv run 的演示。目标服务器为 Ubuntu 24.04、2 vCPU、2 GB RAM、约 2 GB swap、40 GB 磁盘。这个预算排除了 Kubernetes、Redis、 Elasticsearch、Kafka、Docker Swarm 等额外控制面和常驻进程。

部署选择不是“最流行的生产架构”,而是 Nginx + FastAPI/Uvicorn + SQLite + systemd:组件少、 故障面清晰、数据库可备份,且符合当前单助手服务规模。

运行架构

flowchart TD
    I[Internet] --> D[ask.zglab.fun / HTTPS]
    D --> N[Nginx]
    N --> W[Vue SPA\n/var/www/zglab-assistant]
    N --> A[Uvicorn / FastAPI\n127.0.0.1:8000]
    A --> K[(runtime/knowledge.db)]
    A --> M[本地 Embedding 模型缓存]
    A --> L[外部 LLM Provider]
    T[systemd timer] --> B[SQLite backup]
    T --> S[受控 source sync]

应用代码位于 /opt/zglab-rag/app;运行数据位于 /opt/zglab-rag/runtime;模型缓存位于 /opt/zglab-rag/models;密钥只在服务器 .env 中保存。数据库、模型、日志、备份与构建产物 都不进入 Git。

服务生命周期与健康检查

zglab-rag-api.service 以非特权 zglab 用户运行,WorkingDirectory 指向应用目录,仅监听 127.0.0.1:8000。服务从 EnvironmentFile 读取配置,异常退出使用 Restart=always,以 SIGINT 进行优雅关闭,并限制运行时的可写目录。

关键点是不让第一个用户请求承担模型加载:应用 startup 阶段加载 Embedding、验证 SQLite、 检查 LLM 配置,完成后才将 ready 标记设为 true。

sequenceDiagram
    participant SD as systemd
    participant API as FastAPI
    participant DB as SQLite
    participant E as Embedding
    SD->>API: process start
    API->>DB: 打开并检查索引
    API->>E: 加载本地模型
    API->>API: 验证 LLM 配置
    API-->>SD: ready=true
    Note over API: 此后才接受用户请求

GET /health 表示进程存活;GET /ready 表示 runtime、数据库、Embedding Provider 与 LLM 配置都已满足服务条件。二者不能混用,否则发布脚本容易把“端口已监听”误判为“请求已可处理”。

Nginx、HTTPS 与 SSE

Nginx 提供 ask.zglab.fun 的 Vue history fallback、gzip 和静态缓存;/api/ 反代到本机 API。 SSE 使用 HTTP/1.1,显式关闭 proxy_buffering 和缓存,设置读写超时,并发送 X-Accel-Buffering: no。这保证状态事件不会在 Nginx 中攒到请求结束才出现。

证书由 ACME 签发。首次使用 bootstrap 配置只开放 challenge,签发成功后切换到 TLS 配置; 2026-08-21 的验收中证书有效期至 2026-11-19。服务日志采用 JSON,包含 request ID、path、 latency、status 和 error code,不记录 API Key、完整问题、Prompt、Evidence 或内部路径。

备份和知识同步

zglab-rag backup 使用 SQLite backup API 复制一致性快照:写入临时文件、fsync、原子 rename, 保留最近 7 份。zglab-rag-backup.timer 每日 02:45 触发。

同步流程只处理已登记 Git checkout:先检查工作区干净,再 fetch --prune 与 fast-forward-only merge;随后执行 revision、Chunk 和 embedding input 的差异规划,只嵌入 new/changed Chunk,并原子 更新索引。失败发生在 ingestion 或 embedding 阶段时,旧 knowledge.db 继续服务。

flowchart LR
    G[已登记 Git source] --> F[fetch + fast-forward]
    F --> P[revision / chunk diff]
    P --> E[仅嵌入 changed]
    E --> X[atomic index apply]
    X --> R[重启 API 加载新 runtime]
    F -.失败.-> O[保留旧索引继续服务]

实际部署中服务器到 GitHub 的 HTTPS 出站连接超时。首次建库使用已经受控克隆到服务器的 checkout, 并用 sync apply --skip-git-fetch 写入 1,058 个 Chunk;常规 Git 命令设 60 秒上限,超时发生在 ingestion 前,验证了数据库不变。恢复可信的 GitHub 出站网络后,定时同步才可真正取得新 revision。

实际验证与取舍

2026-08-21 的生产验收记录包括:服务预热约 5.6~8.2 秒,稳定 RSS 约 439 MiB;受控重启后 /ready 约 8.2 秒恢复;一次普通问答为 28.094 秒,一次 SSE 请求在 6.691 秒完成。/health/ready、普通 API、SSE、history fallback、HTTPS、备份和服务重启均完成实际验证。

Reranker 虽有离线质量收益,却会将完整评测进程峰值推至约 1.49 GB RSS,因此不作为这个服务器 上的默认在线路径。这里的取舍不是否定 Reranker,而是保留系统给 OS、Uvicorn、SQLite 和并发请求的 生存空间。

方法论总结

  • 先让启动有明确 ready 语义,再讨论访问量和扩容。
  • 模型、数据库、密钥与代码目录分离,才能安全更新与恢复。
  • SSE 的正确性包含反向代理配置,不是后端单独能保证的。
  • 同步失败应影响“新知识是否进入”,不能影响“旧服务能否继续回答”。

有限资源环境迫使工程决策更诚实:每个常驻组件、每次模型加载和每个同步步骤都必须说明它占用什么、 失败后保留什么,以及如何恢复。