PHP-FPM的内核级监控:利用Syscall Tracing分析阻塞I/O的等待时间分布

PHP-FPM 的内核级监控:利用 Syscall Tracing 分析阻塞 I/O 的等待时间分布

大家好,今天我们来聊聊如何利用 Syscall Tracing 技术来深入监控 PHP-FPM 的 I/O 性能,特别是阻塞 I/O 的等待时间分布。这对于优化 PHP 应用的性能瓶颈至关重要。

为什么需要内核级监控?

传统的 PHP 性能分析工具,如 Xdebug、xhprof 等,主要关注 PHP 代码的执行时间,可以帮助我们找到慢函数和性能瓶颈。然而,它们对于 I/O 操作的细节往往不够深入,无法精确地分析 I/O 等待时间。

PHP 应用的性能瓶颈经常出现在 I/O 操作上,例如:

  • 数据库查询: 连接数据库、发送查询请求、接收查询结果都需要时间。
  • 文件操作: 读取/写入文件,尤其是网络文件系统上的文件。
  • 网络请求: 向外部 API 发起 HTTP 请求。
  • Redis/Memcached: 访问缓存服务器。

这些 I/O 操作通常会阻塞 PHP-FPM 进程,导致请求处理时间延长。要精确地分析这些 I/O 等待时间,我们需要更底层的监控手段,也就是内核级监控。

Syscall Tracing 简介

Syscall Tracing 是一种强大的内核级调试技术,它可以跟踪进程执行过程中所有的系统调用。系统调用是用户空间程序与操作系统内核交互的唯一方式。通过跟踪系统调用,我们可以了解进程在内核中做了什么,花费了多少时间。

常见的 Syscall Tracing 工具包括:

  • strace: 简单易用,可以打印出进程执行的系统调用和参数。
  • perf: Linux 内核自带的性能分析工具,可以用于跟踪系统调用、CPU 采样、内存分析等。
  • SystemTap: 一种脚本语言,可以编写复杂的内核探针,进行更深入的性能分析。
  • eBPF (Extended Berkeley Packet Filter): 一种基于内核的虚拟机,可以安全高效地执行用户定义的代码,用于跟踪系统调用、网络包等。

在本文中,我们将主要使用 perfeBPF 来分析 PHP-FPM 的 I/O 等待时间。

使用 perf 跟踪 I/O 系统调用

perf 是一个功能强大的性能分析工具,可以用于跟踪系统调用。我们可以使用 perf record 命令来记录进程的系统调用,然后使用 perf report 命令来查看报告。

以下是一个使用 perf 跟踪 PHP-FPM 的示例:

  1. 找到 PHP-FPM 的进程 ID (PID):

    ps aux | grep php-fpm

    通常会看到多个 php-fpm 进程,我们需要选择 worker 进程的 PID。

  2. 使用 perf record 记录系统调用:

    sudo perf record -e syscalls:sys_enter_* -p <php-fpm-worker-pid> -g -- sleep 30
    • -e syscalls:sys_enter_*: 指定要跟踪的事件,这里是所有系统调用的入口。
    • -p <php-fpm-worker-pid>: 指定要跟踪的进程 ID。
    • -g: 记录调用栈信息。
    • -- sleep 30: 指定记录的时间,这里是 30 秒。
  3. 使用 perf report 查看报告:

    sudo perf report -g --stdio

    perf report 会生成一个交互式的报告,我们可以通过上下键选择函数,左右键展开/折叠调用栈。我们可以找到与 I/O 相关的系统调用,例如 readwriterecvsend 等,并查看它们的调用栈和执行时间。

    示例 perf report 输出 (部分):

    Samples  Percent   Command      Shared Object         Symbol
    -------  -------  -------------  --------------------  -----------------------------------------------------
    10       10.00%   php-fpm       [kernel.kallsyms]     read
             |
             ---read
                php-fpm
                zend_stream_read
                ... (PHP code callstack)
    
    5        5.00%    php-fpm       [kernel.kallsyms]     recvfrom
             |
             ---recvfrom
                php-fpm
                php_socket_recv
                ... (PHP code callstack)
    

    这个报告显示了 read 系统调用占用了 10% 的采样,recvfrom 系统调用占用了 5% 的采样。 通过展开调用栈,我们可以找到 PHP 代码中调用这些 I/O 操作的位置。

分析 perf 报告的局限性:

