219 lines
7.0 KiB
Markdown
219 lines
7.0 KiB
Markdown
# 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
|
||
```
|
||
|
||
### 修复
|
||
|
||
将戳文件路径从 `/tmp/` 改到持久路径:
|
||
|
||
```bash
|
||
AI_PROVIDER_BUILD_STAMP_FILE="$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。配置优先来自:
|
||
|
||
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 时主动杀进程;如果没有 PID 但端口暂时不可绑定,它会记录诊断并把最终确认交给服务启动流程。`start_frontend_with_retry()` 也只在发现监听 PID 时进入预清理重试,避免在宿主机或外部 network namespace 尚未释放端口时做无意义的“空杀重试”。这意味着第一次重启时看到“未发现监听进程但端口仍不可绑定”通常是外部环境仍在释放端口;脚本不会再把这种情况当成立刻失败的本地进程清理问题。
|
||
|
||
## 问题三:端口检测用 Python
|
||
|
||
### 原因
|
||
|
||
`can_bind_port` 用 `python3 -c "import socket..."` 检测端口,每次调用约 300ms。
|
||
|
||
### 修复
|
||
|
||
优先使用系统工具(~10ms),Python 作为兜底:
|
||
|
||
```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` 完全一致。无需额外代码变更。
|
||
|
||
## 其他:移除不必要的 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(未改动)
|