eBPF实战教程二|数据库网络流量最精准的量化方法(含源码) ...

打印 上一主题 下一主题

主题 984|帖子 984|积分 2952

本文纯技能文章,无任何广告,感兴趣的小伙伴可仔细阅读,有疑问可加入最下方技能交流群讨论。
媒介

自从DBdoctor率先将eBPF技能深度应用于数据库领域后,便迅速在业界引起了广泛的关注和讨论。阿里、美团、京东、字节跳动等众多头部企业纷纷主动与我们展开了深入的技能交流。与此同时,我们也收获了大量eBPF爱好者的关注,在第一篇关于uprobe的文章发布后,很多小伙伴都已按教程乐成跑通代码并举行深入自学应用。
应广大读者的热情敦促,现推出第二篇eBPF的纯技能分享文章——如何手码一个Kprobe函数来分析MySQL数据库的网络流量。旨在为各人提供更多关于eBPF的深入分析和实用指南,希望本文能对各人有所资助,后续我们也将一连在此专题内发布更多技能文章,欢迎关注公众号!
什么是Kprobe

Kprobe是Linux内核提供的一种动态跟踪技能,它可以在运行时动态地在函数的开头、返回点或指令地址处插入探测点。利用kprobe技能,可以在内核函数中动态插入探测点,收集有关内核执行流程、寄存器状态、全局数据结构等详细信息,无需重新编译或修改内核代码,实现对函数的监控和分析。Kprobe极大地增强了内核调试和性能分析的灵活性,可应用于网络优化、安全控制、性能监控、故障诊断等场景,使得开发者能够更深入地理解内核的举动。特别是数据库性能诊断这块,Probe重新界说数据库可观测,可以快速精确找出潜在性能标题并优化。
Kprobe函数的选取

1)网络协议栈分析,获取MySQL SQL执行返回给客户端的函数
基于Kprobe的流量探测,需要对网络协议栈举行分析,下图是网络数据包的发送过程:

从上面的协议栈的函数调用可以看到,tcp_sendmsg函数是发送包的入口函数:
  1. int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
  2. {
  3.   int ret;
  4.   
  5.   lock_sock(sk);
  6.   ret = tcp_sendmsg_locked(sk, msg, size);
  7.   release_sock(sk);
  8.   return ret;
  9. }
复制代码
从函数的源码我们可以得知,该函数返回值即为我们需要的效果(即发送数据包的巨细)。
因此,我们可以选取传输层中的tcp_sendmsg函数作为探测点,来统计数据库发送应用端每秒的数据包总量。
2)网络协议栈分析,找到MySQL从客户端接收数据包的函数
基于Kprobe的流量探测,需要对网络协议栈举行分析,下图是网络数据包的接收过程:

对于统计TCP接收的网络流量,应该选择tcp_cleanup_rbuf函数,而不是选择tcp_recvmsg。选用tcp_recvmsg函数会存在统计的重复和遗漏:


  • tcp_recvmsg()是一个在TCP接收路径上较高层的函数,它负责从TCP层向用户空间复制数据。当应用步调调用如recv()或read()这类函数来从TCP缓冲区读取数据时,tcp_recvmsg()会被触发。如果数据在TCP接收缓冲区中未被应用步调完全读取(例如,应用步调两次调用recv()读取同一数据段的差别部分),每次调用tcp_recvmsg()都会被触发。这可能导致在统计时同一数据被计算多次。
  • TCP数据可能由于内核的优化处理(如告急数据处理、某些安全检查导致的数据抛弃)而未达到tcp_recvmsg()层会导致统计的遗漏。
  • 使用某些直接输入输出操纵(如splice系统调用)可以绕过常规的recvmsg路径,直接从内核缓冲区向用户空间或其他文件描述符传输数据,这些操纵不会触发tcp_recvmsg()。
tcp_cleanup_rbuf 这个函数在TCP数据确认已被接收(即数据已经从内核传输到了用户空间,并得到了处理)后调用,因此可以更可靠地统计到实际被应用消费的数据量,而不会重复也不会遗漏。
该函数原型如下:
  1. void tcp_cleanup_rbuf(struct sock *sk, int copied)
  2. {
  3.   struct sk_buff *skb = skb_peek(&sk->sk_receive_queue);
  4.   struct tcp_sock *tp = tcp_sk(sk);
  5.   WARN(skb && !before(tp->copied_seq, TCP_SKB_CB(skb)->end_seq),
  6.        "cleanup rbuf bug: copied %X seq %X rcvnxt %X\n",
  7.        tp->copied_seq, TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt);
  8.   __tcp_cleanup_rbuf(sk, copied);
  9. }
