4、软件Watchdog实现:从原理到实战

大家好,我是老赵。今天我们来聊聊软件Watchdog的实现。说实话,很多运维同学对硬件Watchdog比较熟悉,但一提到软件Watchdog就觉得有点虚。其实,软件Watchdog用好了,一样能扛住不少问题。

我在项目中遇到过好几次这样的情况:硬件Watchdog因为主板设计问题,在某些服务器上根本不起作用。那时候,软件Watchdog就成了最后的救命稻草。所以,这一块内容,我建议你认真看看。

4.1 软件Watchdog的原理

软件Watchdog,说白了就是一段程序。它不停地在后台跑,盯着系统或者某个进程的状态。如果发现系统卡死了,或者进程挂了,它就执行预设的恢复动作——通常是重启系统或者重启服务。

它的核心机制其实很简单:

  • 定时器机制:内核或者用户态程序维护一个定时器,每隔一段时间就检查一次。
  • 喂狗动作:被监控的进程或者系统,需要定期向Watchdog发送“我还活着”的信号。
  • 超时判定:如果超过规定时间没有收到喂狗信号,就判定为异常。
  • 恢复动作:执行预设的恢复操作,比如重启、杀掉进程、触发告警。

核心要点:软件Watchdog和硬件Watchdog最大的区别在于——软件Watchdog依赖CPU和操作系统本身。如果CPU彻底死掉,软件Watchdog也救不了你。但大多数场景下,系统只是卡在某个进程上,CPU还在跑,这时候软件Watchdog就很有用了。

为什么会这样?你想想看,系统卡死通常是因为某个进程占用了太多资源,或者内核态发生了死锁。但CPU的中断处理机制还在,定时器还能触发。软件Watchdog就是利用这个窗口期来执行恢复动作的。

软件Watchdog工作原理流程图 定时器模块 每N秒触发一次检查 喂狗信号 进程定期发送“我还活着” 超时判定 超过阈值未收到信号 正常:继续监控 异常:执行恢复 恢复动作 • 重启系统 • 重启进程 • 触发告警 软件Watchdog依赖CPU和操作系统,无法处理CPU完全死掉的场景

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的内容就讲到这里。记住,工具是死的,人是活的。根据你的实际场景来配置,才是最好的方案。


可可精品资料库