一. 技术背景
在云服务器上排查网络连接异常的问题,常用的有几种方式。一个方式是,使用tcpdump抓包,然后分析目标连接的报文。这种方式依赖运维或研发人员的经验。由于tcpdump抓包性能消耗大,不适宜长期开启。所以需要在异常连接发生时段才开启抓包,容易错过定位问题的时机。另一个方式是,在业务日志中对传输层的错误输出采集日志,此方式采集的信息不一定完整,可能无法精确定位问题。
基于Linux内核协议栈实现异常连接监控,具备精确,低资源消耗的有点。但是这涉及到修改相关的内核模块,并且技术门槛较高。同时,对内核的修改可能需要较长的时间才能部署到生产环境,无法适应当今的devops流程。eBPF技术的出现,为内核安全执行动态代码,提供了革命性的技术。
eBPF可以看作是内核态的虚拟机。开发人员通过C语言或者其他支持的语言编写eBPF代码,通过编译器编译成eBPF字节码,注入到内核提供的hook点。在相应的内核事件触发时,执行相关的代码。通过eBPF技术,可以动态的往内核添加监控代码,实现高效,精确的监控需求。本文尝试通过几个案例,介绍利用eBPF工具链监控tcp连接异常的方法。
二. eBPF工具简介
2.1 bpftool
下载地址:https://github.com/libbpf/bpftool
bpftool是用来加载,检视eBPF程序,eBPF Map数据的主要工具。
查看已加载的eBPF程序:
# bpftool prog show 3: cgroup_device tag 3650d9673c54ce30 gpl |
查看单个eBPF程序的字节码:
# bpftool prog dump xlated id 3 0: (61) r2 = *(u32 *)(r1 +0) |
查看eBPF map:
# bpftool map show 49: lru_hash name IPV4_FRAG_DATAG flags 0x0
# bpftool map dump id 5761 [{ |
2.2 bpftrace
bpftrace 是基于eBPF开发的高级动态追踪工具。bpftrace提供类脚本语言的前端,利用LLVM后端编译成eBPF代码。利用bpftrace,只需要很少的代码,就可以追踪CPU使用率,协议栈,系统调用等丰富的内核事件,例如:追踪进程读取的字节数
# bpftrace -e 'tracepoint:syscalls:sys_exit_read /args->ret/ { @[comm] = sum(args->ret); }' @[containerd]: 4 |
三. 追踪TCP连接案例
3.1 追踪tcp连接socket关闭调用栈:
编辑如下代码,保存为tcpdrop.bt
#!/usr/bin/env bpftrace
/*
* tcpdrop.bt Trace TCP kernel-dropped packets/segments.
* For Linux, uses bpftrace and eBPF.
*
* USAGE: tcpdrop.bt
*
* This is a bpftrace version of the bcc tool of the same name.
* It is limited to ipv4 addresses, and cannot show tcp flags.
*
* This provides information such as packet details, socket state, and kernel
* stack trace for packets/segments that were dropped via kfree_skb.
*
* For Linux 5.17+ (see tools/old for script for lower versions).
*
* Copyright (c) 2018 Dale Hamel.
* Licensed under the Apache License, Version 2.0 (the "License")
*
* 23-Nov-2018 Dale Hamel created this.
* 01-Oct-2022 Rong Tao use tracepoint:skb:kfree_skb
*/
#ifndef BPFTRACE_HAVE_BTF
#include <linux/socket.h>
#include <net/sock.h>
#else
#include <sys/socket.h>
#endif
BEGIN
{
printf("Tracing tcp drops. Hit Ctrl-C to end.\n");
printf("%-8s %-8s %-16s %-21s %-21s %-8s\n", "TIME", "PID", "COMM", "SADDR:SPORT", "DADDR:DPORT", "STATE");
// See https://github.com/torvalds/linux/blob/master/include/net/tcp_states.h
@tcp_states[1] = "ESTABLISHED";
@tcp_states[2] = "SYN_SENT";
@tcp_states[3] = "SYN_RECV";
@tcp_states[4] = "FIN_WAIT1";
@tcp_states[5] = "FIN_WAIT2";
@tcp_states[6] = "TIME_WAIT";
@tcp_states[7] = "CLOSE";
@tcp_states[8] = "CLOSE_WAIT";
@tcp_states[9] = "LAST_ACK";
@tcp_states[10] = "LISTEN";
@tcp_states[11] = "CLOSING";
@tcp_states[12] = "NEW_SYN_RECV";
}
tracepoint:skb:consume_skb
{
$skb = (struct sk_buff *)args->skbaddr;
$sk = ((struct sock *) $skb->sk);
$inet_family = $sk->__sk_common.skc_family;
if ($inet_family == AF_INET || $inet_family == AF_INET6) {
if ($inet_family == AF_INET) {
$daddr = ntop($sk->__sk_common.skc_daddr);
$saddr = ntop($sk->__sk_common.skc_rcv_saddr);
} else {
$daddr = ntop($sk->__sk_common.skc_v6_daddr.in6_u.u6_addr8);
$saddr = ntop($sk->__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr8);
}
$lport = $sk->__sk_common.skc_num;
$dport = $sk->__sk_common.skc_dport;
// Destination port is big endian, it must be flipped
$dport = bswap($dport);
$state = $sk->__sk_common.skc_state;
$statestr = @tcp_states[$state];
if ($state <= 3) {
return;
}
time("%H:%M:%S ");
printf("%-8d %-16s ", pid, comm);
printf("%39s:%-6d %39s:%-6d %-10s\n", $saddr, $lport, $daddr, $dport, $statestr);
printf("%s\n", kstack);
}
}
END
{
clear(@tcp_states);
}
|
执行:bpftrace tcpdrop.bt,输出
16:09:51 0 swapper/1 0.0.0.0:6 0.0.0.0:0 CLOSE napi_consume_skb+134 |
这段代码跟踪内核函数consume_skb,此函数在连接关闭,释放skb时调用。统计并输出连接五元组信息及调用栈。