3893 字
19 分钟
让宝塔邮局服务实现“自愈”与“主动告警”
2025-10-07

让宝塔邮局服务实现“自愈”与“主动告警”#

你是否也在使用宝塔面板自建的邮局服务?你是否也曾被 SMTP 服务偶尔无响应 的“玄学”问题所困扰?明明面板显示服务运行中,Webmail 却无法发信,必须手动重启一系列服务才能恢复。本文将带你从诊断到解决,最终实现一个能自我修复、并主动通过 Telegram 向你汇报的自动化运维方案。

一、问题的症状:令人困惑的“假死”#

很多使用宝塔邮局的朋友都可能遇到过以下情况:

  1. SMTP 服务突然不可用:你的 Webmail 客户端(如 Roundcube 或 Rainloop)在发送邮件时,提示“连接服务器失败”或“SMTP 认证失败”。
  2. 面板状态“正常”:你立刻登录宝塔面板查看,发现 PostfixDovecot 等服务都显示为“运行中”,没有任何停止的迹象。
  3. 重启大法好:你尝试手动重启 PostfixOpendkimDovecot 这三个服务,之后一切又恢复了正常。
  4. 问题复发:然而,过了一段时间(几小时或几天),问题又会再次出现。

这个现象的核心在于,服务进程本身没有崩溃(所以宝塔认为它在运行),但其负责处理网络连接的子进程却陷入了**“假死”状态**,不再接受新的连接请求。

二、诊断思路:从现象到本质#

经过一系列排查(包括检查内存、Swap、防火墙、SELinux),我们最终通过一个决定性的观察锁定了问题的根源:

手动重启时,必须重启 Dovecot 服务,Postfix (SMTP) 才能恢复正常。

这揭示了宝塔邮局环境下,服务之间存在一个强认证依赖链客户端 -> Postfix (SMTP) -> Dovecot (SASL认证)

Postfix 在处理用户发信请求时,需要向 Dovecot 的认证服务验证用户密码。如果 Dovecot 的认证模块出现问题或与 Postfix 的通信管道中断,Postfix 负责处理连接的子进程就会被“卡住”,从而导致整个 SMTP 服务对外表现为“无响应”。

因此,我们的解决方案必须围绕确保 DovecotPostfix 之间的通信健康来构建。

三、解决方案:打造一个“看门狗”自愈脚本#

既然重启是目前最有效的恢复手段,我们就让服务器自己来完成这个任务。我们将创建一个 Shell 脚本,通过宝塔的“计划任务”功能,让它成为一个 7x24 小时不间断的“看门狗”(Watchdog)。

这个脚本将实现:

  1. 精准诊断:使用 openssl 命令,完美模拟真实客户端,对需要 SSL/TLS 加密的 SMTP 端口进行连接测试。
  2. 智能重启:一旦发现连接失败,就严格按照我们验证过的、最有效的顺序 (Postfix -> Opendkim -> Dovecot) 重启服务。
  3. 主动告警:在完成修复操作后,通过 Telegram Bot 发送一条通知到你的手机,让你对服务器的状态了如指掌。

步骤1:准备 Telegram Bot#

你需要一个 Telegram Bot 的 Token 和你自己的 Chat ID

  1. 在 Telegram 中与 @BotFather 对话,发送 /newbot 命令创建机器人,获取 Token
  2. 与你创建的机器人对话,然后访问 https://api.telegram.org/bot<你的Token>/getUpdates,在返回的 JSON 数据中找到你的 Chat ID

步骤2:创建自愈脚本#

通过 SSH 登录你的服务器,创建一个脚本文件。推荐路径:/root/scripts/watchdog_mail_services.sh

Terminal window
# 在SSH中执行:
mkdir -p /root/scripts
nano /root/scripts/watchdog_mail_services.sh

然后,将下面的完整脚本内容粘贴进去:

