第 15 章 部署与进程拓扑
MoE 大模型低成本部署框架第 15 章 部署与进程拓扑
CHAPTER 15

第 15 章 部署与进程拓扑

10 分钟阅读 · 2,309 字

这一章回答运维层面的问题:机器上到底跑着哪几个进程、谁拉起谁、改一个参数要重启谁、
Pod 重建后会发生什么
。所有事实来自 2026-10-19 当场读盘(supervisord.conf、run-*.sh、
deploy/engines/、ss -ltnp、日志目录 ls)。


15.1 拓扑:五个进程、三个端口

Pod(K3s,命名空间 clouds)
│  PID1 = tini → bash → sshd
│
├─ supervisord(-c ~/deploy/supervisord.conf)
│   ├─ rag-proxy      python rag-proxy/rag_proxy.py --config rag-proxy/config.json   → 0.0.0.0:8080  ← 唯一公网面
│   ├─ strata-a       Strata/run-iq3_xxs-a.sh   → taskset 0-15  python serve/server.py --port 8091   → 127.0.0.1:8091(快道 A)
│   ├─ strata-b       Strata/run-iq3_xxs-b.sh   → taskset 38-53 python serve/server.py --port 8092   → 127.0.0.1:8092(普道 B)
│   ├─ strata         Strata/run-iq3_xxs.sh(单实例布局用,dual 下不启)
│   └─ layout-restore Strata/layout-restore.sh(一次性,autorestart=false)
│
├─ 每个 serve 进程内部再 fork 子进程:
│   ├─ 引擎 C++ 子进程(strata --serve --pack …,A 用 SSEStraBed/build/strata,B 用 Strata/build/strata)
│   └─ strata-vision 常驻图像编码器(llama.cpp mtmd + mmproj,配置里有 "vision" 时才起)
│
└─ 对外链路:Pod :8080 → Service(NodePort) → frps 中继(TLS 在此终结)→ 公网
   22 端口 = SSH。8091/8092 只绑 127.0.0.1,外部不可达。

ss -ltnp 实测(2026-10-19)只有三个业务监听:0.0.0.0:8080、127.0.0.1:8091、127.0.0.1:8092。
没有 8090——那是双 lane 之前单实例布局的端口(13 章 §13.1 的过期默认值)。

15.2 supervisord 的五个程序

~/deploy/supervisord.conf(平台维护的目录约定,但这份 conf 是本项目自己写的;
~/deploy/supervisord.conf 里 [unix_http_server] file=…/deploy/supervisor.sock):

program 命令 autostart 备注
strata Strata/run-iq3_xxs.sh true 单实例(含 split 布局)用;dual 布局下被 layout.sh dual 停掉
strata-a Strata/run-iq3_xxs-a.sh false conf 注释原文:“实验布局:每卡一个实例(layout.sh dual 时才启动)”
strata-b Strata/run-iq3_xxs-b.sh false 同上
rag-proxy Strata/.venv/bin/python rag-proxy/rag_proxy.py --config rag-proxy/config.json true startsecs=5、stopwaitsecs=15
layout-restore Strata/layout-restore.sh true,autorestart=false、startsecs=0 一次性脚本:pod 重建后把 dual 恢复回来

共同参数(五程序一致,proxy 的 stopwaitsecs 更短):

directory=<各程序工作目录>   autorestart=true   startsecs=15   startretries=3
stopasgroup=true   killasgroup=true   stopsignal=TERM   stopwaitsecs=30
stdout/stderr → ~/deploy/logs/<name>.{out,err}.log   (maxbytes=20MB,backups=3)
environment=HF_ENDPOINT="https://hf-mirror.com"

startsecs=15 对引擎进程是有意义的:引擎要先把权重/PLE 摸进页缓存才算"起来了",
15 秒内不退出才算成功;killasgroup=true 保证 supervisorctl stop 能把 serve 与它 fork 的
引擎、vision 子进程一起收掉(否则留下孤儿引擎占着 GPU)。

15.3 薄分发器:run-*.sh 只决定"跑哪套引擎"

