服务半夜挂了没人知道,我用 journalctl 加个小脚本搭了个土法监控
上周二早上,同事在群里说网站打不开了。我上去一看,服务是凌晨三点多停的,停了整整五个小时,没人知道。服务器本身好好的,SSH 能连,磁盘也没满,就是那个服务进程没了。
这台机器没有上任何正经监控,之前一直觉得“小服务,没必要”。那天之后我改主意了——监控可以土,但不能没有。
先搞清楚它是怎么死的
服务是用 systemd 管的,第一步当然是看日志:
journalctl -u myapp.service --since "yesterday" --until "today" | tail -50
输出里最后几行是这样的:进程先是报了几次数据库连接超时,然后直接退出了,systemd 尝试重启了三次,三次都因为同一个连接错误秒退,最后放弃了。
再往前翻,数据库超时不是突然出现的:
# 只看错误级别,省得翻几千行
journalctl -u myapp.service --since "2026-11-02 00:00" -p err
凌晨一点左右,连接超时的报错开始零星出现,两点多越来越密,三点多彻底连不上,进程退出。也就是说服务不是猝死,是先抽搐了两个小时才咽气。
数据库那头也查了一下,是云数据库那边的连接数打满了——另一个新上线的服务没配连接池上限,把连接数吃光了。这属于别人的锅,但我的服务连重试都没有,直接退了,这就是我自己的问题。
journalctl 的几个用法,这次真用上了
以前查日志我都是 tail /var/log/syslog,这次被迫把 journalctl 用熟了,记几个实用的:
# 实时跟日志,跟 tail -f 一个意思
journalctl -u myapp.service -f
# 按时间段查,定位故障窗口很好用
journalctl -u myapp.service --since "2026-11-02 01:00" --until "2026-11-02 04:00"
# 只看某个级别以上的
journalctl -u myapp.service -p warning
# 看某次启动之后的日志(-b 是本次启动,-b -1 是上一次启动)
journalctl -u myapp.service -b -1
还有个坑:默认 journalctl 的日志是存在内存里的(/run/log/journal),机器一重启,旧日志就没了。这次能查到凌晨的记录,是因为这台机器没重启过。想让日志落盘,得改个配置:
sudo mkdir -p /var/log/journal
# 然后重启 systemd-journald,或者直接重启机器
sudo systemctl restart systemd-journald
建了 /var/log/journal 这个目录之后,journald 会自动把日志写到磁盘上。这个我之前一直没配,纯粹是运气好才没丢日志。另外可以用 journalctl --disk-usage 看日志占了多少盘,配个 SystemMaxUse=500M 之类的上限,免得日志反过来把磁盘吃满——日志吃磁盘这事,我前几天刚在另一台机器上见过一次。
顺手修了服务的“死法”
光知道死因不够,还得让它下次别死得这么干脆。两处改动:
第一,systemd 的重启策略原来是 Restart=on-failure,配了 StartLimitBurst=3,三次失败就彻底放弃。改成这样:
# /etc/systemd/system/myapp.service
[Service]
Restart=always
RestartSec=10
StartLimitIntervalSec=300
StartLimitBurst=10
十分钟内允许重启十次,每次间隔十秒。数据库那边只是连接数打满,抖一抖就恢复了,服务多坚持几次就能自己活过来。改完记得 systemctl daemon-reload 再 systemctl restart myapp.service。
第二,应用本身加了连接重试。原来是连不上直接抛异常退出,改成带退避的重试:连不上就等 2 秒、4 秒、8 秒这样指数退避,最多重试十次。这点改动不大,但区别就是“抖一下就过去”和“半夜挂掉等天亮”。
土法监控:挂了至少要有人知道
服务能自愈是好事,但自愈不代表不用管。万一哪天是真的起不来了,还是得有人知道。我没上 Prometheus——就一台小机器,不值得。写了个几十行的 shell 脚本,cron 每五分钟跑一次:
#!/bin/bash
# /usr/local/bin/check_myapp.sh
SERVICE="myapp.service"
LOG="/var/log/myapp-check.log"
if ! systemctl is-active --quiet $SERVICE; then
echo "$(date '+%F %T') $SERVICE 不活跃,尝试重启" >> $LOG
systemctl restart $SERVICE
sleep 10
if systemctl is-active --quiet $SERVICE; then
echo "$(date '+%F %T') 重启成功" >> $LOG
else
echo "$(date '+%F %T') 重启失败,需要人工处理" >> $LOG
# 发告警:这里用的是企业微信机器人 webhook
curl -s -X POST "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY" \
-H 'Content-Type: application/json' \
-d '{"msgtype":"text","text":{"content":"告警:myapp 服务挂了,自动重启失败,请处理"}}' > /dev/null
fi
fi
# 顺带看一眼磁盘,别再重蹈覆辙
USED=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$USED" -gt 80 ]; then
echo "$(date '+%F %T') 磁盘使用率 ${USED}%,超过 80%" >> $LOG
fi
cron 里加一行:
*/5 * * * * /usr/local/bin/check_myapp.sh
脚本第一次跑就踩了个坑:cron 环境里 systemctl 不在 PATH 里。交互式 shell 下 systemctl 在 /usr/bin/systemctl,cron 的 PATH 默认只有 /usr/bin:/bin,其实是有的——真正的问题是我脚本里用了 df / | awk ...,这都没问题,出问题的是 curl 发告警那行在 cron 下静默失败了。查了半天发现是 webhook 的 key 我贴错了一个字符。教训:凡是 cron 里跑的脚本,先在命令行手动跑一遍,再用 env -i /bin/bash /usr/local/bin/check_myapp.sh 模拟干净环境跑一遍,两个都过了再进 crontab。
另外脚本里发告警用的 webhook key 是直接写在脚本里的,权限记得收紧:
chmod 700 /usr/local/bin/check_myapp.sh
chown root:root /usr/local/bin/check_myapp.sh
现在的状态
这套土办法跑了一周,期间服务又抖过两次,都是数据库那边连接数波动,systemd 自己重启就恢复了,脚本的日志里留了记录,我早上扫一眼就知道夜里发生过什么。
它当然比不上正经监控:没有图表,没有历史曲线,告警只有一个企业微信群消息。但对一台小服务器、一个小服务来说,够用了——核心诉求其实就两个:服务自己能扛住小故障,扛不住的时候五分钟内有人知道。
回头看,这次故障最贵的部分不是修的那两个小时,是服务悄悄挂了五小时没人知道。监控这东西,平时觉得可有可无,出一次事就明白:它不是锦上添花,是兜底。
- 点赞
- 收藏
- 关注作者
评论(0)