复制代码
函数中第二个形参copied即为接收的数据包巨细。
因此,我们可以选取传输层中的tcp_cleanup_rbuf函数作为探测点,来统计数据库每秒从应用端接收的数据包总量。
eBPF kprobe如何探测MySQL的每秒发送和接收数据包?

1)情况准备

  1. 准备一台 Linux 机器,安装好g++和bcc
复制代码
2)基于BCC工具实现探测MySQL

要实现包量的统计,我们起首界说一个存储结构用来存放进程的收发包的总Size,基于Kprobe分别对接收包和发送包举行累加并存储到该结构中,然后每秒去读并打印当前存储结构中累加的数据包量,即可实现每秒的接收和发送数据包的采集。
接下来我们将基于BCC,利用Kprobe写一个eBPF步调,观测MySQL的接收和发送的数据包(即MySQL的NetIO统计)。
a)分析内核网络协议栈源码相关网络数据包处理的函数

  1. //内核网络协议栈的发送包函数(返回值)
  2. int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size){
  3.   ...
  4. }
  5. //内核网络协议栈的接收包函数(入参)
  6. void tcp_cleanup_rbuf(struct sock *sk, int copied){
  7.   ...
  8. }
复制代码
b)导入BCC的BPF对象
  1. //这个对象可以将我们的观测代码嵌入到观测点中执行
  2. #include <bcc/BPF.h>
  3. #include <string>
  4. #include <iostream>
  5. #include <thread>
  6. #include <time.h>
复制代码
c)用c编写eBPF代码
  1. std::string strBPF = R"(
  2. #include <linux/ptrace.h>
  3. #include <net/sock.h>
  4. #include <bcc/proto.h>
  5. #include <linux/in6.h>
  6. #include <linux/net.h>
  7. #include <linux/socket.h>
  8. #include <net/inet_sock.h>
  9. //定义采集的指标存储结构key
  10. struct key_t {
  11.     u32 pid;
  12.     u16 type;
  13. };
  14. //定义采集的指标存储结构value
  15. BPF_HASH(net_map, struct key_t,u64);
  16. //获取mysql执行sql返回的数据包,hook对返回值进行处理
  17. int kretprobe__tcp_sendmsg(struct pt_regs *ctx)
  18. {
  19.     /*获取当前进程的pid*/
  20.     u32 pid = bpf_get_current_pid_tgid() >> 32;
  21.     if(FILTER_PID) return 0;
  22.     int size = PT_REGS_RC(ctx);
  23.     if(size <= 0)
  24.         return 0;
  25.     struct key_t key= {};
  26.     key.pid = pid;
  27.     key.type = 1;
  28.     u64 zero = 0;
  29.     u64 *val = net_map.lookup_or_init(&key, &zero);
  30.     zero = *val + size;
  31.     net_map.update(&key, &zero);
  32.     return 0;
  33. }
  34. //获取发送给mysql的数据包,hook对函数入参进行处理
  35. int kprobe__tcp_cleanup_rbuf(struct pt_regs *ctx, struct sock *sk, int copied)
  36. {
  37.     /*获取当前进程的pid*/
  38.     u32 pid = bpf_get_current_pid_tgid() >> 32;
  39.     if(FILTER_PID) return 0;
  40.     /*检错*/
  41.     if (copied <= 0) return 0;
  42.     struct key_t key = {};
  43.     key.pid = pid;
  44.     key.type = 2;
  45.     u64 zero = 0;
  46.     u64 *val = net_map.lookup_or_init(&key, &zero);
  47.     zero = *val + copied;
  48.     net_map.update(&key, &zero);
  49.     return 0;
  50. }
  51. )";
