跳转至

token_proxy 开机自启与每日定时任务 变更方案

主机:dell-R620(Ubuntu,用户 rain) 工程目录:/home/rain/Documents/services/token_proxy 交付物目录:autostart/ 编制日期:2026-09-14


1. 背景

2026-09-14 00:56 完成一次环境重建(用户名 rian → rain,工程路径随之变更), 02:17 完成网卡静态 IP 变更(网段 192.168.0.0/24 → 192.168.31.0/24)。 重建与改 IP 后代理本身运行正常,但暴露出两个遗留问题:

问题一:代理没有开机自启能力。 当前进程是 run.sh 以 nohup ... & 方式手工拉起的(PID 23319,09-14 00:56:13 启动)。 排查结果:无 systemd 单元、/etc/rc.local 不存在、用户 crontab 无 @reboot 条目。 主机一旦重启,代理不会自动恢复,所有经代理转发的 LLM 请求会中断。

问题二(已被证伪,实为一次性时序缺口):每日 0 点成本报表未生成 09-14 的记录。 logs/cost_cron.log 最后写入停留在 09-13 00:00:04,余额快照最后一条为 09-14 00:58:21(重建时手工执行)。

根因证据(/var/log/syslog):

2026-09-13T16:53:20Z crontab[22827]: (rain) LIST (rain)
2026-09-13T16:58:16Z crontab[24058]: (rain) REPLACE (rain)   # = 北京时间 09-14 00:58:16

crontab 于北京时间 09-14 00:58:16 才被写入,晚于当日 00:00 的触发点 58 分钟; 该时刻与重建时序完全吻合(venv 备份 00:57:46、代理重启 00:56:13)。 因此 09-14 00:00 那次任务不是执行失败,而是当时 crontab 尚不存在,任务从未被调度。

crontab 当前内容正确,无需修改:

0 0 * * * cd /home/rain/Documents/services/token_proxy && \
  /home/rain/Documents/services/token_proxy/venv/bin/python cost_report.py \
  >> /home/rain/Documents/services/token_proxy/logs/cost_cron.log 2>&1

cost_report.py 默认读取脚本同目录的 config.yaml(cost_report.py:855、:877-880), venv/bin/python 为绝对路径,cd 到工程目录后再执行,cron 的最小化环境不会造成影响。


2. 目标

变更完成后应达到的可验证状态:

编号 目标 判定方式
G1 主机重启后代理自动运行,无需人工登录 重启后 systemctl --user is-active token-proxy 返回 active,curl 127.0.0.1:8788/healthz 返回 200
G2 代理进程退出后自动拉起(含异常退出与人为 kill) Restart=always 生效;kill -9(SIGKILL)与 kill(SIGTERM)后均在 5 秒内恢复,NRestarts 递增
G3 用户未登录时服务依然运行 loginctl show-user rain \| grep Linger 返回 Linger=yes
G4 每日 0 点报表任务恢复正常节奏 09-15 00:00 后 logs/cost_cron.log 追加新记录,reports/ 下 xlsx 更新时间戳
G5 变更脚本可重复执行且幂等 连续执行两次,第二次输出全部为 [跳过],零变更

3. 前置条件

权限

  • 安装 systemd 用户级单元:~/.config/systemd/user/,归 rain 所有,无需 sudo。
  • loginctl enable-linger rain:用户为自己开启 linger,正常无需 sudo。 若被 polkit 拒绝,需 sudo loginctl enable-linger rain。当前 sudo 为免密可用, 但执行前会单独向你确认,不做静默提权。
  • 本方案不写 /etc/ 下任何目录(备选方案 B 除外,见附录 A)。

依赖检查项(脚本会自动校验,缺失即中止)

检查项 期望值
工程目录 /home/rain/Documents/services/token_proxy
proxy.py 存在且可读
config.yaml 存在且可读
解释器 venv/bin/python 存在(当前 Python 3.12.3)
venv 依赖 含 yaml(proxy.py 唯一第三方依赖)
端口 8788 未被无关进程占用
磁盘 /home 剩余充足(当前 194G 可用)

执行前检查

systemctl --user is-active token-proxy      # 期望输出 active(已启用则无需重复安装)
crontab -l                                  # 确认 0 0 任务存在且路径为 /home/rain/...

4. 影响范围

涉及主机:单机,dell-R620(192.168.31.249 / eno1)。 本机无冗余,出问题没有其他节点可顶上,故第 6、7 章写得偏保守。

涉及组件:仅 token 计量代理进程的托管方式(nohup 手工管理 → systemd 用户服务)。

