9.2 KiB
Xray 定时重启:可配置的「每 N 天 + 固定时间点」
日期:2026-08-06
涉及文件:vps-xray/deploy.sh、vps-xray/.env.example、vps-xray/uninstall.sh、vps-xray/README.md
背景
当前 deploy.sh 只有一个布尔开关 XRAY_DAILY_RESTART,为真时创建 OnCalendar=*-*-* 04:00:00 的每日重启 timer,为假时删除该 timer。默认关闭,理由是崩溃恢复已由 Restart=always + RestartSec=3 覆盖(秒级),而每日硬重启会切断全部活动连接。
这个判断本身没变,但「一天一次」粒度太粗:想要「每 7 天凌晨 4 点重启一次」这种低频维护性重启时,只能在「每天重启」和「完全不重启」之间二选一。
目标
- 支持任意 N 天间隔 + 指定时间点,如「每 7 天 04:00」「每 3 天 05:30」
- 保持默认关闭,已部署机器重跑脚本时行为不变
- 提供一个轻量命令行入口,只改重启计划,不触碰 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:00 与 04: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 同一位置:
- 校验 N 与 HH:MM,时间归一化为
HH:MM - 就地更新
.env:命令行给了 N 就写XRAY_RESTART_EVERY_DAYS,给了时间才写XRAY_RESTART_TIME;两者都没给则不改.env,只按现值重建 timer。字段不存在时追加(TIME追加时用默认值04:00) - 调用
configure_restart_timer() systemctl daemon-reload,按需 enable / disable timer- 打印结果与
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 >全量重写) --restart走sed就地替换 —— 轻量入口只应改自己那两行,不该抹掉用户在.env里加的注释和自定义字段
关闭与清理
XRAY_RESTART_EVERY_DAYS=0 时:
systemctl disable --now xray-restart.timer- 删除
xray-restart.timer、xray-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_env、usage |
vps-xray/.env.example |
三个新字段与说明 |
vps-xray/uninstall.sh |
清理 guard 脚本与状态文件 |
vps-xray/README.md |
定时重启一节 |
验证方案
在真实 VPS 上执行,不接受仅靠阅读代码判断:
- 语法 ——
bash -n deploy.sh - 启用 ——
bash deploy.sh --restart 7 04:00,确认:.env两字段已更新;systemctl list-timers xray-restart.timer有下次触发时间;xray 主服务的ActiveEnterTimestamp未变(证明没重启) - 守卫生效 —— 手动连跑
xray-restart-guard两次,第一次应重启并写入时间戳,第二次应打印「跳过」且 xray 未重启 - 关闭 ——
bash deploy.sh --restart 0,确认 timer / service / guard 三个文件均被删除,状态文件仍在 - 迁移 —— 构造一份只含
XRAY_DAILY_RESTART=true的.env,跑完整部署,确认得到XRAY_RESTART_EVERY_DAYS=1且 timer 已建立 - 校验 ——
bash deploy.sh --restart 7 25:00与--restart abc均应报错退出,且不留下任何单元文件 - 互斥 ——
bash deploy.sh --restart 7 --redetect应报错退出
已知取舍
- 间隔锚点是「上次实际重启时间」,不是固定日历日期。 若某次因机器关机错过了触发点,下次开机后的第一个触发点会补上(
Persistent=true),此后间隔重新从那一刻起算,会缓慢偏移触发日期。这是「真实间隔」语义的必然结果,也是用户选择的语义。 - 重启失败时时间戳仍已更新,本轮被跳过,下一轮照常。理由见上文「先写时间戳」。
- 时区错误要到 timer 启动时才暴露。脚本不预校验时区字符串,靠
systemctl报错——自行维护时区白名单成本高且易过期。