作者:孙文杰

本文地址:https://www.ebpf.top/post/ebpf_network_exporter

公有云网络问题,是一个老生常谈的事情了。今天主要简单说明下,在内网环境下去分析 & 定位这个问题。也并不是一开始就需要 eBPF 加持的,可以从简单的基础工具到监控配合去相关比较快速的定位问题,解决问题。下面会从发现问题到一步步配合使用 Or 开发一些简单的东西去解决问题的大致思路。

1. 常见的网络相关问题

  • 业务负载不均衡:业务系统会越发的复杂,不可能单个服务承接所有流量,一定会逐步迭代为一组服务承接流量,那我们是可以先用一些负载均衡组件去解决这个问题(不推荐一上来就喊,我要自研,图啥呢?),可以使用 HaProxy、Nginx 配合公有云的弹性能力去解决问题。伴随着业务的发展慢慢的当全站使用 K8s 的时候,还会遇到 Kube-Proxy 存在负载不均衡的问题,这个时候就可以应入 Envoy 到最终上线 Istio 去解决问题,做到内部服务的负载均衡(当然服务的健康检查 & 优雅停止需要考虑下)。配合边缘公有云 LB 做入流量控制,到 Kong 这种非常自动化的网关来解决业务整个边缘路由的分流能力。至此 xLB + Kong/Openresty/Apisix/Envoy + Istio(Evnoy) 基本上已经到了天花板了。(应该还有更优的)
  • 网络异常抖动:日常会发生抖动的情况大致有几种情况吧。服务间异常、交换机过载、存储系统异常、业务系统 Bug,如上问题都需要专业的同学去帮助解决,并不能也不是每次都是网工的锅是吧。
  • 网络延时增大:抛开业务系统问题,Slow Query、Big Key、Hot Key、消费生产(消息队列)延时等问题的话。那只能朝着网络层面去找问题了。(你想,交换机里面也有 CPU)举一个例子,家里 Wifi 为什么没啥人用也会卡,有一个东西叫信道,就好比虽然有很多信道但是呢流量都在一个信道里面跑(1个人干活,一群人看)能不卡吗?(应该能解的,这个就不懂了,不懂不捣乱)

2. 如何定位

Istio

  • Istio 监控指标里面其实已经可以发现服务链路之间的调用的情况了,就是会发现为什么有些快有些慢呢,在排除 xDS(服务发现、路由下发等,Envoy API 与 Pilot 交互) 的问题后(其实没几家公司可以触发 Istio 的 Bug 的),依然慢的莫名其妙。

Ping

  • 这个是一个好东西哦,简单看看绝对富裕了。闲来无事基于 ping_exporter 加了一个 RESTful API,这样想 Ping 哪里 Ping 哪里,如果需要 K8s 里面互相 Ping 的话,其实只要依靠 K8s API 做自发现 Node 互 Ping 就能一目了然的发现很多问题了(原来网络真的很糟糕)

