# Chatbot (Scheduler-Agent) 集群部署与验证手册 > 版本:2026-08-01 > 分支:`chatbot`(myxce fork) > 目标集群:t1 (172.31.1.30) / t2 (172.31.1.31) / t3 (172.31.1.32) 本文档记录 scheduler-agent(chatbot 分支)在真实测试集群上的**部署**与**验证**步骤,可完整复现本次会话的操作。验证覆盖 Phase 0(只读)+ Phase 1(写操作:submit),以及发现的 apiserver 预存 bug。 --- ## 1. 架构总览 ``` 用户自然语言 → Claude Code → MCP stdio → mcp-server → schedulerclient → apiserver REST API → controller (读:6 只读工具 + 写:4 写工具) ``` | 组件 | 二进制 | 部署位置 | 端口 | |------|--------|---------|------| | controller | `xce_controller` | t1(单节点,非 HA) | 9990 | | apiserver | `xce_apiserver` | t1 | 8080 | | scheduler | `xce_scheduler` | t1 | 7890 | | worker | `xcecd` | t1 / t2 / t3 | — | **拓扑说明**:本次验证采用 **t1 单节点 controller**(3 节点 HA 选举因遗留 term 状态卡在 STANDBY,见 §7 已知问题)。单节点用 `--peers` 省略即可。 --- ## 2. 前置:构建 AppImage AppImage 是部署的载体——`/opt/newxce/` 目录 = AppImage 解包后的 store 树 + `run.sh` 调度器。 ```bash # 在本地仓库根目录 cd /home/lchen/code/xce/src # 1. 构建 xce wrapper(含 xcecore 闭包) nix build .#xce # 2. 打包 AppImage(280MB,mksquashfs 单遍) # 输入:新 xce 闭包(nix path-info -r)+ AppRun + entrypoint # 详见 /home/lchen/xce-appimage/package.sh ./package.sh # → /home/lchen/xce-appimage/xce-x86_64.AppImage ``` **AppImage 结构**(与旧版一致,验证 `unsquashfs -offset 1002016 -l`): ``` squashfs-root/AppRun squashfs-root/entrypoint # → 指向 xcecore 的 bin/xce squashfs-root/mountroot squashfs-root/nix/store/… # 99 个 store 路径闭包 ``` --- ## 3. 部署到 t1/t2/t3 ### 3.1 部署 AppImage 结构 三台主机均为 `lchen` 用户,SSH 免密(`~/.ssh/id_rsa`),有 passwordless sudo。 ```bash # t1 完整部署(store 树 + run.sh + config) # /opt/newxce/ 结构: # bin/xce # AppImage 二进制(280MB 静态 ELF) # app/nix/store/… # 99 个 store 路径(xcecore + 依赖) # run.sh # daemon 调度器(controller/worker/scheduler/apiserver) # config/ # 集群 + apiserver 配置 # t2/t3 从 t1 同步 store 树(worker 需要 xcecd + 依赖闭包) ssh t1 'rsync -a /opt/newxce/app/ t2:/opt/newxce/app/ && rsync -a /opt/newxce/run.sh t2:/opt/newxce/run.sh' ssh t1 'rsync -a /opt/newxce/app/ t3:/opt/newxce/app/ && rsync -a /opt/newxce/run.sh t3:/opt/newxce/run.sh' ``` ### 3.2 停旧 systemd 服务(可选,若存在旧 XCE) ```bash ssh t1 'sudo systemctl stop scheduler.service workerd.service; sudo systemctl disable scheduler.service workerd.service' ssh t2 'sudo systemctl stop workerd.service; sudo systemctl disable workerd.service' ``` ### 3.3 准备 controller workdir + 证书 + cluster.bin ```bash # 每台 controller 需要 workdir + TLS 证书(HA 复制用;单节点也生成以防切换) for h in t1 t2 t3; do ssh $h "sudo mkdir -p /opt/xce/controller/conf /opt/xce/controller/data /opt/xce/controller/certs /opt/xce/logs \ && sudo chown -R lchen:lchen /opt/xce/controller /opt/xce/logs \ && openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \ -keyout /opt/xce/controller/certs/xce.key -out /opt/xce/controller/certs/xce.crt -subj '/CN=xce'" done # 生成 cluster.bin(t1 上,用 AppImage 内的 xceadmin) ssh t1 '/opt/newxce/app/nix/store//bin/xceadmin config build /opt/newxce/config -o /tmp/cluster.bin' # 部署到 t1 的 controller conf(指针文件 + 版本化 bin) # tag = cluster.bin 的 sha256 前 12 位(上面命令 stderr 输出) ssh t1 'sudo mkdir -p /opt/xce/controller/t1/conf \ && sudo cp /tmp/cluster.bin /opt/xce/controller/t1/conf/cluster.bin. \ && printf "cluster.bin.\n" | sudo tee /opt/xce/controller/t1/conf/current' ``` > **conf/current 指针机制**:`//conf/current` 内容为 `cluster.bin.<12hex>`,实际配置在 `cluster.bin.<12hex>`。controller 通过 `load_primary_config` 读取。tag 是配置 SHA-256 前 12 位。 ### 3.4 准备 apiserver 配置 ```bash # apiserver 需要 /config/{app,auth,rbac,logger}.yaml ssh t1 'sudo mkdir -p /opt/xce/apiserver/config \ && cp /opt/newxce/config/app.yaml /opt/newxce/config/auth.yaml \ /opt/newxce/config/rbac.yaml /opt/newxce/config/logger.yaml \ /opt/xce/apiserver/config/' # 注意:/opt/newxce/config/rbac.yaml 是旧版(无 mcp_operator)。 # 必须用本地仓库的 rbac.yaml(含 mcp_operator + queue:control) scp src/frontend/apiserver/config/rbac.yaml t1:/opt/xce/apiserver/config/rbac.yaml ``` ### 3.5 启动 daemon 所有 daemon 通过 `/opt/newxce/run.sh` 启动(设置 LD_LIBRARY_PATH + 正确的 glibc 加载器)。用 `setsid … &` 完全脱离 SSH 会话。 ```bash # t1: controller(单节点,省略 --peers = 非 HA) ssh t1 'sudo bash -c "cd /opt/newxce && setsid ./run.sh controller -n t1 \ -w /opt/xce/controller --certdir /opt/xce/controller/certs \ -l /opt/xce/logs/xce_controller.log --vcpus 4 -v /dev/null 2>&1 &"' # t1: apiserver(backend 指向 localhost:9990,配置在 /opt/xce/apiserver) ssh t1 'sudo bash -c "cd /opt/newxce && setsid ./run.sh apiserver -d /opt/xce/apiserver /dev/null 2>&1 &"' # t1: scheduler + worker ssh t1 'sudo bash -c "cd /opt/newxce && setsid ./run.sh scheduler --peers t1=127.0.0.1:9990 -l /opt/xce/logs/xce_scheduler.log -v /dev/null 2>&1 &"' ssh t1 'sudo bash -c "cd /opt/newxce && setsid ./run.sh worker -c /opt/xce/config/worker.yaml /dev/null 2>&1 &"' # t2/t3: worker ssh t2 'sudo bash -c "cd /opt/newxce && setsid ./run.sh worker -c /opt/xce/config/worker.yaml /dev/null 2>&1 &"' ssh t3 'sudo bash -c "cd /opt/newxce && setsid ./run.sh worker -c /opt/xce/config/worker.yaml /dev/null 2>&1 &"' ``` > **启动要点**: > - 必须加 `--vcpus 4`(`--vcpus` 缺失会导致 vtask pool 未初始化,`vtask unavailable (pool=nil)`,无法完成选举) > - controller 以 root 运行(`sudo`) > - `setsid … &` 使进程脱离会话,否则 SSH 会话会挂起 > - 检查进程用 `sudo fuser 9990/tcp` / `sudo ps aux | grep ld-linux.*xce`(进程名是 `ld-linux-x86-64.so.2`,不是 `xce_controller`) ### 3.6 启动验证 ```bash # 端口监听 ssh t1 'sudo ss -tlnp | grep -E ":9990|:8080|:7890"' # controller 应为 CTRL_PRIMARY ssh t1 'sudo grep -E "CTRL_PRIMARY|become_leader" /opt/xce/logs/xce_controller.log | tail' # apiserver 响应 ssh t1 'curl -s http://localhost:8080/xce/v1/cluster' # 应返回 401(需认证)而非连接失败 ``` 预期 controller 日志: ``` raft_cb_become_leader: → CTRL_PRIMARY (term=1, first_boot=1) committed_lsn 0 -> 1 ``` --- ## 4. 验证 Chatbot(Phase 0-1) ### 4.1 登录 + 生成 mcp_operator API Key ```bash # 1. 登录(auth.yaml 有 admin/admin, cluster_admin) TOKEN=$(curl -s -X POST http://localhost:8080/xce/v1/auth/logon \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"admin"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])") # 2. 生成 mcp_operator key(服务账号,供 mcp-server 用) curl -s -X POST http://localhost:8080/xce/v1/apikeys \ -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \ -d '{"username":"scheduler-agent","role_name":"mcp_operator","description":"phase1","expiry_days":365}' # → 返回 xce_ak_<64hex> ``` ### 4.2 Phase 0 — 只读验证 ```bash KEY="xce_ak_<生成的key>" BASE="http://localhost:8080" # 全部应返回 200 curl -s -H "Authorization: $KEY" "$BASE/xce/v1/queues" curl -s -H "Authorization: $KEY" "$BASE/xce/v1/hosts" curl -s -H "Authorization: $KEY" "$BASE/xce/v1/jobs" curl -s -H "Authorization: $KEY" "$BASE/xce/v1/resources/definition" ``` ### 4.3 Phase 1 — 写操作验证 ```bash # submit → 201,返回 jobid JOB=$(curl -s -X POST -H "Authorization: $KEY" -H "Content-Type: application/json" \ -d '{"command":"sleep 60","job_name":"verify","queue_name":"normal"}' \ "$BASE/xce/v1/jobs/create" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['jobid'])") # 作业应进入 controller(PEND) curl -s -H "Authorization: $KEY" "$BASE/xce/v1/jobs" ``` **当前限制**:`hold/release/cancel`(job control)返回 **HTTP 500** —— apiserver 预存 bug(bug-1490),见 §7。 --- ## 5. 接入 Claude Code 让 Claude Code(终端里的 AI 助手)直接用自然语言操作集群。Claude Code 通过 MCP 协议连接我们的 `mcp-server`,后者再经 `schedulerclient` 调 apiserver。 ``` Claude Code ──stdio/MCP──> mcp-server ──HTTP──> apiserver ──> controller (我们的二进制) ``` ### 5.1 构建 mcp-server 二进制 ```bash cd /home/lchen/code/xce/src/frontend/mcp-server go build -o /tmp/xce-mcp ./cmd/main.go # 产物 ~12MB,CGO_ENABLED=0 纯静态(生产可放到 /usr/local/bin/xce-mcp) ``` > 前置:集群必须已在跑(见 §3),且有 `mcp_operator` API Key(见 §4.1)。 ### 5.2 方式 A:CLI 注册(推荐) `claude mcp add` 会写配置并校验语法(Claude Code v2.x 验证过的格式): ```bash claude mcp add xce-scheduler -s project \ -e XCE_API_URL=http://172.31.1.30:8080 \ -e XCE_API_KEY=<你的 mcp_operator key> \ -- /tmp/xce-mcp ``` - `xce-scheduler`:服务器名(Claude 看到的工具前缀) - `-s project`:写进项目根 `.mcp.json`(可随 git 共享给团队)。其他选项:`local`(默认,只在本机本项目)、`user`(本机所有项目) - `-e`:传给二进制两个环境变量(`XCE_API_URL` 是 apiserver 地址;`XCE_API_KEY` 必填,缺了 mcp-server 会 `log.Fatal`) - `--` 之后是二进制路径 + 参数;`-e/-s` 等标志必须在 `--` 之前 - stdio 是默认传输,无需 `-t stdio` ### 5.3 方式 B:手写 `.mcp.json` 项目根 `.mcp.json`(当前是空的 `{"mcpServers": {}}`)填成: ```json { "mcpServers": { "xce-scheduler": { "type": "stdio", "command": "/tmp/xce-mcp", "args": [], "env": { "XCE_API_URL": "http://172.31.1.30:8080", "XCE_API_KEY": "<你的 mcp_operator key>" } } } } ``` - `"type": "stdio"` 必须(本地子进程服务器);远程 HTTP 服务器用 `"type": "http"` + `"url"` - `command` 可以是绝对路径 - `env` 支持 `${VAR}` 插值——**不要提交明文 key**,推荐写成 `"XCE_API_KEY": "${XCE_API_KEY}"`,启动 Claude Code 前 `export XCE_API_KEY=...` ### 5.4 验证接入 ```bash claude mcp list # 列出所有服务器,带健康检查 claude mcp get xce-scheduler # 查看某服务器的详情(类型/命令/env/状态) claude mcp remove xce-scheduler # 移除 claude mcp reset-project-choices # 重新触发审批 ``` - 项目级(`.mcp.json`)服务器**首次需审批**:`claude mcp list` 显示 `⏸ Pending approval`,下次在项目里启动 `claude` 时接受即可。`local`/`user` 级无需审批。 - 会话内可用 `/mcp` 命令查看/重连服务器,无需重启。 - 审批通过后,Claude 就能用这些工具了。可以问:**"查看集群里有哪些作业在排队"**(走 `scheduler_list_jobs`)或 **"帮我提交一个 sleep 60 的作业"**(走 `scheduler_submit`,会先弹 review 卡确认)。 ### 5.5 注意事项 - **stdio 约束**:mcp-server 只能往 stdout 写 MCP 协议消息;任何 `fmt.Println` 到 stdout 会损坏连接。本项目安全——`mcp-go` 的 `ServeStdio` 用 stdout 走协议,`main.go` 的 `log.Printf` 写 stderr。改代码时保持这个约定。 - **`XCE_API_URL` 必须显式设**:不设默认 `https://localhost:8080`,生产环境几乎肯定不对。 - **写操作默认要确认**:`scheduler_submit` 有 `dry_run` review 模式 + destructive 注解(Claude Code 会先弹确认);cancel/hold/release 也会弹确认。这是设计文档 §4.5 的"require confirm"在 MCP 层的体现。 - **当前 cancel/hold/release 不可用**:底层 apiserver control 链路有 bug(bug-1490,HTTP 500)。接入后 submit 可用,control 类工具会报错,直到 bug-1490 修复。 --- ## 6. schedulerclient / mcp-server 本地验证 在本地仓库(`chatbot` 分支)可独立运行 Go 测试(不需要集群): ```bash # schedulerclient(读 + 写) cd src/frontend/schedulerclient && go vet ./... && go build ./... && go test ./... -count=1 # mcp-server(含模板 + handlers) cd src/frontend/mcp-server && go vet ./... && go build ./... && go test ./... -count=1 # apiserver RBAC cd src/frontend/apiserver && CGO_ENABLED=0 go test ./controllers/... -run 'RBAC|APIKey' -count=1 ``` schedulerclient 用 API key 连真实集群(若集群在跑): ```bash cd src/frontend/schedulerclient cat > /tmp/demo.go <<'EOF' package main import ( "context" "fmt" sclient "schedulerclient" ) func main() { c := sclient.NewClient("http://172.31.1.30:8080", "xce_ak_") qs, _ := c.ListQueues(context.Background()) for _, q := range qs { fmt.Printf("queue %s status=%s run=%d pend=%d\n", q.Name, q.Status, q.RunningJobs, q.PendingJobs) } } EOF go run /tmp/demo.go ``` --- ## 7. 已知问题 | 问题 | 状态 | Bug | |------|------|-----| | **job control (cancel/hold/release) HTTP 500** | 未修。请求未达 controller(controller 日志无 control 事件)。已修 `job_status` int64 断言(从 "Invalid job_status" 变为 500),500 本体待排查 | bug-1490 | | **3 节点 HA 选举死锁** | 遗留 term 状态(`ha-term.bin` 残留 `voted_for=t1, term=1`)+ standby 无 pool 导致所有节点 STANDBY。单节点绕过 | 记录在会话 | | **apikeys 路由 whitelist** | 已修:`WhitelistPOSTURL` 加 `/xce/v1/apikeys` | bug-1489(已修) | | **mcp_operator 缺 queue:control** | 已修:rbac.yaml 加 `queue:control`(job-control handler 也检查它) | — | | **HA 3 节点需 xceadmin push** | `xceadmin config push` 有 discovery.py 解包 bug(deployment.md 已知) | 已知 | --- ## 8. 快速参考 | 操作 | 命令 | |------|------| | 查 controller 进程 | `sudo fuser 9990/tcp` | | 查 apiserver | `curl -s http://localhost:8080/xce/v1/cluster` | | controller 日志 | `sudo tail /opt/xce/logs/xce_controller.log` | | apiserver 日志 | `sudo tail /opt/xce/apiserver/logs/apiserver_out.log` | | 停 controller | `sudo fuser -k 9990/tcp` | | 重启 apiserver | `kill $(fuser 8080/tcp); cd /opt/newxce && setsid ./run.sh apiserver -d /opt/xce/apiserver &` | | 本地 Go 测试 | 见 §6 |