从零搭建高可用守护进程
目录导读
- 为什么需要进程异常重启脚本?
- 脚本编写的核心原则与常见陷阱
- 实战:四种主流重启脚本写法详解
- 进阶技巧:健康检查与日志规范
- 常见问题问答(QA)
为什么需要进程异常重启脚本?
在服务器运维中,进程因内存泄漏、连接超时、资源耗尽等原因意外退出,是最常见的故障之一,根据某IT运维平台的统计,超过60%的应用层故障表现为进程非正常终止,如果依赖人工盯防,不仅效率低下,而且无法在凌晨等非工作时间快速响应。

进程异常重启脚本的核心价值在于:
- 自动化:当进程退出时,自动恢复服务
- 稳定性:减少因单次故障导致的服务中断时长
- 可观测:记录异常发生的时间、原因,便于后续排查
但需注意:重启脚本是应急手段,不是根治措施,生产环境中应配合监控系统(如Prometheus+Grafana)深度追踪根因。
脚本编写的核心原则与常见陷阱
1 五大核心原则
| 原则 | 说明 |
|---|---|
| 幂等性 | 重复执行不会产生副作用 |
| 防循环 | 启动后应检测是“已运行”还是“刚启动” |
| 日志记录 | 每次重启原因、时间、PID需落盘 |
| 资源释放 | 重启前关闭旧socket、清理临时文件 |
| 退出策略 | 连续重启失败N次后发告警并停止尝试 |
2 三大常见陷阱
- 陷阱1:未判断进程是否僵尸 – 使用
ps aux时可能捕获到defunct进程导致误判 - 陷阱2:直接kill -9无等待 – 进程未完全释放资源就立即重启,可能端口被占用
- 陷阱3:crontab频率过高 – 每1分钟检测一次可能造成服务抖动,建议5-10分钟
实战:四种主流重启脚本写法详解
1 方法一:Bash循环监控(最基础)
#!/bin/bash
# 进程名称
PROCESS="nginx"
# 启动命令
START_CMD="/usr/sbin/nginx"
# 日志文件
LOG_FILE="/var/log/process_monitor.log"
while true; do
# 检测进程是否存在(排除grep本身)
PID=$(pgrep -x $PROCESS)
if [ -z "$PID" ]; then
echo "$(date) - $PROCESS 已崩溃,正在重启..." >> $LOG_FILE
$START_CMD
# 等待3秒确认启动成功
sleep 3
NEW_PID=$(pgrep -x $PROCESS)
echo "$(date) - 新PID: $NEW_PID" >> $LOG_FILE
fi
sleep 10
done
适用场景:开发环境或单机简单服务
缺点:无错误处理,死循环占用cpu
2 方法二:systemd服务自愈(生产推荐)
[Unit] Description=My App Service After=network.target [Service] Type=simple User=appuser ExecStart=/opt/app/start.sh ExecStop=/opt/app/stop.sh Restart=always RestartSec=5 StartLimitInterval=0 StartLimitBurst=5 [Install] WantedBy=multi-user.target
核心参数:
Restart=always:无论退出码都重启RestartSec=5:重启延迟5秒StartLimitBurst=5:连续5次失败后停止
注意:需使用systemctl daemon-reload加载配置,配合systemd-cgtop监控资源更佳。
3 方法三:Python增强版(含错误分级)
import subprocess
import time
import logging
process_name = "java -jar app.jar"
max_retries = 3
retry_count = 0
logging.basicConfig(
filename='/var/log/process_guard.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def is_running():
result = subprocess.run(
['pgrep', '-f', process_name],
capture_output=True,
text=True
)
return result.returncode == 0
def start_process():
subprocess.Popen(
process_name.split(),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
time.sleep(2)
return is_running()
while retry_count < max_retries:
if not is_running():
logging.warning(f"进程不存在,尝试第{retry_count+1}次重启")
if start_process():
logging.info("进程启动成功")
retry_count = 0
else:
retry_count += 1
time.sleep(5 * retry_count) # 指数退避
else:
retry_count = 0
time.sleep(30)
else:
logging.critical("已达最大重试次数,放弃重启")
# 可替换为发送告警:requests.post(url, data=...)
优势:可扩展告警模块、指数退避避免雪崩
4 方法四:Docker容器自动重启
version: '3.8'
services:
app:
image: myapp:latest
restart: unless-stopped # 除非手动停止,否则自动重启
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval: 15s
timeout: 3s
retries: 3
start_period: 10s
最佳实践:定义HEALTHCHECK时,应同样监控CPU、内存(如ps -p %PID -o %cpu,resident)
进阶技巧:健康检查与日志规范
1 健康检查的三种层级
| 层级 | 方式 | 示例 |
|---|---|---|
| L1-进程存在 | ps/pgrep | 返回PID即存活 |
| L2-端口监听 | netstat/ss | 检查TCP端口状态 |
| L3-业务可用 | HTTP请求 | 返回200且body含特定关键字 |
建议组合使用:先L1快速检测,若通过再L3深度检查,减少误报。
2 日志规范
# 日志示例格式
{
"timestamp": "2024-01-15T14:32:07Z",
"level": "WARN",
"process": "myapp",
"action": "restart",
"reason": "pid文件丢失",
"previous_pid": 12345,
"new_pid": 67890,
"restart_count": 3
}
关键字段:时间戳、动作(start/stop/restart)、原因、PID前后值、启动耗时
3 防重复启动
使用flock实现文件锁:
exec 200>/tmp/monitor.lock flock -n 200 || exit 1 # 若已有实例运行则退出
常见问题问答(QA)
Q1:脚本启动后导致进程频繁重启怎么办?
A:检查两点:1) 启动命令是否使用了相对路径,建议用绝对路径;2) 是否忘记添加nohup或setsid,使用Restart=always的systemd服务在进程崩溃时可能会卷入“启动->崩溃->启动”循环,可设置StartLimitBurst=3限制频率。
Q2:如何区分进程是主动停止还是异常退出?
A:在启动命令中添加exec命令,并捕获退出信号,在脚本中设置trap 'echo "Got SIGTERM, exiting"' SIGINT SIGTERM,若未捕获到用户信号而退出,则判定为异常。
Q3:多个脚本监控同一个进程会冲突吗?
A:绝对会,同一进程若被两个监控脚本检测,可能导致“A刚重启完成,B又检测到未运行并触发重启”,形成竞争条件,解决方案:单例模式,使用文件锁(如前面用flock)确保只有一个监控实例。
Q4:重启后需要保留原进程的上下文吗?
A:视业务而定,有状态服务(如游戏服务器、消息队列)需小心处理,建议:1) 在停止前将状态持久化到共享存储 2) 使用优雅关闭(SIGTERM而非SIGKILL) 3) 启动时恢复状态,无状态服务(如HTTP API)则直接重启即可。
Q5:在Kubernetes中如何实现类似功能?
A:K8s中有内置的livenessProbe和readinessProbe,示例:
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 10
Pod状态变为CrashLoopBackOff时,kubelet会自动重启,并且有指数退避(最长5分钟)。
总结建议
编写进程异常重启脚本时,不要追求复杂功能,而是遵循最小可用、可观测、可停止的原则,生产环境首选systemd或Docker内置机制,其次才是自定义脚本(务必包含退出策略和告警),推荐组合:systemd + healthcheck + 日志采集,既保证高可用,又避免雪崩效应。