From 69b1887eae26483fc934472152239be72bb4ae54 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 14 Sep 2026 08:43:54 +0800 Subject: [PATCH] =?UTF-8?q?chore(deploy):=20=E6=B8=85=E7=90=86=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=AE=8B=E7=95=99=EF=BC=881.0GB=EF=BC=89+=20=E6=8A=8A?= =?UTF-8?q?=E6=B8=85=E7=90=86=E5=86=99=E6=88=90=E5=8F=AF=E5=A4=8D=E8=B7=91?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户说「清理一下」。实测 /opt/agentmail 累计 1.3GB: - 网关旧二进制 33 份 × 24MB ≈ 790MB(每次 redeploy 留一份,从 09-05 起没清过) - 插件快照:opencode 6 份 339MB、dsh 7 份 195MB、zcode 16 份、pi 9 份 - /tmp 里的部署前数据库备份 10 份(tmpfs,占内存) 新增 `deploy/prune-deploy-artifacts.sh`(可复跑,不手敲 rm),两条硬规矩: ① **保留回滚窗口**:网关留最新 3 份、每个插件留最新 3 份快照,`current` 永远保留 (发布纪律要求有回滚目标,所以不是全清); ② ★ **绝不删正在使用的快照**:扫 /proc 的 cmdline 与 cwd,命中就跳过。 执行结果:**/opt/agentmail 1305MB → 302MB(释放 1003MB)**,7 个服务全部 active, `current` 指向未变,`check-deploy-drift` 仍报「四个宿主都在跑当前代码」。 ## 过程中的一处自纠 "在用"闸门第一版有**自我匹配**缺陷:我的测试命令把路径写在命令行里,于是扫 /proc 时 扫到了自己 ⇒ 对不存在的路径也判"在用"(与 `pkill -f` 杀掉自己那条命令同一类)。 已改为排除本进程及其祖先链,并**换正确方式重测**(路径从文件读、不进 cmdline): 在用快照判"在用" ✓、不存在的路径不误判 ✓ —— 判据两侧都验过才敢执行删除。 文档:docs/DEV-TOOLING.md 记了用法与那两条规矩。 --- deploy/prune-deploy-artifacts.sh | 107 +++++++++++++++++++++++++++++++ docs/DEV-TOOLING.md | 16 +++++ 2 files changed, 123 insertions(+) create mode 100755 deploy/prune-deploy-artifacts.sh diff --git a/deploy/prune-deploy-artifacts.sh b/deploy/prune-deploy-artifacts.sh new file mode 100755 index 0000000..c5efb55 --- /dev/null +++ b/deploy/prune-deploy-artifacts.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# 清理部署残留:旧网关二进制、旧插件快照、/tmp 里的部署前数据库备份。 +# +# # 为什么要脚本而不是手敲 rm +# +# 这些东西**天天在长**:每次 redeploy 留一个 24MB 的网关二进制、每个插件留一份快照 +# (opencode/dsh 的快照含 node_modules,一份几十 MB)。2026-09-14 实测: +# /opt/agentmail 1.3G,其中旧二进制 ~790MB、插件快照 ~540MB。 +# 手敲的清理不可复现、下次还得再想一遍,而且很容易漏掉"这一份还在被用"的判断。 +# +# # 两条硬规矩 +# +# ① **保留回滚窗口**:网关二进制留最新 N 份、每个插件留最新 N 份快照 +# (`current` 永远保留)。发布纪律要求"有回滚目标",所以不是全清。 +# ② ★ **绝不删正在使用的快照**:任何进程的 cmdline 里出现过的快照路径一律跳过。 +# 删掉它 = 进程一重启就找不到自己的代码(而这类事故此前真发生过)。 +# +# 用法: +# bash deploy/prune-deploy-artifacts.sh # 干跑,只报告 +# bash deploy/prune-deploy-artifacts.sh --apply # 真删 +# KEEP_GATEWAY=2 KEEP_SNAPSHOTS=2 bash ... --apply # 调保留窗口 +set -uo pipefail + +ROOT="${AGENTMAIL_ROOT:-/opt/agentmail}" +KEEP_GATEWAY="${KEEP_GATEWAY:-3}" +KEEP_SNAPSHOTS="${KEEP_SNAPSHOTS:-3}" +KEEP_TMP_DB="${KEEP_TMP_DB:-2}" +APPLY=0 +[ "${1:-}" = "--apply" ] && APPLY=1 + +freed_kb=0 +removed=0 +skipped=0 + +report() { printf ' %s %s\n' "$1" "$2"; } + +# 收集所有"正在被进程使用"的路径(cmdline + cwd),用来做安全闸。 +# 自己这条命令的 cmdline 里就可能含这个路径(例如调用方把路径写在命令行里), +# 于是"检查在用"会永远为真 —— 与 `pkill -f` 杀掉自己那条命令是同一个坑。 +# 所以先把本进程及其祖先链排除掉。 +_SELF_CHAIN=" " +_p=$$ +while [ -n "$_p" ] && [ "$_p" != "1" ] && [ "$_p" != "0" ]; do + _SELF_CHAIN="$_SELF_CHAIN$_p " + _p=$(awk '{print $4}' "/proc/$_p/stat" 2>/dev/null || echo "") +done + +in_use() { + local needle="$1" p pid + for p in /proc/[0-9]*; do + pid=${p#/proc/} + [ -r "$p/cmdline" ] || continue + case "$_SELF_CHAIN" in *" $pid "*) continue;; esac + if tr '\0' ' ' <"$p/cmdline" 2>/dev/null | grep -qF "$needle"; then return 0; fi + local cwd; cwd=$(readlink "$p/cwd" 2>/dev/null || true) + case "$cwd" in *"$needle"*) return 0;; esac + done + return 1 +} + +del() { + local path="$1" + local kb; kb=$(du -sk "$path" 2>/dev/null | cut -f1); kb=${kb:-0} + if [ "$APPLY" = "1" ]; then rm -rf "$path"; fi + freed_kb=$((freed_kb + kb)); removed=$((removed + 1)) + report " $([ "$APPLY" = 1 ] && echo 删除 || echo 待删)" "$path($((kb/1024))MB)" +} + +echo "==> 网关旧二进制(保留最新 $KEEP_GATEWAY 份)" +mapfile -t bins < <(ls -1t "$ROOT"/agentmail-gateway.bak-* 2>/dev/null) +for i in "${!bins[@]}"; do + [ "$i" -lt "$KEEP_GATEWAY" ] && { report " 保留" "${bins[$i]}"; continue; } + if in_use "${bins[$i]}"; then report " ★跳过(正在被使用)" "${bins[$i]}"; skipped=$((skipped+1)); continue; fi + del "${bins[$i]}" +done + +echo "==> 插件快照(每个插件保留最新 $KEEP_SNAPSHOTS 份,current 永远保留)" +for plug in "$ROOT"/plugins/*/; do + [ -d "$plug" ] || continue + name=$(basename "$plug") + mapfile -t snaps < <(ls -1dt "$plug"*/ 2>/dev/null | sed 's:/$::') + for i in "${!snaps[@]}"; do + s="${snaps[$i]}" + [ "$(basename "$s")" = "current" ] && continue + # current 指向的那一份也必须保留 + cur=$(readlink -f "$plug/current" 2>/dev/null || true) + [ "$cur" = "$(readlink -f "$s")" ] && { report " 保留" "$s(current 指向)"; continue; } + if [ "$i" -lt "$KEEP_SNAPSHOTS" ]; then report " 保留" "$s"; continue; fi + if in_use "$s"; then report " ★跳过(正在被使用)" "$s"; skipped=$((skipped+1)); continue; fi + del "$s" + done +done + +echo "==> /tmp 的部署前数据库备份(保留最新 $KEEP_TMP_DB 份)" +mapfile -t dbs < <(ls -1t /tmp/agentmail-pre-deploy-*.db 2>/dev/null) +for i in "${!dbs[@]}"; do + [ "$i" -lt "$KEEP_TMP_DB" ] && { report " 保留" "${dbs[$i]}"; continue; } + del "${dbs[$i]}" +done + +echo +if [ "$APPLY" = "1" ]; then + echo " 已删除 $removed 项,释放约 $((freed_kb/1024))MB;跳过(在用)$skipped 项" +else + echo " 干跑:将删除 $removed 项,可释放约 $((freed_kb/1024))MB;跳过(在用)$skipped 项" + echo " 加 --apply 执行。" +fi diff --git a/docs/DEV-TOOLING.md b/docs/DEV-TOOLING.md index 7ea6c92..4f5fa71 100644 --- a/docs/DEV-TOOLING.md +++ b/docs/DEV-TOOLING.md @@ -67,3 +67,19 @@ pi-lens 在编辑文件后会走 smart-default 回退选择格式化器(见它 - `biome.jsonc`:只挡得住 biome,**挡不住 prettier** —— 两者是并列的候选格式化器, 各有各的配置。所以真正的开关是 `.pi-lens.json`,它两条改写路径一起关。 + +## 部署残留清理(`deploy/prune-deploy-artifacts.sh`) + +部署会留下两类持续增长的东西:网关旧二进制(每份 ~24MB)与插件快照(opencode/dsh 的 +快照含 node_modules,一份几十 MB)。2026-09-14 实测 `/opt/agentmail` 累计到 **1.3GB**, +其中旧二进制 ~790MB、快照 ~540MB。 + +```bash +bash deploy/prune-deploy-artifacts.sh # 干跑,只报告 +bash deploy/prune-deploy-artifacts.sh --apply # 真删(默认:网关留 3 份、每插件留 3 份快照) +``` + +两条硬规矩:**保留回滚窗口**(发布纪律要求有回滚目标,所以不是全清);**绝不删正在使用的 +快照**(扫 /proc 的 cmdline 与 cwd,命中就跳过 —— 删掉它进程一重启就找不到自己的代码)。 +注意那个"在用"判断必须排除**本进程及其祖先链**:调用方常把路径写在命令行里, +不排除就会出现"永远判为在用"(与 `pkill -f` 杀掉自己那条命令同一个坑,已写进脚本注释)。