Files
server-deploy/docs/superpowers/specs/2026-08-06-xray-restart-schedule-design.md
2026-08-07 11:47:52 +08:00

9.2 KiB
Raw Permalink Blame History

Xray 定时重启:可配置的「每 N 天 + 固定时间点」

日期:2026-08-06 涉及文件:vps-xray/deploy.shvps-xray/.env.examplevps-xray/uninstall.shvps-xray/README.md

背景

当前 deploy.sh 只有一个布尔开关 XRAY_DAILY_RESTART,为真时创建 OnCalendar=*-*-* 04:00:00 的每日重启 timer,为假时删除该 timer。默认关闭,理由是崩溃恢复已由 Restart=always + RestartSec=3 覆盖(秒级),而每日硬重启会切断全部活动连接。

这个判断本身没变,但「一天一次」粒度太粗:想要「每 7 天凌晨 4 点重启一次」这种低频维护性重启时,只能在「每天重启」和「完全不重启」之间二选一。

目标

  1. 支持任意 N 天间隔 + 指定时间点,如「每 7 天 04:00」「每 3 天 05:30」
  2. 保持默认关闭,已部署机器重跑脚本时行为不变
  3. 提供一个轻量命令行入口,只改重启计划,不触碰 Xray 与现有 vless 链接

非目标

  • 不做「每月/每季度」等日历语义(真有需要时用户可直接编辑 timer)
  • 不做多时间点(如一天重启两次)
  • 不改动 Restart=always 这一层崩溃恢复逻辑

关键约束:systemd 表达不了任意 N 天

OnCalendar*-*-1/7 04:00:00 是「每月的 1、8、15、22、29 号」,跨月会重置——29 号到下月 1 号只隔 2~3 天。因此不能直接用 OnCalendar 表达「每 N 天」。

OnUnitActiveSec=7d 是真实间隔,但无法钉住具体时间点,且会随每次触发缓慢漂移。

本设计采用第三种:timer 每天到点唤醒,由 service 里的守卫脚本决定这次要不要真的重启。

配置模型

.env 三个字段:

XRAY_RESTART_EVERY_DAYS=0             # 0 或缺失 = 关闭;N = 每 N 天重启一次
XRAY_RESTART_TIME=04:00               # 24 小时制 HH:MM,默认 04:00
XRAY_RESTART_TIMEZONE=Asia/Shanghai   # 默认保持现状

首次部署默认 0(关闭),与当前行为一致。

旧字段迁移

只在 XRAY_RESTART_EVERY_DAYS 未设置时才回退去读 XRAY_DAILY_RESTART

旧值 迁移结果
true / yes / on / 1(大小写不敏感) XRAY_RESTART_EVERY_DAYS=1
其它或缺失 XRAY_RESTART_EVERY_DAYS=0

save_env 之后只写新字段,不再写 XRAY_DAILY_RESTART.env 随重跑自动换代。两个字段同时存在时以新字段为准。

现存四台服务器的 .env 都是 XRAY_DAILY_RESTART=false,迁移后为 0,行为不变。

实现架构

xray-restart.timer          OnCalendar=*-*-* <HH:MM>:00      每天到点唤醒
                            TimeZone=<tz>
                            Persistent=true
        ↓
xray-restart.service        Type=oneshot
                            ExecStart=/usr/local/bin/xray-restart-guard
        ↓
xray-restart-guard          读 /var/lib/xray/last-restart
                            距上次不足 N 天 → 打日志后 exit 0
                            满 N 天 → 写入新时间戳 → systemctl restart xray

「每 N 天」因此是距上次实际重启的真实间隔,不受跨月重置影响,N=3 与 N=7 同样准确。

守卫脚本的两个关键细节

300 秒容差。 systemd timer 默认 AccuracySec=1min,第 N 次触发的实际间隔可能是 N×86400 - 30s。严格比较会判定「不满 N 天」从而顺延整整一天,且误差会持续累积。阈值取 N×86400 - 300 规避。容差远小于一天,不会造成重复触发。

先写时间戳,再重启。 systemctl restart xray 会中断 guard 所在的 systemd 事务。顺序反了会丢失时间戳记录,导致每天都重启。代价是:重启失败时时间戳仍已更新,本轮被跳过——这比陷入每日重启循环可接受。

状态文件

/var/lib/xray/last-restart,内容为一个 Unix 时间戳。guard 以 root 运行,读写无权限问题。内容非法(非纯数字)时按 0 处理,即立即允许重启。

参数校验

在脚本早期(.env 载入后)完成,不合法直接报错退出,不静默回退到默认值——避免用户以为设了每 7 天、实际在每天重启。

字段 规则
XRAY_RESTART_EVERY_DAYS ^[0-9]+$
XRAY_RESTART_TIME ^([01]?[0-9]|2[0-3]):[0-5][0-9]$,校验通过后补零归一化HH:MM
XRAY_RESTART_TIMEZONE 非空即可,交由 systemd 校验并在启动失败时报错

