飞牛重启容器异常
摘要(TL;DR)
大家好呀!我在飞牛NAS上遇到了一个头疼的问题,每次重启机器就会导致Docker的容器启动异常,这个问题是一定的。
于是我自己写了个Bash脚本lsd来解决这个问题。这个脚本可以自动监控容器状态,发现停止就重启,还能记录事件、提供交互界面,甚至支持开机自启。不过在开发过程中我也踩了不少坑,比如脚本在非Bash环境报错、误把“all”当容器名、启动时网络不稳定导致误操作,还有担心长期运行内存占用过高。现在问题终于解决啦,我把经验整理成这篇文章,希望能帮到有同样困扰的小伙伴。
说明:
脚本通过 MONITORED 配置管理要监控的容器名或 “all”,以守护进程循环定期检测容器状态并在停止时尝试重启,提供交互菜单与 systemd 自启支持以便运维管理
操作
以下示例假设你把脚本保存为
/usr/local/bin/lsd。
- 写入脚本并赋予权限
sudo tee /usr/local/bin/lsd > /dev/null <<'EOF'
#!/bin/bash
# lsd - Docker 容器监控守护脚本 (优化版)
# 功能:监控指定容器,停止时自动重启;支持交互管理(添加/删除容器、启停守护进程、开机自启等)
CONFIG_DIR="/etc/lsd"
CONFIG_FILE="CONFIG_DIR/monitored.conf"
LOG_FILE="/var/log/lsd-monitor.log"
PID_FILE="/var/run/lsd-monitor.pid"
SERVICE_FILE="/etc/systemd/system/lsd-monitor.service"
INTERVAL=15 # 检测间隔(秒)
STARTUP_DELAY=30 # Docker启动后延迟时间(秒)
# 初始化配置目录
mkdir -p "CONFIG_DIR"
# 彩色输出函数
green() { echo -e "\033[32m1\033[0m"; }
red() { echo -e "\033[31m1\033[0m"; }
bold() { echo -e "\033[1m1\033[0m"; }
# 日志函数(同时输出到终端和日志文件)
log() {
local msg="[(date '+%F %T')] 1"
echo "msg" | tee -a "LOG_FILE"
}
# 加载监控配置(从文件读取监控的容器列表)
load_config() {
if [[ -f "CONFIG_FILE" ]]; then
# 去除多余空格和换行,确保格式为"container1 container2"或"all"
MONITORED=(cat "CONFIG_FILE" | tr -d '\n' | tr -s ' ' | sed 's/^ *//;s/ *//')
else
MONITORED=""
fi
}
# 保存监控配置(写入文件)
save_config() {
echo "MONITORED" > "CONFIG_FILE"
}
# 获取所有容器(运行/停止的)的名称
get_all_containers() {
docker ps -a --format "{{.Names}}" 2>/dev/null || {
red "错误:无法连接Docker服务,请检查Docker是否运行。"
return 1
}
}
# 守护进程主循环(监控容器状态)
daemon_loop() {
local start_time=SECONDS
log "[daemon] 初始化:等待Docker服务启动..."
# 等待Docker服务启动(避免Docker未启动导致监控失败)
until docker info >/dev/null 2>&1; do
local wait_time=((SECONDS - start_time))
log "[daemon] 等待Docker服务启动...(已等待{wait_time} 秒)"
sleep 1
done
# 延迟等待Docker稳定(避免容器启动时网络未就绪)
log "[daemon] Docker已启动,延迟 {STARTUP_DELAY} 秒等待稳定..."
sleep "STARTUP_DELAY"
# 开始监控
log "[daemon] 监控开始:间隔={INTERVAL}秒,监控对象={MONITORED}"
while true; do
local containers
if [[ "MONITORED" == "all" ]]; then
containers=(get_all_containers)
else
containers="MONITORED"
fi
# 遍历所有监控的容器
for c incontainers; do
# 跳过空字符串(避免因空格导致的无效循环)
[[ -z "c" ]] && continue
# 1. 检查容器是否存在(防止容器被删除后仍监控)
if [[ "MONITORED" != "all" ]] && ! docker inspect "c" >/dev/null 2>&1; then
log "容器c 不存在,从监控列表中移除..."
MONITORED=(echo "MONITORED" | sed "s/\bc\b//g" | tr -s ' ' | sed 's/^ *//;s/ *//')
save_config
continue
fi
# 2. 检查容器状态(是否在运行)
local status=(docker ps --filter "name=^{c}" --format "{{.Status}}")
if [[ -z "status" ]]; then
log "容器 c 已停止,尝试重启..."
if docker start "c" >/dev/null 2>&1; then
log "容器 c 重启成功"
else
log "容器c 重启失败(请检查容器日志:docker logs c)"
fi
fi
done
sleep "INTERVAL"
done
}
# 启动守护进程(后台运行)
start_daemon() {
# 检查守护进程是否已运行
if [[ -f "PID_FILE" ]] && kill -0(cat "PID_FILE") 2>/dev/null; then
red "错误:守护进程已在运行(PID:(cat "PID_FILE"))"
return 1
fi
# 启动守护进程(后台运行)
(daemon_loop&echo! > "PID_FILE") &
# 等待PID文件创建(确保启动成功)
sleep 1
if [[ -f "PID_FILE" ]] && kill -0 (cat "PID_FILE") 2>/dev/null; then
green "成功:守护进程已启动(PID: (cat "PID_FILE"))"
else
red "错误:守护进程启动失败,请检查日志(LOG_FILE)"
rm -f "PID_FILE" # 清理无效PID文件
fi
}
# 停止守护进程
stop_daemon() {
if [[ -f "PID_FILE" ]] && kill -0(cat "PID_FILE") 2>/dev/null; then
kill(cat "PID_FILE")
rm -f "PID_FILE"
green "成功:守护进程已停止"
else
red "错误:守护进程未运行"
fi
}
# 查看守护进程状态
status_daemon() {
if [[ -f "PID_FILE" ]] && kill -0(cat "PID_FILE") 2>/dev/null; then
green "守护进程运行中(PID:(cat "PID_FILE"))"
else
red "守护进程未运行"
fi
}
# 启用开机自启动(systemd)
enable_autostart() {
# 生成systemd服务文件
cat>"SERVICE_FILE" <<EOF
[Unit]
Description=lsd Docker 容器监控守护进程
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
ExecStartPre=/bin/sleep {STARTUP_DELAY}
ExecStart=/usr/local/bin/lsd monitor
Restart=always
User=root
Group=root
[Install]
WantedBy=multi-user.target
EOF
# 重新加载systemd配置并启用服务
systemctl daemon-reload
systemctl enable --now lsd-monitor.service
if systemctl is-active --quiet lsd-monitor.service; then
green "成功:开机自启动已启用"
else
red "错误:开机自启动启用失败,请检查服务配置(SERVICE_FILE)"
fi
}
# 取消开机自启动
disable_autostart() {
systemctl disable --now lsd-monitor.service
rm -f "SERVICE_FILE"
systemctl daemon-reload
green "成功:开机自启动已取消"
}
# 交互菜单(核心交互逻辑)
menu() {
while true; do
load_config
clear
# 菜单头部(显示当前状态)
bold "== lsd Docker 容器监控守护进程 =="
echo "守护进程状态:(status_daemon | awk '{print NF}')" # 提取状态(运行中/未运行)
echo -n "当前监控对象:"
if [[ -z "MONITORED" ]]; then
echo "(red '(空)')"
elif [[ "MONITORED" == "all" ]]; then
echo "(green '所有容器')"
else
echo "(echo "MONITORED" | tr ' ' ', ')" # 用逗号分隔,更清晰
fi
echo "----------------------------------------"
# 菜单选项
echo "操作选项:"
echo " 1) 添加监控容器(支持批量添加)"
echo " 2) 删除监控容器(支持批量删除)"
echo " 3) 启动守护进程(后台运行)"
echo " 4) 停止守护进程"
echo " 5) 前台运行监控(调试用,Ctrl+C停止)"
echo " 6) 查看守护进程状态"
echo " 7) 启用开机自启动(systemd)"
echo " 8) 取消开机自启动"
echo " 9) 手动重启指定容器"
echo " 0) 退出菜单"
echo -n "请选择操作(0-9):"
read -r choice
# 处理用户选择
case "choice" in
1) # 添加监控容器
echo "----------------------------------------"
echo "可用容器列表(运行/停止的):"
get_all_containers | while read -r c; do
echo " c"
done
echo "提示:输入容器名(多个用空格分隔),输入\"all\"监控所有容器,输入\"q\"取消。"
echo -n "请输入:"
read -r names
[[ "names" == "q" ]] && continue
# 处理输入(分割为多个容器名)
for name in names; do
# 验证容器是否存在(除了"all")
if [[ "name" != "all" ]] && ! docker inspect "name" >/dev/null 2>&1; then
red "容器name 不存在,跳过添加。"
continue
fi
# 避免重复添加
if [[ "name" == "all" ]]; then
MONITORED="all"
break # 选择"all"后忽略其他输入
elif echo "MONITORED" | grep -q "\bname\b"; then
red "容器name 已在监控列表中,跳过添加。"
continue
fi
# 添加到监控列表
MONITORED="MONITOREDname"
done
# 保存配置(去除多余空格)
MONITORED=(echo "MONITORED" | tr -s ' ' | sed 's/^ *//;s/ *//')
save_config
green "成功:监控列表已更新(当前监控:MONITORED)"
read -p "按回车键继续..."
;;
2) # 删除监控容器
echo "----------------------------------------"
if [[ "MONITORED" == "all" ]]; then
red "当前监控所有容器,无法删除单个容器(请先切换到监控特定容器)。"
read -p "按回车键继续..."
continue
elif [[ -z "MONITORED" ]]; then
red "当前没有监控任何容器,无法删除。"
read -p "按回车键继续..."
continue
fi
echo "当前监控的容器:"
echo "MONITORED" | tr ' ' '\n' | while read -r c; do
echo "c"
done
echo "提示:输入容器名(多个用空格分隔),输入\"q\"取消。"
echo -n "请输入:"
read -r names
[[ "names" == "q" ]] && continue
# 处理输入(分割为多个容器名)
for name innames; do
# 验证容器是否在监控列表中
if ! echo "MONITORED" | grep -q "\bname\b"; then
red "容器 name 不在监控列表中,跳过删除。"
continue
fi
# 从监控列表中删除
MONITORED=(echo "MONITORED" | sed "s/\bname\b//g" | tr -s ' ' | sed 's/^ *//;s/ *//')
done
# 保存配置
save_config
green "成功:监控列表已更新(当前监控:MONITORED)"
read -p "按回车键继续..."
;;
3) # 启动守护进程
echo "----------------------------------------"
start_daemon
read -p "按回车键继续..."
;;
4) # 停止守护进程
echo "----------------------------------------"
stop_daemon
read -p "按回车键继续..."
;;
5) # 前台运行监控(调试用)
echo "----------------------------------------"
echo "提示:前台运行监控,按Ctrl+C停止。"
read -p "按回车键开始..."
daemon_loop
;;
6) # 查看守护进程状态
echo "----------------------------------------"
status_daemon
read -p "按回车键继续..."
;;
7) # 启用开机自启动
echo "----------------------------------------"
enable_autostart
read -p "按回车键继续..."
;;
8) # 取消开机自启动
echo "----------------------------------------"
disable_autostart
read -p "按回车键继续..."
;;
9) # 手动重启容器
echo "----------------------------------------"
echo "可用容器列表:"
get_all_containers | while read -r c; do
echo " c"
done
echo "提示:输入容器名(多个用空格分隔),输入\"q\"取消。"
echo -n "请输入:"
read -r names
[[ "names" == "q" ]] && continue
# 处理输入(分割为多个容器名)
for name in names; do
# 验证容器是否存在
if ! docker inspect "name" >/dev/null 2>&1; then
red "容器 name 不存在,跳过重启。"
continue
fi
# 重启容器
if docker restart "name" >/dev/null 2>&1; then
green "容器 name 重启成功"
else
red "容器name 重启失败(请检查日志:docker logs name)"
fi
done
read -p "按回车键继续..."
;;
10) # 退出菜单
echo "----------------------------------------"
green "退出菜单。"
exit 0
;;
*) # 无效选择
red "无效选择,请输入0-9之间的数字。"
read -p "按回车键继续..."
;;
esac
done
}
# 命令行参数处理(支持非交互模式)
case "1" in
monitor) # 启动守护进程(非交互模式)
load_config
daemon_loop
;;
start) # 启动守护进程(非交互模式)
start_daemon
;;
stop) # 停止守护进程(非交互模式)
stop_daemon
;;
status) # 查看守护进程状态(非交互模式)
status_daemon
;;
enable-autostart) # 启用开机自启动(非交互模式)
enable_autostart
;;
disable-autostart) # 取消开机自启动(非交互模式)
disable_autostart
;;
*) # 默认进入交互菜单
menu
;;
esac
EOF
sudo chmod +x /usr/local/bin/lsd
- 第一次配置(添加要监控的容器或输入all)
sudo /usr/local/bin/lsd
# 在交互菜单里选择“添加监控容器”,输入容器名或all,然后保存
- 立即后台运行(临时测试)
sudo /usr/local/bin/lsd start # 脚本会把PID写到/var/run/lsd-monitor.pid
- 建议用systemd开机自启
sudo /usr/local/bin/lsd enable-autostart
# 或手动操作:
sudo tee /etc/systemd/system/lsd-monitor.service > /dev/null <<'UNIT'
[Unit]
Description=lsd Docker container watchdog
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
ExecStartPre=/bin/sleep 30
ExecStart=/usr/local/bin/lsd monitor
Restart=always
RestartSec=5
User=root
StandardOutput=syslog
StandardError=syslog
[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload
sudo systemctl enable --now lsd-monitor.service
- 查看日志
sudo tail -f /var/log/lsd-monitor.log
# 若启用了systemd,也可以用:
sudo journalctl -u lsd-monitor.service -f
- 添加日志轮转(避免日志过大)
sudo tee /etc/logrotate.d/lsd-monitor > /dev/null <<'ROTATE'
/var/log/lsd-monitor.log {
daily
rotate 14
compress
missingok
notifempty
create 0640 root adm
}
ROTATE
最终感想
在飞牛NAS这种定制系统上,开机流程和服务依赖有很多细节需要注意。直接在启动早期运行自动重启脚本很容易出问题。通过强制使用Bash、正确处理“all”配置、等待网络和Docker稳定,以及合理配置systemd,这个问题基本解决了。Bash脚本对于几十个容器来说足够用了,但如果需要更复杂的功能或者监控更多容器,建议用更专业的语言开发。希望这篇文章能帮到你,有问题欢迎留言讨论!