是否需停服:需要,中断为秒级。 迁移必须停掉现有 nohup 进程,否则新服务因端口 8788 被占用而启动失败。 自启能力在重启后才生效,故本次变更本身不重启主机、不改内核、不动网络,无节点级影响。

窗口期:选择无 LLM 请求时段(如当前无对话进行时)。 代理中断期间,Windows 侧 Trae 客户端的请求会连接失败并报错,需即时恢复。

在跑的业务:主机上未运行 Slurm / LSF 作业,本次变更不涉及调度节点,无作业影响面。

受影响对象:所有 base_url 指向 http://192.168.31.249:8788/{ds,glm} 的客户端, 中断窗口内不可用。


5. 分步操作

全部动作由 autostart/install-autostart.sh 完成,脚本幂等。 以下为脚本内部步骤,便于人工核对与单独执行。

步骤 5.1 — 前置校验

ls -l /home/rain/Documents/services/token_proxy/{proxy.py,config.yaml}
/home/rain/Documents/services/token_proxy/venv/bin/python -c "import yaml; print('yaml ok')"

预期:文件存在,输出 yaml ok。 失败判据:任一文件缺失或 import 报错 → 中止,不继续(改解释器或补依赖需另行评估)。

步骤 5.2 — 写入 systemd 用户单元

目标文件:~/.config/systemd/user/token-proxy.service

[Unit]
Description=Trae Token 计量代理 (token_proxy)
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=60
StartLimitBurst=5

[Service]
Type=simple
WorkingDirectory=/home/rain/Documents/services/token_proxy
ExecStart=/home/rain/Documents/services/token_proxy/venv/bin/python /home/rain/Documents/services/token_proxy/proxy.py /home/rain/Documents/services/token_proxy/config.yaml
Restart=always
RestartSec=5
StandardOutput=append:/home/rain/Documents/services/token_proxy/logs/proxy.stdout.log
StandardError=append:/home/rain/Documents/services/token_proxy/logs/proxy.stdout.log

[Install]
WantedBy=default.target

要点说明:

  • Type=simple 且 ExecStart 直接调用 python,不经 run.sh。 run.sh 内部是 nohup ... & 后台化,systemd 会立即判定主进程退出,造成反复重启。
  • Restart=always + StartLimitBurst=5/StartLimitIntervalSec=60(置于 [Unit] 段): 进程以任何方式退出都会自动拉起,但失败达 5 次即停止重试,不会无限重启。 Restart=always 不会覆盖 systemctl --user stop——显式停止被 systemd 抑制重启,维护性停止不受影响。
  • 信号语义(本次实测踩过的坑):systemd 将 SIGHUP/SIGINT/SIGTERM/SIGPIPE 判定为「干净退出」, Restart=on-failure 明确排除这四种信号,因此用 kill(默认 SIGTERM)验证自愈会得到「不重启」的 假结论。on-failure 只覆盖崩溃、OOM(SIGKILL)、非零退出码等;若要连人为 kill 也自愈,必须用 Restart=always。本项目取 always,实测 kill -9 触发重启、kill 亦触发重启。
  • StandardOutput/Error 追加到现有 logs/proxy.stdout.log,保持既有排障习惯不变。
  • 解释器选用 venv/bin/python:与 cron 任务保持一致,且满足工程内独立 venv 的规范要求。
  • 幂等:脚本先比对文件内容,内容一致则整体跳过,不重写、不 reload。

步骤 5.3 — 停旧进程(高危,脚本醒目提示)

systemctl --user is-active token-proxy    # 若已 active,说明本次是重复执行 → 跳过本步与 5.5 的启动
cd /home/rain/Documents/services/token_proxy && ./run.sh stop

预期:[已停止] 原 PID=23319。 失败判据:进程仍在 → 进入步骤 5.4 的失败分支,切勿强行继续。

步骤 5.4 — 加载、启用并(必要时)重启

systemctl --user daemon-reload            # 仅在 5.2 发生过写入时执行
systemctl --user enable token-proxy       # 已 enabled 则跳过
systemctl --user restart token-proxy      # 仅当单元内容变更且服务已在运行时执行
loginctl enable-linger rain               # 若被拒绝:sudo loginctl enable-linger rain(先确认)

说明:单元内容变更后仅 daemon-reload 不足以保证 Restart 策略对已运行进程立即生效, 故内容有变化时显式 restart;内容无变化时跳过 restart,以维持幂等。

预期:无报错。 失败判据:Failed to start → 执行 journalctl --user -u token-proxy -n 50 定位, 同一逻辑连续失败 2 次即停止,不做改参重试。

步骤 5.5 — 补跑与 cron 确认(本次先不执行补跑)