虽然 perf 可以帮助我们找到 I/O 相关的系统调用,但它也存在一些局限性:

  • 采样: perf 默认使用采样的方式来收集数据,这意味着它可能会错过一些短时间的系统调用。
  • 聚合: perf report 只能显示系统调用的总体情况,无法提供单个系统调用的详细信息,例如等待时间。
  • 内核符号: perf 需要内核符号才能正确地解析调用栈,这可能需要安装额外的软件包。

使用 eBPF 进行更精细的 I/O 监控

eBPF 是一种更强大的内核级监控技术,它可以让我们编写自定义的内核探针,进行更精细的性能分析。eBPF 程序运行在内核中,可以安全高效地访问内核数据,而无需修改内核代码。

我们将使用 bcc (BPF Compiler Collection) 工具包来编写 eBPF 程序。bcc 提供了一系列 Python 脚本,可以方便地编写和运行 eBPF 程序。

环境准备:

首先,需要安装 bcc 工具包。具体的安装方法可以参考 bcc 的官方文档:https://github.com/iovisor/bcc

安装完成后,我们需要安装内核头文件,以便 bcc 可以访问内核数据。

sudo apt-get install linux-headers-$(uname -r)

编写 eBPF 程序:

我们将编写一个 eBPF 程序来跟踪 readwrite 系统调用,并记录它们的等待时间。

#!/usr/bin/env python3

from bcc import BPF
import time

# 定义 eBPF 程序
program = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>

struct data_t {
    u64 ts;
    u32 pid;
    int rw; // 0: read, 1: write
    char comm[TASK_COMM_LEN];
};

BPF_PERF_OUTPUT(events);

BPF_HASH(start, u32, u64);

// read/write 系统调用入口
int kprobe__vfs_read(struct pt_regs *ctx, struct file *file, char *buf, size_t count, loff_t *pos) {
    u32 pid = bpf_get_current_pid_tgid();
    u64 ts = bpf_ktime_get_ns();

    start.update(&pid, &ts);
    return 0;
}

int kprobe__vfs_write(struct pt_regs *ctx, struct file *file, char *buf, size_t count, loff_t *pos) {
    u32 pid = bpf_get_current_pid_tgid();
    u64 ts = bpf_ktime_get_ns();

    start.update(&pid, &ts);
    return 0;
}

// read/write 系统调用返回
int kretprobe__vfs_read(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid();
    u64 *tsp = start.lookup(&pid);
    if (tsp == NULL) {
      return 0;
    }

    u64 ts = bpf_ktime_get_ns();
    u64 delta = ts - *tsp;

    struct data_t data = {};
    data.ts = ts;
    data.pid = pid;
    data.rw = 0;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));

    events.perf_submit(ctx, &data, sizeof(data));
    start.delete(&pid);
    return 0;
}