Strata/run-iq3_xxs-b.sh 全文(A 道同构):

#!/bin/sh
# 引擎分发器(B lane / GPU1 / 8092)。
# 实际跑哪个引擎由 ~/deploy/engines/active-b.env 决定(switch_engine.py 写入)。
# 没有这个文件时,行为与原来的直接启动完全一致。
ENVF=/home/cloud/deploy/engines/active-b.env
[ -f "$ENVF" ] && . "$ENVF"
: "${ENGINE_NAME:=legacy}"
: "${PYTHON:=/home/cloud/Strata/.venv/bin/python}"
: "${SERVER_PY:=/home/cloud/Strata/serve/server.py}"
: "${CONFIG:=/home/cloud/Strata/strata-iq3_xxs-b.json}"
: "${CWD:=/home/cloud/Strata}"
: "${TASKSET:=38-53}"
: "${PORT:=8092}"
cd "$CWD" || exit 1
exec taskset -c "$TASKSET" "$PYTHON" "$SERVER_PY" --engine strata --config "$CONFIG" --port "$PORT" --open

这个设计有三层意义:

  1. supervisord 的 command 永远不变 → 换引擎不需要 supervisorctl update,也不需要改平台维护的目录;
  2. TASKSET 是变量而不是硬编码,但它有默认值(A 0-15、B 38-53)——
    11 章的钉核纪律在脚本层有兜底,但任何覆盖它的 active-*.env 都要复查(17 章红线 1);
  3. exec 让分发器变成进程本身,supervisord 的 PID 追踪与 killasgroup 才有效。

当前两条 lane 的实际取值(读 deploy/engines/ 当场核实):

lane active-<lane>.env 生效配置
A(快道) 存在(active-a.env,0600,274 B,文件头注释 “written by switch_engine.py at 2026-10-17 20:44:35”) ENGINE_NAME=ssebed、CONFIG=deploy/engines/profiles/ssebed/strata-a.json、CWD=/home/cloud/SSEStraBed、TASKSET=0-15、PORT=8091
B(普道) 2026-10-21 13:46 起存在(active-b.env,0600,275 B,文件头 “written by switch_engine.py at 2026-10-21 13:46:50”) ENGINE_NAME=ssebed、CONFIG=deploy/engines/profiles/ssebed/strata-b.json、CWD=/home/cloud/SSEStraBed、TASKSET=38-53、PORT=8092

→ 这就是"两条 lane 的版本由 active-<lane>.env 决定"的机制真相(引擎版本字段与构建号见 16 章)。
10-21 之前 B 道没有这个文件、走脚本默认 legacy;当天中午 switch_engine.py switch ssebed --lanes b 把它写成
ssebed,两条 lane 从此跑同一份 build 5 二进制(现状快照 docs/reports/CURRENT-VERSION-2026-10-21.md)。
deploy/engines/switch.lock 是个 0 字节残留文件(2026-10-17 12:44),不是运行中的锁。

15.4 两套 profile:legacy 与 ssebed

deploy/engines/profiles/<name>/profile.json 描述"这套引擎在哪、二进制是谁、每条 lane 用哪份配置":

字段 legacy ssebed
home /home/cloud/Strata /home/cloud/SSEStraBed
binary /home/cloud/Strata/build/strata /home/cloud/SSEStraBed/build/strata
server_py /home/cloud/Strata/serve/server.py 同一个文件(服务层不随引擎换)
python /home/cloud/Strata/.venv/bin/python 同一个 venv
lane a port 8091 / taskset 0-15 / gpu 0 / Strata/strata-iq3_xxs-a.json port 8091 / taskset 0-15 / gpu 0 / profiles/ssebed/strata-a.json
lane b port 8092 / taskset 38-53 / gpu 1 / Strata/strata-iq3_xxs-b.json port 8092 / taskset 38-53 / gpu 1 / profiles/ssebed/strata-b.json
vision_exe /home/cloud/Strata/engine/strata-vision 同一个

要点:换引擎只换 binary + config(+ CWD),服务层 Python 与 venv 不变。
这就是为什么"引擎切换"能做成按 lane 滚动而不是整体重部署(16 章)。

