1.背景
实验环境为centos8。
tc是一个很强大的网络管理工具,比如增加网络延迟、带宽流量管理等,本篇将基于流量管理来详细讲解下相关功能。
tc主要有三种类型:
每个网卡,默认会有一个根的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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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窗口等门户企业功能,帮助企业快速构建云上数据库融合生态。