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): 一种基于内核的虚拟机,可以安全高效地执行用户定义的代码,用于跟踪系统调用、网络包等。
在本文中,我们将主要使用 perf 和 eBPF 来分析 PHP-FPM 的 I/O 等待时间。
使用 perf 跟踪 I/O 系统调用
perf 是一个功能强大的性能分析工具,可以用于跟踪系统调用。我们可以使用 perf record 命令来记录进程的系统调用,然后使用 perf report 命令来查看报告。
以下是一个使用 perf 跟踪 PHP-FPM 的示例:
-
找到 PHP-FPM 的进程 ID (PID):
ps aux | grep php-fpm通常会看到多个 php-fpm 进程,我们需要选择 worker 进程的 PID。
-
使用
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 秒。
-
使用
perf report查看报告:sudo perf report -g --stdioperf report会生成一个交互式的报告,我们可以通过上下键选择函数,左右键展开/折叠调用栈。我们可以找到与 I/O 相关的系统调用,例如read、write、recv、send等,并查看它们的调用栈和执行时间。示例
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 程序来跟踪 read 和 write 系统调用,并记录它们的等待时间。
#!/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_read和kprobe__vfs_write: 定义了两个 kprobe,分别用于跟踪vfs_read和vfs_write系统调用的入口。当这两个系统调用被调用时,eBPF 程序会记录当前进程的 PID 和时间戳,并将其存储到start哈希表中。kretprobe__vfs_read和kretprobe__vfs_write: 定义了两个 kretprobe,分别用于跟踪vfs_read和vfs_write系统调用的返回。当这两个系统调用返回时,eBPF 程序会从start哈希表中读取该进程的开始时间戳,计算等待时间,并将数据发送到eventsPerf 输出缓冲区。print_event函数: 定义了一个 Python 函数,用于处理从eventsPerf 输出缓冲区接收到的数据。该函数会将数据格式化并打印到控制台。b["events"].open_perf_buffer(print_event): 将print_event函数绑定到eventsPerf 输出缓冲区。当 eBPF 程序将数据发送到events时,print_event函数会被自动调用。b.perf_buffer_poll(): 循环读取eventsPerf 输出缓冲区,并将数据传递给print_event函数。
运行 eBPF 程序:
将上述代码保存为 rw_latency.py,然后使用 sudo 权限运行它:
sudo python3 rw_latency.py
输出结果:
运行后,程序会不断地打印出 read 和 write 系统调用的信息,包括时间戳、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 进程在 read 和 write 系统调用上花费的时间。我们可以根据 PID 和进程名过滤输出,找到特定的 PHP-FPM 进程,并分析它的 I/O 等待时间。
更复杂的 eBPF 程序:
上述 eBPF 程序只是一个简单的示例。我们可以编写更复杂的 eBPF 程序来跟踪更多的 I/O 系统调用,例如 recv、send、open、close 等。我们还可以使用 eBPF 程序来统计 I/O 等待时间的分布,并将结果可视化。
分析 I/O 等待时间分布
为了更好地理解 I/O 等待时间的分布,我们可以使用 Python 的 matplotlib 库将数据可视化。
-
修改 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,进程名,读写标志。 -
编写 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库绘制直方图,显示read和write操作的次数分布。
代码解释:
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 的等待时间分布。借助 perf 和 eBPF 等工具,我们可以精确地跟踪系统调用,并将数据可视化,从而找到需要优化的 I/O 操作,进而提升 PHP 应用的性能。