配置文件里含明文 api_key(引擎自身的鉴权)——值不进书、不进 git(17 章红线 4);
profiles/ssebed/strata-*.json 权限是 0600。

15.5 布局切换与自动恢复

Strata/layout.sh dual|split|status(注释原文):

#   layout.sh dual    每卡一个独立实例:GPU0→8091、GPU1→8092,网关按会话粘性分流
#   layout.sh split   现生产:一个实例跨两卡分层切分(8090)
#   layout.sh status  看当前是哪套、各实例健康
# 切换会停服:dual 起两个实例各需 2-3 分钟加载。

⚠️ 注释里的"现生产"已经过期:~/deploy/layout.mode 当前内容是 dual(2026-10-10 起),
split 才是可回退的实验布局。脚本注释没跟着改——读脚本时要带着这个警觉(18 章"注释腐烂")。

dual 分支做的事(顺序有意义):

echo dual > ~/deploy/layout.mode
supervisorctl reread && update          # 让 conf 变更生效
stop strata                             # 先停切分实例,腾出两张卡
set_proxy dual                          # 改 rag-proxy/config.json:upstream=8091,upstreams=[8091,8092]
restart rag-proxy                       # 网关读新上游表
start strata-a ; start strata-b        # 两个实例并行加载(各 2-3 分钟)
轮询 /health 最多 60 次 × 5s,两个都 ok 才算完

split 分支是镜像操作(stop a/b → set_proxy split → restart rag-proxy → start strata → 轮询 8090)。

layout-restore.sh 全文只有四行逻辑:

MODE=$(cat /home/cloud/deploy/layout.mode 2>/dev/null || echo split)
if [ "$MODE" = dual ] && ! curl -sf --max-time 5 -H "Authorization: Bearer $(cat ~/logs/strata-api-key.txt)" \
      http://127.0.0.1:8091/health >/dev/null; then
  /home/cloud/Strata/layout.sh dual
fi

它存在的原因写在注释里:dual 的 a/b 是 autostart=false,不恢复就断链——
pod 重建后 supervisord 只会自动起 strata 与 rag-proxy,两条 lane 不会自己活过来。

15.6 日志与留档:谁写哪

路径 内容 谁写
~/deploy/logs/<program>.{out,err}.log(maxbytes 20MB × 3 backups) 进程 stdout/stderr supervisord
~/deploy/logs/supervisord.log supervisor 自身 supervisord
~/Strata/strata-iq3_xxs-b.log(52 MB,2026-10-19 17:36 仍在写) B lane 引擎明细:prompt N = R reused + F read、conversation cache: … evictions=K、命中率、KV streaming 引擎(legacy 配置里的 log 字段)
~/SSEStraBed/logs/strata-iq3_xxs-a.log A lane 引擎明细(ssebed 配置 "log": "/home/cloud/SSEStraBed/logs/…") 引擎
~/Strata/strata-iq3_xxs-a.log(最后写于 2026-10-17 12:27) A lane 换引擎之前的日志,已停更 旧配置
~/logs/infer-segs/<日期>/seg-*.jsonl.gz 每请求留档分段(gzip) serve 的 infer_store
~/logs/infer.db(+ -wal / -shm) 请求留档索引(SQLite WAL) serve
~/logs/chats/ 会话记录 服务层
~/logs/strata-api-key.txt 引擎鉴权 key(不进书、不进 git) 部署时生成
~/logs/minio-upload-*.log 每次 MinIO 备份的上传日志 备份脚本
~/bench/runs/… 压测原始数据(13 章) 压测台

取证时的对齐技巧(12 章 §12.7 说过,这里再钉一次):引擎明细行没有时间戳,
要跨进程对齐必须借 ~/deploy/logs/rag-proxy.out.log 里的 ctx[...] 时间戳。

15.7 Pod 重建时会发生什么

