menu

云手SAAS

  • date_range 27/07/2021 03:51
    点击量:
    info
    sort
    leetcode
    label
    云原生
    数字孪生
    微服务

云手 SaaS 运维手册(compose 项目 cloudhandstack

全部命令的工作目录都是 /Users/andrewyang/dockersofts/CloudHandStack。 先 cd 过去再执行,否则 docker compose 会因为找不到 docker-compose.yml.env 而拿到完全不同的一套解析结果。

★本手册里的每一条「为什么」都对应一次真实发生过的故障,不是通则。


0. 30 秒速查

cd /Users/andrewyang/dockersofts/CloudHandStack

# 现在怎么样
docker compose ps
curl -s -o /dev/null -w '%{http_code}\n' -k https://127.0.0.1/          # C 端,应 302
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8090/healthz  # 引擎,应 200

# ★跑的到底是哪一版(看 OCI 标签,不看镜像名)
for c in andrewyg-agenthands andrewyg-agent-ui cli-proxy-api; do
  docker inspect --format ' → rev= ver=' $c
done

# 一键真伪核对(版本 / 远端摘要 / 进程自报 / 冒烟,四层)
bash scripts/check-release-truth.sh

1. 拓扑与身份对照

唯一对外入口是 agent-ui(80/443),其余全部只在内网 cloudhandstack_default 上。

compose 服务名 容器名 是什么 对外
agent-ui andrewyg-agent-ui C 端 + nginx ✅ 80/443
agenthands andrewyg-agenthands 自研引擎 127.0.0.1:8090
agentllm cli-proxy-api 网关(★服务名≠容器名,唯一一个) 127.0.0.1:18317
searxng andrewyg-searxng 自建检索(WS1) 内网
mcp-fixture andrewyg-mcp-fixture MCP 测试夹具(★用的是引擎镜像 内网
postgres langfuse-postgres-1 主库,不只是 langfuse 的 内网
langfuse-web langfuse-langfuse-web-1 可观测控制台 127.0.0.1:3000
langfuse-worker langfuse-langfuse-worker-1 可观测后台 内网
clickhouse langfuse-clickhouse-1 可观测存储 内网
minio langfuse-minio-1 对象存储 内网
redis langfuse-redis-1 缓存 内网

容器名与服务名到处不一致,这是这套部署最容易出错的地方:

docker compose restart agentllm     # ✅ compose 用服务名
docker restart cli-proxy-api        # ✅ docker 用容器名
docker compose restart cli-proxy-api  # ❌ "no such service"

三份 compose 由根文件 include: 拉入(langfuse/deploy/gateway/deploy/retention/), ⇒ 只能从仓根跑 compose,进子目录跑会起出一套平行的容器。


2. ★★★三条铁律

铁律一:C 端必须最后起

nginx 的 proxy_pass 只在配置加载那一刻解析一次上游主机名。 引擎/网关被 recreate 后拿到新 IP,而 C 端手里还是旧的 ⇒ 全站 502/500,且三端各自都 healthy

# ✅ 正确顺序
docker compose up -d agenthands agentllm
sleep 10 && curl -sf http://127.0.0.1:8090/healthz
docker restart andrewyg-agent-ui        # ★最后

# 复核起动顺序(C 端时间戳必须最大)
for c in cli-proxy-api andrewyg-agenthands andrewyg-agent-ui; do
  docker inspect -f ' ' $c
done

2026-08-10 实发:重建引擎没重启 C 端 ⇒ 67 次 5xx,而当时查的三个读数全部显示正常

铁律二:restart 保 IP,up -d 换 IP

docker restart andrewyg-agenthands      # 同一个容器,IP 不变 ⇒ 不必动 C 端
docker compose up -d agenthands         # recreate,IP 可能变 ⇒ ★之后必须重启 C 端

只改挂载的配置、不换镜像时,一律用 docker restart 判据:

IP0=$(docker inspect -f '' andrewyg-agenthands)
docker restart andrewyg-agenthands && sleep 10
IP1=$(docker inspect -f '' andrewyg-agenthands)
[ "$IP0" = "$IP1" ] && echo "IP 未变,C 端不用动" || echo "★IP 变了,必须 docker restart andrewyg-agent-ui"

铁律三:镜像标签会说谎,OCI 标签不会

docker tag 一秒钟就能把任何版本号贴到任何镜像上;而 org.opencontainers.image.revision构建那一刻写死的,改不了。

# ❌ 不要凭这个下结论
docker ps --format '\t'

# ✅ 判据
docker inspect --format '' andrewyg-agenthands

实发两次:cloudhandstack-engine:v1.0.7 装的是 v1.0.8 的内容,本地和 GHCR 都在说谎。

铁律三的两个配套陷阱(各踩过一次)

git rev-parse <tag> 给的是「tag 对象」,不是提交。 本仓的版本 tag 是附注 tag,直接比对会得出「镜像与 tag 不是同一个提交」这种假结论。

git rev-parse --short v1.0.10          # ⛔ f6741bcf1 —— 这是 tag 对象
git rev-parse --short 'v1.0.10^{commit}'  # ✅ 1e7383a91 —— 这才是提交

② C 端镜像的 Created 是固定值,不能当「有没有重建」的判据。

ui:v1.0.8   created=2026-08-10T05:58:58  rev=076d9172d
ui:v1.0.9   created=2026-08-10T05:58:58  rev=91840ba63
ui:v1.0.10  created=2026-08-10T05:58:58  rev=1e7383a91

三个版本 Created 一模一样revision 各不相同 ⇒ 那是构建时间戳被归一化了 (可复现构建),不是只 retag 没重建。 ★引擎镜像的 Created 是真实递增的(09:32 → 10:48),两者行为不同 —— ⇒ 判「是不是这一版」永远只看 revision,不要看 Created (我按 Created 差 4.5 小时一度判成「UI 只 retag 没重建」,是错的。)


3. 日常巡检

cd /Users/andrewyang/dockersofts/CloudHandStack

# ① 11 个容器全在且健康
docker compose ps --format 'table \t\t'
docker ps -a --filter label=com.docker.compose.project=cloudhandstack \
  --filter status=exited --filter status=created --format '\t'
#   ★卡在 Created 的容器 docker ps 看不见,而站点已经下线 —— 必须用 -a 查
#   ★★必须带 label 过滤:这台机器上还有别的项目的退出容器(openhands-canvas / litellm / …),
#     不过滤的话每次巡检都有假警报 —— 而假警报会让人把这条检查整个关掉。

# ② 四个可达性
printf 'C端 %s  引擎 %s  网关 %s  Langfuse %s\n' \
  "$(curl -s -o /dev/null -w '%{http_code}' -k -m 10 https://127.0.0.1/)" \
  "$(curl -s -o /dev/null -w '%{http_code}' -m 5 http://127.0.0.1:8090/healthz)" \
  "$(curl -s -o /dev/null -w '%{http_code}' -m 5 http://127.0.0.1:18317/)" \
  "$(curl -s -o /dev/null -w '%{http_code}' -m 5 http://127.0.0.1:3000)"

# ③ 插件(应为 5/5)
bash scripts/check-plugins.sh

# ④ 近 10 分钟的错误
docker logs andrewyg-agent-ui   --since 10m 2>&1 | grep -cE ' 50[0-9] '
docker logs andrewyg-agenthands --since 10m 2>&1 | grep -ciE 'panic|fatal|拒绝启动'

# ⑤ ★用量入账健康度(计费是否在静默滞后)
curl -s http://127.0.0.1:8090/v0/management/billing/ingest-health | python3 -m json.tool | head -20

# ⑥ ★挂载的配置与 HEAD 是否漂移
bash scripts/check-mounted-config-vs-head.sh

4. 启停

# 全栈起(compose 自己处理依赖,但 C 端仍建议最后确认一遍)
docker compose up -d
docker restart andrewyg-agent-ui

# 全栈停(★不带 -v,带上会删数据卷)
docker compose down

# 单个组件
docker compose up -d agenthands && docker restart andrewyg-agent-ui
docker compose restart searxng

# 看日志
docker compose logs -f --tail=100 agenthands
docker logs -f --since 5m --timestamps andrewyg-agent-ui

绝不要用 docker run 起这套里的任何容器 —— 那样起出来的容器没有 compose 标签, 之后 docker compose up 会因为名字冲突而失败,且它不在 docker compose ps 里,查不到。


5. 发版与回滚

发版(七步编排,②之后不可逆)

# 干跑:跑完预检与①门禁就停,什么都不改
bash scripts/release-all.sh --agenthands v1.0.11 --isolate --dry-run

# 正式发(★有别的线在写工作区时必须带 --isolate)
bash scripts/release-all.sh --agenthands v1.0.11 --isolate --to 6

# 中途失败后续跑
bash scripts/release-all.sh --agenthands v1.0.11 --isolate --from 5 --to 6
  • --to 6 = 只上线,不发公告。第⑦步公告是对外动作,且需要具名 admin 令牌 (.env 里的 ADMIN_TOKEN 是 bootstrap 共享令牌,notices 端点会 403 拒掉 —— 设计如此)。
  • --allow-few:CHANGELOG 未攒够 20 条时显式破例。
  • 发版前 CHANGELOG 碎片必须归档:bash scripts/changelog-collect.sh(先 --dry-run 看一眼)。

★发版前自己把两道闸算一遍(每轮重跑要 6 分钟,值得先算)

# 碎片闸
ls -1 docs/changelog.d/*.md | grep -v README | wc -l    # 必须为 0

# 编号闸:提交里出现的编号必须都在任务清单里
for id in $(git log --format='%s' $(git describe --tags --abbrev=0)..HEAD \
            | grep -oE '\b[A-Z]{1,4}[0-9]+(-[A-Z])?\b' | sort -u); do
  grep -q "\b${id}\b" docs/任务清单.md || echo "⛔ 清单里查无此编号: $id"
done

回滚

# ① 最快:把 .env 的 STACK_VERSION 改回上一版,重跑上线两步
sed -i '' 's/^STACK_VERSION=.*/STACK_VERSION=v1.0.9/' .env
bash scripts/release-all.sh --agenthands v1.0.9 --isolate --from 5 --to 6

# ② 临时钉住某一次构建(不动版本标签,删这一行即秒退)
echo 'ENGINE_IMAGE=ghcr.io/andrewyghub/andrewyg-agenthands:<commit>' >> .env
docker compose up -d agenthands && docker restart andrewyg-agent-ui

⚠️ 用了 ENGINE_IMAGE 之后,scripts/check-stack-version-sync.sh 会在下次发版判红 (compose 解析出来的不是 :vX.Y.Z)—— 那是正确行为:显式钉住就该在发版时被看见并做一次决定。


6. 排障剧本(按症状)

症状 A:C 端 500 / 502,但三端都 healthy

九成是铁律一。

# 时间戳对一下:C 端不是最后起的 ⇒ 就是它
for c in cli-proxy-api andrewyg-agenthands andrewyg-agent-ui; do
  docker inspect -f ' ' $c
done
docker restart andrewyg-agent-ui && sleep 8
curl -s -o /dev/null -w '%{http_code}\n' -k https://127.0.0.1/

症状 B:引擎起不来 / restarting 循环

几乎总是配置与代码不同步(fail-fast 是刻意的:配置错了不静默退回无护栏的普通对话)。

docker logs andrewyg-agenthands --since 5m 2>&1 | grep -m1 '拒绝启动' \
  | python3 -c 'import sys,json;print(json.loads(sys.stdin.read()).get("err"))'

典型错误 field xxx not found in type flowprovider.stepYAML: YAML 里加了新键,但 adapter/flowprovider 的结构体没跟上。

# 立刻恢复:回滚配置,重启(★用 restart 保 IP,C 端就不用动)
git checkout -- deploy/flows/
docker restart andrewyg-agenthands

★这个形状已经发生三次stuck_limit / evidence_block / evidence_contract)。 规矩:配置与它依赖的代码必须同一次提交

症状 C:模型调不通 / 「模型服务内部出错」

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18317/           # 网关活着?
docker logs cli-proxy-api --since 10m 2>&1 | grep -icE 'error|429|401'
docker exec andrewyg-agenthands getent hosts cli-proxy-api                 # 容器内 DNS 通不通

★ Go 的默认解析器是 timeout:5 attempts:2 = 一次失败要等 10 秒。 本栈已在 compose 里设 dns_opt: [timeout:2, attempts:3]; ⛔ 不要改成 dns: —— 那会顶掉 Docker 内建解析器 127.0.0.11,容器名直接解析不了。

症状 D:站点整个下线,docker ps 却什么异常都看不到

docker ps -a --filter status=created --format '\t'
# ★卡在 Created 的容器 docker ps 不显示,而它已经不在提供服务
docker compose up -d --force-recreate <服务名>

症状 E:Web 检索(WS1)没生效

docker exec andrewyg-agenthands printenv AGENTHANDS_WEB_SEARCH WS_SEARCH_BACKEND SEARXNG_URL
docker exec andrewyg-agenthands wget -qO- http://searxng:8080/ >/dev/null && echo "searxng 可达"

★ 「环境变量在 .env 里」不等于「注入进容器了」:compose 的 environment: 得显式透传。 2026-08-10 实测就是这一条 —— 代码早就合了,线上一直没生效。


7. 数据与备份

★先认清哪一组卷是活的

docker volume ls --format '' | grep langfuse

这台机器上有两组同名后缀的卷,只有 cloudhandstack_ 前缀那组在用:

状态
cloudhandstack_langfuse_postgres_data 活的(主库)
langfuse_langfuse_postgres_data ⛔ 孤儿(0 个容器在用),历史项目名残留
# 判据:这个卷现在被谁挂着
docker ps -a --filter volume=cloudhandstack_langfuse_postgres_data --format ''

备错一组的后果是「备份天天在跑、需要时是空的」,而它不报错。

备份主库

TS=$(date +%Y%m%d-%H%M)
docker exec langfuse-postgres-1 pg_dumpall -U postgres \
  | gzip > ~/backups/cloudhandstack-pg-$TS.sql.gz
ls -lh ~/backups/cloudhandstack-pg-$TS.sql.gz     # ★核大小,0 字节的备份也会"成功"

ClickHouse 日志维护

bash scripts/clickhouse-log-maintenance.sh

磁盘

docker system df -v | head -30
docker image prune -f                 # 只删悬空镜像
# ⛔ 不要 docker system prune -a:会删掉回滚要用的历史版本镜像

8. 凭据与安全红线

  • .envdeploy/gateway/auths/deploy/gateway/config.yamlletsencrypt/acme-sh/ 一律不进 git,也不进任何输出、日志、工单
  • 排查时回显配置按整行掩码,不要按字段
# ✅ 只看键名
grep -oE '^[A-Z0-9_]+=' .env | sort

# ✅ 只看某个键存不存在
grep -c '^CLIPROXY_KEY=' .env

# ❌ 绝不要:grep CLIPROXY_KEY .env / printenv / docker inspect 全量 Env

2026-08-03 与 08-10 各泄露过一次,两次都是「替换式打码遇到没预料到的排版」。

  • 推 GHCR / GitHub 之前必须扫凭据(推上去撤不回来,镜像层会被缓存与索引):
bash scripts/scan-image-secrets.sh <镜像名>
  • 契约测试会 TRUNCATE 整张表AGENTHANDS_TEST_PG_DSN 的库名必须含 test, 仓库里有闸在拦 —— ⛔ 不要为了让测试跑起来把它指向生产库

9. 配置变更:挂载的走工作区,代码走 HEAD

deploy/flowsdeploy/skillsdeploy/templates只读挂载, 改完不需要重建镜像,但需要重启引擎(启动时加载一次)。

vim deploy/flows/imes-query.yaml

# ★先在本机用真加载器验一遍,别拿线上当测试环境
export PATH="$HOME/.gvm/gos/go1.26.2/bin:$PATH" GOTOOLCHAIN=local
go test ./adapter/flowprovider/

docker restart andrewyg-agenthands       # restart 保 IP ⇒ C 端不用动
docker logs andrewyg-agenthands --since 2m 2>&1 | grep -c '拒绝启动'   # 必须为 0

⚠️ 代码走 HEAD、配置走工作区 ⇒ 两者可能不同步,而不同步的表现是「下一次重启才炸」, 那次重启可能是任何人做的。用 bash scripts/check-mounted-config-vs-head.sh 定期对账。


10. 版本漂移怎么判(2026-08-10 实例)

发版进行中时,「.env 写的版本」与「容器在跑的版本」必然有一段不一致 —— .env 在第④步被抬版,容器要到第⑤步才换。⇒ 这不是故障,是瞬态。

★下结论前必须先问一句:是不是有人正在发版?

ps -Ao pid,etime,command | grep -E 'release-all\.sh|release\.sh' | grep -v grep
git worktree list        # --isolate 会留下 /tmp/relall-<commit> 这样的临时工位

有进程在跑 ⇒ 什么都不要动,等它结束再读它自己的判决:

bash scripts/check-release-truth.sh

2026-08-10 实例:.env 写 v1.0.10 而三端跑 v1.0.9,看起来像「配置改了没生效」。 实际是另一条线正在跑 release-all.sh --agenthands v1.0.10 --isolate --to 6 --allow-few --from 4, 两次查询之间三端就被换掉了(网关 → 引擎 → C 端最后,铁律一守住了), 最终真伪核对四层全绿。 ★★中途我还看到「远端三个 :v1.0.10 全都不存在」——那只是推送还没跑到, 不是发版失败。发版进行中的任何快照都不能当结论。


附:一次性核对

cd /Users/andrewyang/dockersofts/CloudHandStack
bash scripts/ops-check.sh            # 人读
bash scripts/ops-check.sh --quiet    # cron 用:全绿时一个字都不输出
bash scripts/ops-check.sh --no-net   # 跳过 HTTP 检查

退出码0 全绿 · 1 有问题 · 2 判不了(发版/构建进行中)

★★★第三个退出码是这个脚本最重要的设计。2026-08-10 实发:.env 写 v1.0.10 而 三端跑 v1.0.9,看起来像「配置改了没生效」,实际是另一条线正在发版 —— .env 在第④步抬版、容器第⑤步才换,中间那段不一致是必然的瞬态。 ⇒ 那一刻判绿判红都是错的:判红会有人去”修”一个正在正常进行的发版 (最坏是回滚掉一半),判绿则掩盖了「这一刻的读数不可信」。

它覆盖十项:发版是否在跑 · 容器齐不齐 · unhealthy · 卡在 created/exited · 三端 revision 一致 · .env 与在跑的版本对得上 · C 端最后起 · 可达性 · 近 10 分钟错误 · 主库数据卷与孤儿卷 · 外加 check-plugins.shcheck-mounted-config-vs-head.sh

⚠️ 绿只代表「这几条查过了」 —— 它不覆盖业务功能是否正确、计费是否在滞后、 证据契约是否真的拦住了东西。脚本自己在末尾也会这么说。

★脚本做过 7 条故障注入,每一条都翻红且报出正确的那一行 (发版在跑 / 构建在跑 / 三端版本不一致 / C 端不是最后起 / .env 漂移 / 容器卡 Created / 取不到 OCI revision)。


评论:


技术文章推送

手机、电脑实用软件分享

微信搜索公众号: AndrewYG的算法世界
wechat 微信公众号:AndrewYG的算法世界

热门文章