likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 10

 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开始,详细的可参考:arthurchiao.art/blog/unders…

这里只讲解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的入参为这个结构,可以通过这个结构,直接获取信息。

这个结构体的定义可以参考:elixir.bootlin.com/linux/v4.18…

下面展示基于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实现了,可参考:docs.cilium.io/en/v1.13/ne…

4. 环境清理

tc qdisc del dev enp1s0 root

本文来自沃趣科技产品研发部专家:李龙辉

更多技术干货请关注公号【云原生数据库】

Squids.cn,基于公有云基础资源,提供云上RDS、云备份、云迁移、SQL窗口等门户企业功能,帮助企业快速构建云上数据库融合生态。