From 094f4b0cd95f7f8a993fbceb348b85cef93b55e0 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Tue, 19 May 2026 21:43:18 +0800 Subject: [PATCH] =?UTF-8?q?doc(RUN):=20Ubuntu=20systemd=20=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=20unit=20+=20=E6=97=A0=E6=84=9F=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=8C=87=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- RUN.md | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/RUN.md b/RUN.md index a29b28e..f2bffcc 100644 --- a/RUN.md +++ b/RUN.md @@ -2,7 +2,7 @@ > 怎么把 zcbot 跑起来。env / 常用命令 / 故障兜底。设计看 `DESIGN.md`,进度看 `PROGRESS.md`。 -最后更新:2026-05-19(dev SPA 登录改 邮箱+密码;`POST /v1/auth/login_password`;`main.py user add` CLI) +最后更新:2026-05-19(dev SPA 登录改 邮箱+密码;`POST /v1/auth/login_password`;`main.py user add` CLI;Ubuntu systemd 部署 + 无感更新指引) --- @@ -136,6 +136,101 @@ curl --noproxy '*' -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8765/v1/ta --- +## 部署(Ubuntu / systemd) + +> 单机最小拓扑:`/opt/zcbot/`(代码 + `.venv` + `.env`)+ systemd unit。按需改路径 / user / port。 + +### 一次性 + +```bash +sudo useradd -r -s /sbin/nologin -d /opt/zcbot zcbot # 跑服务的非 root 用户 +sudo chown -R zcbot:zcbot /opt/zcbot +# 把 .env 权限收紧(含 JWT_SECRET / PLATFORM_KEY) +sudo chmod 600 /opt/zcbot/.env +sudo chown zcbot:zcbot /opt/zcbot/.env +``` + +### unit 文件 `/etc/systemd/system/zcbot.service` + +```ini +[Unit] +Description=zcbot web (FastAPI/uvicorn) +After=network-online.target postgresql.service +Wants=network-online.target + +[Service] +Type=simple +User=zcbot +WorkingDirectory=/opt/zcbot +# 显式让 systemd 装载 .env(KEY=value 行,不展开 ${...},不留 shell 引号) +EnvironmentFile=/opt/zcbot/.env +ExecStart=/opt/zcbot/.venv/bin/python main.py web --host 0.0.0.0 --port 8765 +Restart=on-failure +RestartSec=2 +KillSignal=SIGTERM +# uvicorn graceful shutdown 会等 in-flight 请求(含 SSE 长连接); +# 10s 后 systemd 兜底 SIGKILL,避免 SSE 拖住 restart 卡死 +TimeoutStopSec=10 +KillMode=mixed +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target +``` + +启用 + 日常: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now zcbot +sudo systemctl status zcbot | head +sudo journalctl -u zcbot -f # 实时日志 +sudo systemctl restart zcbot # 重启(REST 抖动 ~2s,SSE 连接断) +sudo systemctl stop zcbot +``` + +> **不要再用 `kill -HUP`**:uvicorn 不响应 SIGHUP(没装 handler,落 Python 默认),也不会 reload 代码。Ubuntu 上要么 `systemctl restart`,要么用下面 `--reload` 自动模式。 + +### 无感更新(对 SSE 也尽量不抖) + +zcbot 现在 5 人级 + SSE 长连接,**严格"零中断"**(蓝绿 + nginx + SSE 客户端 reconnect 设计)代价高,不值得。有性价比的两挡: + +**A. 简易档:`--reload`**(推荐当前规模) +ExecStart 加 `--reload`,`git pull` 后 uvicorn 监听到文件变动自动重起子进程,REST 抖动 < 1s。**代价**:SSE 连接被切断(浏览器看到 "load failed",dev.html 自动跳登录页或同事重发一次消息;DB 里被切的 task 走启动 reaper 标 `run_status=error`)。 + +```ini +ExecStart=/opt/zcbot/.venv/bin/python main.py web --host 0.0.0.0 --port 8765 --reload +``` + +`sudo systemctl restart zcbot` 一次生效。之后**只 `git pull` 即可**,不用再 restart;改 unit 文件本身才需 daemon-reload + restart。 + +**B. 真无感档:nginx + 蓝绿双实例**(将来流量上来再上) +两个 systemd 实例 `zcbot@blue` / `zcbot@green`(模板 unit,`--port 8765` / `--port 8766`),nginx upstream 在中间切。流程: +1. 部署到空闲实例(假设 green):`sudo systemctl restart zcbot@green` +2. `curl 127.0.0.1:8766/healthz` 验新版起来 +3. 改 nginx upstream 指向 green,`nginx -s reload` — **新 REST 走 green,旧 SSE 还连在 blue 不断** +4. 等 blue SSE 自然清空(`ss -tnp | grep :8765` 为空)再关 blue + +**zcbot 端额外要做的事**:消息 broker 当前在 task 进程内(`web/broker.py`),蓝绿期间同 task 不同进程会丢事件。nginx 侧用 `hash $arg_task_id consistent` 保同 task 落同实例可以缓解,但 task 创建分布是另一回事。要做这条得先把 broker 改成 Redis pub/sub。10 人内**不推**,留到真有需要再上。 + +### 部署 SOP(目前推荐:方案 A `--reload`) + +```bash +# 在服务器上 +cd /opt/zcbot +sudo -u zcbot git pull --ff-only # 拉新代码 +sudo -u zcbot .venv/bin/python -m pip install -r requirements.txt # 依赖有变才需要 +sudo -u zcbot .venv/bin/python main.py db upgrade head # migration 有新版才需要 +# 这一步通常不用做:--reload 监听到 .py 文件变动会自动重起 +# 但 .env / unit 改了 → 手动: +# sudo systemctl restart zcbot +sudo systemctl status zcbot | head +sudo journalctl -u zcbot -n 50 # 看新进程起没起干净 +``` + +--- + ## 故障兜底 | 现象 | 原因 / 处理 | @@ -155,6 +250,8 @@ curl --noproxy '*' -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8765/v1/ta | `POST /v1/tasks/{id}/cancel` 返 409 `task not running` | `run_status` 不是 `running`(idle / cancelling / error 都不能 cancel);dev SPA 自动忽略不报错 | | 点 stop 后流式没立刻停 | LLM 同步 call 不可中断,最坏等当前一轮跑完(几十秒);loop 进入下个 check 点(每轮 LLM 前 / 每个 tool_call 前)就退,emit `cancelled` → SSE `done` → UI 收回 stop | | `[startup] reaped N stale active run(s)` | 上次 web 进程未正常 finish 留下 N 个孤儿 run,启动 lifespan 自动标 error。info 级,无需处理 | +| `kill -HUP ` 后 `/openapi.json` 没新接口 | uvicorn **不响应 SIGHUP**(没装 handler,落 Python 默认终止;Windows 上信号本身无效)。Ubuntu 上用 `systemctl restart zcbot`,或 unit 加 `--reload` 让 uvicorn 监听文件自动重起(见"部署"段)。验证:`curl -s http://127.0.0.1:8765/openapi.json \| python3 -c 'import sys,json;print([p for p in json.load(sys.stdin)["paths"] if "auth" in p])'` | +| `systemctl restart zcbot` 卡 10s 才退 | 有 SSE 长连接,uvicorn graceful shutdown 等 in-flight。unit 已设 `TimeoutStopSec=10` 兜 SIGKILL,正常现象;真急用 `systemctl kill -s KILL zcbot` | | `POST /v1/files/delete` 返 409 `folder ... 仍被 N 个 task 引用` | 顶层目录被 task 引用 working_dir;先 `DELETE /v1/tasks/{id}` 删完关联 task 再删目录。子目录不受此限 | | `POST /v1/files/rename` 返 409 `folder has active run(s)` | 顶层目录被某 running/cancelling 的 task 占用;先 cancel 等流式 done 再 rename | | `POST /v1/files/rename` 返 409 `... 前缀嵌套` | 改名后会与其他 task 的 working_dir 形成嵌套;换不冲突的 new_name |