#!/bin/bash
# ==============================================================================
# 邮件服务健康检查与自愈脚本 v1.0
#
# 功能:
# 1. 周期性(由cron定义)检查服务状态。
# 2. 如果服务失败,执行完整的“告警-修复-验证-报告”流程。
# 3. 【v1.0 核心】只要服务未恢复,每个cron周期都会重复完整的修复尝试,
# 实现“持久重试”,直到服务恢复为止。
# ==============================================================================
# --- 在这里配置你的 Telegram 信息 ---
BOT_TOKEN="在这里填入你的Bot_Token"
CHAT_ID="在这里填入你的Chat_ID"
# --- 配置结束 ---
# --- 高级配置 ---
# 日志文件
LOG_FILE="/var/log/mail_watchdog.log"
# 锁文件
LOCK_FILE="/var/run/mail_watchdog.pid"
# 状态文件
STATE_FILE="/tmp/mail_service_is_down.state"
# 冷却时间(秒)。【重要】设置为略小于你的cron周期。
# 例如,cron是20分钟(1200秒)一次,这里设置为19分钟(1140秒)。
# 这确保了每个cron周期都能触发一次新的尝试。
COOLDOWN_PERIOD=1140
# 连接测试超时(秒)
CONNECT_TIMEOUT=10
# 重启后等待多长时间(秒)再进行最终验证。2分钟 = 120秒
RESTART_WAIT_PERIOD=120
# --- 配置结束 ---
# 定义要测试的端口和服务
HOST="127.0.0.1"
PORT="465"
# 记录日志的函数
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
# 发送Telegram通知的函数
send_telegram_notification() {
local message_type=$1 # "PROBLEM", "RECOVERY", or "ESCALATION"
local MESSAGE=""
if [ "$message_type" == "PROBLEM" ]; then
MESSAGE="🚨 **邮件服务告警** 🚨
**主机:** $(hostname)
**时间:** $(date '+%Y-%m-%d %H:%M:%S')
**事件:** 检测到 SMTP 服务 (端口 ${PORT}) 无响应。
**操作:** 正在尝试自动重启服务,请稍候...
"
elif [ "$message_type" == "RECOVERY" ]; then
MESSAGE="✅ **邮件服务恢复** ✅
**主机:** $(hostname)
**时间:** $(date '+%Y-%m-%d %H:%M:%S')
**事件:** SMTP 服务 (端口 ${PORT}) 已恢复正常。
"
elif [ "$message_type" == "ESCALATION" ]; then
MESSAGE="🔥 **告警升级:自动修复失败** 🔥
**主机:** $(hostname)
**时间:** $(date '+%Y-%m-%d %H:%M:%S')
**事件:** 自动重启邮件服务后,SMTP (端口 ${PORT}) **仍然无响应**。
**后续:** 将在下一个周期 (${COOLDOWN_PERIOD}秒后) 再次尝试修复。
"
fi
curl --connect-timeout 15 -s -X POST "https://api.telegram.org/bot${BOT_TOKEN}/sendMessage" \
-d chat_id="${CHAT_ID}" \
-d text="${MESSAGE}" \
-d parse_mode="Markdown" > /dev/null
}
# 封装连接测试为一个函数
check_smtp_connection() {
echo "QUIT" | timeout ${CONNECT_TIMEOUT}s openssl s_client -connect ${HOST}:${PORT} -brief -ign_eof > /dev/null 2>&1
return $?
}
# --- 脚本主逻辑 ---
# 1. 锁机制
if [ -e "$LOCK_FILE" ] && kill -0 "$(cat "$LOCK_FILE")" 2>/dev/null; then
log_message "另一个实例正在运行。退出。"
exit 1
fi
echo $$ > "$LOCK_FILE"
trap 'rm -f "$LOCK_FILE"' EXIT
# 2. 执行首次连接检查
check_smtp_connection
CONNECT_STATUS=$?
if [ ${CONNECT_STATUS} -ne 0 ]; then
# 如果首次检查失败
# 3. 检查冷却期,这可以防止脚本被意外地在短时间内(小于cron周期)频繁触发
if [ -e "$STATE_FILE" ]; then
LAST_FAILURE_TIME=$(stat -c %Y "$STATE_FILE")
CURRENT_TIME=$(date +%s)
if [ $((CURRENT_TIME - LAST_FAILURE_TIME)) -lt ${COOLDOWN_PERIOD} ]; then
log_message "SMTP on port ${PORT} is DOWN, but within the retry interval. Waiting for the next scheduled cron run."
exit 0
fi
fi
# 4. 【持久重试核心流程】
log_message "SMTP on port ${PORT} is DOWN. Initiating automated recovery protocol..."
touch "$STATE_FILE" # 更新故障时间戳,启动一个新的20分钟周期
# a. 发送初始告警
log_message "Sending PROBLEM notification..."
send_telegram_notification "PROBLEM"
# b. 按顺序重启服务
log_message "Restarting Postfix..."
/bin/systemctl restart postfix
/bin/sleep 3
log_message "Restarting Opendkim..."
/bin/systemctl restart opendkim
/bin/sleep 3
log_message "Restarting Dovecot..."
/bin/systemctl restart dovecot
# c. 等待服务完全启动
log_message "Service restart sequence completed. Waiting ${RESTART_WAIT_PERIOD}s for services to fully initialize..."
sleep ${RESTART_WAIT_PERIOD}
# d. 执行最终验证检查
log_message "Performing post-restart validation check..."
check_smtp_connection
if [ $? -eq 0 ]; then
# e. 如果恢复成功
log_message "SUCCESS: Service has recovered after restart."
send_telegram_notification "RECOVERY"
rm -f "$STATE_FILE" # 清除故障状态,停止重试循环
else
# f. 如果仍然失败
log_message "CRITICAL: Service FAILED to recover after restart. Will retry on the next cycle."
send_telegram_notification "ESCALATION"
# 保留STATE_FILE,但它的时间戳已被更新。下一个cron周期将再次触发此流程。
fi
else
# 如果首次检查成功
if [ -e "$STATE_FILE" ]; then
# 如果状态文件存在,说明这是从一次或多次失败尝试中恢复过来的
log_message "SMTP on port ${PORT} is UP. Service has RECOVERED since last check."
send_telegram_notification "RECOVERY"
rm -f "$STATE_FILE"
else
# 服务一直正常,静默处理
:
fi
fi
exit 0

