从零到一构建高可用服务监控体系
目录导读
- 为什么需要进程守护脚本?——守护≠简单重启
- 核心设计原则:五种常见的进程监控模型对比
- 实战编写指南:基于Shell和Python的守护脚本模板
- 进阶优化:日志轮转、告警集成与自愈策略
- 常见问题FAQ:守护进程vs. systemd/ supervisor到底选谁?
为什么需要进程守护脚本?
Q:系统已经有systemd和supervisor,为什么还要自己写守护脚本? A:在企业生产环境中,systemd适合系统级服务,supervisor适合Python应用,但对于以下场景,自定义守护脚本更灵活:
- 需要监控特定进程名称(如
gunicorn工作进程数多于预期)- 需要在进程崩溃后自动拉取最新代码或清理残留在内存中的状态文件
- 需要按业务逻辑恢复(例如先关闭端口再启动,而非简单kill)
- 需要在不修改系统服务文件的情况下快速部署临时监控(如运维应急脚本)
守护脚本的核心价值在于按需定制故障恢复策略,而非简单的“挂了就重启”。
核心设计原则:五种进程监控模型
| 监控模型 | 实现方式 | 适用场景 | 缺点 |
|---|---|---|---|
| PID文件检测 | 检查 /var/run/xxx.pid 是否存在且进程存活 |
传统服务(如mysqld) | PID文件可能残留 |
| 进程名匹配 | ps aux \| grep [x]xx 计数 |
无PID文件的脚本服务 | 需防grep自匹配 |
| 端口检测 | netstat -tnlp \| grep :8080 |
网络服务(如Nginx) | 不适用内部进程 |
| 健康检查接口 | curl http://localhost:8080/health |
Web服务(最可靠) | 需应用配合 |
| 系统服务接口 | systemctl is-active xxx |
系统级守护 | 依赖systemd |
最佳实践:组合使用“端口检测+进程名匹配”,
# 检测端口存在且进程名匹配
if ! netstat -tnlp 2>/dev/null | grep -q ":80 "; then
systemctl restart nginx
fi
实战编写指南:Shell版守护脚本模板
1 基础版:每秒检测一次,崩溃立即重启
#!/bin/bash
# 守护进程名称
PROCESS_NAME="myapp"
WORK_DIR="/opt/app"
LOG_FILE="/var/log/guard-${PROCESS_NAME}.log"
# 启动命令
start_app() {
cd "$WORK_DIR" || exit 1
nohup ./${PROCESS_NAME} > /dev/null 2>&1 &
echo "$(date) - 已启动 $PROCESS_NAME PID: $!" >> "$LOG_FILE"
}
# 主监控循环
while true; do
if ! pgrep -x "$PROCESS_NAME" > /dev/null; then
echo "$(date) - 检测到 $PROCESS_NAME 进程不存在" >> "$LOG_FILE"
start_app
fi
sleep 3
done
关键点:pgrep -x 匹配精确进程名,避免因 bash ./myapp 导致的误判。
2 进阶版:增加最大重启次数与健康检查
import os
import subprocess
import time
import requests
PROCESS_CMD = ["python3", "/opt/app/main.py"]
HEALTH_URL = "http://localhost:8080/health"
MAX_RESTARTS = 5
RESTART_INTERVAL = 60 # 秒
def is_process_alive():
"""通过端口+进程树双重检测"""
try:
# 检测端口是否监听
requests.get(HEALTH_URL, timeout=2)
return True
except:
return False
def restart_process():
subprocess.run(PROCESS_CMD, cwd="/opt/app")
def main():
restart_count = 0
while True:
if not is_process_alive():
restart_count += 1
if restart_count > MAX_RESTARTS:
# 超过阈值时发送告警(可替换为邮件/钉钉)
print("ALERT: 进程频繁崩溃,需人工介入")
time.sleep(300) # 冷却5分钟
continue
restart_process()
time.sleep(RESTART_INTERVAL)
else:
restart_count = 0 # 正常则重置计数器
time.sleep(5)
if __name__ == "__main__":
main()
进阶优化:告警集成与自愈策略
1 日志轮转(防止磁盘写满)
# 在守护脚本中追加日志管理
LOG_MAX_SIZE=10485760 # 10MB
if [ -f "$LOG_FILE" ] && [ "$(stat -c%s "$LOG_FILE")" -gt $LOG_MAX_SIZE ]; then
mv "$LOG_FILE" "${LOG_FILE}.old"
fi
2 多层级恢复策略
- 一级恢复:仅重启进程(耗时2秒)
- 二级恢复:杀死所有相关子进程后重启(解决僵尸进程)
- 三级恢复:执行预定义的修复脚本(如清理临时文件)
3 集成告警(以钉钉为例)
import requests
def send_alert(msg):
webhook = "https://oapi.dingtalk.com/robot/send?access_token=xxx"
requests.post(webhook, json={"msgtype":"text","text":{"content":msg}})
常见问题FAQ
Q:守护脚本本身崩溃了怎么办? A:使用双重守护策略——将脚本注册为systemd服务,或使用crontab每分钟检测脚本是否存活:
* * * * * pgrep -x guard.sh || /usr/local/bin/guard.sh &
Q:如何区分“正常退出”和“崩溃”? A:为进程设置退出信号捕获,正常退出时写入退出码文件,守护脚本优先检查退出码文件,若无文件则视为崩溃。
Q:守护脚本与Docker/K8s的冲突点? A:在容器内部应避免启动守护脚本,因为K8s健康检查(Readiness/Liveness Probe)已提供类似功能,守护脚本更适合裸金属服务器或被隔离的VM环境。
Q:是否应该使用kill -9(SIGKILL)暴力杀死进程?
A:不要!kill -9无法让进程清理连接池和临时文件,应优先使用kill -TERM(SIGTERM),若进程无响应,再升级到kill -KILL。
守护脚本的黄金法则
- 不要写死启动命令:将命令路径提取为变量或配置文件
- 保持幂等性:连续执行两次启动函数不应产生副作用
- 监控自身消耗:确保守护脚本CPU占用<0.1%,内存<10MB
- 测试故障场景:使用
kill -9模拟崩溃、pkill模拟误杀、systemctl stop模拟管理员操作
对于需要高可用性的服务,建议将上述示例代码部署前进行混沌工程测试——故意杀死进程,验证脚本能否在5秒内恢复,只有经过严苛测试的守护脚本,才能成为线上环境的可靠“看门狗”。