cron 部分不做任何修改。09-14 缺失记录属一次性时序缺口, 当晚 00:00 起的自动任务会正常触发并覆盖全量统计,补跑价值有限且会多写一次余额快照, 故建议不补跑,改为次日核对。

若你仍希望立即补出 09-14 的报表,单独确认后执行(写操作,会生成 xlsx 与余额快照):

cd /home/rain/Documents/services/token_proxy
venv/bin/python cost_report.py --date 2026-09-14

6. 验证

6.1 服务状态

systemctl --user is-active token-proxy        # 期望 active
systemctl --user status token-proxy --no-pager | head -n 12
ss -lntp | grep 8788                          # 期望 LISTEN 0.0.0.0:8788,进程为 venv 下 python
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8788/healthz   # 期望 200

6.2 Linger 生效

loginctl show-user rain | grep Linger         # 期望 Linger=yes

6.3 自愈能力(会短暂中断,请在无请求时做)

测异常退出(SIGKILL,模拟崩溃 / OOM)

kill -9 "$(systemctl --user show -p MainPID --value token-proxy)"
sleep 8
systemctl --user is-active token-proxy                          # 期望 active(自动拉起)
systemctl --user show -p NRestarts --value token-proxy          # 期望 >= 1
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8788/healthz   # 期望 200

测人为 kill(SIGTERM)——Restart=on-failure 下此场景不会重启,本项目取 always 才会:

kill "$(systemctl --user show -p MainPID --value token-proxy)"
sleep 8
systemctl --user is-active token-proxy                          # 期望 active

测维护性停止(不应被自动拉起)

systemctl --user stop token-proxy
sleep 5
systemctl --user is-active token-proxy                          # 期望 inactive(显式停止被抑制)
systemctl --user start token-proxy                              # 恢复

注意:用 kill(默认 SIGTERM)单独验证自愈会得到「不重启」的假结论, 原因见第 5.2 节的信号语义说明——验证异常自愈必须用 kill -9。

6.4 计量未断流

tail -n 3 logs/usage.jsonl                    # 期望出现新时间戳的记录
tail -n 5 logs/proxy.log | grep -c ERROR      # 期望 0

6.5 幂等复跑

再次执行 autostart/install-autostart.sh,预期输出全部为 [跳过],无任何写入、 无 daemon-reload、无服务重启,退出码 0。

6.6 开机自启的最终验证

sudo reboot 属高危,不在本次变更范围内自动执行。 两个可选路径:

  • 等主机下次自然重启后,登录前从其他机器执行 curl http://192.168.31.249:8788/healthz,返回 200 即证明开机自启成功;
  • 或在获得你明确确认的维护窗口内,人工重启一次并立即执行上述验证。

6.7 cron 恢复验证(次日)