int kretprobe__vfs_write(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid();
    u64 *tsp = start.lookup(&pid);
    if (tsp == NULL) {
      return 0;
    }

    u64 ts = bpf_ktime_get_ns();
    u64 delta = ts - *tsp;

    struct data_t data = {};
    data.ts = ts;
    data.pid = pid;
    data.rw = 1;
    bpf_get_current_comm(&data.comm, sizeof(data.comm));

    events.perf_submit(ctx, &data, sizeof(data));
    start.delete(&pid);
    return 0;
}
"""

# 加载 eBPF 程序
b = BPF(text=program)

# 定义事件处理函数
def print_event(cpu, data, size):
    event = b["events"].event(data)
    print(f"{time.strftime('%H:%M:%S')} PID: {event.pid} ({event.comm.decode()}) RW: {'read' if event.rw == 0 else 'write'} ")

# 绑定事件处理函数
b["events"].open_perf_buffer(print_event)

# 循环读取事件
while True:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

代码解释:

  • BPF_PERF_OUTPUT(events) 定义一个名为 events 的 Perf 输出缓冲区,用于将数据从内核空间传递到用户空间。
  • BPF_HASH(start, u32, u64) 定义一个哈希表 start,用于存储每个进程的系统调用开始时间。Key 是进程 ID (PID),Value 是时间戳 (u64)。
  • kprobe__vfs_readkprobe__vfs_write 定义了两个 kprobe,分别用于跟踪 vfs_readvfs_write 系统调用的入口。当这两个系统调用被调用时,eBPF 程序会记录当前进程的 PID 和时间戳,并将其存储到 start 哈希表中。
  • kretprobe__vfs_readkretprobe__vfs_write 定义了两个 kretprobe,分别用于跟踪 vfs_readvfs_write 系统调用的返回。当这两个系统调用返回时,eBPF 程序会从 start 哈希表中读取该进程的开始时间戳,计算等待时间,并将数据发送到 events Perf 输出缓冲区。
  • print_event 函数: 定义了一个 Python 函数,用于处理从 events Perf 输出缓冲区接收到的数据。该函数会将数据格式化并打印到控制台。
  • b["events"].open_perf_buffer(print_event)print_event 函数绑定到 events Perf 输出缓冲区。当 eBPF 程序将数据发送到 events 时,print_event 函数会被自动调用。
  • b.perf_buffer_poll() 循环读取 events Perf 输出缓冲区,并将数据传递给 print_event 函数。

运行 eBPF 程序:

将上述代码保存为 rw_latency.py,然后使用 sudo 权限运行它:

sudo python3 rw_latency.py

输出结果:

运行后,程序会不断地打印出 readwrite 系统调用的信息,包括时间戳、PID、进程名、等待时间。

14:30:00 PID: 1234 (php-fpm) RW: read
14:30:01 PID: 1234 (php-fpm) RW: write
14:30:02 PID: 5678 (php-fpm) RW: read

分析 eBPF 程序的输出:

通过分析 eBPF 程序的输出,我们可以了解 PHP-FPM 进程在 readwrite 系统调用上花费的时间。我们可以根据 PID 和进程名过滤输出,找到特定的 PHP-FPM 进程,并分析它的 I/O 等待时间。

更复杂的 eBPF 程序:

上述 eBPF 程序只是一个简单的示例。我们可以编写更复杂的 eBPF 程序来跟踪更多的 I/O 系统调用,例如 recvsendopenclose 等。我们还可以使用 eBPF 程序来统计 I/O 等待时间的分布,并将结果可视化。

分析 I/O 等待时间分布

为了更好地理解 I/O 等待时间的分布,我们可以使用 Python 的 matplotlib 库将数据可视化。

  1. 修改 eBPF 程序,将数据存储到文件中:

    #!/usr/bin/env python3
    
    from bcc import BPF
    import time
    import sys
    
    # 定义 eBPF 程序
    program = """
    #include <uapi/linux/ptrace.h>
    #include <linux/sched.h>
    
    struct data_t {
        u64 ts;
        u32 pid;
        int rw; // 0: read, 1: write
        char comm[TASK_COMM_LEN];
    };
    
    BPF_PERF_OUTPUT(events);
    
    BPF_HASH(start, u32, u64);
    
    // read/write 系统调用入口
    int kprobe__vfs_read(struct pt_regs *ctx, struct file *file, char *buf, size_t count, loff_t *pos) {
        u32 pid = bpf_get_current_pid_tgid();
        u64 ts = bpf_ktime_get_ns();
    
        start.update(&pid, &ts);
        return 0;
    }
    
    int kprobe__vfs_write(struct pt_regs *ctx, struct file *file, char *buf, size_t count, loff_t *pos) {
        u32 pid = bpf_get_current_pid_tgid();
        u64 ts = bpf_ktime_get_ns();
    
        start.update(&pid, &ts);
        return 0;
    }
    
    // read/write 系统调用返回
    int kretprobe__vfs_read(struct pt_regs *ctx) {
        u32 pid = bpf_get_current_pid_tgid();
        u64 *tsp = start.lookup(&pid);
        if (tsp == NULL) {
          return 0;
        }
    
        u64 ts = bpf_ktime_get_ns();
        u64 delta = ts - *tsp;
    
        struct data_t data = {};
        data.ts = ts;
        data.pid = pid;
        data.rw = 0;
        bpf_get_current_comm(&data.comm, sizeof(data.comm));
    
        events.perf_submit(ctx, &data, sizeof(data));
        start.delete(&pid);
        return 0;
    }
    
    int kretprobe__vfs_write(struct pt_regs *ctx) {
        u32 pid = bpf_get_current_pid_tgid();
        u64 *tsp = start.lookup(&pid);
        if (tsp == NULL) {
          return 0;
        }
    
        u64 ts = bpf_ktime_get_ns();
        u64 delta = ts - *tsp;
    
        struct data_t data = {};
        data.ts = ts;
        data.pid = pid;
        data.rw = 1;
        bpf_get_current_comm(&data.comm, sizeof(data.comm));
    
        events.perf_submit(ctx, &data, sizeof(data));
        start.delete(&pid);
        return 0;
    }
    """
    
    # 加载 eBPF 程序
    b = BPF(text=program)
    
    # 定义输出文件
    output_file = open("io_latency.txt", "w")
    
    # 定义事件处理函数
    def print_event(cpu, data, size):
        event = b["events"].event(data)
        output_file.write(f"{event.pid},{event.comm.decode()},{event.rw}n") # Simplified output
    
    # 绑定事件处理函数
    b["events"].open_perf_buffer(print_event)
    
    # 循环读取事件
    try:
        while True:
            b.perf_buffer_poll()
    except KeyboardInterrupt:
        pass  # Let the file closing happen
    
    finally:  # Always close the file.
        output_file.close()
        print("Data collection finished. Data written to io_latency.txt")

    这个版本将数据写入 io_latency.txt 文件,格式为 PID,进程名,读写标志

  2. 编写 Python 脚本,读取数据并绘制直方图:

    import matplotlib.pyplot as plt
    import pandas as pd
    
    # 读取数据
    try:
        df = pd.read_csv("io_latency.txt", header=None, names=["PID", "Comm", "RW"])
    except FileNotFoundError:
        print("io_latency.txt not found.  Make sure the eBPF script was run.")
        exit(1)
    
    # 过滤 PHP-FPM 进程
    df_php = df[df["Comm"].str.contains("php-fpm")]
    
    #统计读写次数
    read_count = len(df_php[df_php["RW"] == 0])
    write_count = len(df_php[df_php["RW"] == 1])
    
    print(f"Read count: {read_count}")
    print(f"Write count: {write_count}")
    
    # 创建直方图
    plt.figure(figsize=(10, 6))
    df_php['RW'].hist(bins=2, grid=False) # More appropriate number of bins
    plt.xticks([0, 1], ['Read', 'Write'])  # Setting x-axis labels
    plt.xlabel("Operation Type")
    plt.ylabel("Frequency")
    plt.title("Distribution of Read/Write Operations in PHP-FPM")
    
    # 显示直方图
    plt.show()

    这个脚本使用 pandas 库读取数据,并使用 matplotlib 库绘制直方图,显示 readwrite 操作的次数分布。

代码解释:

  • pd.read_csv("io_latency.txt", ...) 使用 pandas 库读取 io_latency.txt 文件,并将数据存储到 DataFrame 中。
  • df[df["Comm"].str.contains("php-fpm")] 使用 pandas 库过滤 DataFrame,只保留进程名包含 "php-fpm" 的数据。
  • plt.hist(df_php["Latency"], ...) 使用 matplotlib 库绘制直方图,显示 I/O 等待时间的分布。
  • plt.xticks 用来设置x轴的刻度标签。
  • plt.grid(False): 关闭网格,使图形更简洁。

运行 Python 脚本:

将上述代码保存为 plot_io_latency.py,然后运行它:

python3 plot_io_latency.py

运行后,程序会显示一个直方图,显示 PHP-FPM 进程的 I/O 等待时间分布。

通过分析直方图,我们可以了解 I/O 等待时间的主要范围,并找到需要优化的 I/O 操作。

优化建议

通过 Syscall Tracing 分析,我们可以找到 PHP-FPM 的 I/O 瓶颈,并采取相应的优化措施。以下是一些常见的优化建议:

  • 数据库查询优化:
    • 使用索引加速查询。
    • 避免全表扫描。
    • 优化 SQL 语句。
    • 使用连接池减少数据库连接的开销。
  • 文件操作优化:
    • 使用缓存减少文件读取次数。
    • 避免频繁的小文件读写。
    • 使用异步 I/O。
  • 网络请求优化:
    • 使用连接池减少连接建立的开销。
    • 使用压缩减少数据传输量。
    • 使用异步请求。
    • 检查网络延迟。
  • 缓存优化:
    • 合理设置缓存过期时间。
    • 避免缓存雪崩。
    • 使用多级缓存。
  • 使用高性能存储介质: 例如 SSD 硬盘,以降低 I/O 延迟。
  • 调整 PHP-FPM 配置: 例如增加 worker 进程的数量,以提高并发处理能力。

总结本次分享的内容

通过 Syscall Tracing 技术,我们可以深入了解 PHP-FPM 的 I/O 性能瓶颈,包括阻塞 I/O 的等待时间分布。借助 perfeBPF 等工具,我们可以精确地跟踪系统调用,并将数据可视化,从而找到需要优化的 I/O 操作,进而提升 PHP 应用的性能。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注