注意:粘贴完代码后,请务必修改脚本文件顶部的 BOT_TOKENCHAT_ID 为你自己的真实信息。然后按 Ctrl+X -> Y -> Enter 保存退出。

对于绝大多数用户来说,只需要关心和修改这三个地方:*

  1. BOT_TOKEN=""
    • 修改为BOT_TOKEN="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" (换成你自己的 Bot Token)
  2. CHAT_ID=""
    • 修改为CHAT_ID="123456789" (换成你自己的 Chat ID)
  3. COOLDOWN_PERIOD=1140
    • 根据您的 cron 周期来调整。这个设置是脚本能否按预期进行“持久重试”的关键。
    • 计算公式(你的 cron 周期分钟数 * 60) - (30到60秒的缓冲)
    • 常见示例
      • cron 每 5 分钟 (*/5 * * * *): (5 * 60) - 60 = 240。设置 COOLDOWN_PERIOD=240

      • cron 每 10 分钟 (*/10 * * * *): (10 * 60) - 60 = 540。设置 COOLDOWN_PERIOD=540

      • cron 每 20 分钟 (*/20 * * * *): (20 * 60) - 60 = 1140。默认的 1140 正好适用。

      • cron 每 30 分钟 (*/30 * * * *): (30 * 60) - 60 = 1740。设置 COOLDOWN_PERIOD=1740

其他高级配置 (CONNECT_TIMEOUT, RESTART_WAIT_PERIOD 等) 在绝大多数情况下,使用默认值就已经非常好了,无需改动它们。