复制代码
d)观测代码关联网络协议栈中需要观测的函数
  1. //用于ebpf代码程序中的pid替换
  2. static std::string str_replace(std::string r, const std::string& s, const std::string& n)
  3. {
  4.         std::string y = std::move(r);
  5.         std::string::size_type pos = 0;
  6.         while((pos = y.find(s)) != std::string::npos)  
  7.             y.replace(pos, s.length(), n);
  8.         return y;
  9. }
  10. struct net_key_t {
  11.     uint32_t pid;
  12.     uint16_t type;
  13. };
  14. //指定进程pid进行kprobe包统计
  15. int main(int argc, char* argv[]) {
  16.     int pid = std::stoull(argv[1]);
  17.     ebpf::BPF bpf;
  18.    
  19.     std::string strFilerPid = "pid != " + std::to_string(pid);
  20.     std::string code = str_replace(strBPF, "FILTER_PID", strFilerPid);
  21.     auto initRes = bpf.init(code);
  22.     if (!initRes.ok()) {
  23.         std::cerr << "bpf init error,msg: " << initRes.msg() << std::endl;
  24.         return 1;
  25.     }
  26.     /*探测tcp_sendmsg*/
  27.     auto attachRes = bpf.attach_kprobe("tcp_sendmsg", "kretprobe__tcp_sendmsg",0,BPF_PROBE_RETURN);
  28.     if(!attachRes.ok()) {
  29.         std::cerr << "attach tcp_sendmsg error,msg: "<< attachRes.msg() << std::endl;
  30.         return 1;
  31.     }
  32.      /*探测tcp_cleanup_rbuf*/
  33.     attachRes = bpf.attach_kprobe("tcp_cleanup_rbuf", "kprobe__tcp_cleanup_rbuf");
  34.     if(!attachRes.ok()) {
  35.         std::cerr << "attach tcp_cleanup_rbuf error,msg: "<< attachRes.msg() << std::endl;
  36.         return 1;
  37.     }
  38.     /*每秒完成一次读取并打印*/
  39.     while (true){
  40.             std::this_thread::sleep_for(std::chrono::seconds(1));
  41.             auto net_map = bpf.get_hash_table<net_key_t, uint64_t>("net_map");
  42.             auto table = net_map.get_table_offline();
  43.             for (auto &item : table) {
  44.                 std::cout << "time: " << std::time(0) << "pid: " << item.first.pid << " type: " << (item.first.type == 1 ? "sendMsg" : "recvMsg") << " size: " << item.second << std::endl;
  45.             }
  46.         }
  47.     return 0;
  48. }
复制代码
e)效果演示
编译并执行该eBPF步调
  1. #编译命令
  2. g++ -std=c++17 -o static_netio static_netio.cpp -lbcc -pthread
复制代码
指定mysqld进程pid 2004756举行netio采集:

远程执行毗连MySQL的下令并执行SQL

打印观测的效果

从上面的演示中我们能看到,客户端和MySQL创建毗连,每秒会打印日志,显示这个读取累加send和recv数据包的时间、mysqld的进程pid、send累加的数据包和recv累加的数据包巨细。然后我们针对采集上来的数据就可以做分析了:


  • 如果存在send数据包过大,说明数据库上存在较大的流量或者单条大SQL执行完会有大量的数据返回,比如全表查询返回这种,会导致应用出现内存大量占用标题,以致引发OOM。
  • 如果存在recv数据包过大,说明用户应用端发送给数据库的SQL文本存在过大标题,需要业务进一步关注业务逻辑是否正常。
总结

利用eBPF技能探测MySQL ,具有更高效,更扩展,更安全等优势,不消修改内核就可观测数据库性能。通过上面例子您是否发现接纳eBPF跟踪数据库实在并不难,主要门槛在于需精通数据库内核和Linux编程,而且要对代码有精益求精的意识。
您的MySQL每秒发送和接收的数据包统计出现了吗?欢迎加入技能交流群讨论!
DBdoctor推出长久free版

DBdoctor是一款企业级数据库全方位性能监控与诊断平台,致力于解决一切数据库性能标题。可以对贸易数据库、开源数据库、国产数据库举行同一性能诊断。具备:SQL考核、巡检报表、监控诉警、存储诊断、审计日志、权限管理等免费功能,不限实例个数,可基于长久免费版快速搭建企业级数据库监控诊断平台。同时拥有:性能洞察、锁分析、根因诊断、索引推荐、SQL发布前性能评估等高阶功能,官网可快速下载,零依靠,一分钟快速一键摆设。如果您想要试用全部功能可添加公众号自助申请专业版license。成为企业用户可获得产物定制、OpenAPI集成、一对一专家等高阶服务。迎添加小助手微信了解详细信息!
免费下载/在线试用:
https://dbdoctor.hisensecloud.com/h-col-126.html?statId=9
公众号:DBdoctor
如果您是开发或DBA欢迎关注公众号,关注公众号复兴:“进群”,可拉您进入技能交流群。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

花瓣小跑

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表