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 的正确性包含反向代理配置,不是后端单独能保证的。
- 同步失败应影响“新知识是否进入”,不能影响“旧服务能否继续回答”。
有限资源环境迫使工程决策更诚实:每个常驻组件、每次模型加载和每个同步步骤都必须说明它占用什么、 失败后保留什么,以及如何恢复。