最后,给脚本赋予执行权限:

Terminal window
chmod +x /root/scripts/watchdog_mail_services.sh

步骤3:设置宝塔计划任务#

  1. 登录宝塔面板,进入 计划任务 页面。
  2. 任务类型: 选择 Shell脚本
  3. 任务名称: 填写一个有意义的名字,如 邮件服务自愈与通知
  4. 执行周期: 选择 N分钟,推荐设置为 2030
  5. 脚本内容: 填入脚本的绝对路径 /root/scripts/watchdog_mail_services.sh
  6. 点击 添加任务

四、最终效果#

从现在起,你的服务器就拥有了一个不知疲倦的自动化运维助理。

  • 自动巡检:每隔20分钟,它就会检查一次 SMTP 服务的健康状况。
  • 自动修复:一旦发现服务无响应,它会立即执行你设定的重启流程。
  • 主动汇报:完成修复后,你的 Telegram 会收到一条类似这样的消息:

🚨 邮件服务告警 🚨

主机: mail 时间: 2025-10-07 17:28:29

事件: 检测到 SMTP 服务 (端口 465) 无响应。 操作: 已自动按顺序重启 Postfix, Opendkim, Dovecot。

请尽快通过诊断页面检查服务是否已恢复正常。

五、场景模拟#

基本设定:

  • 您的 crontab 设置为每20分钟运行一次脚本,分别在 xx:00, xx:20, xx:40 执行。
  • 脚本中的 RESTART_WAIT_PERIOD 设置为 120 秒(2分钟)。
  • 脚本中的 COOLDOWN_PERIOD 设置为 1140 秒(19分钟)。

场景一:一切正常 (The ‘All is Well’ Scenario)#

状态: 您的邮件服务一直正常运行。

  • 10:00 - cron 任务启动脚本。

    1. 连接测试: 脚本执行 check_smtp_connection,连接 127.0.0.1:465 成功。
    2. 决策过程:
      • CONNECT_STATUS 为 0,进入 else (连接成功) 分支。
      • 脚本检查 /tmp/mail_service_is_down.state 文件是否存在。
      • 文件不存在。
      • 脚本判断为“服务一直正常”,执行 : (空操作),然后安静退出。
    3. 最终结果: 无任何操作,无任何通知。 脚本完美地完成了它的静默监控任务。
  • 10:20, 10:40, … - 只要服务正常,每次运行结果都和 10:00 完全一样。


场景二:服务临时故障,一次修复成功#

状态:09:59 时,邮件服务因未知原因卡死。

  • 10:00 - cron 任务启动脚本。

    1. 连接测试: 连接失败。CONNECT_STATUS 非 0。
    2. 决策过程:
      • 进入 if (连接失败) 分支。
      • 检查 STATE_FILE,文件不存在,说明这是新发现的故障。
      • 脚本创建 STATE_FILE,并更新其时间戳为 10:00
      • 发送 【🚨 邮件服务告警】 通知。内容是:“检测到服务无响应,正在尝试自动重启…”
      • 依次重启 Postfix, Opendkim, Dovecot。
      • 执行 sleep 120,脚本暂停等待,此时真实时间已来到 10:02 左右
      • 执行第二次连接测试(最终验证)。这次服务已恢复,连接成功!
      • 进入“验证成功”的逻辑分支。
      • 发送 【✅ 邮件服务恢复】 通知。内容是:“SMTP 服务已恢复正常。”
      • 删除 STATE_FILE 文件。
      • 脚本执行完毕,退出。
    3. 最终结果: 您在大约 10:02 左右,先后收到了“告警”和“恢复”两条通知。问题在脚本的一次运行周期内被发现并解决。
  • 10:20 - cron 再次启动脚本。

    • 此时服务是正常的,STATE_FILE 也已被删除。因此,这次运行会和场景一完全一样,脚本会静默退出。

场景三:顽固性故障,脚本持续尝试修复#