3. eBPF

  • eBPF(extended Berkeley Packet Filter) 是一种可以在 Linux 内核中运行用户编写的程序,而不需要修改内核代码或加载内核模块的技术。简单说,eBPF 让 Linux 内核变得可编程化了。eBPF 程序是一个事件驱动模型。Linux 内核提供了各种 hook point,比如 system calls, function entry/exit, kernel tracepoints, network events 等。eBPF 程序通过实现想要关注的 hook point 的 callback,并把这些 callback 注册到相应的 hook point 来完成“内核编程”。 Linux bcc/BPF Tracing Tools

  • bcc 是一个基于 eBPF 的工具包,内部包装了 LLVM。你可以用 C 语言 + Python、Lua 等高级语言编写 eBPF 程序,同时还提供了不少有用的工具。BCC是一个用于跟踪内核和操作程序的工具集,其软件包中包含了一些有用的工具和例子,它扩展了BPF(Berkeley Packet Filters),通常被称为eBPF , 在Linux3.15中首次引入,但大多数BCC的功能需要Libux4.1及以上版本。(可以话时间研究下下的,基础功能 & BPF 代码里面多有)

  • bpftrace 是一门基于 eBPF 的高级追踪语言,可以方便快捷地编写 Linux 追踪程序。

  • 工作原理:eBPF 通过 C 语言自由扩展(这些扩展通过 LLVM 转换为 BPF 字节码后,加载到内核中执行。从图中你可以看到,eBPF 的执行需要三步:从用户跟踪程序生成 BPF 字节码;加载到内核中运行;向用户空间输出结果。 BPF for Tracing

  • 实际上,在 eBPF 执行过程中,编译、加载还有 maps 等操作,对所有的跟踪程序来说都是通用的。把这些过程通过 Python 抽象起来,也就诞生了 BCC(BPF Compiler Collection)。

  • BCC 把 eBPF 中的各种事件源(比如 kprobe、uprobe、tracepoint 等)和数据操作(称为 Maps),也都转换成了 Python 接口(也支持 lua)。这样,使用 BCC 进行动态追踪时,编写简单的脚本就可以了。

  • 不过要注意,因为需要跟内核中的数据结构交互,真正核心的事件处理逻辑,还是需要我们用 C 语言来编写。

cloudflare/ebpf_exporter

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
programs:
  # See:
  # * https://github.com/iovisor/bcc/blob/master/tools/tcpconnect.py
  # * https://github.com/iovisor/bcc/blob/master/tools/tcpconnlat_example.txt
  - name: tcpconnlat
    metrics:
      counters:
        - name: tcpconnlat_ipv4
          help: tcp4 connection latency(ms)
          table: ipv4_events
          labels:
            - name: pid
              size: 4
              decoders:
                - name: uint
            - name: sst_addr
              size: 4
              decoders:
                - name: inet_ip
            - name: dst_addr
              size: 4
              decoders:
                - name: inet_ip
            - name: lst_port
              size: 2
              decoders:
                - name: uint
            - name: dst_port
              size: 2
              decoders:
                - name: uint
            - name: proc_name
              size: 16
              decoders:
                - name: string
    kprobes:
      tcp_rcv_state_process: trace_tcp_rcv_state_process
      tcp_v4_connect: trace_connect
    code: |
      #include <uapi/linux/ptrace.h>
      #include <net/sock.h>
      #include <net/tcp_states.h>
      #include <bcc/proto.h>

      struct info_t {
          u64 ts;
          u32 pid;
          char task[TASK_COMM_LEN];
      };
      BPF_HASH(start, struct sock *, struct info_t);

      // separate data structs for ipv4
      struct ipv4_data_t {
          u32 pid;
          u32 saddr;
          u32 daddr;
          u16 lport;
          u16 dport;
          char task[TASK_COMM_LEN];
      };
      BPF_HASH(ipv4_events, struct ipv4_data_t);

      int trace_connect(struct pt_regs *ctx, struct sock *sk)
      {
          u32 pid = bpf_get_current_pid_tgid();
          struct info_t info = {.pid = pid};
          info.ts = bpf_ktime_get_ns();
          bpf_get_current_comm(&info.task, sizeof(info.task));
          start.update(&sk, &info);
          return 0;
      };

      // See tcp_v4_do_rcv(). So TCP_ESTBALISHED and TCP_LISTEN
      // are fast path and processed elsewhere, and leftovers are processed by
      // tcp_rcv_state_process(). We can trace this for handshake completion.
      // This should all be switched to static tracepoints when available.
      int trace_tcp_rcv_state_process(struct pt_regs *ctx, struct sock *skp)
      {
          // will be in TCP_SYN_SENT for handshake
          if (skp->__sk_common.skc_state != TCP_SYN_SENT)
              return 0;
          // check start and calculate delta
          struct info_t *infop = start.lookup(&skp);
          if (infop == 0) {
              return 0;   // missed entry or filtered
          }
          u64 ts = infop->ts;
          u64 now = bpf_ktime_get_ns();
          u64 delta_us = (now - ts) / 1000ul;
          u64 delta_ms = delta_us / 1000;
      #ifdef MIN_LATENCY
          if ( delta_us < DURATION_US ) {
              return 0; // connect latency is below latency filter minimum
          }
      #endif

          // pull in details
          u16 family = 0, lport = 0, dport = 0;
          family = skp->__sk_common.skc_family;
          lport = skp->__sk_common.skc_num;
          dport = skp->__sk_common.skc_dport;

          // emit to appropriate data path
          if (family == AF_INET) {
              struct ipv4_data_t data4 = {.pid = infop->pid};
              data4.saddr = skp->__sk_common.skc_rcv_saddr;
              data4.daddr = skp->__sk_common.skc_daddr;
              data4.lport = lport;
              data4.dport = ntohs(dport);
              __builtin_memcpy(&data4.task, infop->task, sizeof(data4.task));
              ipv4_events.increment(data4, delta_ms);
          }
          start.delete(&skp);
          return 0;
      }      

基于上面一轮干下来,基本上哪里慢,为什么慢,怎么慢也就差不多定位了。剩下么,该干嘛干嘛。记得调用链还是非常有用的哦,配合网络监测 + 调用链,问题自然而然的就暴露啦(当然调用链接入姿势是另外一个问题啦)

最后推荐几本书吧,BPF之巅:洞悉Linux系统和应用性能 & Linux内核观测技术BPF。

原文地址:基于 eBPF 的网络检测实践