公司的数据库服务器在纯内网,安全策略不允许它主动连外网,但企业微信机器人的 webhook 必须公网可达——告警链路天然是断的。机器出事没人知道等于裸奔,这周把整条链路搭通了:内网巡检脚本用 ncat 把告警打到应用服务器的 8899 端口,应用服务器监听转发企微,再配一个每日巡检机器人做全量体检。全是最朴素的工具,没有任何新增中间件。文中所有 key、域名、手机号均已脱敏。
整体架构
- 内网 DB 服务器:db_monitor.sh 每分钟 cron 巡检(系统层 + MySQL 内部 + 慢 SQL 自动 kill),异常通过 ncat 发往应用服务器 8899 端口
- 外网应用服务器:alert_handler.sh 常驻监听 8899,收到一行转发一条企微;service_alarm.sh 每分钟巡检本机(进程/磁盘/业务接口探测),直接推企微
- 巡检机器人:每日定时全量体检报告,包括短信验证码这种业务级链路巡检
为什么用 ncat 打端口而不是 SSH 拉取或消息队列?应用服务器本来就部署在内网可达的位置(业务需要),DB 侧只要往 8899 发一行文本就完事,不用建任何中间件、不用开放新的管理通道,暴露面最小,安全评审一次过。
内网 DB 巡检脚本
配置区集中在头部,阈值改起来不用翻代码:
REPORT_HOST="192.168.10.6" # 应用服务器
REPORT_PORT="8899"
CPU_THRESHOLD=85
MEM_THRESHOLD=85
DISK_THRESHOLD=90
SLOW_SEC=60 # 慢SQL告警阈值
KILL_SEC=600 # 超过10分钟自动kill
LOCK_FILE="/tmp/db_monitor.lock"
LOCK_EXPIRE=30 # 防抖30秒
巡检项:CPU/内存 85%、磁盘(/、/data、/boot)90%、mysqld 进程存活;MySQL 内部用 --defaults-extra-file=/root/.my.cnf 免密连接(配置文件 600 权限,密码不进脚本),检查连接数(阈值 750)、innodb_trx 锁等待、主从延迟(阈值 30 秒)。核心是慢 SQL 这段:
SLOW_SQL_LIST=$(${MYSQL_BIN} --defaults-extra-file=/root/.my.cnf -N -e \
"SELECT ID,TIME,INFO FROM information_schema.processlist \
WHERE command NOT IN ('Sleep','Daemon') \
AND STATE NOT LIKE 'Killed%' AND time > ${SLOW_SEC};")
LONG_SQL=$(echo "${SLOW_SQL_LIST}" | awk 'NF {count++} END {print count+0}')
if [[ -n "${LONG_SQL}" && "${LONG_SQL}" -gt 0 ]];then
DB_ALERT+="\n⚠️【DB服务器】慢SQL(>${SLOW_SEC}s): ${LONG_SQL}个"
while read -r SQL_ID SQL_TIME SQL_TEXT; do
[[ -z "${SQL_ID}" ]] && continue
DB_ALERT+="\n👉线程ID:${SQL_ID} 执行时长:${SQL_TIME}s"
DB_ALERT+="\nSQL语句:${SQL_TEXT}"
# 超过10分钟,自动kill
if [[ ${SQL_TIME} -ge ${KILL_SEC} ]];then
${MYSQL_BIN} --defaults-extra-file=/root/.my.cnf -N -e "KILL ${SQL_ID};"
DB_ALERT+="\n✅已自动Kill线程ID:${SQL_ID},执行超过${KILL_SEC}秒"
fi
done <<< "${SLOW_SQL_LIST}"
fi
两个真实的坑,都值得记一笔:
- wc -l 统计空结果是 1:慢 SQL 列表为空时
echo "$LIST" | wc -l返回 1(空字符串带换行),导致空告警刷屏。换成awk 'NF {count++} END {print count+0}'按非空行统计,空结果老老实实返回 0 - STATE NOT LIKE 'Killed%':不加这个过滤,刚被 kill 的会话(状态 Killed)下一轮还会被统计进来,重复告警
慢 SQL 分级是这套设计的核心分寸:超 60 秒先告警不动作(可能是正常的大查询),超 600 秒才自动 kill——这个阈值放得足够宽,正常业务碰不到,敢自动动手的前提是"误杀概率趋近于零"。每次 kill 都带线程 ID、执行时长和 SQL 原文进告警,事后可追责可优化。
上报:单行文本 + ncat
有个格式问题必须处理:企微 markdown 消息是 JSON,换行必须是转义后的 \n 字符串;而 ncat 管道传输又是按行的。解法是把整段告警的物理换行统一替换成字面 \n,拼成单行再发送:
RAW_FULL="【数据库服务器异常告警】
> ⏰ $(date '+%Y-%m-%d %H:%M:%S')${ALERT_CONTENT}"
# 物理换行替换成字符串\n,输出变成单行
FINAL_MSG=$(echo "${RAW_FULL}" | sed ':a;N;$!ba;s/\n/\\n/g')
echo "${FINAL_MSG}" | ncat ${REPORT_HOST} ${REPORT_PORT}
防抖用文件锁:每次运行写时间戳,30 秒内的重复触发直接退出,配合每分钟的 cron,最坏情况一分钟一条,告警风暴压得住。
脚本里还做了一项上报链路自检:每次巡检都 ncat -zv 探测一次应用服务器 8899 的连通性,不通就在告警内容里追加"上报链路故障"。诚实地说这条有个逻辑缺陷——链路都断了,这条告警自己也发不出去,它只对间歇性抖动有意义。真正的兜底是应用侧的接收日志:哪个时间段没有收到任何心跳类记录,就该怀疑链路而不是机器。
应用服务器:监听转发 + 本机巡检
监听端 alert_handler.sh,用 ncat -lk 8899 常驻(systemd 托管,挂了自动拉起),逐行读取、落日志、转发企微,三件事:
#!/bin/bash
WEBHOOK="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxx"
LOG_FILE="/home/hd/receive_db_alert.log"
while read -r line
do
if [ -n "$line" ];then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 收到消息: $line" >> "${LOG_FILE}"
curl -s "${WEBHOOK}" -H "Content-Type:application/json" \
-d "{"msgtype":"markdown","markdown":{"content":"$line"}}" >> "${LOG_FILE}" 2>&1
fi
done
收到即转发、先记日志再推送,这样"内网有没有发现异常"和"企微有没有送达"两条证据链分开可查。
本机巡检端 service_alarm.sh,crontab 每分钟跑一次(*/1 * * * * /home/hd/service_alarm.sh >> service_alarm.log 2>&1),同样 30 秒防抖。除了 CPU/内存/磁盘(/、/resources、/boot,数据盘 /data 单独策略)和 nginx/java 进程存活,最有价值的是业务接口探测——进程活着不代表服务能用。容错分三档:
HTTP_CODE=$(curl -s -o "${TMP_BODY}" -w "%{http_code}" --max-time ${API_TIMEOUT} \
-X POST -H "Content-Type: application/json" \
-b "${WAF_COOKIE}" -d "${API_POST_DATA}" "${API_URL}")
if [ "${HTTP_CODE}" = "0" ];then
API_ERR="🔴 业务接口 超时${API_TIMEOUT}s,无响应"
elif [ "${HTTP_CODE}" != "200" ];then
API_ERR="🔴 业务接口 HTTP返回${HTTP_CODE}\n完整响应:\n$(cat ${TMP_BODY} | sed 's/"/\\"/g')"
else
# 无 jq 的环境,grep 抓业务字段判断接口是否真的活着
TOTAL_SIZE=$(sed -e 's/\r//g' -e '1s/^\xEF\xBB\xBF//' "${TMP_BODY}" \
| grep -oE '"totalSize":[0-9]+' | grep -oE '[0-9]+')
[[ -z "${TOTAL_SIZE}" ]] && API_ERR="🔴 业务接口 获取totalSize失败\n完整响应:..."
fi
HTTP 200 不代表业务正常(网关可能返回 200 + 错误 JSON),所以必须再抓一个业务字段做断言。顺手的两个细节:CentOS7 没 jq,grep -oE 照样够用;响应体先清 BOM 头和 Windows 换行 \r,不然正则抓不到,这个坑排查了半小时。
巡检机器人:每日体检报告
告警是被动的(出事才知道),巡检是主动的(没事也每天看一眼)。每天定时推一份全量报告到群里:负载、内存、TOP3 资源进程、nginx/java 存活、df -h 全量 + 超阈值高亮。最得意的设计是短信验证码链路巡检——"收不到验证码"是最影响用户的第一故障,而且平时不触发根本不知道它挂了:
# 1. 先删测试手机号的旧key,防止上轮残留干扰判断
${REDIS_CLI} -h ${REDIS_HOST} -p ${REDIS_PORT} -a "${REDIS_PWD}" \
DEL "AUTH-CODE-${TEST_PHONE}" "AUTH-PHONE-${TEST_PHONE}" >/dev/null 2>&1
sleep 1
# 2. 调真实发送接口(必须是闲置测试手机号!)
SMS_RESP=$(curl -s --connect-timeout 10 -X POST "https://srm.example.com/.../getLoginVcode?account=xxx" ...)
API_OK=$(echo "$SMS_RESP" | awk -F'"success":' '{print $2}' | awk -F',' '{print $1}' | tr -d ' ')
# 3. 查Redis里验证码key是否生成(EXISTS先判空,防integer expression报错)
REDIS_KEY_EXIST=$(${REDIS_CLI} ... EXISTS "AUTH-CODE-${TEST_PHONE}" 2>/dev/null)
[ -z "${REDIS_KEY_EXIST}" ] && REDIS_KEY_EXIST=0
if [ "${API_OK}" = "true" ] && [ "${REDIS_KEY_EXIST}" -eq 1 ];then
echo "- ✅ 短信验证码链路正常(接口成功 && Redis key已生成)" >> "$TMP"
else
echo "- ❌ 短信验证码链路异常!接口返回:${SMS_RESP} | RedisKey存在标识:${REDIS_KEY_EXIST}" >> "$TMP"
fi
这一个巡检把 API 网关、业务服务、短信通道、Redis 四个环节全测了,比单独探活每个组件真实得多。两个注意点:测试手机号必须用闲置号,验证码发到真实用户手机上是事故;Redis 的 EXISTS 结果一定要先判空再比较,Redis 连不上时变量为空,直接 -eq 会报 integer expression expected 并且结果不可信。
安全清单(血泪教训)
- MySQL 密码放 --defaults-extra-file(600 权限),不写进脚本、不进 git
- 企微 webhook key、WAF cookie 这类凭据,写进博客/文档前一律脱敏,泄露过的 key 直接作废重建——本文里的 key 已重置
- 巡检用的测试手机号、测试账号单独建,和真实业务隔离
- ncat 监听端口收到的内容直接拼进 JSON 转发,端口务必只对内网 DB 开放,防注入
小结
整套东西数下来:cron、ncat、curl、awk、sed,三个互相独立的脚本,任意一个挂了不影响另外两个,没有引入任何新组件。DB 侧只往一个端口发一行文本,应用侧收一行转发一条。链路上每多一个组件就多一个会挂的东西,能用现有通道解决的,就别加依赖。

Comments 6 条评论
good~
用 ncat 打端口不加中间件,思路很轻
每个组件都是一个会挂的点,说得对
慢 SQL 自动 kill 会不会误杀
三个脚本互相独立这点设计得好
lock 文件防重复执行这个细节好