当前位置: 首页 > news >正文

监测fastapi服务并自动拉起(不依靠dockerfile)

目前有一个需求,在容器中启动一个fastapi服务后,需要监测其运行状态,当运行中断后,需要进行自动的拉起。则需要处理两种情况:

  • 程序所在容器由于服务器重启或者其他原因关闭,导致服务关闭。此时需要自动重启容器,并将服务自动拉起。
  • 容器正常运行,服务未知原因终端,需要将中断的服务拉起。

1 服务中断

1.1 fastapi程序

所需要被监测的fastapi程序文件为app_test.py,简单的示例:

from fastapi import FastAPI
import uvicorn
from datetime import datetime

app = 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 restart
    log "INFO" "Cron任务添加完成, 并重启cron服务"
}
# 检查cron运行状态, 若未开启则开启
check_and_start_cron() {
    if ! pgrep -x cron > /dev/null; then
        log "WARN" "监测到cron服务未运行, 正在重启..."
        service cron restart
        log "INFO" "cron服务已重启"
    else
        log "INFO" "cron服务正常运行"
    fi
}
# 检查fastapi程序是否运行
check_and_start_service() {
    if ! pgrep -f "$SERVICE_NAME" > /dev/null; then
        if [ -f "$LOCK_FILE" ]; then
            log "INFO" "检测到锁文件,服务不会被重启"
            return
        fi

        # 检查失败计数(若文件不存在, 则赋值为0)
        fail_count=0
        if [ -f "$RESTART_FAIL_COUNT_FILE" ]; then
            content=$(cat "$RESTART_FAIL_COUNT_FILE")
            if [[ "$content" =~ ^[0-9]+$ ]]; then
                fail_count=$content
            else
                log "WARN" "失败计数文件内容非法,已重置为 0"
                fail_count=0
            fi
        fi

        if [ "$fail_count" -ge "$RESTART_FAIL_THRESHOLD" ]; then
            log "ERROR" "⚠️⚠️⚠️服务多次重启失败 ($fail_count 次),已暂停自动拉起,请人工检查!!!"
            return
        fi

        # 尝试重启服务
        log "ERROR" "⚠️服务$SERVICE_NAME 已停止,准备重启 (失败计数: $fail_count)..."
        nohup $SERVICE_COMMAND >> "$LOG_DIR/service_app_output.log" 2>&1 &
        sleep 2
        if pgrep -f "$SERVICE_NAME" > /dev/null; then
            log "INFO" "服务$SERVICE_NAME 已确认启动成功"
            echo 0 > "$RESTART_FAIL_COUNT_FILE"
        else
            log "ERROR" "⚠️服务$SERVICE_NAME 启动后未检测到进程"
            fail_count=$((fail_count + 1))
            echo "$fail_count" > "$RESTART_FAIL_COUNT_FILE"
        fi
    else
        log "INFO" "服务$SERVICE_NAME 正在运行中"
        echo 0 > "$RESTART_FAIL_COUNT_FILE"  # 正常运行时清零
    fi
}

# >>>>>>>>>>>>>>>>>>>>>>>>>>>> 主逻辑 >>>>>>>>>>>>>>>>>>>>>>>
main() {
    log "INFO" ">>>>>>>>>> 开始一次服务监测 >>>>>>>>>>"
    
    if ! check_cron_job_exists; then
        log "WARN" "未监测到cron任务, 正在添加..."
        add_cron_job
    else
        log "INFO" "cron任务已存在"
        check_and_start_cron
    fi

    check_and_start_service

    log "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_testdocker restartdocker 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, 再开启服务

http://www.dtcms.com/a/122877.html

相关文章:

  • 低代码开发平台:飞帆画 echarts 仪表盘
  • Redis最佳实践——用户会话管理详解
  • 金陵幻境录——六朝古都的科技诗篇-南京
  • go游戏后端开发29:实现游戏内聊天
  • 用 HTML 网页来管理 Markdown 标题序号
  • 【微服务架构】SpringCloud Alibaba(九):分布式事务Seata使用和源码分析(TCC模式、Saga模式)
  • 分布式锁阿
  • 软件功能性测试有多重要?功能性测试工具有哪些?
  • Cocos Creator新手学习
  • day25学习Pandas库
  • mysql的主从复制
  • 中文语义相似度 + 去除标签后的网页文本(爬虫数据)
  • 彩色路径 第32次CCF-CSP计算机软件能力认证
  • 服务器运维ACL访问控制列表如何配置
  • 【Leetcode-Hot100】字母异位词分组
  • echarts图表相关
  • 【智能体开发】智能体前后端开发方案
  • 信奥赛之c++课后练习题及解析(算数运算符)
  • Java学习总结-线程池
  • 【NLP 56、实践 ⑬ LoRA完成NER任务】
  • 【golang】堆和栈的区别
  • MySQL主从复制技术详解:原理、实现与最佳实践
  • Docker与Kubernetes在ZKmall开源商城容器化部署中的应用
  • Linux内核页表缓存(TLB)与巨型页
  • 使用Alamofire下载网站首页内容
  • PDFBox/Itext5渲染生成pdf文档
  • Php laravel 留言板 curd 实战
  • 2025数据库系统工程师上午考试知识点汇总
  • 【C++游戏引擎开发】第10篇:AABB/OBB碰撞检测
  • error: RPC failed; HTTP 408 curl 22 The requested URL returned error: 408