进程守护脚本:探活、拉起与告警三件套

systemd 能管大多数服务,但总有例外:容器里跑的业务进程、第三方不给 unit 文件的程序、需要按业务逻辑判断存活的场景。这时候一个轻量守护脚本比强行套 systemd 更实际。 探活:端口 vs 进程 只看进程活着不够——进程在但服

systemd 能管大多数服务,但总有例外:容器里跑的业务进程、第三方不给 unit 文件的程序、需要按业务逻辑判断存活的场景。这时候一个轻量守护脚本比强行套 systemd 更实际。

探活:端口 vs 进程

只看进程活着不够——进程在但服务已经僵死(死锁、线程打满)是常见故障。要看端口是否真的能建立连接:

def probe(host, port, timeout=3):
    try:
        with socket.create_connection((host, port), timeout=timeout):
            return True
    except OSError:
        return False

更进一步可以发一个业务健康检查请求,但要注意别让探活本身成为压力,控制好频率和超时。

核心逻辑

#!/usr/bin/env python3
"""proc_guard.py —— 进程守护:探活失败则拉起,连续失败则告警"""
import os
import socket
import subprocess
import sys
import time

NAME = "myapp"
PORT = 8080
CMD = ["/usr/local/bin/myapp", "--config", "/etc/myapp.conf"]
MAX_FAIL = 3          # 连续失败多少次才告警
CHECK_INTERVAL = 30   # 巡检间隔(秒)
ALERT_CMD = ["/usr/local/bin/notify.sh"]


def probe():
    try:
        with socket.create_connection(("127.0.0.1", PORT), timeout=3):
            return True
    except OSError:
        return False


def is_running():
    """用 pidof 判断进程是否存在,避免误判"""
    r = subprocess.run(["pgrep", "-f", NAME], capture_output=True)
    return r.returncode == 0


def start():
    with open("/tmp/%s.out" % NAME, "ab") as out:
        subprocess.Popen(CMD, stdout=out, stderr=subprocess.STDOUT,
                         start_new_session=True)   # 脱离守护进程,防止被一起杀


def alert(msg):
    subprocess.run(ALERT_CMD + [msg], capture_output=True)
    print("[ALERT] %s" % msg, flush=True)


fails = 0
while True:
    if probe():
        fails = 0
    else:
        fails += 1
        print("探活失败第 %d 次" % fails, flush=True)
        if not is_running():
            print("进程不存在,执行拉起", flush=True)
            start()
            time.sleep(10)          # 给启动留时间,别立刻又判失败
            continue
        if fails >= MAX_FAIL:
            alert("%s 端口 %d 连续 %d 次探活失败,进程存在但无响应"
                  % (NAME, PORT, fails))
            fails = 0               # 重置,避免每分钟刷一条告警
    time.sleep(CHECK_INTERVAL)

三个关键设计

1. start_new_session=True

让被拉起的进程脱离守护进程的进程组。否则守护脚本退出时,子进程可能收到 SIGHUP 一起死掉。

2. 拉起后等待再探活

进程启动要时间,拉起后立刻探活必然失败,会误判成”拉起无效”反复重启。等 10 秒再判断。

3. 告警后重置计数

否则每 30 秒一条告警,一小时 120 条,告警渠道直接被刷爆,真正的故障反而被淹没。

配合 systemd 更稳

守护脚本自己也可能死。用 systemd 托管守护脚本,双保险:

# /etc/systemd/system/proc-guard.service
[Unit]
Description=proc guard

[Service]
ExecStart=/usr/bin/python3 /usr/local/bin/proc_guard.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

这样:进程挂了 → 守护脚本拉起;守护脚本挂了 → systemd 拉起守护脚本。

日志要轮转

守护脚本持续输出,日志会无限增长。加一条 logrotate:

/var/log/proc_guard.log {
    daily
    rotate 7
    compress
    missingok
    notifempty
}

什么时候别用这套

如果服务本身能用 systemd 管,就用 systemd。Restart=always 加 WatchdogSec 已经覆盖大部分场景,自己写守护反而多一个要维护的故障点。脚本只用于 systemd 搞不定的情况。