当前位置:   article > 正文

cilium原理之ebpf尾调用与trace

cilium原理之ebpf尾调用与trace

背景

在深入剖析cilium原理之前,有两个关于epbf的基础内容需要先详细介绍一下:

1. ebpf尾调用

尾调用类似于程序之间的相互跳转,但它的功能更加强大。

2. trace

虽然之前使用trace_printk输出日志,但这个函数不能多用,会有性能问题。而且它的输出可读性差,不利于程序进行分析。本文将着重讲讲如何进行分析,方便程序后续跟踪。

1.ebpf尾调用介绍

尾调用能够从一个程序调到另一个程序,提供了在运行时(runtime)原子地改变程序行为的灵活性。为了选择要跳转到哪个程序,尾调用使用了程序数组map( BPF_MAP_TYPE_PROG_ARRAY),将map及其索引(index)传递给将要跳转到的程序。跳转动作一旦完成,就没有办法返回到原来的程序;但如果给定的map索引中没有程序(无法跳转),执行会继续在原来的程序中执行。

特殊在于,由于函数的调用是由map来保存的,可以使用TC更新map对应的函数为新的函数,实现ebpf程序的局部更新。当然,这就对功能编码也会有更高的要求,需要明确更新可能的影响范围。因为ebpf的单个程序大小是有限制的,而尾调用可以绕开这种限制。

