基于ebpf统计docker容器网络流量

基于ebpf统计docker容器⽹络流量
Linux下统计⽹络带宽的⼯具很多,但⼤部分是按⽹卡、进程、IP进⾏统计。统计容器维度的⽹络流量,虽然可在容器的namespace查看,或者由cgroup提供的net_cls做标记再配合iptable统计,但使⽤起来并不⽅便,这⾥介绍⼀种基于ebpf来统计容器维度⽹络流量的办法,以及使⽤时遇到的坑。
原理与实现
ebpf的原理不再介绍了,⽤来做流量统计,最⼤的优势是⼗分灵活,能够根据需求对统计项⽬进⾏定制。基本原理就是记录内核中各⽹络相关函数的参数,再按不同维度,如 cgroupID, 进程,IP地址等进⾏分类,统计,计算,输出。
统计流量
ebpf可通过跟踪内核函数,统计不同层次的⽹络流量。各层的流量差异主要在于包头,重传,控制报⽂等等。
L4 TCP 纯数据流量:
上⾏:kprobe统计  tcp_sendmsg(struct sock *sk,struct msghdr *msg, size_t size)  size
下⾏:kprobe统计  tcp_cleanup_rbuf(struct sock *sk, int copied)  copied
L4 UDP 纯数据流量:
上⾏:kprobe统计 udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)  len
下⾏:kprobe统计 skb_consume_udp(struct sock *sk, struct sk_buff *skb, int len)  len
L3 IP 流量
上⾏: kprobe统计 ip_output(struct net *net, struct sock *sk, struct sk_buff *skb) skb->len
下⾏: kprobe统计 ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,struct net_device *orig_dev) skb->len
L2 全部⽹络包流量:
上⾏:tracepoint统计 net/net_dev_queue  args->len
下⾏:tracepoint统计 net/netif_receive_skb  args->len
获取容器ID
容器基于cgroup创建,容器内的进程⽣成时其 task_struct->cgroups 会记录其所属cgroup相关信息。ebpf程序运⾏在内核态,可直接访问进程的task_struct结构,进⽽获取其cgroupID, 在统计流量时即可⽅便的区分当前⽹络流量属于哪个cgroup。
1static void fill_container_id(char *container_id) {
2  struct task_struct *curr_task;
3  struct css_set *css;
4  struct cgroup_subsys_state *sbs;
5  struct cgroup *cg;
6  struct kernfs_node *knode, *pknode;
7
磁流变阻尼器>无尘布激光切割机8  curr_task = (struct task_struct *) bpf_get_current_task();
9  css = curr_task->cgroups;平面度怎么测量
10  bpf_probe_read(&sbs, sizeof(void *), &css->subsys[0]);液压缸位移传感器
11  bpf_probe_read(&cg,  sizeof(void *), &sbs->cgroup);
12
13  bpf_probe_read(&knode, sizeof(void *), &cg->kn);
14  bpf_probe_read(&pknode, sizeof(void *), &knode->parent);
15
16  if(pknode != NULL) {
17    char *aus;
18
19    bpf_probe_read(&aus, sizeof(void *), &knode->name);
20    bpf_probe_read_str(container_id, CONTAINER_ID_LEN, aus);
21  }
22}
DEMO实现
为实现⽅便,demo基于BCC 编写,为每个统计参数建⼀个HASH_MAP,key结构加⼊需要统计的维度信息,如pid,cgroupID等,应⽤层周期遍历HASH_MAP,计算⽹络带宽,并清理HASH_MAP。
1int kprobe__udp_sendmsg(struct pt_regs *ctx, struct sock *sk,
2    struct msghdr *msg, size_t len)
3{
4    if (container_should_be_filtered()) {
5        return 0;
6    }
7
8    u32 pid = bpf_get_current_pid_tgid() >> 32;
9    FILTER_PID
10
11    family = sk->__sk_common.skc_family;
12
13    if (family == AF_INET) {
14        struct ipv4_key_t ipv4_key = {.pid = pid};
15  fill_container_id(ainer_id);
16        L4_udp_send_bytes.increment(ipv4_key, len);
17    }
18    // else drop
19    return 0;
20}
遇到的坑
参考BCC 的例⼦,demo很快写完,初步测试也正常,但离真正能使⽤还差的很远,这⾥记录⼀些遇到的深坑。
L3 L2 部分流量统计不到cgroupID
发现⼀部分L3 L2层的流量统计不到pid和cgroupID。经分析这些流量都是tcp流量,由于tcp协议栈相对复杂,当skb进⼊下层时可能已经不再进程上下⽂中,就⽆法通过task_struct结构来获取pid以及cgroupID。
这类问题在BCC 的例⼦中有很多,解决思路都是先选定特征变量,在前置流程中将其信息记录在hash_map中,后⾯再查询。以tcp上⾏为例,可以以skb->sock作为特征变量,在tcp_sendmsg中将其所属的cgroupID记录在hash_map中,统计ip_output的流量时先通过解析skb->head,分辨该skb的L4协议,若是tcp根据skb->sock从hash_map中查cgroupID,其他协议直接通过task_struct获取cgroupID。当然这样会增加额外的开销。
统计到的流量⼤于⽹卡流量
发现L4 L3 L2 层上⾏流量都⼤于⽹卡流量。因为发包时skb逐层向下封装,可判定在L2到⽹卡驱动之前出现了异常。通过分析代码,L2层流量统计tracepoint net/net_dev_queue 设置在__dev_queue_xmit()中,该函数将skb加⼊qdisc准备发给⽹卡驱动。该函数有可能由于qdisc的规则返回错误,从⽽丢弃skb,但tracepoint net/net_dev_queue⽆法识别这种情况。在出问题的设备上通过统计
__dev_queue_xmit()返回值,发现有⼤量NET_XMIT_DROP返回,可以确认该问题是由qdisc丢包引起的。
解决⽅法是将统计tracepoint net/net_dev_queue  改为统计__dev_queue_xmit(),并过滤返回值不等于0的流量。
L4 L3 流量正常 L2 流量翻倍
发现L4 L3 层流量与⽹卡流量基本⼀致,L2层流量⼏乎翻倍。从现象来看很可能是L2的统计函数在⼀次发包流程中被调⽤了两次,通过跟踪tracepoint net/net_dev_queue的调⽤栈,发现有macvlan相关的调⽤分⽀,查看配置后发现该设备配置了macvlan虚拟⽹卡,L2 层的包会两次经过tracepoint net/net_dev_queue,出现流量翻倍。
hash_map clear ⽅法导致性能问题
发现在流量较⾼的设备上,BCC 提供的针对hash_map的clear⽅法会造成长时间阻塞以及cpu消耗。通过strace跟踪发现clear⽅法是通过bpf 系统调⽤,遍历hash_map 并⼀个⼀个删除其中元素。
解决⽅法是调整统计⽅法为记录每次统计周期前后的差值计算带宽,避免调⽤clear⽅法。另⼀个问题是不断有新的容器/进程启动,不清除hash_map 可能会使key达到上限⽽导致⽆法记录新的进程。好在bpf提供了lru_hash_map,这类hash_map以lru维护key,当key达到上限再加⼊新的key时,会删除不再使⽤的key。
increment ⽅法靠谱吗
⽬前的设备绝⼤多数都采⽤多核CPU,流量统计实际是⼀个并发计数问题,必须要考虑竞争,性能,准确性等等。BCC 针对hash_map 提供的increment⽅法⽤于⽅便实现累加操作,但在⾼并发场景下该⽅法的性能,准确性如何,需要深⼊探究⼀下。
bpf_map是⼀个⼗分重要的特性,它的作⽤是让应⽤层程序与内核中的bpf程序可以通过共享⼀块数据完成交互。简单看了下内核中的实现,对bpf_map的操作路径分为两种,内核态的bpf程序直接操作,或者应⽤层通过bpf系统调⽤设置
BPF_MAP_LOOKUP_ELEM,BPF_MAP_UPDATE_ELEM,BPF_MAP_DELETE_ELEM 等cmd来操作。这⾥的increment⽅法是内核态操作。
查看BCC 源码到increment⽅法的实现如下:
1else if (memb_name == "increment") {
2          string name = string(Ref->getDecl()->getName());
3          string arg0 = rewriter_.getRewrittenText(expansionRange(Call->getArg(0)->getSourceRange()));
4
5          string increment_value = "1";
6          if (Call->getNumArgs() == 2) {
7            increment_value = rewriter_.getRewrittenText(expansionRange(Call->getArg(1)->getSourceRange()));
8
9          }
10
数字天线11          string lookup = "bpf_map_lookup_elem_(bpf_pseudo_fd(1, " + fd + ")";
12          string update = "bpf_map_update_elem_(bpf_pseudo_fd(1, " + fd + ")";
13          txt  = "({ typeof(" + name + ".key) _key = " + arg0 + "; ";
14          txt += "typeof(" + name + ".leaf) *_leaf = " + lookup + ", &_key); ";
15
16          txt += "if (_leaf) (*_leaf) += " + increment_value + ";";
17          if (desc-&pe == BPF_MAP_TYPE_HASH) {
18            txt += "else { typeof(" + name + ".leaf) _zleaf; __builtin_memset(&_zleaf, 0, sizeof(_zleaf)); ";
19            txt += "_zleaf += " + increment_value + ";";
20            txt += update + ", &_key, &_zleaf, BPF_NOEXIST); } ";
21          }
22          txt += "})";
23}
BCC 实际将increment扩展成bpf_map的函数调⽤,流程⼤致为⾸先在bpf_map中查相应的key,若到则直接更新其中value指针指向的数据,当bpf_map的类型为hash_map且没到key时,加⼊新的key。整个流程没⽤锁的保护,可以预见的是,若多个cpu同时执⾏该代码,统计结果会变⼩。
滑环电机
内核中对于并发计数问题,通常做法是通过percpu变量,统计每个cpu上的数据,最终统计时再进⾏汇总。这样既不⽤加锁避免性能下降同时也保证了准确性。幸运的是bpf也提供了percpu_hash_map,该类hash_map每个key的value是⼀个长度为cpu个数的数组,其中存放针对每个cpu的数据,BCC 还针对percpu_hash_map实现了sum,avg,max等函数。
测试验证中发现,在容器中⽤iperf多线程模式测速,hash_map统计的流量数据会⽐percpu_hash_map偏⼩。
基于BCC 使⽤percpu_hash_map
BCC 对于percpu_hash_map的⽀持还不完善,没有类似BPF_HASH()的宏定义percpu_hash_map,需要⽤BPF_TABLE()显⽰定义。percpu_hash_map ⽆法使⽤increment⽅法,原因可能是increment⽅法代码中只适配了BPF_MAP_TYPE_HASH类型,对其累加操作可参考increment⽅法实现。percpu_hash_map的value参数类型需要8bytes对齐,估计是为了⽅便内核态的cacheline对齐。在应⽤层更新percpu_hash_map时必须同时更新所有cpu的数据,这是bpf系统调⽤的限制,后续内核应该会提供⽅法实现更新单个cpu的数据。
总结
ebpf由于其灵活易⽤,在内核跟踪⽅向应⽤⼴泛,在开发跟踪观测⼯具时,选择跟踪点需要再三斟酌,尤其对于IO,⽹络等复杂模块,需要了解跟踪点的上下游代码流程,进⾏多场景测试验证。对于⼀些已有的⼯具也要持怀疑态度,究其原理。
ebpf可能是⽬前内核社区最活跃的模块,更新⼗分频繁,应尽量在较新版本的内核中使⽤。其相配套的⼯具也在快速迭代中,难免有⽀持不够完善的问题,遇到问题还是要从⼯具以及内核源码出发,寻原因和解决办法。
参考项⽬:

本文发布于:2024-09-25 14:27:11,感谢您对本站的认可!

本文链接:https://www.17tex.com/tex/2/182142.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:统计   流量   内核   跟踪   容器
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2024 Comsenz Inc.Powered by © 易纺专利技术学习网 豫ICP备2022007602号 豫公网安备41160202000603 站长QQ:729038198 关于我们 投诉建议