暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

TC网络带宽控制(包含与ebpf结合方案)

沃趣技术 2023-07-07
490

沃趣技术

关注我们

1.背景

实验环境为centos8。

tc是一个很强大的网络管理工具,比如增加网络延迟、带宽流量管理等,本篇将基于流量管理来详细讲解下相关功能。


tc主要有三种类型

  1. qdisc队列,用来存放需要处理的数据包。基于队列类型有相应的规则,可以通过man tc来查看。

  2. class: 类别,比如针对流量的htb qdisc,也会有很多种类别。比如限制 100m带宽的,1000m带宽的,他们属于不同的类别。类别必需要挂靠在qdisc 上。

  3. filter过滤器,filter同样需要挂靠在qdisc上,由filter定义数据包对应哪个class,并应用相应的规则。

每个网卡,默认会有一个根的qdisc,类型为:fq_codel,当我们添加其它qdisc时,这个默认的会自动被替换。由于ebpf的引入,qdisc还有一个特殊的根,可通过tc add qdisc dev eth0 clsact添加,这个是针对ebpf的da功能而添加的,从linux内核4.4开始,详细的可参考(链接)[https://arthurchiao.art/blog/understanding-tc-da-mode-zh/]


这里只讲解root的qdisc的使用。

tc的规则,以名为root的qdisc为根,基于filter进行分类,并应用对应class下的规则。而class又可以再继续挂载qdisc,而这个子qdisc又可以挂载class与filter。就这样形成了一棵树形结构,而这个树形结构上的每个元素都有一个唯一的id标识。


根root的id为1:,对应的16进制为0x10000,而后面新添加的元素,则由用户指定id



2.基于tc实现流量管理

1.给根的qdisc规则设置为htb(如果需要修改,需要先将原有的删除,再添加;如果是同规则不同参数,则可以直接用replace)。

    tc qdisc add dev enp1s0 root handle 1: htb
    复制


    2.基于根,添加两个class,分别对应两种不同的限速方案。

      tc class add dev enp1s0 parent 1: classid 1:1 htb rate 1mbit prio 0
      tc class add dev enp1s0 parent 1: classid 1:2 htb rate 2mbit prio 0
      复制


      3.基于class 1:1添加子qdisc,并添加限速方案。

        tc qdisc add dev enp1s0 parent 1:1 handle 2: htb 
        tc class add dev enp1s0 parent 2: classid 2:1 htb rate 3mbit prio 0
        # 同样的格式
        tc qdisc add dev enp1s0 parent 2:1 handle 3: htb
        tc class add dev enp1s0 parent 3: classid 3:1 htb rate 4mbit prio 0
        复制


        4.添加filter规则,用于指定流量分类规则。

          tc filter add dev enp1s0 protocol ip parent 1: prio 1 u32 match ip dst 10.10.40.25/32 flowid 1:2
          复制

          这里是基于目的ip进行的分类。最后的flowid,指定的是它将采用哪个class进行策略管理。


          需要注意的是,虽然这里创建的三个qdisc存在父子关系,但不是说一定要从上往下应用下来。比如下面这种分类也是可行的。

            tc filter add dev enp1s0 protocol ip parent 1: prio 1 u32 match ip dst 10.10.40.25/32 flowid 3:1
            复制


            这种直接从最上层跳到下层,也是可行的。只要规则到达了叶子节点,则这个分类结束。


            5.结果验证

            可通过scp复制文件,检查流量是否被限制。

              scp ../kernel_4.18_el8.tgz root@10.10.40.25:/tmp/
              kernel_4.18_el8.tgz                                           2% 5808KB   233KB/s   01:30
              复制


              由于这里限制的是2m,对应KB需要除以8,就在256KB的左右。



              3. 与ebpf功能结合

              tc的功能很强大,同时也提供了很多种filter功能,可通过man tc-ematch或者man tc-u32来查看各种匹配规则。


              使用ebpf的好处:struct __sk_buff *skb ebpf的入参为这个结构,可以通过这个结构,直接获取信息。这个结构体的定义可以参考:https://elixir.bootlin.com/linux/v4.18/source/include/uapi/linux/bpf.h#L2238


              下面展示基于ebpf程序来设置tc_classid,实现流量控制功能。


              bandwidth_limit.c 里面有些未使用的变量与注释,是为了方便调试。printk的输出,可以通过cat sys/kernel/debug/tracing/trace_pipe查看。

                #include <unistd.h>
                #include <linux/bpf.h>
                #include <linux/pkt_cls.h>
                #include <stdint.h>
                #include <stddef.h>
                #include <iproute2/bpf_elf.h>
                #include <linux/if_ether.h>
                #include <linux/ip.h>
                #include <linux/tcp.h>
                #include <linux/string.h>
                #include <arpa/inet.h>


                #define bpf_ntohs(x)            __builtin_bswap16(x)
                #define bpf_htons(x)            __builtin_bswap16(x)
                #define bpf_ntohl(x)            __builtin_bswap32(x)
                #define bpf_htonl(x)            __builtin_bswap32(x)


                #ifndef __section
                #define __section(NAME)                  \
                  __attribute__((section(NAME), used))
                #endif


                #ifndef __inline
                #define __inline                         \
                  inline __attribute__((always_inline))
                #endif


                #ifndef offsetof
                #define offsetof(TYPE, MEMBER) ((size_t) & ((TYPE *)0)->MEMBER)
                #endif


                #ifndef BPF_FUNC
                # define BPF_FUNC(NAME, ...)              \
                  (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME
                #endif


                static void *BPF_FUNC(map_lookup_elem, void *map, const void *key);
                static void BPF_FUNC(trace_printk, const char *fmt, int fmt_size, ...);
                static long (*bpf_skb_load_bytes)(const struct __sk_buff *, __u32,
                                                  void *, __u32) =
                       (void *) BPF_FUNC_skb_load_bytes;
                static long (*bpf_skb_store_bytes)(void *ctx, int off, void *from, int len, int flags) =
                       (void *) BPF_FUNC_skb_store_bytes;


                #ifndef printk
                # define printk(fmt, ...)                                      \
                   ({                                                         \
                       char ____fmt[] = fmt;                                  \
                       trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \
                   })
                #endif


                unsigned long long load_word(void *skb,
                                            unsigned long long off) asm("llvm.bpf.load.word");


                static __u64 BPF_FUNC(ktime_get_ns);


                #ifndef __READ_ONCE
                # define __READ_ONCE(X)         (*(volatile typeof(X) *)&X)
                #endif


                #ifndef __WRITE_ONCE
                # define __WRITE_ONCE(X, V)     (*(volatile typeof(X) *)&X) = (V)
                #endif


                static __inline unsigned int set_bandwidth(struct __sk_buff *skb)
                {
                 __u32 proto;
                 __u64 delay, now, t, t_next;
                 __u64 ret;


                 proto = skb->protocol;
                 if (proto != bpf_htons(ETH_P_IP) &&
                     proto != bpf_htons(ETH_P_IPV6))
                   return 0;


                 void *data = (void *)(long)skb->data;
                 void *data_end = (void *)(long)skb->data_end;


                 if (data + sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct tcphdr) > data_end) {
                   return 0;
                 }


                 struct tcphdr *tcph = data + sizeof(struct ethhdr) + sizeof(struct iphdr);
                 unsigned long long daddr = load_word(skb, ETH_HLEN + offsetof(struct iphdr, daddr));
                 //unsigned long long saddr = load_word(skb, ETH_HLEN + offsetof(struct iphdr, saddr));
                 uint16_t dstPortNumber = ntohs(tcph->dest);


                 //if (dstPortNumber != 60443)
                 //  return 0;
                   
                 if (daddr != 0x0a0a2819)  // 10.10.40.25
                   return 0;
                   
                 //printk("get classid ok %x", skb->tc_classid);
                 //skb->tc_classid=0x10001;


                 //printk("set classid ok");
                 return  0x10002;
                }


                __section("bandwidth")
                unsigned int tc_bandwidth(struct __sk_buff *skb)
                {
                 return set_bandwidth(skb);
                }


                char __license[] __section("license") = "GPL";
                复制


                编译c : 编译环境所需要的依赖为:yum install -y gcc ncurses-devel elfutils-libelf-devel bc openssl-devel libcap-devel clang llvm graphviz bison flex glibc-devel make。

                  clang -O2 -Wall -target bpf  -c bandwidth_limit.c -o bandwidth_limit.o
                  复制


                  加载filter。

                    tc filter replace dev enp1s0 parent 1: prio 1 handle 1 bpf obj bandwidth_limit.o sec bandwidth
                    复制


                    通过ebpf程序返回的值,可以实现filter中设置classid的目的,这样就可以与tc的功能相结合起来。

                    当然,从内核5.1开始,可以通过edt的方式实现源生的ebpf流量控制,而老版本还仍然需要依赖tc。由于ebpf程序的引入,可以通过ebpf map实现用户态与内核态的数据交互,而map数据结构则相比tc的规则更加直观,也更加好管理,cilium已经基于edt实现了,可参考:https://docs.cilium.io/en/v1.13/network/kubernetes/bandwidth-manager/


                    4. 环境清理

                      tc qdisc del dev enp1s0 root
                      复制


                      文章转载自沃趣技术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                      评论