凌晨两点服务挂了,早上八点才知道——这种事故的根因不是服务太脆,而是没有东西在看着它。这篇写一个进程守护脚本:发现进程、观测指标、检测掉线、自动拉起,全流程走通。本篇演示只操作脚本自己创建的子进程,不碰系统里任何真实服务。
一、守护逻辑的三要素
不管用什么语言写,一个看门狗(watchdog)只有三件事:
- 识别:在几百个进程里,准确找到「我的」那个进程;
- 观测:它活着吗?吃了多少 CPU 和内存?跑了多久?
- 行动:发现掉线 → 重新拉起 → 确认拉起成功。
psutil 一把梭全搞定,它是这么用的:
import psutil
# 识别:遍历进程,按命令行里的标记串匹配
for proc in psutil.process_iter(["pid", "name", "cmdline"]):
cmdline = proc.info["cmdline"] or []
if any("my-worker" in (arg or "") for arg in cmdline):
print("找到了:", proc.info["pid"])
# 观测:oneshot() 一次快照取多个指标,减少系统调用
with proc.oneshot():
proc.cpu_percent(interval=0.2)
proc.memory_percent()
proc.create_time()
# 行动:温和终止 vs 强杀
proc.terminate() # Linux 发 SIGTERM(进程有机会善后)
proc.kill() # Linux 发 SIGKILL(无法捕获,立即死)
为什么用「命令行标记串」而不是进程名识别? 因为 python.exe 这类名字一抓一大把,而我们的启动命令里可以埋一个独一无二的标记(如 opsdemo-sleepy-worker),按标记匹配永远不会误伤同类进程。生产中守护 Nginx 就用 exe/name 精确匹配加 pid 文件,思路相同:识别键必须唯一。
二、完整演示(demos/service_watch_demo.py)
脚本自己拉起一个子进程当「被监护对象」,完整走一遍守护闭环:
import subprocess
import sys
import time
import psutil
MARKER = "opsdemo-sleepy-worker" # 用命令行里的标记串识别「我们的」进程
def spawn_worker() -> psutil.Process:
p = subprocess.Popen(
[sys.executable, "-c", f"import time; time.sleep(120) # {MARKER}"],
)
return psutil.Process(p.pid)
def find_worker() -> psutil.Process | None:
for proc in psutil.process_iter(["pid", "name", "cmdline"]):
try:
cmdline = proc.info["cmdline"] or []
if any(MARKER in (arg or "") for arg in cmdline):
return proc
except psutil.NoSuchProcess: # 迭代期间进程可能刚好退出
continue
return None
def wait_gone(pid: int, timeout: float = 5.0) -> bool:
deadline = time.time() + timeout
while time.time() < deadline:
if not psutil.pid_exists(pid):
return True
time.sleep(0.1)
return False
if __name__ == "__main__":
print("[1] 拉起工作进程")
worker = spawn_worker()
print(f" 已启动 pid={worker.pid}")
print("[2] 巡检:找到它并读取指标")
found = find_worker()
with found.oneshot():
print(f" pid={found.pid} name={found.name()} "
f"cpu={found.cpu_percent(interval=0.2):.1f}% "
f"mem={found.memory_percent():.1f}% "
f"create_time={time.strftime('%H:%M:%S', time.localtime(found.create_time()))}")
print("[3] 模拟进程被杀(kill -9 级别的不可捕获退出)")
worker.kill()
gone = wait_gone(worker.pid)
print(f" 进程已消失: {gone}")
print("[4] 守护逻辑发现掉线,自动重启")
time.sleep(0.3)
found = find_worker()
print(f" 巡检发现存活: {found is not None}")
if found is None:
worker2 = spawn_worker()
print(f" 已自动重启, 新 pid={worker2.pid} (旧 pid={worker.pid})")
found = find_worker()
print(f" 重启后巡检确认存活: {found is not None} pid={found.pid if found else '?'}")
print("[5] 收尾:清理演示进程")
found.terminate()
print(f" 清理完成, 进程已退出: {wait_gone(found.pid)}")
三、真实运行输出
[1] 拉起工作进程
已启动 pid=14124
[2] 巡检:找到它并读取指标
pid=14124 name=python.exe cpu=0.0% mem=0.1% create_time=20:55:52
[3] 模拟进程被杀(kill -9 级别的不可捕获退出)
进程已消失: True
[4] 守护逻辑发现掉线,自动重启
巡检发现存活: False
已自动重启, 新 pid=4704 (旧 pid=14124)
重启后巡检确认存活: True pid=4704
[5] 收尾:清理演示进程
清理完成, 进程已退出: True
闭环完整:kill() 强杀后,巡检立刻发现「存活: False」,自动重启拿到新 pid 4704,再次巡检确认存活。真实 watchdog 的主循环不过是把 [2]→[4] 包进 while True,配上告警通知(复用 01 篇的 notify())。
四、踩坑与细节,都在输出里
psutil.NoSuchProcess必须捕。进程列表是动态的,process_iter迭代期间任何进程都可能退出——01 篇提过这个坑,本篇代码里正式加上了try/except。不捕的守护脚本自己会先崩,那就成了「看门人先睡着的笑话」。- Windows 的
terminate()就是强杀。上表那套 SIGTERM/SIGKILL 的区别只在 Linux 存在;Windows 上terminate()调用的是TerminateProcess,没有「温和退出」。所以跨平台脚本里,优雅停机要靠应用自己的信号处理或管理端口,不能指望terminate()。 pid_exists是轮询不是瞬间真相。wait_gone里轮询 5 秒就是在等操作系统彻底回收进程;杀完立刻检查可能得到「还在」的假象。
五、说点实话:Python 守护脚本的定位
诚实地说,核心服务的守护不应该用 Python 脚本做,理由和 01 篇讲过的一样:守护者自己也是进程,也会挂、会被重启风暴拖垮。成熟方案是:
- Linux:
systemd托管服务,Restart=always+RestartSec=5,由 init 进程(它不会挂)负责拉起; - Windows:任务计划程序或 NSSM 注册成 Windows 服务;
- Python watchdog 的正确位置:轻量场景——个人工具、测试环境、以及「systemd 管不到的三方闭源程序」的兜底看护。
另外必须防重启风暴:如果服务起来就崩(配置坏了),守护循环会无限「拉起→崩溃→拉起」。加一个窗口计数(如「5 分钟内重启超过 3 次就停止拉起并告警」),把故障降级成显式的「我放弃了,快来人」。
六、下一层:从「一个进程」到「一批机器」
本篇盯的是一台机器上的一个进程。下一篇把视角抬高一格:十台机器,每台若干端口,怎么在五分钟内做完一轮批量体检。
本篇代码:demos/service_watch_demo.py,已在 Python 3.13.15 / Windows 真实运行,输出原样收录。演示全程只操作脚本自建的子进程。