概述 @

systemtap是一个强大的动态跟踪工具,能够深入Linux内核和用户空间程序,实时监控系统行为和性能特征。与coredump(事后分析)和auditd(偏向安全审计)不同,systemtap提供了一种主动式、低开销的监控方法,可以在不中断服务的情况下,持续监控系统状态,特别适合排查那些难以复现或间歇性出现的崩溃问题。

在复杂的分布式系统中,systemtap能够帮助工程师在生产环境中进行"无损调试",捕获导致服务崩溃的微小异常和竞争条件,为问题排查提供前所未有的深度和灵活性。

安装指南 @

使用systemtap需要确保内核支持必要的调试特性:

# 检查内核是否支持所需特性
cat /boot/config-$(uname -r) | grep -E "CONFIG_DEBUG_INFO|CONFIG_KPROBES|CONFIG_DEBUG_FS|CONFIG_RELAY"
# 所有选项都应该显示为y或m

如果缺少这些配置,需要重新编译内核或安装支持这些特性的内核版本

安装必要的开发包和调试信息:

# CentOS/RHEL系统
yum install gcc kernel-devel-$(uname -r) kernel-debuginfo-$(uname -r) kernel-debuginfo-common-$(uname -m)-$(uname -r) -y
# 或从官方仓库下载对应版本的debuginfo包
# http://debuginfo.centos.org/$(rpm -E %rhel)/x86_64/

# Ubuntu/Debian系统
apt-get install gcc linux-headers-$(uname -r) -y
# 安装调试信息包
echo "deb http://ddebs.ubuntu.com $(lsb_release -cs) main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list.d/ddebs.list
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 428D7C01
apt-get update
apt-get install linux-image-$(uname -r)-dbgsym -y

安装systemtap工具:

# CentOS/RHEL系统
yum install systemtap systemtap-runtime -y

# Ubuntu/Debian系统
apt-get install systemtap systemtap-sdt-dev -y

# manjaro
yay -S systemtap

# 验证安装是否成功
stap -v -e 'probe vfs.read { exit() }'

安装完成后,可以通过以下命令验证安装是否成功:

sudo stap -v -e 'probe begin { printf("Systemtap 安装成功!\n"); exit() }'

Systemtap实战脚本 @

以下是几个经过实战验证的、用于服务监控和崩溃排查的实用systemtap脚本,这些脚本可以直接在生产环境中使用或根据具体需求进行定制:

1. 进程IO监控脚本(增强版) @
#!/usr/bin/stap
// 监控进程IO活动,可用于排查IO导致的服务崩溃

global reads, writes, start_time
start_time = gettimeofday_s()

// 监控读操作
probe vfs.read.return {
    if (bytes_read > 0) {
        reads[execname(), pid(), uid()] += bytes_read
    }
}

// 监控写操作
probe vfs.write.return {
    if (bytes_written > 0) {
        writes[execname(), pid(), uid()] += bytes_written
    }
}

// 每5秒打印一次top 10 IO进程
probe timer.s(5) {
    now = gettimeofday_s()
    elapsed = now - start_time
    printf("\n===== IO统计报告 (过去%d秒) =====\n", 5)
    printf("%16s\t%8s\t%8s\t%12s\t%12s\t%12s\n", "进程名", "PID", "UID", "读取(KB)", "写入(KB)", "总IO(KB)")
    
    // 计算总IO并排序输出
    foreach ([name, pid, uid] in reads-) {
        total_read = reads[name, pid, uid]/1024
        total_write = writes[name, pid, uid]/1024
        total_io = total_read + total_write
        if (total_io > 0) {
            printf("%16s\t%8d\t%8d\t%12d\t%12d\t%12d\n", name, pid, uid, total_read, total_write, total_io)
        }
    }
    
    // 清除统计数据,准备下一轮监控
    delete reads
    delete writes
}

// 捕获SIGINT信号,优雅退出
probe signal.handle(SIGINT) {
    printf("\n监控已停止,共运行%d秒\n", gettimeofday_s() - start_time)
    exit()
}

