Files
planet/docs/technical/zh/ops-planet-sh-startup.md
linkong 899e3bce43
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
release / images (push) Has been cancelled
ci / delivery (push) Has been cancelled
release: bump version to 0.71.0
2026-06-11 16:47:24 +08:00

392 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# planet.sh 启动性能优化
## 背景
`planet.sh` 管理所有服务的启动/停止/重启。原有实现存在以下问题:
1. AI Provider 每次都重新构建(即使代码未变)
2. 杀端口速度极慢(最长等 45 秒)
3. 端口绑定检测用 Python 子进程(每次 ~300ms
4. 无参 `restart``restart -b` 行为不一致
## 问题一AI Provider 每次重建
### 根因
构建戳文件存放在 `/tmp/`WSL/Linux 重启后 `/tmp` 被清空,导致三个条件中的"戳文件非空"这一条始终不满足,进而判定需要重建:
```bash
# 三个条件必须同时成立才跳过重建
image_exists AND stamp_non_empty AND fingerprint_match
```
### 修复
将戳文件路径从临时目录改到持久缓存路径:
```bash
AI_PROVIDER_BUILD_STAMP_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/planet/aiprovider_build.sha256"
```
写入时确保目录存在:
```bash
write_ai_provider_build_stamp() {
mkdir -p "$(dirname "$AI_PROVIDER_BUILD_STAMP_FILE")"
compute_ai_provider_build_fingerprint > "$AI_PROVIDER_BUILD_STAMP_FILE"
}
```
### fingerprint 计算提速
原实现对整个 `aiprovider/` 打 tar 包再算 SHA大目录下耗时可达数秒。改为 `find + stat`(只读文件元信息,不读内容):
```bash
compute_ai_provider_build_fingerprint() {
find aiprovider \
-type f \
! -path '*/__pycache__/*' \
! -name '.env' \
! -name '.env.*' \
! -name '*.pyc' \
! -name '*.pyo' \
| LC_ALL=C sort \
| xargs -r stat --format="%Y %s %n" 2>/dev/null
sha256sum docker-compose.yml docker-compose.simple.yml 2>/dev/null
python3 "$SCRIPT_DIR/scripts/compute_aiprovider_dependency_fingerprint.py" 2>/dev/null
}
```
速度提升约 10 倍大量小文件场景误报率相同mtime+size 变化 ≡ 文件被修改)。
`.env``.env.*` 被排除在 fingerprint 外。它们属于运行期配置,不应该因为修改模型、密钥或 Base URL 触发镜像重建。
### Docker build context 收敛
AI Provider 镜像只需要根目录的 `pyproject.toml``uv.lock``aiprovider/` 代码。仓库中还包含前端静态大图、PDF、历史数据和 Unreal 资料,如果 build context 使用整个仓库,`transferring context` 会浪费大量时间。
当前通过根目录 `.dockerignore` 收敛上下文:
```dockerignore
**
!pyproject.toml
!uv.lock
!aiprovider/
!aiprovider/**
aiprovider/.env
aiprovider/.env.*
!aiprovider/.env.example
```
Dockerfile 也从全仓复制改为只复制 AI Provider 代码:
```dockerfile
COPY pyproject.toml uv.lock /app/
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
COPY aiprovider /app/aiprovider
```
`uv sync` 使用 BuildKit cache mount 后,首次构建仍可能受网络影响;后续构建会复用 `/root/.cache/uv`,依赖下载不再重复从零开始。
### 运行期配置来源
`planet.sh` 启动 AI Provider 前会生成受当前用户保护的运行期 env-file并把它传给 Compose 或手动 `docker run` fallback。默认路径位于 `${XDG_STATE_HOME:-$HOME/.local/state}/planet/aiprovider_runtime.env`。配置优先来自:
1. `aiprovider/.env`
2. `~/.zshrc` 中简单的 `export AI_...=...``AI_...=...`
默认解析是静态的,只覆盖 AI Provider、镜像、代理相关变量避免执行交互 shell 初始化。如果确实需要复杂 shell 展开,可以显式启用:
```bash
PLANET_LOAD_ZSHRC_ENV=source ./planet.sh start -a
```
如果排查时需要忽略个人 shell 配置:
```bash
PLANET_LOAD_ZSHRC_ENV=0 ./planet.sh start -a
```
### 跳过重建的原理
fingerprint 一致时不执行 `docker compose build`,而是:
```bash
docker start planet_aiprovider # 启动已存在的容器,几秒内完成
```
`docker stop` 停容器,不删镜像;`cleanup_exit_containers` 删已退出容器,不删镜像。下次 `docker start` 会从现有镜像直接创建并启动容器。
## 问题二:杀端口速度慢
### 原因
`wait_for_port_release` 默认最多等 45 秒15 次 × 3 秒)。
### 修复
将后台进程清理场景的超时缩短至 3 秒TERM→1.5s→KILL→1.5s
```bash
PORT_RELEASE_ATTEMPTS=15
PORT_RELEASE_INTERVAL=0.2 # 每次等 0.2s,总计 3s
# cleanup_backend_processes / kill_port_if_requested
wait_for_port_release "$port" 15 0.2
```
`wait_for_port_release` 增加可选参数,允许不同场景使用不同超时:
```bash
wait_for_port_release() {
local port="$1"
local max_attempts="${2:-$PORT_RELEASE_ATTEMPTS}"
local interval="${3:-$PORT_RELEASE_INTERVAL}"
...
}
```
当前启动前端时还有一层预清理重试:
- `PORT_PRESTART_RETRIES`:默认 3 次。
- `PORT_PRESTART_RETRY_INTERVAL`:默认 2 秒。
`kill_port_if_requested()` 优先清理当前环境能找到的监听 PID只有检测到当前运行在 WSL 且没有可杀 PID、但端口仍不可绑定时才会检查 Windows 侧 listener并请求管理员 PowerShell 删除旧 `portproxy`、停止占用端口的服务或强制结束对应进程。若管理员请求被取消,或 `iphlpsvc` 这类系统服务拒绝停止,脚本会打印 Windows listener 详情和管理员 PowerShell 处理命令,然后立即停止启动,不再继续拉起服务碰同一个端口错误。前端 Vite 启动后才发现 `Port 3000 is already in use` 时,也会打印同一套 Windows listener 处理命令。非 WSL 环境不会尝试 Windows 清理路径。`--allow-lan` 直接开放 `3000` / `8000` / `8010`,不再启动额外的 Windows 端口转发进程;旧的持久 portproxy 规则应清理掉。
## 问题三:端口检测用 Python
### 原因
`can_bind_port``python3 -c "import socket..."` 检测端口,每次调用约 300ms。
### 修复
优先使用系统工具(~10msPython 作为兜底:
```bash
can_bind_port() {
local port="$1"
if command -v ss >/dev/null 2>&1; then
! ss -tlnH 2>/dev/null | awk '{print $4}' | grep -qE ":${port}$"
return
fi
if command -v lsof >/dev/null 2>&1; then
[ -z "$(lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null)" ]
return
fi
python3 - "$port" <<'PY'
import sys, socket
p = int(sys.argv[1])
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
s.bind(("", p)); s.close(); sys.exit(0)
except OSError:
sys.exit(1)
PY
}
```
## 问题四restart 行为不一致
### 现象
- `restart -b`:停全部服务 → 检查 AI Provider fingerprint → 按需重建 → 启动
- `restart`(无参):停全部服务 → AI Provider 总是判定需要重建(因戳文件在 /tmp
### 修复
修复戳文件路径后,无参 `restart` 同样使用 `stop + start`fingerprint 检查正常生效,行为与 `restart -b` 完全一致。无需额外代码变更。
## 状态文件、日志与失败清理
`planet.sh` 不再把 PID、日志和运行期 env-file 写入固定 `/tmp/planet_*` 路径。默认状态目录为:
```bash
${XDG_STATE_HOME:-$HOME/.local/state}/planet
```
脚本启动时会创建该目录并尽量设置为 `700`。当前使用的文件包括:
- `backend.pid` / `frontend.pid` / `motion_agent.pid`
- `backend.log` / `frontend.log` / `motion_agent.log`
- `aiprovider_build.log`
- `aiprovider_runtime.env`
- `ports.env`
PID 文件写入前会校验 PID 为正整数,写入时带换行并尽量设置为 `600`。读取 PID 文件时,如果内容不是数字,脚本会忽略该文件,不会把垃圾内容传给 `kill`
`start` 成功后会把本次端口写入 `ports.env`。后续执行 `./planet.sh health` 时,会优先检查上次启动端口;如果没有状态文件,则回退到默认端口 `8000``3000``8010``8765`。这避免了用自定义端口启动后,健康检查仍只看默认端口的问题。
启动过程有轻量失败清理:如果 `start` 中途失败脚本只清理本轮已经拉起的本地进程backend、frontend、Motion Agent不会在正常启动完成后停止服务。AI Provider、PostgreSQL 和 Redis 容器仍按原有容器生命周期管理。
## 健康检查与安全加固
HTTP 健康检查统一使用 `curl -fsS --max-time`。因此 `/health` 返回 4xx/5xx 不再被视为在线。
进程终止路径现在会校验:
- signal 只允许 `TERM``KILL``INT``HUP`
- PID 必须是正整数;
- 进程组 PGID 必须是正整数。
这可以避免坏 PID 文件或错误 signal 造成不可预期的 `kill` 行为。
前端和 Motion Agent 启动失败时,现在也会调用 `print_port_listener_details()`输出与后端一致的端口监听诊断。WSL 下如果端口看起来被 Windows 侧占用,脚本仍只在检测到 WSL 时才调用 PowerShell 诊断或清理路径。
## 跨平台注意事项
当前脚本是 Linux-first并带有 WSL 增强。普通 Linux 不会执行 WSL PowerShell 逻辑WSL 下会额外提供 Windows listener、portproxy 和摄像头提示。
如果要把同一份脚本扩展为 Linux、macOS、WSL 三平台通用,还需要继续封装这些命令差异:
- `stat --format``sort -V``xargs -r` 是 GNU 风格macOS 默认 BSD 工具不完全兼容。
- `hostname -I``ss``fuser``systemctl` 在 macOS 上通常不可用。
- `tac` 在 macOS 上不一定存在,可用 `awk` 或 Python 兜底。
- Docker Desktop on macOS 不适用 `systemctl` daemon 诊断。
- 摄像头自动发现依赖 `/dev/video*` / `v4l2-ctl`,这是 Linux 路线macOS 应显式使用 camera URL 或另做 AVFoundation 检测。
维护方向是增加一个小的 platform compatibility 层,把端口监听检测、版本比较、文件元信息、反向 tail、LAN IP 获取和 Docker daemon 诊断集中处理,而不是在业务启动流程里继续散落平台判断。
## 正式交付边界
`planet.sh` 是本地开发便利脚本,不作为正式生产启动入口。正式交付应通过 Kubernetes 的 `Deployment``Service``Ingress`、readiness/liveness probe 管理端口、健康检查、重启和滚动发布。这样生产环境不需要脚本抢占宿主机端口,也不会依赖 Vite dev server。
前端生产形态是 `vite build` 生成静态资源,再由 nginx/Caddy 等 HTTP 服务器托管。不要在生产中使用 `bun run dev``vite preview`。当前不维护 Webpack 双构建链;如果未来需要评估更企业化的构建生态,优先做 Rsbuild/Rspack spike。Electron 仅在正式目标变成离线桌面软件时再单独评估。
## Motion Agent 默认启动
`planet.sh` 现在默认随 `start` 和全量 `restart` 启动本地 Motion Agent。这样星球端、UE 或调试客户端可以直接连接 `ws://127.0.0.1:8765/ws/gestures`。如果当前机器没有可用摄像头,默认隐式启动会降级为 dry-run 协议服务,不会阻断后端/前端启动;只有显式传入 `--motion-agent`、摄像头 index、摄像头 URL 或 WSL USB 参数时live 模式缺摄像头才会硬失败。
如果本次不需要 Motion Agent
```bash
./planet.sh start --non-motion-agent
./planet.sh restart --non-motion-agent
```
常用参数:
- `--non-motion-agent`:本次启动或全量重启不拉起 Motion Agent。
- `--motion-agent` / `-m`:显式要求本次启动或重启拉起 Motion Agent此时 live 摄像头失败会作为错误反馈。
- `--motion-agent-port <端口>`:覆盖默认 WebSocket 端口 `8765`
- `--motion-agent-mode <模式>`:指定输入模式,可选 `auto``single``dual_redundant``single_fallback``calibrated_3d``dual` 作为兼容别名会进入双路冗余。
- `--motion-agent-camera-indexes <indexes>`:覆盖自动发现的摄像头 index例如 `0``0,1`。也可以用环境变量 `MOTION_AGENT_CAMERA_INDEXES=0,1`
- `--motion-agent-camera-urls <urls>`:使用 RTSP/HTTP 摄像头流,适合 WSL、手机摄像头或网络摄像头。也可以用环境变量 `MOTION_AGENT_CAMERA_URLS=...`
- `--motion-agent-wsl-usbipd`:在 WSL 中尝试通过 `usbipd-win` 自动把唯一的 Windows USB 摄像头透传到 Linux。
- `--motion-agent-wsl-usbipd-busid <BUSID>`:在 WSL 中指定 `usbipd list` 里的摄像头 BUSID 后透传,适合多摄像头设备。
- `--motion-agent-dry-run`:不打开摄像头、不加载 CV 依赖,只启动协议服务,适合调试 Web 端连接。
非 dry-run 的 live 模式会在启动前检查 `mediapipe``opencv-python`。如果当前 `.venv` 缺包,脚本会自动执行:
```bash
uv add mediapipe opencv-python
```
如需禁止启动时自动安装 Python CV 依赖,可设置:
```bash
PLANET_MOTION_AGENT_AUTO_INSTALL=0 ./planet.sh start
```
`./planet.sh init` 会在 WSL 中预检查 `usbipd-win`。如果没有 `usbipd.exe`,脚本会先尝试 `winget install -e --id dorssel.usbipd-win`,失败后复用仓库内置的 dorssel.usbipd-win MSI fallback如果缓存缺失或需要其他架构版本再下载 MSI 并请求管理员 PowerShell 安装。该步骤是 best-effort失败会提示后续处理方式但不会阻断普通初始化。可通过 `./planet.sh init --non-motion-agent` 跳过该预检。
live 模式会自动寻找 `/dev/video*`,并优先用 OpenCV 实测过滤出真正能打开并读帧的 index再传给 Motion Agent。在 WSL/USB 摄像头场景中,一个摄像头可能暴露多个 `/dev/video*` 节点,其中部分是 metadata 或非采集节点,脚本会跳过这类不可读 index。在 WSL 中Windows 摄像头通常不会自动出现在 `/dev/video*`。可先用下面命令看设备:
默认 live 采集使用低延迟参数:`640x360` 输入、约 `15Hz` 识别事件worker 内部用 latest-frame 读帧线程,只保留每路摄像头的最新帧,避免 MediaPipe 慢帧时继续排队识别旧画面。骨架调试流默认关闭,只在星球端打开动捕调试面板时按约 `8Hz` 推送,避免日常手势控制被调试数据拖慢。状态事件会同时上报采集 FPS 与识别 FPS方便区分摄像头掉帧和识别耗时。
```bash
ls /dev/video*
```
如需覆盖自动发现结果,可手动指定 index
```bash
./planet.sh start --motion-agent --motion-agent-camera-indexes 1,2
```
WSL 下更通用的方式是把手机摄像头或网络摄像头以 RTSP/HTTP 流接入:
```bash
./planet.sh start --motion-agent --motion-agent-camera-urls http://192.168.1.20:8080/video
```
如果希望直接使用 Windows USB 摄像头,可以让脚本调用 `usbipd-win` 透传。该能力是显式开启的,因为摄像头附加到 WSL 期间通常会从 Windows 应用中暂时断开。
只有一个摄像头时:
```bash
./planet.sh start --motion-agent --motion-agent-wsl-usbipd
```
多个摄像头时,先查看 BUSID再指定设备
```bash
usbipd.exe list
./planet.sh start --motion-agent --motion-agent-wsl-usbipd-busid 3-2
```
如果 `usbipd attach` 提示设备未共享或未绑定,脚本会尝试弹出 Windows 管理员 PowerShell 自动执行 `usbipd bind`,然后重试 attach。若 UAC 被取消或自动 bind 失败,可在 Windows 管理员 PowerShell 中手动执行:
```powershell
usbipd bind --busid 3-2
usbipd attach --wsl --busid 3-2
```
如果 WSL 中没有发现 `/dev/video*`,且没有提供 `--motion-agent-camera-urls`,默认隐式启动会降级为 dry-run。显式 live 启动会停止并提示处理方式。可选处理:
```bash
./planet.sh start --motion-agent --motion-agent-camera-urls http://<手机IP>:8080/video
./planet.sh start --motion-agent --motion-agent-wsl-usbipd
./planet.sh start --motion-agent --motion-agent-dry-run
```
显式 live 启动时,只有设置 `PLANET_MOTION_AGENT_WSL_ALLOW_DRY_RUN_FALLBACK=1`WSL 无摄像头才会自动降级。
`--non-motion-agent` 是命令级跳过入口。环境变量仍可调整服务启动方式:
```bash
MOTION_AGENT_DRY_RUN=1 ./planet.sh start
```
日志入口:
```bash
./planet.sh log -m
```
和前端一起开放局域网时:
```bash
./planet.sh start --allow-lan --motion-agent
```
此时 Motion Agent 会绑定 `0.0.0.0`,启动输出会同时显示本机 WebSocket 地址和推荐局域网 WebSocket 地址。局域网浏览器访问智能星球时,需要把 `motionAgent` 参数指向这台大屏主机,例如:
```text
http://<LAN_IP>:3000/earth?motion=1&motionAgent=ws://<LAN_IP>:8765/ws/gestures
```
停止时 `./planet.sh stop` 会一并停止已由脚本启动的 Motion Agent。健康检查会显示 `Motion Agent` 的 online/offline 状态。Earth 页面仍需用 `?motion=1` 或浏览器本地存储显式启用 Web 端连接。
如果只是普通网页/WSL/无安装演示场景,可以不启动 Motion Agent直接在 Earth 设置里选择 `浏览器摄像头` 输入源并打开动捕调试模式;该路线使用浏览器 `getUserMedia`,需要 HTTPS 或 localhost 和摄像头权限。
## 其他:移除不必要的 sleep
启动链路中两处 `sleep 3` 在实际已有健康检查覆盖的情况下多余,已移除:
- `start_backend_service`:数据库健康检查通过后的 `sleep 3`
- `restart_database_service`:重启后的等待 `sleep 3`
## 相关文件
- `planet.sh` — 全量修改
- `.dockerignore` — 收敛 AI Provider Docker build context
- `aiprovider/Dockerfile` — 只复制 AI Provider 代码,并为 `uv sync` 启用 BuildKit cache mount
- `docker-compose.yml` / `docker-compose.simple.yml` — 读取 `planet.sh` 生成的运行期 env-file
- `scripts/compute_aiprovider_dependency_fingerprint.py` — 依赖 fingerprint未改动