4、软件Watchdog实现:从原理到实战
大家好,我是老赵。今天我们来聊聊软件Watchdog的实现。说实话,很多运维同学对硬件Watchdog比较熟悉,但一提到软件Watchdog就觉得有点虚。其实,软件Watchdog用好了,一样能扛住不少问题。
我在项目中遇到过好几次这样的情况:硬件Watchdog因为主板设计问题,在某些服务器上根本不起作用。那时候,软件Watchdog就成了最后的救命稻草。所以,这一块内容,我建议你认真看看。
4.1 软件Watchdog的原理
软件Watchdog,说白了就是一段程序。它不停地在后台跑,盯着系统或者某个进程的状态。如果发现系统卡死了,或者进程挂了,它就执行预设的恢复动作——通常是重启系统或者重启服务。
它的核心机制其实很简单:
- 定时器机制:内核或者用户态程序维护一个定时器,每隔一段时间就检查一次。
- 喂狗动作:被监控的进程或者系统,需要定期向Watchdog发送“我还活着”的信号。
- 超时判定:如果超过规定时间没有收到喂狗信号,就判定为异常。
- 恢复动作:执行预设的恢复操作,比如重启、杀掉进程、触发告警。
核心要点:软件Watchdog和硬件Watchdog最大的区别在于——软件Watchdog依赖CPU和操作系统本身。如果CPU彻底死掉,软件Watchdog也救不了你。但大多数场景下,系统只是卡在某个进程上,CPU还在跑,这时候软件Watchdog就很有用了。
为什么会这样?你想想看,系统卡死通常是因为某个进程占用了太多资源,或者内核态发生了死锁。但CPU的中断处理机制还在,定时器还能触发。软件Watchdog就是利用这个窗口期来执行恢复动作的。
4.2 用户态Watchdog工具
用户态的Watchdog工具,我用的最多的是 watchdog 这个包。它在大多数Linux发行版里都能直接装。嗯,这里要注意,不同发行版的配置方式略有不同,但核心逻辑是一样的。
4.2.1 安装与基本配置
# Debian/Ubuntu
apt-get install watchdog
# CentOS/RHEL
yum install watchdog
安装完之后,配置文件在 /etc/watchdog.conf。我个人习惯先把默认配置备份一下,再改自己的配置。
# 备份默认配置
cp /etc/watchdog.conf /etc/watchdog.conf.bak
# 编辑配置
vim /etc/watchdog.conf
4.2.2 核心配置项
| 配置项 | 说明 | 我的建议值 |
|---|---|---|
watchdog-device |
指定Watchdog设备文件 | /dev/watchdog |
interval |
喂狗间隔(秒) | 10 |
timeout |
超时时间(秒) | 30 |
realtime |
是否使用实时优先级 | yes |
priority |
实时优先级值 | 1 |
repair-binary |
修复脚本路径 | /usr/local/bin/watchdog-repair.sh |
小技巧:我曾经在配置 interval 和 timeout 时吃过亏。一开始我把 interval 设成5秒,timeout 设成10秒。结果系统负载高的时候,喂狗线程被调度延迟了,导致误重启。后来我改成 interval=10,timeout=30,留出足够的余量,就再也没出过问题。
4.2.3 监控进程示例
除了监控系统整体状态,Watchdog还可以监控特定进程。比如,我想监控Nginx进程:
# 在 /etc/watchdog.conf 中添加
file = /var/run/nginx.pid
change = 300
# 或者用ping方式监控
ping = 127.0.0.1
interface = eth0
启动服务:
systemctl start watchdog
systemctl enable watchdog
4.3 内核态Watchdog模块开发基础
用户态工具虽然方便,但有些场景下不够可靠。比如,如果系统负载极高,用户态进程可能被饿死,这时候Watchdog自己都跑不起来了。所以,有时候我们需要在内核态实现Watchdog。
内核态Watchdog模块,说白了就是一个内核模块。它利用内核的定时器机制,在内核空间直接监控系统状态。这样做的好处是——即使用户态全部卡死,内核模块依然能正常工作。
4.3.1 最简单的内核Watchdog模块
我来写一个最简单的例子。这个模块会创建一个内核定时器,每隔10秒检查一次系统状态。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/timer.h>
#include <linux/reboot.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple Kernel Watchdog");
static struct timer_list watchdog_timer;
static int heartbeat_count = 0;
static int max_heartbeat = 3;
// 喂狗函数 - 由被监控的进程调用
void watchdog_feed(void)
{
heartbeat_count = 0;
printk(KERN_INFO "Watchdog: Heartbeat received\n");
}
EXPORT_SYMBOL(watchdog_feed);
// 定时器回调函数
static void watchdog_timer_callback(struct timer_list *t)
{
heartbeat_count++;
if (heartbeat_count >= max_heartbeat) {
printk(KERN_CRIT "Watchdog: System hang detected! Rebooting...\n");
// 执行系统重启
machine_restart(NULL);
} else {
printk(KERN_WARNING "Watchdog: Missed heartbeat %d/%d\n",
heartbeat_count, max_heartbeat);
}
// 重新设置定时器
mod_timer(&watchdog_timer, jiffies + msecs_to_jiffies(10000));
}
// 模块初始化
static int __init watchdog_init(void)
{
printk(KERN_INFO "Watchdog: Module loaded\n");
timer_setup(&watchdog_timer, watchdog_timer_callback, 0);
mod_timer(&watchdog_timer, jiffies + msecs_to_jiffies(10000));
return 0;
}
// 模块退出
static void __exit watchdog_exit(void)
{
del_timer(&watchdog_timer);
printk(KERN_INFO "Watchdog: Module unloaded\n");
}
module_init(watchdog_init);
module_exit(watchdog_exit);
警告:内核模块开发一定要谨慎。我曾经在测试时忘记加超时限制,结果模块加载后系统直接重启了,连日志都没来得及看。建议先在虚拟机里测试,确认逻辑没问题再上生产环境。
4.3.2 编译与加载
写好了模块代码,接下来就是编译和加载。你需要一个内核头文件包。
# 安装内核头文件
apt-get install linux-headers-$(uname -r)
# 编写Makefile
cat > Makefile << EOF
obj-m += watchdog_module.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
EOF
# 编译
make
# 加载模块
insmod watchdog_module.ko
# 查看内核日志
dmesg | tail -5
# 卸载模块
rmmod watchdog_module
4.3.3 用户态程序调用内核喂狗
内核模块写好了,用户态程序怎么调用它呢?可以通过 /proc 或者 /sys 文件系统来通信。我习惯用 /proc,因为更简单。
// 用户态喂狗程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd;
char buffer[1] = {0};
// 打开proc文件
fd = open("/proc/watchdog_feed", O_WRONLY);
if (fd < 0) {
perror("Failed to open watchdog feed");
return 1;
}
while (1) {
// 喂狗
write(fd, buffer, 1);
printf("Watchdog fed\n");
// 每隔8秒喂一次
sleep(8);
}
close(fd);
return 0;
}
对应的内核模块需要添加 /proc 接口:
// 在模块初始化中添加
static struct proc_dir_entry *proc_entry;
static ssize_t proc_write(struct file *file, const char __user *buffer,
size_t count, loff_t *pos)
{
watchdog_feed();
return count;
}
static const struct proc_ops proc_fops = {
.proc_write = proc_write,
};
// 在 watchdog_init 中添加
proc_entry = proc_create("watchdog_feed", 0666, NULL, &proc_fops);
4.4 实战经验总结
做了这么多年运维,我总结了几条关于软件Watchdog的经验:
- 不要过度依赖软件Watchdog:它只能处理部分场景。CPU彻底死掉、内核panic等情况,软件Watchdog也无能为力。
- 喂狗间隔要留余量:我一般把喂狗间隔设为超时时间的1/3。比如超时30秒,就每10秒喂一次。
- 监控链不要过长:我曾经见过一个项目,Watchdog监控A进程,A进程监控B进程,B进程监控C进程...结果某个环节卡住,整个链都断了。尽量保持监控链扁平化。
- 日志一定要记录:不管是用户态还是内核态,Watchdog触发的动作一定要记日志。不然系统重启了,你都不知道为什么。
避坑指南:我曾经在一个项目中,用软件Watchdog监控数据库进程。结果数据库在做全量备份时,CPU占用率飙到100%,喂狗线程被饿死了,Watchdog判定异常,直接重启了系统。备份做到一半就断了,恢复起来特别麻烦。后来我加了负载判断——只有在CPU负载低于某个阈值时,才执行重启动作。这个坑,希望大家别踩。
好了,软件Watchdog的内容就讲到这里。记住,工具是死的,人是活的。根据你的实际场景来配置,才是最好的方案。
可可精品资料库