前言
感谢各位小伙伴对我们eBPF专题系列文章的持续关注与支持!在上一篇eBPF技术文章中,我们探讨了如何手写一个Kprobe函数来观测MySQL的网络流量。许多热心的小伙伴纷纷私信我们,希望我们可以分享更多eBPF在数据库领域的应用场景。
为了满足大家的期待,我们特别推出该系列第四篇纯技术分享文章——如何手码一个Kprobe函数来分析MySQL数据库表维度的磁盘IO。我们希望通过这篇文章,为大家提供更深入的eBPF技术分析和实用的操作指南。同时,我们也将持续更新eBPF实战系列文章,敬请关注我们的公众号,获取更多精彩内容!
MySQL从5.6版本开始默认是独立表空间,Innodb每个表或者索引对应一个文件。那么我们可以基于Kprobe来探测表文件的读写。首先我们需要理解MySQL是如何进行读写,下图是数据库表文件IO的读写过程:
Linux中的VFS(Virtual File System,虚拟文件系统)负责管理系统中所有的文件和文件系统。VFS提供了一个统一的接口,使得不同类型的文件系统可以在Linux中无缝协作。
读写操作是一个常见的文件系统操作,它用于向文件中写入数据。当应用程序需要向文件中读写入数据时,它会向VFS发出写请求。VFS负责将这个请求传递给相应的文件系统内核模块,然后由文件系统模块负责实际的读写操作。
1)表维度的IO写入
vfs_write()函数是负责处理写操作的主要函数之一。当应用程序调用write()系统调用时,实际上是调用了vfs_write()函数,该函数负责将数据写入文件中。在调用vfs_write()函数之前,应用程序需要先打开文件,并获取到文件的文件描述符。然后,通过文件描述符就可以向vfs_write()函数传递写操作的数据和参数。
下面是Linux vfs_write()函数源码
ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos){ssize_t ret;//判断文件是否可写if (!(file->f_mode & FMODE_WRITE))return -EBADF;if (!(file->f_mode & FMODE_CAN_WRITE))return -EINVAL;if (unlikely(!access_ok(buf, count)))return -EFAULT;//写校验ret = rw_verify_area(WRITE, file, pos, count);if (ret)return ret;if (count > MAX_RW_COUNT)count = MAX_RW_COUNT;file_start_write(file);//调用文件写操作方法if (file->f_op->write)ret = file->f_op->write(file, buf, count, pos);else if (file->f_op->write_iter)ret = new_sync_write(file, buf, count, pos);elseret = -EINVAL;if (ret > 0) {fsnotify_modify(file);add_wchar(current, ret);}inc_syscw(current);file_end_write(file);return ret;}
函数的第一个形参file即为文件对象,第三个形参count是写入大小。
file对象中的f_inode成员变量是文件的元数据信息对象指针,该对象中的i_ino即为文件的inode号,该id可作为全局唯一的文件标识。
file->f_path.dentry即为该文件的dentry信息,从该指针中可分别获取文件名dentry->name(数据库的表名)以及父目录名de->d_parent->d_name.name(数据库的库名)
通过vfs_write()函数,可以方便地进行写操作,而无需关心底层文件系统的具体实现细节。VFS的设计使得Linux系统更加灵活和高效,为用户提供了方便的文件系统管理功能。
因此,我们可以选取vfs_write()函数作为探测点,来统计数据库库表维度的每秒数据写入量。
2)表维度的IO读
vfs_read()函数是负责处理读操作的主要函数之一,函数的这些参数与返回值与 vfs_write() 函数如出一辙,就不再赘述了。
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos){ssize_t ret;//判断文件是否可读if (!(file->f_mode & FMODE_READ))return -EBADF;if (!(file->f_mode & FMODE_CAN_READ))return -EINVAL;if (unlikely(!access_ok(buf, count)))return -EFAULT;//读校验ret = rw_verify_area(READ, file, pos, count);if (ret)return ret;if (count > MAX_RW_COUNT)count = MAX_RW_COUNT;//调用文件写操作方法if (file->f_op->read)ret = file->f_op->read(file, buf, count, pos);else if (file->f_op->read_iter)ret = new_sync_read(file, buf, count, pos);elseret = -EINVAL;if (ret > 0) {fsnotify_access(file);add_rchar(current, ret);}inc_syscr(current);return ret;}
我们也可以选取vfs_read()函数作为探测点,来统计数据库库表维度的每秒数据读取量。
eBPF Kprobe如何探测MySQL表维度的磁盘IO读写量?
准备一台 Linux 机器,安装好g++和bcc要实现库表维度的磁盘IO读写统计,我们首先定义一个存储结构用来存放进程库表维度读写的总Size,基于Kprobe分别对磁盘库表维度的读写量进行累加并存储到该结构中,然后每秒去读并打印当前存储结构中累加的磁盘库表读写量,即可实现每秒的库表磁盘读写的采集。
接下来我们将基于BCC,利用Kprobe写一个eBPF程序,观测MySQL库表维度的磁盘IO的读写。
a)分析内核文件系统源码相关VFS磁盘读写处理的函数
//数据库写vfsssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos){...}//数据库读vfsssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos){...}
b)导入BCC的BPF对象
//这个对象可以将我们的观测代码嵌入到观测点中执行#include <bcc/BPF.h>#include <string>#include <iostream>#include <thread>#include <time.h>
std::string strBPF = R"(#include <linux/ptrace.h>#include <bcc/proto.h>#include <linux/blkdev.h>//定义采集的指标存储结构keystruct key_t{u32 pid;u64 inode;};struct val_t{u64 reads;u64 writes;u64 rbytes;u64 wbytes;char name[32];char path[64];};//定义采集的指标存储结构valuekey_t,struct val_t,10240);BPF_HASH(flag,u32,u32,2);static inline bool isWork(){u32 key = 1;v = flag.lookup(&key);&& *v == 1) return true;return false;}static inline int do_entry(struct pt_regs *ctx, struct file *file,char __user *buf, size_t count, int is_read){u32 pid = bpf_get_current_pid_tgid() >> 32;return 0;return 0;struct key_t k = {};= pid;= file->f_inode->i_ino;struct val_t *v = map.lookup(&k);if(v){if(is_read){v->reads++;=count;else{v->writes++;=count;}else{struct val_t tv = {};struct dentry *de = file->f_path.dentry;struct qstr d_name = de->d_name;if (d_name.len == 0) return 0;sizeof(tv.name), d_name.name);bpf_probe_read_kernel(&tv.path,sizeof(tv.path),de->d_parent->d_name.name);if(is_read){tv.reads++;=count;else{tv.writes++;=count;}map.update(&k,&tv);}return 0;}int kprobe__vfs_read(struct pt_regs *ctx, struct file *file,char __user *buf, size_t count){return do_entry(ctx,file,buf,count,1);}int kprobe__vfs_write(struct pt_regs *ctx, struct file *file,char __user *buf, size_t count){return do_entry(ctx,file,buf,count,0);})";
//用于ebpf代码程序中的pid替换static std::string str_replace(std::string r, const std::string& s, const std::string& n){std::string y = std::move(r);std::string::size_type pos = 0;while((pos = y.find(s)) != std::string::npos)y.replace(pos, s.length(), n);return y;}struct io_key{u32 pid;u64 ino;};struct io_val{u64 reads;u64 writes;u64 rbytes;u64 wbytes;char name[32];char path[64];};//指定进程pid进行kprobe diskio统计int main(int argc, char* argv[]) {int pid = std::stoull(argv[1]);ebpf::BPF bpf;std::string strFilerPid = "pid != " + std::to_string(pid);std::string code = str_replace(strBPF, "FILTER_PID", strFilerPid);auto initRes = bpf.init(code);if (!initRes.ok()) {std::cerr << "bpf init error,msg: " << initRes.msg() << std::endl;return 1;}std::cout << "-----------------start to sample MySQL DiskIO (table and index read_bytes/write_bytes)-------------- " << std::endl;/*探测vfs_read*/auto attachRes = bpf.attach_kprobe("vfs_read", "kprobe__vfs_read",0,BPF_PROBE_ENTRY);if(!attachRes.ok()) {std::cerr << "attach vfs_read error,msg: "<< attachRes.msg() << std::endl;return 1;}/*探测vfs_write*/attachRes = bpf.attach_kprobe("vfs_write", "kprobe__vfs_write");if(!attachRes.ok()) {std::cerr << "attach vfs_write error,msg: "<< attachRes.msg() << std::endl;return 1;}u32 on=1,off=0,key=1;auto flag = bpf.get_hash_table<uint32_t,uint32_t>("flag");/*每秒完成一次读取并打印*/while (true){flag.update_value(key,on);std::this_thread::sleep_for(std::chrono::seconds(1));flag.update_value(key,off);auto io_map = bpf.get_hash_table<io_key, io_val>("map");auto table = io_map.get_table_offline();for (auto &item : table) {std::cout << "pid: " << item.first.pid << " inode: " << item.first.ino << " reads: " << item.second.reads << " writes: " << item.second.writes << " rbytes: " << item.second.rbytes << " wbytes: " << item.second.wbytes << " name: " << item.second.name << " path: " << item.second.path << std::endl;io_map.remove_value(item.first);}}return 0;}
编译并执行该eBPF程序
#编译命令g++ -std=c++17 -o io io.cpp -lbcc -pthread
指定mysqld进程pid 2004756进行diskio采集:
远程执行连接MySQL的命令并执行SQL
总结
DBdoctor推出长久免费版
DBdoctor是一款企业级数据库全方位性能监控与诊断平台,致力于解决一切数据库性能问题。可以对商业数据库、开源数据库、国产数据库进行统一性能诊断。具备:SQL审核、巡检报表、监控告警、存储诊断、审计日志、权限管理等免费功能,不限实例个数,可基于长久免费版快速搭建企业级数据库监控诊断平台。同时拥有:性能洞察、锁分析、根因诊断、索引推荐、SQL发布前性能评估等高阶功能,官网可快速下载,零依赖,一分钟快速一键部署。如果您想要试用全部功能可添加公众号自助申请专业版license。成为企业用户可获得产品定制、OpenAPI集成、一对一专家等高阶服务。迎添加小助手微信了解详细信息!
https://dbdoctor.hisensecloud.com/col.jsp?id=126