stat -c '%y %n' logs/cost_cron.log reports/*.xlsx    # 期望均更新为 2026-09-15 00:00 之后
tail -n 8 logs/cost_cron.log                          # 期望出现 2026-09-15 00:00:xx 的区间/对账记录

7. 回滚

备份:变更前对将被改动/删除的对象做带时间戳备份,不做原地覆盖。

对象 备份方式
现有 crontab crontab -l > autostart/backup/crontab.$(date +%Y%m%d-%H%M%S).bak(本次不改,仅留档)
若 token-proxy.service 已存在 cp ~/.config/systemd/user/token-proxy.service autostart/backup/token-proxy.service.$(date +%Y%m%d-%H%M%S).bak
现有代理进程 由 run.sh stop 停止,PID 23319 记录在 logs/proxy.pid;如需精确恢复可重新 ./run.sh start

回滚步骤(顺序执行)

# 1. 停用并禁用服务
systemctl --user disable --now token-proxy

# 2. 删除单元文件(脚本会先备份,此处为人工回滚路径)
rm -f /home/rain/.config/systemd/user/token-proxy.service
systemctl --user daemon-reload
systemctl --user reset-failed token-proxy

# 3. 关闭 linger(仅当确认不再需要无人登录运行)
loginctl disable-linger rain

# 4. 恢复原运行方式
cd /home/rain/Documents/services/token_proxy && ./run.sh start

回滚后的验证

./run.sh status                               # 期望 [运行中] PID=xxxxx
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8788/healthz   # 期望 200
systemctl --user is-active token-proxy        # 期望 inactive(已交还给手工管理)
tail -n 3 logs/proxy.log                      # 期望新请求正常记账

回滚触发判据(量化,达标即回滚,不靠临场判断)

判据 阈值
/healthz 非 200 连续 2 次探测均失败
服务启动后 30 秒内未监听 8788 超时即回滚
journalctl --user -u token-proxy 出现非 0 退出码且 Restart 触发 达 5 次上限即回滚
客户端出现连续 403/502 立即回滚

8. 风险说明

# 风险 影响 缓解措施
R1 迁移瞬间端口占用冲突,新服务起不来 代理中断 脚本严格按「先停旧进程 → 确认端口释放 → 再启动」顺序;启动失败自动回滚到 run.sh start
R2 loginctl enable-linger 被 polkit 拒绝 无法实现 G3,服务随会话退出而停 脚本检测退出码;被拒时暂停并提示,由你决定是否授权 sudo,不静默提权
R3 run.sh 与 systemd 两套机制混用 出现双进程抢占 8788 或状态误判 迁移后以 systemd 为唯一入口;SOP 明确禁止再执行 run.sh start;logs/proxy.pid 在 systemd 模式下不再维护
R4 解释器由系统 python3 切到 venv/bin/python 若 venv 缺包则启动失败 前置校验 import yaml;实测 venv 内含 yaml,且 proxy.py 无其他第三方依赖
R5 主机重启验证需真实重启 业务短暂中断 默认不自动重启;改为「下次自然重启后验证」或在确认的窗口内人工执行
R6 Restart=always 掩盖真实故障(反复重启) 排障困难 限制 5 次/60 秒后停止重试;日志统一落在 logs/proxy.stdout.log;NRestarts 与 journalctl --user -u token-proxy 可查重启历史
R7 cron 报错无告警,静默失败 报表缺失不易发现 本次不引入告警,维持现状;仅在次日按 6.7 人工核对
R8 代理监听 0.0.0.0:8788 内网端口暴露 已有 allow_cidr 白名单(192.168.31.0/24 + 127.0.0.1/32),本次不变更
R9 用 kill(SIGTERM)验证自愈,得到「配置失效」的假结论 误判、误改配置 验证异常自愈一律用 kill -9;SIGTERM/SIGHUP/SIGINT/SIGPIPE 属 systemd 定义的「干净退出」,on-failure 不覆盖,本项目用 always 规避

附录 A:开机自启备选方案对比

维度 方案 A(本方案,推荐) 方案 B:systemd 系统级单元 方案 C:crontab @reboot
单元/入口 ~/.config/systemd/user/token-proxy.service /etc/systemd/system/token-proxy.service(User=rain) @reboot sleep 30; cd ... && ./run.sh start
权限 无需 sudo(linger 偶发需确认) 必须 sudo 无需 sudo
自动重启 支持 支持 不支持
未登录时运行 依赖 linger 开启 原生支持 原生支持
日志 追加文件 / journalctl --user 追加文件 / journalctl 仅文件
主要缺点 需额外开启 linger 写系统目录、需 sudo 无自愈、依赖 cron 时序、排查不便
切换成本 — 删除 user 单元,另建系统单元并 sudo systemctl enable --now 删除 user 单元,改用 crontab 行

选择建议:工程位于用户家目录,方案 A 的权限边界与之一致,故推荐 A。 若你有统一用系统级托管管理服务的习惯,或 enable-linger 策略受限,则改选 B, 切换时需把单元文件整体搬到 /etc/systemd/system/ 并补 User=rain、Group=rain。

附录 B:本次排查发现的遗留项(不在本次变更范围)

项 现状 说明
requirements.txt 缺失 工程根无此文件 规范要求依赖记录在 requirements.txt;建议补 PyYAML、openpyxl
.gitignore 缺失 工程根无此文件 规范要求排除 venv;且 .bak/、logs/、secret.yaml 也不应入库
venv 目录名 venv/ 而非 .venv/ 规范建议 .venv/;改名会牵动 crontab 与 .bak,需单独评估后再动
SOP.md 归档 工程根无 SOP.md,仅 .bak/ 内有历史版本 重建后未补写主 SOP,建议后续合并本方案与静态 IP 方案
客户端计量中断(22:22:57 起) 代理健康、监听正常、healthz 200,但无新计量记录,ss 显示 8788 无客户端连接 现象出现在代理被手工停止又恢复之后,属客户端侧行为,非本次变更引入。需在 Trae 客户端确认自定义模型 base_url 仍指向 http://192.168.31.249:8788/ds,否则计量将持续漏记。服务端无法强制客户端回落

以上均不改变当前运行状态,是否处理由你决定。


来源:/home/rain/Documents/services/token_proxy/autostart/SOP.md | 最后更新:2026-09-17 02:23(由 build/sync.py 同步,请勿在站点侧修改)