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

183 lines
9.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 点重启一次」这种低频维护性重启时,只能在「每天重启」和「完全不重启」之间二选一。
## 目标
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` 三个字段:
```bash
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
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 >` 全量重写)
- `--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 上执行,不接受仅靠阅读代码判断:
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` 报错——自行维护时区白名单成本高且易过期。