AI 生成的 systemd 服务文件即使通过验证也可能在低内存 VPS 上 OOM,提供了静态检查加约束运行时的验证方法。
一个生成的 systemd unit 可以通过 systemd-analyze verify 检查,但仍然会将一台小规格虚拟机推向 OOM kill。核心问题不是语法;而是 unit 文件编码了关于内存、swap 和重启行为的假设,这些假设从未被声明。在 RAM 有限的免费主机上,这些假设应该在服务启用前被检查。
因为这个工作流始于 MonkeyCode 提供的免费模型访问和免费服务器选项,每一条生成的指令都被视为一种声明而非已知事实。披露:本文作为 MonkeyCode 产品推广的一部分编写。下方描述的验收门使用静态检查和受限的临时运行来测试对小型主机重要的那些声明。
从内置验证器开始。它在服务被加载之前捕获悬空引用、无效选项和 unit 图问题。
systemd-analyze verify /etc/systemd/system/worker.service
干净的结果只意味着该文件结构有效。它不会告诉你进程是否会保持在免费主机的内存范围内、它在停止时可能挂起多长时间,或者自动重启是否会循环。这些风险存在于 [Service] 小节,一个小型解析器可以将它们显式化。
下方脚本读取 unit 文件并将失败相关字段报告为 JSON。它有意不尝试修复任何内容;只是将假设浮现出来供审查。
#!/usr/bin/env python3
import json
import sys
def parse_service(path):
service = {}
section = None
with open(path, encoding='utf-8') as handle:
for raw in handle:
line = raw.strip()
if line.startswith('[') and line.endswith(']'):
section = line[1:-1]
continue
if section != 'Service' or not line or line.startswith(('#', ';')):
continue
if '=' in line:
key, value = line.split('=', 1)
service.setdefault(key.strip(), []).append(value.strip())
return service
def build_report(path):
service = parse_service(path)
report = {
'has_memory_max': 'MemoryMax' in service,
'has_memory_swap_max': 'MemorySwapMax' in service,
'has_timeout_start_sec': 'TimeoutStartSec' in service,
'has_timeout_stop_sec': 'TimeoutStopSec' in service,
'restart_on_failure': any('on-failure' in v for v in service.get('Restart', [])),
'has_restart_sec': 'RestartSec' in service,
'runs_as_root': any(v.strip() == 'root' for v in service.get('User', [])),
'exec_start_uses_absolute_path': all(
v.lstrip('-+!@:').split()[0].startswith('/')
for v in service.get('ExecStart', [])
if v.strip()
),
}
return report
if __name__ == '__main__':
print(json.dumps(build_report(sys.argv[1]), indent=2))
用 unit 路径运行它:
python3 check_unit_failure_surface.py /etc/systemd/system/worker.service
一个典型的生成草稿会产生类似如下的输出,其中缺失的内存字段就是需要在启用 unit 之前停止并添加显式限制的原因。输出是一份风险清单,而非裁决。
{
"has_memory_max": false,
"has_memory_swap_max": false,
"has_timeout_start_sec": true,
"has_timeout_stop_sec": false,
"restart_on_failure": true,
"has_restart_sec": true,
"runs_as_root": true,
"exec_start_uses_absolute_path": true
}
没有 MemoryMax 的服务可以一直增长,直到内核的 OOM handler 杀死它或同一主机上的另一个进程。免费级服务器通常提供少量 RAM 分配,而生成的 unit 很少能猜对这个限制。添加 MemoryMax 和 MemorySwapMax 会创建一个可预测的边界,临时探测可以在真实服务运行之前验证它。
同样的推理适用于 TimeoutStopSec。如果生成的命令忽略 SIGTERM 且 unit 没有停止超时,部署或重启可能会在服务管理器的默认超时上挂起。一个有界的停止超时将未知的延迟转化为可衡量的失败。
静态审查确认字段存在;下一步是确认二进制文件可以在这些限制内运行。下方命令在临时 unit 中运行生成的 worker,带有 256 MB 限制且没有 swap。
sudo systemd-run --unit=memory-probe --wait \
-p MemoryMax=256M \
-p MemorySwapMax=0 \
-p TimeoutStopSec=15 \
-- ./run_worker.py
运行后,检查临时结果和服务管理器记录的状态。
systemctl show memory-probe -p Result -p ExecMainStatus
journalctl -u memory-probe --since '10 minutes ago' | grep -i -E 'oom|killed process|memory'
一次健康的运行通常退出码为零,且在日志中不留下 OOM 事件。被推过限制的进程通常会以非零状态终止,通常是 ExecMainStatus=137,这表示内核不得不杀死它。这种区别是您在具有有限内存的主机上启用 unit 之前需要的最低信号。
这些检查可以作为存储生成或手写 unit 文件的仓库的预合并任务按顺序运行。当内存、swap 或停止超时字段缺失时,门会拒绝 unit,然后在声明的限制下实际运行命令几秒钟。
#!/usr/bin/env bash
set -euo pipefail
UNIT="$1"
PROBE="$2"
systemd-analyze verify "$UNIT"
python3 check_unit_failure_surface.py "$UNIT" > unit_report.json
for field in has_memory_max has_memory_swap_max has_timeout_stop_sec; do
grep -q "$field: true" unit_report.json || {
echo "reject: $field is missing"
exit 1
}
done
sudo systemd-run --unit=memory-probe --wait \
-p MemoryMax=256M \
-p MemorySwapMax=0 \
-p TimeoutStopSec=15 \
-- "$PROBE"
将探测路径替换为实际服务的冒烟命令,例如短生命周期的 worker 调用或干运行模式。这个门故意做得很浅:它证明服务可以启动并保持在限制内,而不是证明它在生产流量下永远不会失败。
静态解析器只查看 unit 文件。它不检查子命令、共享库、语言运行时或应用程序内部的动态内存增长。临时运行使用的 unit 名称和 cgroup 与已安装服务不同,因此时序和调度器行为可能与真实部署不同。内存限制也无法捕获只有在数小时流量后才出现的泄漏。
如果主机不使用 systemd、cgroup 配置不可用,或应用程序已由具有自己限制的编排器管理,请跳过此工作流。它对于小型 VPS 部署最有用,在这些部署中生成的服务即将与其他进程共享一个小型内存池。
如果下一个草稿来自 MonkeyCode,在启用它之前运行相同的检查。生成的 unit 成为候选,而静态报告加上受限运行决定它是否安全到可以晋升。