时间同时接受 4:0004:00,但写入 .env 和 timer 前一律归一化成两位小时(04:00),保证 OnCalendar 字符串格式统一、.env 内容可预测。

--restart 轻量入口

用法

bash deploy.sh --restart 7 04:00   # 每 7 天 04:00
bash deploy.sh --restart 3         # 每 3 天,时间沿用 .env
bash deploy.sh --restart 0         # 关闭
bash deploy.sh --restart           # 按 .env 现有值重建 timer

参数解析

--restart 后可跟 0~2 个位置参数,按形态判断,不会误吞后续选项:

  • 下一个 token 匹配 ^[0-9]+$ → 取作 N
  • 再下一个匹配 ^([01]?[0-9]|2[0-3]):[0-5][0-9]$ → 取作 HH:MM
  • 不匹配则不消费,沿用 .env 现值

--restart--mode / --show / --redetect 互斥,同时给出直接报错。

执行路径

main() 中早退,与 --show 同一位置:

  1. 校验 N 与 HH:MM,时间归一化为 HH:MM
  2. 就地更新 .env:命令行给了 N 就写 XRAY_RESTART_EVERY_DAYS,给了时间才写 XRAY_RESTART_TIME;两者都没给则不改 .env,只按现值重建 timer。字段不存在时追加(TIME 追加时用默认值 04:00
  3. 调用 configure_restart_timer()
  4. systemctl daemon-reload,按需 enable / disable timer
  5. 打印结果与 systemctl list-timers xray-restart.timer

明确不做的事

不装依赖、不升级 Xray、不生成密钥、不选伪装目标、不重写 config.json、不动防火墙与 sysctl、不重启 xray。因此 vless 链接与现有连接均不受影响。这些约束会以注释形式写在函数头部。

前置检查

  • .env 不存在 → 报错退出(无法确认这是已部署的机器)
  • xray 主单元不存在 → 报错退出(避免在未部署的机器上留下孤儿 timer)

代码组织

timer / service / guard 的创建逻辑抽成独立函数 configure_restart_timer(),由 harden_system()--restart 路径共用。两条路径各写一份实现迟早会漂移成不一致。

.env 更新走两条不同路径,这是有意的:

  • 完整部署走既有的 save_env()cat > 全量重写)
  • --restartsed 就地替换 —— 轻量入口只应改自己那两行,不该抹掉用户在 .env 里加的注释和自定义字段

关闭与清理

XRAY_RESTART_EVERY_DAYS=0 时:

  • systemctl disable --now xray-restart.timer
  • 删除 xray-restart.timerxray-restart.service/usr/local/bin/xray-restart-guard
  • 保留 /var/lib/xray/last-restart

保留状态文件的理由:以后重新启用时,若距上次已超过 N 天则首次触发即重启,符合直觉;若刚重启过则不会立刻再来一次。

uninstall.sh 需补上 guard 脚本与状态文件的清理(当前只删了 timer 和 service)。

受影响文件

文件 改动
vps-xray/deploy.sh 参数解析、配置校验与迁移、configure_restart_timer()--restart 路径、save_envusage
vps-xray/.env.example 三个新字段与说明
vps-xray/uninstall.sh 清理 guard 脚本与状态文件
vps-xray/README.md 定时重启一节

验证方案

在真实 VPS 上执行,不接受仅靠阅读代码判断:

  1. 语法 —— bash -n deploy.sh
  2. 启用 —— bash deploy.sh --restart 7 04:00,确认:.env 两字段已更新;systemctl list-timers xray-restart.timer 有下次触发时间;xray 主服务的 ActiveEnterTimestamp 未变(证明没重启)
  3. 守卫生效 —— 手动连跑 xray-restart-guard 两次,第一次应重启并写入时间戳,第二次应打印「跳过」且 xray 未重启
  4. 关闭 —— bash deploy.sh --restart 0,确认 timer / service / guard 三个文件均被删除,状态文件仍在
  5. 迁移 —— 构造一份只含 XRAY_DAILY_RESTART=true.env,跑完整部署,确认得到 XRAY_RESTART_EVERY_DAYS=1 且 timer 已建立
  6. 校验 —— bash deploy.sh --restart 7 25:00--restart abc 均应报错退出,且不留下任何单元文件
  7. 互斥 —— bash deploy.sh --restart 7 --redetect 应报错退出

已知取舍

  • 间隔锚点是「上次实际重启时间」,不是固定日历日期。 若某次因机器关机错过了触发点,下次开机后的第一个触发点会补上(Persistent=true),此后间隔重新从那一刻起算,会缓慢偏移触发日期。这是「真实间隔」语义的必然结果,也是用户选择的语义。
  • 重启失败时时间戳仍已更新,本轮被跳过,下一轮照常。理由见上文「先写时间戳」。
  • 时区错误要到 timer 启动时才暴露。脚本不预校验时区字符串,靠 systemctl 报错——自行维护时区白名单成本高且易过期。