以下是摘自cilium官方文档中的样例:(官方样例无法编译,下面摘取的内容经过作者调整)

  1. #include <bits/types.h>
  2. #include <linux/bpf.h>
  3. #include <linux/if_ether.h>
  4. #include <linux/in.h>
  5. #include <linux/ip.h>
  6. #include <linux/pkt_cls.h>
  7. #include <linux/tcp.h>
  8. #include <errno.h>
  9. #include <stdio.h>
  10. #include <string.h>
  11. typedef __uint32_t uint32_t;
  12. #define PIN_GLOBAL_NS 2
  13. struct bpf_elf_map {
  14. __u32 type;
  15. __u32 size_key;
  16. __u32 size_value;
  17. __u32 max_elem;
  18. __u32 flags;
  19. __u32 id;
  20. __u32 pinning;
  21. };
  22. #ifndef __stringify
  23. # define __stringify(X) #X
  24. #endif
  25. #ifndef __section
  26. # define __section(NAME) \
  27. __attribute__((section(NAME), used))
  28. #endif
  29. #ifndef __section_tail
  30. # define __section_tail(ID, KEY) \
  31. __section(__stringify(ID) "/" __stringify(KEY))
  32. #endif
  33. #ifndef BPF_FUNC
  34. # define BPF_FUNC(NAME, ...) \
  35. (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME
  36. #endif
  37. #define BPF_JMP_MAP_ID 1
  38. static int (*bpf_trace_printk)(const char *fmt, int fmt_size,
  39. ...) = (void *)BPF_FUNC_trace_printk;
  40. #define printk(fmt, ...) \
  41. do { \
  42. char _fmt[] = fmt; \
  43. bpf_trace_printk(_fmt, sizeof(_fmt), ##__VA_ARGS__); \
  44. } while (0)
  45. static void BPF_FUNC(tail_call, struct __sk_buff *skb, void *map,
  46. uint32_t index);
  47. struct bpf_elf_map jmp_map __section("maps") = {
  48. .type = BPF_MAP_TYPE_PROG_ARRAY,
  49. .id = BPF_JMP_MAP_ID,
  50. .size_key = sizeof(uint32_t),
  51. .size_value = sizeof(uint32_t),
  52. .pinning = PIN_GLOBAL_NS,
  53. .max_elem = 1,
  54. };
  55. __section_tail(BPF_JMP_MAP_ID, 0)
  56. int looper(struct __sk_buff *skb)
  57. {
  58. printk("skb cb: %u\n", skb->cb[0]++);
  59. tail_call(skb, &jmp_map, 0);
  60. printk("skb end looper: cb: %u\n", skb->cb[0]);
  61. return TC_ACT_OK;
  62. }
  63. __section("prog")
  64. int entry(struct __sk_buff *skb)
  65. {
  66. skb->cb[0] = 0;
  67. tail_call(skb, &jmp_map, 0);
  68. printk("skb end: cb: %u\n", skb->cb[0]);
  69. return TC_ACT_OK;
  70. }
  71. 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时指定。

功能测试演示

  1. # 将上面文件保存为tail.c
  2. # build
  3. clang -g -c -O2 -target bpf -c tail.c -o tail.o
  4. # load
  5. tc qdisc add dev enp1s0 ingress
  6. tc filter replace dev enp1s0 ingress prio 1 handle 1 bpf da obj tail.o sec prog verbose
  7. # cat log
  8. cat /sys/kernel/debug/tracing/trace_pipe

虽然上面的程序是死循环,但测试后发现,程序不是真的死循环,会在looper中执行34次后结束。验证函数热更新功能。

  1. <idle>-0 [007] ..s. 443032.404122: 0: skb cb: 29
  2. <idle>-0 [007] ..s. 443032.404122: 0: skb cb: 30
  3. <idle>-0 [007] ..s. 443032.404123: 0: skb cb: 31
  4. <idle>-0 [007] ..s. 443032.404123: 0: skb cb: 32
  5. <idle>-0 [007] ..s. 443032.404123: 0: skb cb: 33
  6. <idle>-0 [007] ..s. 443032.404124: 0: skb end looper: cb: 34
  7. <idle>-0 [007] ..s. 443032.404146: 0: skb cb: 0
  8. <idle>-0 [007] ..s. 443032.404148: 0: skb cb: 1
  9. <idle>-0 [007] ..s. 443032.404149: 0: skb cb: 2
  10. <idle>-0 [007] ..s. 443032.404149: 0: skb cb: 3
  11. <idle>-0 [007] ..s. 443032.404150: 0: skb cb: 4

在之前的源代码中,添加如下函数:

  1. __section("newloop")
  2. int newlooper(struct __sk_buff *skb)
  3. {
  4. printk("skb end in new looper: cb: %u\n", skb->cb[0]);
  5. return TC_ACT_OK;
  6. }
  1. # build
  2. clang -g -c -O2 -target bpf -c tail.c -o tail.o
  3. # update func
  4. tc exec bpf graft m:globals/jmp_map key 0 obj tail.o sec newloop
  5. # 特别注意:由于centos8自带的tc版本太低,无法更新,需要使用高版本的tc.可使用cilium自带的tc
  6. # 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 进入容器后,执行上面的命令
  7. # 结果检查
  8. cat /sys/kernel/debug/tracing/trace_pipe

2. send_trace_notify 跟踪

send_trace_notify –> ctx_event_output –> skb_event_outputevent_output

  1. // 调用样例
  2. send_trace_notify(ctx, TRACE_FROM_LXC, SECLABEL, 0, 0, 0,
  3. TRACE_REASON_UNKNOWN, TRACE_PAYLOAD_LEN);
  4. // 函数定义
  5. send_trace_notify(struct __ctx_buff *ctx, enum trace_point obs_point,
  6. __u32 src, __u32 dst, __u16 dst_id, __u32 ifindex,
  7. enum trace_reason reason, __u32 monitor)
  8. msg = (typeof(msg)) {
  9. __notify_common_hdr(CILIUM_NOTIFY_TRACE, obs_point), // 事件大类:CILIUM_NOTIFY_TRACE,子类:obs_point
  10. __notify_pktcap_hdr(ctx_len, (__u16)cap_len),
  11. .src_label = src, // 源对象id cilium identity list, identity 全局唯一
  12. .dst_label = dst, // 目的对象id
  13. .dst_id = dst_id, // 不同的子类,id取值不同
  14. .reason = reason, // 原因标识
  15. .ifindex = ifindex, // 不同的子类,取值类型不同
  16. };
  17. ctx_event_output(ctx, &EVENTS_MAP,
  18. (cap_len << 32) | BPF_F_CURRENT_CPU,
  19. &msg, sizeof(msg));

上报的信息,就是harbor展示的数据来源。

信息说明    

  1. 发送的信息,会放在ring buffer中。如果未读取,会进行覆盖写入。

  2. 这个buffer是cpu级别的,每个cpu中都会有一个独立的。

  3. 读取时,需要指定读取哪个cpu,-1表示读取所有cpu中的信息。

  4. 读取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进行过滤,具体使用可参考该命令说明。

  1. var err error
  2. path := oldBPF.MapPath(signalmap.MapName)
  3. signalMap, err := ebpf.LoadPinnedMap(path, nil)
  4. if err != nil {
  5. log.WithError(err).Warningf("Failed to open signals map")
  6. return
  7. }
  8. events, err = perf.NewReader(signalMap, os.Getpagesize())
  9. if err != nil {
  10. log.WithError(err).Warningf("Cannot open %s map! Ignoring signals!",
  11. signalmap.MapName)
  12. return
  13. }
  14. go func() {
  15. log.Info("Datapath signal listener running")
  16. for {
  17. record, err := events.Read()
  18. switch {
  19. case err != nil:
  20. signalCollectMetrics(nil, "error")
  21. log.WithError(err).WithFields(logrus.Fields{
  22. logfields.BPFMapName: signalmap.MapName,
  23. }).Errorf("failed to read event")
  24. case record.LostSamples > 0:
  25. signalCollectMetrics(nil, "lost")
  26. default:
  27. signalReceive(&record)
  28. }
  29. }
  30. }()

消息结构如下:

总体分两大类:

  1. lost count,用于记数:数据包丢弃数。

  2. 详细报文说明。

各种sample数据格式说明。

cilium monitor使用样例。

  1. # 使用cilium monitor查看网络策略执行情况:
  2. # remoteID 表示针对对象
  3. # ingress/egress表示入与出流量
  4. # action表示策略结果
  5. cilium monitor -t policy-verdict
  6. Listening for events on 8 CPUs with 64x4096 of shared memory
  7. Press Ctrl-C to quit
  8. level=info msg="Initializing dissection cache..." subsys=monitor
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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,多数据库管理平台(私有云)。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/136937
推荐阅读
相关标签
  

闽ICP备14008679号