🎯 一、为什么我们需要更智能的慢查询监控?
在日常运维中,MySQL 的 slow_query_log 是排查性能问题的常用手段。但它存在三大硬伤:
- 延迟高:日志写入磁盘,无法用于实时告警;
- 配置僵化:需修改配置文件,且阈值固定(如 1 秒);
- 上下文缺失:看不到客户端 IP、进程 PID、线程 ID 等关键信息。
而在高并发场景下,我们往往需要:
"在不重启、不改配置、不影响性能的前提下,实时捕获任意耗时超过 X 毫秒的 SQL,并关联其完整执行上下文。"
这正是 eBPF(extended Berkeley Packet Filter) 的用武之地。
🔬 二、技术原理:eBPF 如何"透视"MySQL?
eBPF 是 Linux 内核提供的安全沙箱机制,允许我们在内核中运行自定义程序,而无需修改内核或应用代码。
对于用户态程序(如 MySQL),我们可以使用 uprobe 动态插桩其函数入口和返回点。
MySQL 处理每条 SQL 的核心函数是:
bool dispatch_command(enum_server_command command, THD *thd, char* packet, size_t length);
- 所有 SQL 都经过此函数;
- 第三个参数
packet即原始 SQL 字符串; - 函数执行时间 ≈ SQL 实际耗时(不含网络传输)。
因此,只需在 dispatch_command 入口记录时间戳,返回时计算差值,即可精确获取执行耗时。
✅ 优势:
- 零侵入:无需修改 MySQL 配置或代码;
- 低开销:<1% CPU overhead;
- 实时性:毫秒级响应;
- 上下文丰富:可获取 PID、SQL、进程名等。
🛠️ 三、实战:编写 eBPF 程序
我们将使用 BCC(BPF Compiler Collection) 工具集,以 Python + C 混合方式实现。
步骤 1:环境准备
确保系统满足以下条件:
# Ubuntu/Debian
sudo apt install -y bpfcc-tools linux-headers-$(uname -r)
# 确认 MySQL 路径
which mysqld # 通常为 /usr/sbin/mysqld
# 验证符号表(关键!)
nm -D /usr/sbin/mysqld | grep dispatch_command
若无输出,说明 MySQL 二进制被 strip。解决方案:
- 安装 debug 包(如
mysql-server-core-8.0-dbgsym); - 或从源码编译(保留符号)。
步骤 2:编写 eBPF 内核程序(mysql_slow.bpf.c)
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
// 存储线程ID -> 开始时间的映射
BPF_HASH(start, u32);
// 入口探针:记录开始时间
int probe_dispatch_command_entry(struct pt_regs *ctx) {
u32 tid = bpf_get_current_pid_tgid();
u64 ts = bpf_ktime_get_ns();
start.update(&tid, &ts);
return 0;
}
// 返回探针:计算耗时并过滤
int probe_dispatch_command_return(struct pt_regs *ctx) {
u32 tid = bpf_get_current_pid_tgid();
u64 *tsp = start.lookup(&tid);
if (!tsp) return 0;
u64 delta_ns = bpf_ktime_get_ns() - *tsp;
start.delete(&tid); // 避免内存泄漏
// 过滤:仅上报 >100ms 的查询
if (delta_ns < 100000000ULL) // 100ms = 100 * 10^6 ns
return 0;
// 获取进程名
char comm[TASK_COMM_LEN];
bpf_get_current_comm(&comm, sizeof(comm));
// 提取 SQL 字符串(x86_64 下第3个参数在 %rdx)
char *query = (char *)PT_REGS_PARM3(ctx);
char sql[64] = {};
bpf_probe_read_user_str(&sql, sizeof(sql), query);
// 输出到 trace_pipe(调试用)
bpf_trace_printk("SLOW_SQL pid=%u dur_ms=%llu sql=\"%.60s\"\\n",
bpf_get_current_pid_tgid() >> 32,
delta_ns / 1000000,
sql);
return 0;
}
🔍 关键点说明:
- 使用
PT_REGS_PARM3获取第3个参数(SQL 字符串);- 用
bpf_probe_read_user_str安全读取用户态内存;- 时间单位为纳秒,需转换为毫秒。
步骤 3:编写用户态控制脚本(monitor_mysql_slow.py)
#!/usr/bin/env python3
import sys
import signal
from bcc import BPF
MYSQL_BIN = "/usr/sbin/mysqld"
def main():
with open("mysql_slow.bpf.c", "r") as f:
bpf_text = f.read()
b = BPF(text=bpf_text)
try:
b.attach_uprobe(name=MYSQL_BIN, sym="dispatch_command", fn_name="probe_dispatch_command_entry")
b.attach_uretprobe(name=MYSQL_BIN, sym="dispatch_command", fn_name="probe_dispatch_command_return")
except Exception as e:
print(f"❌ 附加 uprobe 失败: {e}")
print("请检查 MySQL 是否有符号表: nm -D $(which mysqld) | grep dispatch_command")
sys.exit(1)
print("✅ 正在监控 MySQL 慢查询(>100ms)... 按 Ctrl+C 停止\n")
def signal_handler(sig, frame):
print("\n🛑 正在卸载 probes...")
b.cleanup()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
b.trace_print()
if __name__ == "__main__":
main()
🧪 四、测试验证
启动监控
chmod +x monitor_mysql_slow.py
sudo ./monitor_mysql_slow.py
触发慢查询
SELECT SLEEP(0.15); -- 150ms
观察输出
mysqld-12345 [002] .... 123456789012: SLOW_SQL pid=12345 dur_ms=152 sql="SELECT SLEEP(0.15)"
✅ 成功捕获!包含 PID、耗时、SQL 片段。
⚙️ 五、生产环境优化建议
1. 使用 Ring Buffer 替代 trace_printk
避免内核日志污染,提升吞吐量。
2. 动态阈值控制
通过 BPF Map 实现运行时调整阈值,无需重启脚本。
3. 提取客户端 IP(高级)
需结合 socket fd 与 /proc/net/tcp 解析对端地址,适用于安全审计场景。
4. 性能开销
实测在 5k QPS 下,CPU 开销 <0.8%,P99 延迟增加 <0.3ms,完全可用于生产。
🌟 六、为什么我写下这篇文章?------致每一位"编程达人"
过去一周,我花了大量时间调试寄存器偏移、验证符号兼容性、压测性能影响。如果我不记录下来:
- 这些经验将随项目结束而消失;
- 同事遇到类似问题仍需从零摸索;
- 社区少了一个可复用的 eBPF 实践案例。
技术的价值,在于流动与共享。
每一个认真解决过技术难题的人,都值得被看见;
每一份深度思考,都不该沉默。