状态: 某个配置文件损坏,导致服务重启后也无法正常工作。

  • 10:00 - cron 任务启动脚本。

    1. 连接测试: 连接失败。
    2. 决策过程 (与场景二前半部分相同):
      • 发现是新故障,创建 STATE_FILE
      • 发送 【🚨 邮件服务告警】 通知。
      • 重启所有服务。
      • sleep 120,等待到 10:02 左右。
      • 执行第二次连接测试。由于配置问题,服务依然无法启动,连接再次失败!
      • 进入“验证失败”的逻辑分支 (else)。
      • 发送 【🔥 告警升级:自动修复失败】 通知。内容提示:“自动重启后服务仍然无响应,将在下个周期再次尝试。”
      • 保留 STATE_FILE 文件。
      • 脚本退出。
    3. 最终结果: 您在 10:02 左右收到了“告警”和“升级”两条通知。您知道自动修复失败了。
  • 10:20 - cron 再次启动脚本。

    1. 连接测试: 连接依然失败。
    2. 决策过程:
      • 进入 if (连接失败) 分支。
      • 检查 STATE_FILE,文件存在。
      • 检查文件的时间戳 (10:00) 和当前时间 (10:20) 的差距。10:20 - 10:00 = 20分钟
      • 20分钟 (1200秒) > COOLDOWN_PERIOD (1140秒),条件满足,脚本继续执行修复
      • 脚本更新 STATE_FILE 的时间戳为 10:20
      • 重复 10:00 的完整修复流程:发送“告警” -> 重启服务 -> 等待2分钟 -> 验证失败 -> 发送“升级”通知。
    3. 最终结果:10:22 左右,您再次收到了一套“告警”和“升级”通知。脚本在坚持不懈地尝试。
  • 10:40, 11:00, … - 这个循环会一直持续下去,每20分钟提醒您一次问题依然存在,并不断尝试修复。


场景四:顽固性故障期间,人工介入修复#

状态: 延续场景三,脚本一直在每20分钟尝试修复。您在夜里醒来,看到了多条告警。

  • 03:30 - 您登录服务器,发现是 postfix 的一个配置写错了。您修复了它,并手动 systemctl restart postfix。现在服务恢复正常了。

  • 03:40 - cron 再次启动脚本。

    1. 连接测试: 连接成功!
    2. 决策过程:
      • CONNECT_STATUS 为 0,进入 else (连接成功) 分支。
      • 脚本检查 STATE_FILE 是否存在。它发现文件还存在! (是上一次 03:20 失败时留下的)。
      • 脚本的逻辑是:“连接成功了,但还留着故障记录,这说明服务刚刚从故障中恢复。”
      • 发送 【✅ 邮件服务恢复】 通知。
      • 删除 STATE_FILE
      • 脚本退出。
    3. 最终结果: 脚本智能地识别出服务已经恢复,发送了最终的“恢复”通知,并清除了故障标记,使整个监控系统回归正常。
  • 04:00 - cron 再次启动脚本。

    • 现在服务正常,STATE_FILE 已删除。脚本将再次进入场景一的静默监控模式。

通过这四个场景的模拟,您可以看到脚本设计得相当周全,能够优雅地处理从“一切正常”到“简单故障”再到“顽固故障”以及“人工干预”的各种情况,并始终提供清晰、及时的状态反馈。

结语#

如果执行脚本报错概率是需要把脚本文件里所有的 Windows 换行符 (CRLF) 转换成 Linux 的换行符 (LF)

执行

sed -i 's/\r$//' /root/scripts/watchdog_mail_services.sh
  • 解释:这个命令会在你的脚本文件里 (-i) 查找 (s/) 每一行结尾 ($) 的回车符 (\r),并把它替换成空 (//)。
让宝塔邮局服务实现“自愈”与“主动告警”
https://blog.wlens.top/posts/让宝塔邮局服务实现自愈与主动告警/
作者
Lao Wang
发布于
2025-10-07
许可协议
CC BY-NC-SA 4.0