按 memory: pod-deploy-mechanism(PID1 cmdline 里藏着的行为,代码里看不到):

  1. /home/cloud 是持久化 hostPath → 代码、配置、日志、layout.mode、active-*.env 都还在;
  2. PID1(tini→bash→sshd)自动校验 ~/deploy/supervisord.pid(用 /proc/<pid>/cmdline 防陈旧 pidfile 误判),
    conf 存在就 supervisord -c;
  3. supervisord 起 strata(autostart=true)、rag-proxy、layout-restore;
  4. layout-restore 发现 layout.mode=dual 且 8091 不健康 → 跑 layout.sh dual → 起 a/b、改代理上游、重启代理;
  5. 引擎重新加载模型(分钟级,15.9);
  6. 内存态全丢:网关的会话/嵌入缓存/组装块、引擎的 prompt 检查点与停车(12 章)——
    第一个请求必然冷读。

要现场重载配置:supervisorctl -c ~/deploy/supervisord.conf reread / update / restart <name>。

15.8 一个与文档不符的事实:context.json 不在

CLAUDE.md §4 说平台把上下文写进 ~/deploy/context.json。2026-10-19 实测:该文件不存在
(find /home/cloud /shared -maxdepth 5 -name context.json 无结果),裸 SSH shell 里
$PORT / $PUBLIC_URL / $GPU_IDS 等也全部为空——这与 CLAUDE.md §4 自己的提醒一致
(“SSH 进来的裸 shell 通常看不到这些变量,它们只给 supervisor 拉起的部署进程”)。

本书的硬件事实因此不引 context.json,改用可当场复核的来源:
/sys/fs/cgroup/cpu.max(32 核)、/proc/meminfo(216 GiB)、nvidia-smi(2×4090D 24G)、
/shared/pressure/pressure.json(宿主机负载)。
需要公网地址时用 $PUBLIC_URL(部署进程内)而不是硬编码(CLAUDE.md §1)。

15.9 改一个东西要重启谁(代价表)

改动 动作 停机代价 是否需用户同意
网关路由 / 守卫 / 文档抽取(rag-proxy/config.json、rag_proxy.py) supervisorctl restart rag-proxy 秒级(10-19 15:33 那次即如此) 需要(服务对外在线)
服务层参数(serve/server.py、lane 的 *.json,如 fit_max_tokens) 重启该 lane(restart strata-a) 1–13 分钟(模型重载) 需要
换引擎二进制(legacy ↔ ssebed) switch_engine.py 写 active-<lane>.env + 重启该 lane 1–3 分钟(设计口径);实测 A 冷启动 6 分 36 秒 需要
换布局(dual ↔ split) layout.sh 两实例各 2–3 分钟加载,期间该 lane 不可用 需要
改 taskset 核段 改 run-*.sh 或 active-*.env 同上,且错了会崩回 9 tok/s 需要 + 复查
只改前端静态文件(Strata/serve/web/) 无需重启(serve 直接读盘) 0 否(但要确认构建产物路径)
引擎 C++ 源码 重编 + 换二进制 + 重启 lane 编译 + 1–3 分钟 需要

网关重启与引擎重启的代价差三个数量级——这是"把逻辑尽量往网关放"的运维理由(04 章),
也是本项目当天能修完窗口守卫事故、而四个 prefill 旋钮拖到今天的原因。

15.10 本章小结

  • 五个 supervisord 程序 + 每个 serve fork 出的引擎与 vision 子进程;公网只有 :8080。
  • run-*.sh 是薄分发器:supervisord 的 command 永不变,换引擎只是写一个 active-<lane>.env。
    10-21 之前 B lane 没有这个文件,跑的是 legacy 默认值;当天补上后两条 lane 同跑 ssebed build 5。
  • 布局状态存在 ~/deploy/layout.mode(当前 dual),layout-restore.sh 负责 pod 重建后把它接回来;
    layout.sh 的注释里"split 是现生产"已过期。
  • 日志分三层:supervisord 的 out/err、引擎明细(A 在 SSEStraBed/logs/、B 在 Strata/)、
    请求留档(~/logs/infer-segs + infer.db)。跨层对齐靠代理时间戳。
  • 重启代价决定改动落点:网关秒级、引擎分钟级。凡是能放进网关的,就不要放进引擎。
全部章节