监测fastapi服务并自动拉起(不依靠dockerfile)
目前有一个需求,在容器中启动一个fastapi服务后,需要监测其运行状态,当运行中断后,需要进行自动的拉起。则需要处理两种情况:
- 程序所在容器由于服务器重启或者其他原因关闭,导致服务关闭。此时需要自动重启容器,并将服务自动拉起。
- 容器正常运行,服务未知原因终端,需要将中断的服务拉起。
1 服务中断
1.1 fastapi程序
所需要被监测的fastapi程序文件为app_test.py,简单的示例:
from fastapi import FastAPI
import uvicorn
from datetime import datetimeapp = FastAPI()@app.get("/")
def read_root():return {"message": "FastAPI app_test.py is running!"}@app.get("/health")
def health_check():current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")print(f"[HEALTH CHECK] Heartbeat accessed at {current_time}")return {"status": "ok", "time": current_time}if __name__ == "__main__":uvicorn.run("app_test:app", host="0.0.0.0", port=8000)
启动命令为:python3 app_test.py > app_test.log 2>&1 &
1.2 监测/守护sh脚本
针对fastapi的程序,需要撰写一个sh脚本文件,负责监测fastapi程序的运行状态,当运行中断时负责进行拉起。
#!/bin/bash# === 配置 ===
SERVICE_NAME="app_test.py" # 进程名称
SERVICE_COMMAND="/usr/bin/python3 /data/app_test.py" # 拉起fastapi服务的命令
CRON_JOB="*/1 * * * * root /bin/bash /data/check_service.sh" # 每分钟监测fastapi
CRONTAB_FILE="/etc/crontab"# >>> log设置
LOG_DIR="/data/docker_service_monitor/logs"
LOG_FILE="$LOG_DIR/service_heartbeat_$(date +%Y%m%d).log" # 滚动创建log文件
mkdir -p $LOG_DIR
find $LOG_DIR -name "service_heartbeat_*.log" -mtime +7 -exec rm {} \; # 保留近7天日志文件# >>> 临时锁文件
LOCK_FILE="/tmp/disable_fastapi_restart.lock"# >>> 防无限制重启设置(读取文件中记录的失败次数, 若连续5次未拉取成功则不再继续拉取)
RESTART_FAIL_COUNT_FILE="/tmp/fastapi_restart_fail_count"
RESTART_FAIL_THRESHOLD=5# === 工具函数 ===
log() {local level="$1"local message="$2"echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] $message" >> "$LOG_FILE"
}# 检查cron任务是否存在
check_cron_job_exists() {grep -F "$CRON_JOB" "$CRONTAB_FILE" > /dev/null 2>&1
}
# 添加cron任务
add_cron_job() {echo "$CRON_JOB" >> "$CRONTAB_FILE"service cron restartlog "INFO" "Cron任务添加完成, 并重启cron服务"
}
# 检查cron运行状态, 若未开启则开启
check_and_start_cron() {if ! pgrep -x cron > /dev/null; thenlog "WARN" "监测到cron服务未运行, 正在重启..."service cron restartlog "INFO" "cron服务已重启"elselog "INFO" "cron服务正常运行"fi
}
# 检查fastapi程序是否运行
check_and_start_service() {if ! pgrep -f "$SERVICE_NAME" > /dev/null; thenif [ -f "$LOCK_FILE" ]; thenlog "INFO" "检测到锁文件,服务不会被重启"returnfi# 检查失败计数(若文件不存在, 则赋值为0)fail_count=0if [ -f "$RESTART_FAIL_COUNT_FILE" ]; thencontent=$(cat "$RESTART_FAIL_COUNT_FILE")if [[ "$content" =~ ^[0-9]+$ ]]; thenfail_count=$contentelselog "WARN" "失败计数文件内容非法,已重置为 0"fail_count=0fifiif [ "$fail_count" -ge "$RESTART_FAIL_THRESHOLD" ]; thenlog "ERROR" "⚠️⚠️⚠️服务多次重启失败 ($fail_count 次),已暂停自动拉起,请人工检查!!!"returnfi# 尝试重启服务log "ERROR" "⚠️服务$SERVICE_NAME 已停止,准备重启 (失败计数: $fail_count)..."nohup $SERVICE_COMMAND >> "$LOG_DIR/service_app_output.log" 2>&1 &sleep 2if pgrep -f "$SERVICE_NAME" > /dev/null; thenlog "INFO" "服务$SERVICE_NAME 已确认启动成功"echo 0 > "$RESTART_FAIL_COUNT_FILE"elselog "ERROR" "⚠️服务$SERVICE_NAME 启动后未检测到进程"fail_count=$((fail_count + 1))echo "$fail_count" > "$RESTART_FAIL_COUNT_FILE"fielselog "INFO" "服务$SERVICE_NAME 正在运行中"echo 0 > "$RESTART_FAIL_COUNT_FILE" # 正常运行时清零fi
}# >>>>>>>>>>>>>>>>>>>>>>>>>>>> 主逻辑 >>>>>>>>>>>>>>>>>>>>>>>
main() {log "INFO" ">>>>>>>>>> 开始一次服务监测 >>>>>>>>>>"if ! check_cron_job_exists; thenlog "WARN" "未监测到cron任务, 正在添加..."add_cron_jobelselog "INFO" "cron任务已存在"check_and_start_cronficheck_and_start_servicelog "INFO" ">>>>>>>>>> 本次服务监测结束 <<<<<<<<<<"echo "" >> "$LOG_FILE"
}main "$@"
# >>> 想要真的关闭任务
# touch /tmp/disable_fastapi_restart.lock
# pkill -f app_test.py # 杀掉FastAPI进程# >>> 想要开启任务:由于sh文件一直监测app_test.py, 所以只需将锁文件删除即可
# rm /tmp/disable_fastapi_restart.lock# >>> 重启次数达到5次后,想要重新执行
# rm /tmp/fastapi_restart_fail_count
⚠️⚠️⚠️在终端中手动重启的命令一般为python/python3,所以很容易以为sh脚本拉起时也需要使用python/python3。但实际上cron所使用的环境为干净的环境,与终端的环境并不一致,所以直接使用python/python3可能会报错ModuleNotFoundError: No module named ‘xxx’。此时需要在终端中执行
which python
,然后将sh脚本中的执行命令由python/python3替换为python的实际所在位置,例如SERVICE_COMMAND="/root/anaconda3/bin/python3 /data/hongtao/workspace/northland/docker_service_monitor/app_test.py"
,其中/root/anaconda3/bin/python3
为python的绝对位置。
1.3 测试
此时可以尝试使用pkill -f app_test
将fastapi的进程关闭(app_test为启动fastapi的进程名),然后查看对应的log观察app_test进程是否被成功启动,或者直接使用ps aux | grep app_test
命令查看。
2 容器中断
2.1 容器配置
首先在开启容器时需要添加--restart=always
,使容器一直处以重启状态当中。例:
docker run -it --shm-size 2G --restart=always --name monitor_app_run -p 18100:8000 -p 18101:22 -v /data/:/data/ -w $PWD 3bc338c08e76 bash"
然后进入容器,使用which python/python3
确认python的安装位置,并在守护sh脚本中进行对应的更改,然后在容器中安装fastapi和unicorn。
2.2 宿主机配置
由于不使用dockerfile,所以不考虑entrypoint方案,使用systemd方案。首先在宿主机配置systemd所需使用的文件(最好先确认mycontainer.service文件原始并不存在,防止影响原有的逻辑):
vi /etc/systemd/system/mycontainer.service
编辑mycontainer.service(注意其中的my_container_name需要更改为需要控制的容器名,/data/check_service.sh为守护的sh脚本路径):
[Unit]
Description=My Python Service Container
After=docker.service
Requires=docker.service[Service]
Restart=always
ExecStart=/usr/bin/docker start -a my_container_name
ExecStop=/usr/bin/docker stop my_container_name# 容器启动后执行的脚本(sh脚本的路径为容器内的路径)
ExecStartPost=/usr/bin/docker exec my_container_name /bin/bash /data/check_service.sh[Install]
WantedBy=multi-user.target
然后执行
systemctl daemon-reexec
systemctl daemon-reload
systemctl enable mycontainer.service
systemctl start mycontainer.service
如果执行最后一个命令时报错no device则执行:
sysctl fs.inotify
vim /etc/sysctl.conf
# 添加以下语句,将max_user_watches扩充10倍
fs.inotify.max_user_watches = 81920
# 配置完成后刷新配置
sysctl -p
# 再次查看inotify
sysctl fs.inotify
整体均配置完成后,执行:
systemctl status mycontainer.service
查看配置执行状态是否为running,然后进入容器ps aux | grep app_test
查询监测脚本对应的py进程是否正确被拉起。
3 人为中断进程
当整体的链接被搭建完成后,普通的中断程序操作均会被恢复,此时如果我们真的想要关闭程序该怎么办?
3.1 临时锁文件
在守护sh脚本中已经添加了临时锁文件的判断,即若LOCK_FILE="/tmp/disable_fastapi_restart.lock"中提到的lock文件存在,则跳过拉起操作,并提示存在锁文件,不进行拉起。
如果想要真的关闭任务,进入容器执行:
touch /tmp/disable_fastapi_restart.lock
pkill -f app_test.py # 杀掉FastAPI进程
如果想要再次开启任务,进入容器执行:
rm /tmp/disable_fastapi_restart.lock
经测试,使用pkill -f app_test
、docker restart
、docker stop
的情况均能进行处理,且当真正想要关闭服务时,采用临时文件锁的方式也能够实现。
3.2 模拟报错
# 模拟启动前报错
if __name__ == "__main__":raise RuntimeError("模拟 FastAPI 启动失败") # 模拟启动失败异常uvicorn.run("app_test:app", host="0.0.0.0", port=8000)# 模拟app引用错误
if __name__ == "__main__":uvicorn.run("app_test:non_existing_app", host="0.0.0.0", port=8000) # 模拟端口被占用错误
python3 -m http.server 8000 # 先手动占用8000, 再开启服务