使用方法:

stap io_monitor.stp
# 或保存为文件后执行
chmod +x io_monitor.stp
./io_monitor.stp
2. 进程崩溃监控脚本 @
#!/usr/bin/stap
// 监控进程异常终止,记录信号和堆栈信息

global crash_count

// 监控进程收到致命信号
probe signal.send {
    if (sig_pid && sig_name != "SIGCHLD") {
        // 只关注可能导致崩溃的信号
        if (sig_name ~ /SIGSEGV|SIGABRT|SIGFPE|SIGILL|SIGBUS|SIGTRAP/) {
            crash_count[execname(), sig_pid, sig_name]++
            printf("[%s] 进程 %s (PID: %d) 收到致命信号 %s\n", 
                   ctime(gettimeofday_s()), execname(), sig_pid, sig_name)
            printf("  发送者: %s (PID: %d)\n", cmdline_str(), pid())
            printf("  当前工作目录: %s\n", cwd())
            printf("  进程命令行: %s\n", cmdline_str(sig_pid))
            printf("\n")
        }
    }
}

// 每60秒打印一次统计报告
probe timer.s(60) {
    printf("\n===== 进程崩溃统计报告 =====\n")
    if (@count(crash_count) > 0) {
        printf("%16s\t%8s\t%10s\t%8s\n", "进程名", "PID", "信号", "次数")
        foreach ([name, pid, sig] in crash_count+) {
            printf("%16s\t%8d\t%10s\t%8d\n", name, pid, sig, crash_count[name, pid, sig])
        }
    } else {
        printf("在过去60秒内没有检测到进程崩溃事件\n")
    }
    printf("============================\n\n")
}
3. 系统调用监控脚本 @
#!/usr/bin/stap
// 监控特定进程的系统调用,用于排查异常行为

probe begin {
    printf("开始监控系统调用,按Ctrl+C停止...\n")
    printf("%10s\t%12s\t%10s\t%8s\t%s\n", "时间", "进程名", "PID", "系统调用", "结果")
}

// 监控所有系统调用
probe syscall.* {
    // 可以添加过滤条件,比如只监控特定进程
    // if (execname() == "nginx" || pid() == target_pid) {
        printf("%10d\t%12s\t%10d\t%8s\t", 
               gettimeofday_s(), execname(), pid(), ppfunc())
    // }
}

probe syscall.*.return {
    // 只打印执行时间超过100ms的系统调用
    if (gettimeofday_us() - @entry(gettimeofday_us()) > 100000) {
        printf("%10d\t%12s\t%10d\t%8s\t返回值: %d (耗时: %dms)\n", 
               gettimeofday_s(), execname(), pid(), ppfunc(), 
               returnval(), (gettimeofday_us() - @entry(gettimeofday_us()))/1000)
    }
}

probe end {
    printf("监控已停止\n")
}

4. systemtap排查服务崩溃的实际案例 @

当遇到难以定位的服务崩溃问题时,可以结合多个脚本进行综合分析:

# 场景:Nginx服务间歇性崩溃,但没有coredump文件

# 1. 首先运行崩溃监控脚本
stap crash_monitor.stp &

# 2. 同时运行IO监控脚本,检查是否存在IO问题
stap io_monitor.stp &

# 3. 针对Nginx进程运行系统调用监控
stap -e 'probe syscall.* { if (execname() == "nginx") { printf("%s\t%s\t%d\n", ctime(gettimeofday_s()), ppfunc(), pid()); } }'

# 4. 监控内存分配和释放
stap -e 'global alloc, free
probe process("/usr/sbin/nginx").function("malloc") { alloc[tid()] += $size }
probe process("/usr/sbin/nginx").function("free") { free[tid()] += $ptr }
probe timer.s(10) { printf("当前内存分配: %d bytes\n", @sum(alloc) - @sum(free)) }'

# 5. 当服务再次崩溃时,分析收集到的信息,定位问题根源