哈喽~又到了我们技术分享环节了。eBPF这个系列自分享以来收到了很多朋友的喜欢,真是让博主又惊又喜,感谢大家的支持。话不多说,今天我们将对如何使用eBPF绘制网络拓扑图做一篇分享,文章较长,干货较多,大家可以一键三连先码后看~
新来的朋友点这里,一键回看前期精彩技术干货。
深入浅出运维可观测工具(三):eBPF如何兼容多架构模式性能管理https://mp.csdn.net/mp_blog/creation/editor/132697151
擎创技术流 | 深入浅出运维可观测工具(二):eBPF应用中常见问题https://mp.csdn.net/mp_blog/creation/editor/131919692
擎创技术流 | 深入浅出运维可观测工具(一):聊聊eBPF的前世今生https://mp.csdn.net/mp_blog/creation/editor/131901910
一、eBPF出现的背景
1.传统架构下排查问题过于依赖技术人员的水平
在传统架构下,应用程序通过发布工具部署到虚拟机环境中。在这种模式下,对于网络性能的监控和观测主要依赖一些常规的技术,比如专门的网络监控工具如sniffer,还有一些网络抓包工具比如Wireshark、tcpdump(bpf技术)等,这些工具可以捕获和分析网络流量,检测网络延迟、丢包等性能问题。
比如对应用服务请求响应超时,无法确认是网络问题还是应用程序自身问题,需要指定一段时间并对应用监听的端口号进行抓包,然后把抓到的数据包逐个分析并找到对应的请求。但是整个过程却非常繁琐费时,十分依赖排障人员的技术能力。
2.云原生的应用导致应用间的调用关系更加复杂
随着云原生架构和容器技术的火热,应用程序上云并且作为微服务容器化部署,各种应用中间件实时动态扩缩容,各种复杂的调用关系,比如Pod之间相互调用、Pod通过service请求应用、pod访问kubernetes外的服务、云外通过ingress访问云内应用等等,都给网络排查带来了巨大的困难,完全无法通过原先传统架构下指定ip和port的抓包方式进行排查。
3.部署方式发生重大变化
由于部署方式发生了较大变化,由原先的单一进程(process),变为了容器(container)、容器组(pod)、工作负载(deployment)、服务(service)等等,网络监控需求也由简单的网络接口抓包,衍变为需要近实时看到容器、容器组、工作负载各个维度的tcp、udp流量、http调用访问、网络拓扑关系等。
4.eBPF灵活强大的优势
eBPF 技术的出现为网络性能管理带来了更强大、更灵活、更有效的工具和方法。它能够实时监测和分析网络流量,提供精细的流量控制和优化能力,并提高网络安全性和故障排查能力,对于云原生复杂的网络环境,ebpf允许用户在内核中编写和执行自定义的代码逻辑,以实时监测和分析网络流量。通过eBPF技术能够捕获每一个进程的网络连接信息以及对应的tcp延时、波动、连接数、重传数等指标,进而可以由进程关联到所属的容器,通过每一个容器可以关联到所在的容器组以及每一个工作负载的网络连接
二、如何使用eBPF绘制网络拓扑图
1.了解tcp发送过程
Linux是一个强大的操作系统,提供了丰富的系统调用接口。我们发送网络数据的时候通过调用系统调用发送网络数据。
要绘制网络拓扑图,我们首先要了解linux处理请求过程:
通过上面的流程我们发现一个RPC最基本的调用流程大致如下:
1)客户端建立连接(Establishing Connection)
-
客户端向服务端调用connect函数建立连接。
-
服务端使用accept接收连接。
2)数据传输(Data Transfer)
已建立连接的两端可以可以通过调用send、recv系列函数进行数据的传输。
3)断开连接(Terminating Connection)
当数据传输完毕或不再需要连接时,一方可以通过调用close发送一个FIN(结束)报文段。接收到FIN报文段的一方再次调用close发送一个ACK报文段作为确认。这个过程称为四次挥手,确保连接的安全关闭。
2.总结linux用于网络传输
1)使用的系统调用
我们通过分析<< unix网络编程第一卷 >> 找出网络传输用到的系统调用
基本套接字编程所用到的系统调用
这些函数统一需要传入fd参数,例如send函数,它的系统调用需要入参如下:
size_t send(int sockfd, const void *buf, size_t len, int flags);
我们需要使用eBPF,hook这些函数 找到对应的linux进程下sockfd 所对应的tcp五元组,然后进行汇总统计来绘制网络拓扑图。
3.对监控网络场景进行分析
1)擎创采集器开启后建立的TCP连接
擎创采集器启动后。服务程序通过调用 listen 和 accept 函数,接收客户端的连接请求,并与客户端建立通信。
而对于客户端而言,通过调用 connect 函数来建立和服务程序的连接。这些连接和之后的数据传输过程使用的系统调用,通过使用基于eBPF的技术,都可以eBPF嵌入字节码程序并进行分析。
2)擎创采集器开启前建立的TCP连接
由于调用方和服务器的tcp 连接过程早已创建完成。并不会再触发connect 和 accept的系统调用,只是在数据传输中调用send、recv或者write、read系列的函数。
如果应用程序之间数据传输使用的是write和read,并没有调用sockfd_lookup_light,所以暂时无法通过fd找到对应的sock 五元组。
但是如果应用程序之间的传输使用的是send和recv等系统调用,kernel 在内部会调sockfd_lookup_light,我们可以通过 pid 和fd的组合翻译出对应的sock五元组。
由于connect和accept在采集器启动之前已经建立连接,所以我们无法通过eBPF确认出流量的出入口方向。
例如go的一些应用程序:
https://pkg.go.dev/net@go1.21.0#IPConn.Read
IPConn 只提供了Read和write接口,如果TCP连接在采集器启动之前已经建立了连接,我们暂时无法把fd翻译成TCP五元组,也无法确认流量方向。
4.通过linux源码分析系统调用
1)找到hook点
linux 内核中定义recvmsg
SYSCALL_DEFINE3(recvmsg, int, fd, struct user_msghdr __user *, msg, unsigned int, flags)
内核整个调用过程
SYSCALL_DEFINE3(recvmsg) -> __sys_recvmsg->sockfd_lookup_light->___sys_recvmsg
通过解读recvmsg 我们发现 kernel 会在我们调用系统调用后通过sockfd_lookup_light把传进来的fd整数转化成为kernel的sock数据结构,即在给定的网络命名空间中查找并返回与指定文件描述符(socket 文件描述符)相对应的 struct socket 结构体指针,以便对套接字进行进一步的操作。
sockfd_lookup_light 使用的系统调用有:
read 和 write 并没有使用 sockfd_lookup_light,统一把 read write 处理操作挂到了,文件描述符操作结构体上
struct file_operations { struct module *owner; ......... ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ........ } __randomize_layout;
5.我们使用eBPF
1)hook sockfd_lookup_light
首先我们uprobe 住入口函数,记下进程号和 描述符的对应关系,因为每个进程的fd 都会对应一个sock结构体,所以我们把pid 进程号和 fd 组成一个key。
SEC("kprobe/sockfd_lookup_light") int kprobe_hook_sockfd_lookup_light(struct pt_regs *ctx) { int sockfd = (int)PT_REGS_PARM1(ctx); u64 pid_tgid = bpf_get_current_pid_tgid(); pid_fd_map key = { .pid = pid_tgid >> 32, .fd = sockfd, }; struct sock **sock = bpf_map_lookup_elem(&sock_by_pid_fd_storage, &key); if (sock != NULL) { return 0; } bpf_map_update_elem(&sockfd_save_args, &pid_tgid, &sockfd, BPF_ANY); return 0; }
最后我们再 sockfd_lookup_light 把 pid和fd 的对应关系存储到map
SEC("kretprobe/sockfd_lookup_light") int kretprobe_hook_sockfd_lookup_light(struct pt_regs *ctx) { u64 pid_tgid = bpf_get_current_pid_tgid(); int *sockfd = bpf_map_lookup_elem(&sockfd_save_args, &pid_tgid); if (sockfd == NULL) { return 0; } struct socket *socket = (struct socket *)PT_REGS_RC(ctx); enum sock_type sock_type = 0; bpf_probe_read_kernel(&sock_type, sizeof(short), &socket->type); ................. // 读出sock 结构体 struct sock *sock = NULL; bpf_core_read(&sock, sizeof(sock), (char *)socket); pid_fd_map pid_fd = { .pid = pid_tgid >> 32, .fd = (*sockfd), }; bpf_map_update_elem(pid_fd_by_sock, &sock, &pid_fd, BPF_ANY); cleanup: bpf_map_delete_elem(&sockfd_save_args, &pid_tgid); }
每当内核触发sockfd_lookup_light的时候我们就会把 pid,fd 以及sock 对应关系存储到sockfd_save_args中。
socket 五元组多存储在linux 内核的 struct sock 中
6.使用eBPF 绘制网络拓扑
通过hook accept 标记处的入口流量
SEC("kretprobe/inet_csk_accept")
通过hook connect 标记处的出口流量
SEC("kprobe/tcp_connect") SEC("kprobe/tcp_finish_connect")
通过hook sockfd_lookup_light 翻译出进程的fd对应的sock五元组
SEC("kretprobe/sockfd_lookup_light")
程序在调用系统调用的时候,我们就很容易使用 通过pid fd,在eBPF的 sock_by_pid_fd_storage map中 找到对应的sock,找到了kernel sock 结构体我们就可以通过sock 五元组,绘制出流量方向。
绘制网络拓扑图的过程中,我们使用eBPF可以抓取网络数据包并对其进行解析,获取各个节点之间的连接信息,包括源地址、目标地址、协议类型、端口等。通过收集和分析这些数据,我们可以逐步构建整个网络拓扑,并将其可视化为图形化的形式。
最后结合使用eBPF程序和图形数据库,我们完成了网络拓扑的绘制:
以上即为本次eBPF绘制网络拓扑图的全部内容。可观测之eBPF系列,正在不断创作中,欢迎您长期关注~
如果有用,请辛苦点个赞吧!
擎创科技,Gartner连续推荐的AIOps领域标杆供应商。公司专注于通过提升企业客户对运维数据的洞见能力,为运维降本增效,充分体现科技运维对业务运营的影响力。
行业龙头客户的共同选择
了解更多运维干货与行业前沿动态
可以右上角一键关注
我们是深耕智能运维领域近十年的
连续多年获Gartner推荐的AIOps标杆供应商
下期我们不见不散~