那天下午最严重的问题
事情的开头特别朴素:我把终端的聊天窗口一关,桌面端就聊不上了,界面上显示"网关断了"。
第一反应是网关崩了。翻日志却发现完全不是——日志里那段是"正规关机":Reached target System Power Off,接着 Journal stopped,干净得像你主动点了关机键。而 Windows 的开机时间没变,说明关掉的不是电脑。
真正的原因是 WSL(Windows 里的 Linux 子系统,我的 AI 助手跑在里面)被回收了。
WSL2 默认有两段空闲计时,只看一件事:有没有终端客户端连着。它不管你里面跑着什么——
| 计时 | 默认值 | 后果 |
|---|---|---|
instanceIdleTimeout |
15 秒 | 最后一个终端窗口一消失,停掉 Ubuntu 实例 |
vmIdleTimeout |
60 秒 | 实例停完再 60 秒,整台虚拟机断电 |
我关掉终端 → 15 秒后实例被停 → 里面跑的网关、桌面端后端全跟着消失。那天 WSL 已经这样启停了三次,每次都把网关一起带走。
我此前一直以为"服务死了自己站起来"就万事大吉。这次才发现:systemd 能自愈进程,自愈不了"整台机器被关掉"——因为它自己也在那台机器里。
但这不是最该修的东西
顺着这条线往下挖,我看见了一个更难看的场面。
为了让这套东西"别死",我前后叠了四层保活:
| 层级 | 组件 | 干什么 | 节奏 | 问题 |
|---|---|---|---|---|
| Windows | 计划任务 A | 唤醒 WSL、走启动链 | 15 分钟 | 与下一层职责重叠 |
| Windows | 计划任务 B | 全量启动 | — | 僵尸:最后一次运行是三个月前 |
| WSL | 自研守护循环 | 每 60 秒调一次守护脚本 | 60 秒 | 我自己写的"伪 systemd" |
| WSL | 守护脚本 | 同时管网关、桌面端、心跳、隧道 | 随上层 | 一个脚本四个职责,还在加 |
| 应用 | 定时任务 53 个 | 简报/监控/提醒/备份/采集 | 各自为政 | 同类项重复,没人总览 |
这不是数量问题,是分层缺位:保活本该由操作系统承担,我用 shell 脚本硬凑了三层。所以每出一个新问题,我就再加一层守护——加到后来,几个守护互相踩脚:一个把服务拉起来,另一个判定它"不该在跑",来回拉锯。
保活到底该谁管
我去查了同类方案的横向实测(同一台机器、管 10 个进程):
| 方案 | 额外内存 | 冷启动 | 崩溃恢复 | 结论 |
|---|---|---|---|---|
| systemd | 0 MB(随 PID 1 常驻) | 78 毫秒 | 182 毫秒 | Linux 首选 |
| PM2 | 83 MB | 1247 毫秒 | 412 毫秒 | 能用,开销大 |
| Supervisor | 31 MB | 640 毫秒 | 530 毫秒 | 维护模式 |
| 自研 shell 循环 | — | 秒级 | 最坏要等一个轮询周期 | 全维度落败 |
答案很干净:Linux 上就用 systemd,它是操作系统自带的服务管理器,零额外开销、崩溃秒级拉起、日志自动进 journald。我那套每 60 秒轮询的脚本,在每一个维度上都不如它。
另一件想明白的事:健康不等于"进程还在"
还有一个知识我先抄了过来。Kubernetes 把"健康"拆成三种探针:
- 存活探针:进程还在吗?不在就重启
- 就绪探针:能干活吗?不能就先别派活给它
- 启动探针:刚起来时宽容等待,别急着判死
对照我才发现自己的毛病:所有守护都只判"进程在不在"。结果是启动慢的服务被反复掐死重启,而假死的服务(进程在、但不响应了)永远没人管。
迁移时我给每个服务都写清了"怎么算活着",而不是只有一个"进程存在"。
动手:每个服务只留一个"所有者"
设计原则一句话:每个服务只能有一个所有者,任何一层都不许去干别层的活。
| 层级 | 技术 | 只做这件事 |
|---|---|---|
| L1 · Windows | 任务计划程序 | 保证 WSL 实例存活(最少动作,不碰业务服务) |
| L2 · WSL 进程 | systemd | 保活全部常驻服务、统一日志、开机自启 |
| L3 · 定时任务 | systemd timer + 助手自己的 cron | 系统维护类走 timer;需要 AI 判断的留在 cron |
改完之后,L2 里是六个服务,全部 active 且开机自启:网关、桌面端后端、心跳、语音识别、记忆服务、DNS 兜底。加上一条跨机隧道。
删掉的东西比加上去的多得多:
- 自研守护脚本和那个每 60 秒的轮询循环 —— 整个下线
- 那个僵尸计划任务 —— 清掉
- 记忆服务的"每 10 分钟巡检一次、死了就拉起" —— 删。改由 systemd 管之后它本来就能自愈
- 语音服务的"每小时巡检" —— 删。系统服务本来就管着
- DNS 兜底从"每 5 秒跑一次循环"改成"开机跑一次" —— 顺手把不稳定的公共 DNS 换成了国内的 223.5.5.5
- 跨机节点监控从"每 3 分钟一次"降到 15 分钟 —— 一天从 480 次降到 96 次
效果(都是实测数字,不是估算)
- 强杀自愈:我故意用
kill -9打死记忆服务的主进程,25 秒后自己起来了,健康检查返回 200。原来靠每 10 分钟巡检兜底——最坏情况要等整整 10 分钟 - 重启恢复:重新启动 WSL,systemd 在 20 秒内把网关拉起,四个平台(webhook/企业微信/API/飞书)全部重新连上
- 看全局:以前要翻好几个日志文件,现在一条
systemctl list-units看服务状态、list-timers看下次任务时间 - 日志归位:原先有七八个日志散在临时目录里(谁用管理员身份跑就可能抢权限写不进去),全部迁到固定目录
- 噪音归零:一条每 5 秒刷内核日志报错的守护停掉之后,内核日志再没刷过错误
剩下还在跑的巡检类定时任务,现在都是业务型的了——招标监控、配额检查、知识库索引、跨机回执——没有一个是"保活"性质的。
守护不再两头管,是这次整改最实的收益。
最后一块硬骨头
systemd 再强,也有一件它做不到的事。微软的文档写得很直白:
"系统服务不会使 WSL 实例保持活动状态。"
这句话定了整个架构的底线:虚拟机的生死由 Windows 说了算,里面保活做得再完美,也拦不住外面把整台机器关掉。所以最终形态是"两层各司其职",而不是"一层通吃"。
这一层我用两个手段兜的,一主一备:
- 配置层:在 WSL 的配置文件里把两段空闲计时都设成
-1(永不因空闲而关机),下次重启生效 - 句柄层:常驻一个保活句柄,一直握着一个 WSL 连接,让虚拟机永远算"有客户端"。句柄断了 5 秒自动重连,顺便把虚拟机唤起来——它本身设了登录自启,重启电脑也在
定成规矩,免得再叠回去
改完最怕的是过几个月又长出一堆补丁。所以我趁热定了张"新东西往哪放"的表:
| 新增的东西 | 放哪一层 |
|---|---|
| 常驻进程(服务、常驻端口) | systemd,照模板写一个 unit |
| 系统维护类定时任务(备份/清理/巡检) | systemd timer,带开机补跑 |
| 需要 AI 判断、要生成内容的定时任务 | 留在助手自己的定时任务里,写清投递目标 |
| 必须 7×24 在线、跟本机资源无关的活 | 挪到云服务器,别占本机的守护 |
| "兜底自愈" | 不新增。归 systemd 管 |
最后一行是红线:不再新增自研守护循环。
说句实话
家用电脑会关机、会休眠、会被回收,这是它的天性,不该强求它 7×24。真正该做的是把"必须一直在线的活"分出去——我手里正好有一台 24 小时在线的云服务器(本来用它做中转),下一步就是把监控、探测、简报生成这类"不需要本机硬件和本地文件"的任务挪过去。
本机负责它擅长的:跑模型、处理文件、干重活;云服务器负责"一直在"。
一套系统能不能长期活着,不取决于你为它加了多少保护,取决于每个部件的责任有没有人认领——而且只认领一次。
下一篇:我给自己用的 AI 助手记了一个月的账,算清楚它到底花了我多少钱、钱花在哪一步。
本站内容面向人与 AI 开放:不设反爬,欢迎引用,注明出处即可。