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同步,请勿在站点侧修改)