赞
踩
在深入剖析cilium原理之前,有两个关于epbf的基础内容需要先详细介绍一下:
1. ebpf尾调用
尾调用类似于程序之间的相互跳转,但它的功能更加强大。
2. trace
虽然之前使用trace_printk输出日志,但这个函数不能多用,会有性能问题。而且它的输出可读性差,不利于程序进行分析。本文将着重讲讲如何进行分析,方便程序后续跟踪。
尾调用能够从一个程序调到另一个程序,提供了在运行时(runtime)原子地改变程序行为的灵活性。为了选择要跳转到哪个程序,尾调用使用了程序数组map( BPF_MAP_TYPE_PROG_ARRAY),将map及其索引(index)传递给将要跳转到的程序。跳转动作一旦完成,就没有办法返回到原来的程序;但如果给定的map索引中没有程序(无法跳转),执行会继续在原来的程序中执行。
特殊在于,由于函数的调用是由map来保存的,可以使用TC更新map对应的函数为新的函数,实现ebpf程序的局部更新。当然,这就对功能编码也会有更高的要求,需要明确更新可能的影响范围。因为ebpf的单个程序大小是有限制的,而尾调用可以绕开这种限制。
以下是摘自cilium官方文档中的样例:(官方样例无法编译,下面摘取的内容经过作者调整)
- #include <bits/types.h>
- #include <linux/bpf.h>
- #include <linux/if_ether.h>
- #include <linux/in.h>
- #include <linux/ip.h>
- #include <linux/pkt_cls.h>
- #include <linux/tcp.h>
- #include <errno.h>
- #include <stdio.h>
- #include <string.h>
-
- typedef __uint32_t uint32_t;
-
- #define PIN_GLOBAL_NS 2
-
- struct bpf_elf_map {
- __u32 type;
- __u32 size_key;
- __u32 size_value;
- __u32 max_elem;
- __u32 flags;
- __u32 id;
- __u32 pinning;
- };
-
- #ifndef __stringify
- # define __stringify(X) #X
- #endif
-
- #ifndef __section
- # define __section(NAME) \
- __attribute__((section(NAME), used))
- #endif
-
- #ifndef __section_tail
- # define __section_tail(ID, KEY) \
- __section(__stringify(ID) "/" __stringify(KEY))
- #endif
-
- #ifndef BPF_FUNC
- # define BPF_FUNC(NAME, ...) \
- (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME
- #endif
-
- #define BPF_JMP_MAP_ID 1
-
- static int (*bpf_trace_printk)(const char *fmt, int fmt_size,
- ...) = (void *)BPF_FUNC_trace_printk;
-
- #define printk(fmt, ...) \
- do { \
- char _fmt[] = fmt; \
- bpf_trace_printk(_fmt, sizeof(_fmt), ##__VA_ARGS__); \
- } while (0)
-
- static void BPF_FUNC(tail_call, struct __sk_buff *skb, void *map,
- uint32_t index);
-
- struct bpf_elf_map jmp_map __section("maps") = {
- .type = BPF_MAP_TYPE_PROG_ARRAY,
- .id = BPF_JMP_MAP_ID,
- .size_key = sizeof(uint32_t),
- .size_value = sizeof(uint32_t),
- .pinning = PIN_GLOBAL_NS,
- .max_elem = 1,
- };
-
- __section_tail(BPF_JMP_MAP_ID, 0)
- int looper(struct __sk_buff *skb)
- {
- printk("skb cb: %u\n", skb->cb[0]++);
- tail_call(skb, &jmp_map, 0);
- printk("skb end looper: cb: %u\n", skb->cb[0]);
- return TC_ACT_OK;
- }
-
- __section("prog")
- int entry(struct __sk_buff *skb)
- {
- skb->cb[0] = 0;
- tail_call(skb, &jmp_map, 0);
- printk("skb end: cb: %u\n", skb->cb[0]);
- return TC_ACT_OK;
- }
-
-
- char __license[] __section("license") = "GPL";
官方代码很清晰,需要注意的是:定义函数时,__section_tail(BPF_JMP_MAP_ID, 0)使用的是BPF_JMP_MAP_ID;而在bpf代码中,调用的时候使用的是tail_call(skb, &jmp_map, 0);中的jmp_map。而这两者之间的关系,则是在定义jmp_map时指定。
-
- # 将上面文件保存为tail.c
- # build
- clang -g -c -O2 -target bpf -c tail.c -o tail.o
-
- # load
- tc qdisc add dev enp1s0 ingress
- tc filter replace dev enp1s0 ingress prio 1 handle 1 bpf da obj tail.o sec prog verbose
-
- # cat log
- cat /sys/kernel/debug/tracing/trace_pipe
虽然上面的程序是死循环,但测试后发现,程序不是真的死循环,会在looper中执行34次后结束。验证函数热更新功能。
-
- <idle>-0 [007] ..s. 443032.404122: 0: skb cb: 29
- <idle>-0 [007] ..s. 443032.404122: 0: skb cb: 30
- <idle>-0 [007] ..s. 443032.404123: 0: skb cb: 31
- <idle>-0 [007] ..s. 443032.404123: 0: skb cb: 32
- <idle>-0 [007] ..s. 443032.404123: 0: skb cb: 33
- <idle>-0 [007] ..s. 443032.404124: 0: skb end looper: cb: 34
- <idle>-0 [007] ..s. 443032.404146: 0: skb cb: 0
- <idle>-0 [007] ..s. 443032.404148: 0: skb cb: 1
- <idle>-0 [007] ..s. 443032.404149: 0: skb cb: 2
- <idle>-0 [007] ..s. 443032.404149: 0: skb cb: 3
- <idle>-0 [007] ..s. 443032.404150: 0: skb cb: 4
在之前的源代码中,添加如下函数:
-
- __section("newloop")
- int newlooper(struct __sk_buff *skb)
- {
- printk("skb end in new looper: cb: %u\n", skb->cb[0]);
- return TC_ACT_OK;
- }
-
- # build
- clang -g -c -O2 -target bpf -c tail.c -o tail.o
-
- # update func
- tc exec bpf graft m:globals/jmp_map key 0 obj tail.o sec newloop
- # 特别注意:由于centos8自带的tc版本太低,无法更新,需要使用高版本的tc.可使用cilium自带的tc
- # docker run -it --name=mytest --network=host --privileged -v $PWD:/hosts/ -v /sys/fs/bpf:/sys/fs/bpf -v /run/cilium/cgroupv2/:/run/cilium/cgroupv2 cilium:v1.12.7 bash 进入容器后,执行上面的命令
-
- # 结果检查
- cat /sys/kernel/debug/tracing/trace_pipe
send_trace_notify –> ctx_event_output –> skb_event_outputevent_output
-
- // 调用样例
- send_trace_notify(ctx, TRACE_FROM_LXC, SECLABEL, 0, 0, 0,
- TRACE_REASON_UNKNOWN, TRACE_PAYLOAD_LEN);
-
- // 函数定义
- send_trace_notify(struct __ctx_buff *ctx, enum trace_point obs_point,
- __u32 src, __u32 dst, __u16 dst_id, __u32 ifindex,
- enum trace_reason reason, __u32 monitor)
-
- msg = (typeof(msg)) {
- __notify_common_hdr(CILIUM_NOTIFY_TRACE, obs_point), // 事件大类:CILIUM_NOTIFY_TRACE,子类:obs_point
- __notify_pktcap_hdr(ctx_len, (__u16)cap_len),
- .src_label = src, // 源对象id cilium identity list, identity 全局唯一
- .dst_label = dst, // 目的对象id
- .dst_id = dst_id, // 不同的子类,id取值不同
- .reason = reason, // 原因标识
- .ifindex = ifindex, // 不同的子类,取值类型不同
- };
-
- ctx_event_output(ctx, &EVENTS_MAP,
- (cap_len << 32) | BPF_F_CURRENT_CPU,
- &msg, sizeof(msg));
上报的信息,就是harbor展示的数据来源。
发送的信息,会放在ring buffer中。如果未读取,会进行覆盖写入。
这个buffer是cpu级别的,每个cpu中都会有一个独立的。
读取时,需要指定读取哪个cpu,-1表示读取所有cpu中的信息。
读取event的具体细节,可参考:
https://man7.org/linux/manpages/man2/perf_event_open.2.html
下面为cilium使用go实现了perf读取库的使用说明:"github.com/cilium/ebpf/perf"
cilium目前的信息结构说明:
cilium monitor可以读取perf信息,并且能基于identity进行过滤,具体使用可参考该命令说明。
-
- var err error
-
- path := oldBPF.MapPath(signalmap.MapName)
- signalMap, err := ebpf.LoadPinnedMap(path, nil)
- if err != nil {
- log.WithError(err).Warningf("Failed to open signals map")
- return
- }
- events, err = perf.NewReader(signalMap, os.Getpagesize())
- if err != nil {
- log.WithError(err).Warningf("Cannot open %s map! Ignoring signals!",
- signalmap.MapName)
- return
- }
-
- go func() {
- log.Info("Datapath signal listener running")
- for {
- record, err := events.Read()
- switch {
- case err != nil:
- signalCollectMetrics(nil, "error")
- log.WithError(err).WithFields(logrus.Fields{
- logfields.BPFMapName: signalmap.MapName,
- }).Errorf("failed to read event")
- case record.LostSamples > 0:
- signalCollectMetrics(nil, "lost")
- default:
- signalReceive(&record)
- }
- }
- }()
消息结构如下:
总体分两大类:
lost count,用于记数:数据包丢弃数。
详细报文说明。
各种sample数据格式说明。
cilium monitor使用样例。
-
- # 使用cilium monitor查看网络策略执行情况:
- # remoteID 表示针对对象
- # ingress/egress表示入与出流量
- # action表示策略结果
- cilium monitor -t policy-verdict
- Listening for events on 8 CPUs with 64x4096 of shared memory
- Press Ctrl-C to quit
- level=info msg="Initializing dissection cache..." subsys=monitor
- Policy verdict log: flow 0x95e0951 local EP ID 2467, remote ID kube-apiserver, proto 6, ingress, action allow, auth: disabled, match L3-L4, 172.18.0.6:46866 -> 172.18.0.8:2380 tcp SYN
- Policy verdict log: flow 0x580738e1 local EP ID 2467, remote ID 16777218, proto 6, ingress, action allow, auth: disabled, match L3-L4, 172.18.0.2:40122 -> 172.18.0.8:6443 tcp SYN
- Policy verdict log: flow 0x4dfe98a0 local EP ID 2467, remote ID kube-apiserver, proto 6, ingress, action allow, auth: disabled, match L3-L4, 172.18.0.4:52314 -> 172.18.0.8:2380 tcp SYN
- Policy verdict log: flow 0xb3af0a31 local EP ID 2467, remote ID kube-apiserver, proto 6, egress, action allow, auth: disabled, match L3-L4, 172.18.0.8:42736 -> 172.18.0.6:2380 tcp SYN
- Policy verdict log: flow 0xfdb19557 local EP ID 2467, remote ID kube-apiserver, proto 6, egress, action allow, auth: disabled, match L3-L4, 172.18.0.8:46246 -> 172.18.0.4:2380 tcp SYN
- Policy verdict log: flow 0x5438a30a local EP ID 2467, remote ID 16777218, proto 6, ingress, action allow, auth: disabled, match L3-L4, 172.18.0.2:40164 -> 172.18.0.8:6443 tcp SYN
endpoint id,可通过这个方式查到。
跟踪只允许基于endpointID进行跟踪,无法基于ip、端口等信息,当网络流量在多个节点中流转时,无法有效的进行跟踪。
这一块可基于数据分析组件来实现,它可以导出json结构数据,采集统一汇总分析统计。
1.访问入口与目标pod在同节点上,ip nat情况:
2.访问入口与目标pod在同节点上,流量跟踪情况:
可以看到,源端口在网络流转中是不变的,可以基于此作为跟踪线索。
可基于es+kibana,将cilium trace相关数据统一在es中存储,并基于kibana展示,如下图所示:
红色框分别为源ip、目标ip、源端口、目标端口。而特殊的flb_host_ip是坐着在采集时添加的节点主机的ip,用来分析不同主机间的流量。
作者:沃趣科技产品研发部
更多技术干货请关注公号【云原生数据库】
squids.cn,云数据库RDS,迁移工具DBMotion,云备份DBTwin等数据库生态工具。
irds.cn,多数据库管理平台(私有云)。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。