Published at 2022-10-30 | Last Update 2022-10-30
本文翻译自 KubeCon+CloudNativeCon Europe 2022 的一篇分享: Better Bandwidth Management with eBPF。
作者 Daniel Borkmann, Christopher, Nikolay 都来自 Isovalent(Cilium 母公司)。 翻译时补充了一些背景知识、代码片段和链接,以方便理解。
由于译者水平有限,本文不免存在遗漏或错误之处。如有疑问,请查阅原文。
以下是译文。
下面两张图来自 Sysdig 2022 的一份调研报告,
Source: Sysdig 2022 Cloud Native Security and Usage Report
这两个图说明:容器的部署密度越来越高。这导致的 CPU、内存等资源竞争将更加激烈, 如何管理资源的分配或配额就越来越重要。具体到 CPU 和 memory 这两种资源, K8s 提供了 resource requests/limits 机制,用户或管理员可以指定一个 Pod 需要用到的资源量(requests)和最大能用的资源量(limits),
apiVersion: v1
kind: Pod
metadata:
name: frontend
spec:
containers:
- name: app
image: nginx-slim:0.8
resources:
requests: # 容器需要的资源量,kubelet 会将 pod 调度到剩余资源大于这些声明的 node 上去
memory: "64Mi"
cpu: "250m"
limits: # 容器能使用的硬性上限(hard limit),超过这个阈值容器就会被 OOM kill
memory: "128Mi"
cpu: "500m"
kube-scheduler
会将 pod 调度到能满足 resource.requests
声明的资源需求的 node 上;这种针对 CPU 和 memory 的资源管理机制还是不错的, 那么,网络方面有没有类似的机制呢?
先回顾下基础的网络知识。 下图是往返时延(Round-Trip)与 TCP 拥塞控制效果之间的关系,
结合 流量控制(TC)五十年:从基于缓冲队列(Queue)到基于时间戳(EDT)的演进(Google, 2018), 这里只做几点说明:
现在回到我们刚才提出的问题(k8s 网络资源管理), 在 K8s 中,有什么机制能限制 pod 的网络资源(带宽)使用量吗?
K8s 自带了一个限速(bandwidth enforcement)机制,但到目前为止还是 experimental 状态; 实现上是通过第三方的 bandwidth meta plugin,它会解析特定的 pod annotation,
kubernetes.io/ingress-bandwidth=XX
kubernetes.io/egress-bandwidth=XX
然后转化成对 pod 的具体限速规则,如下图所示,
Fig. Bandwidth meta plugin 解析 pod annotation,并通过 TC TBF 实现限速
bandwidth meta plugin 是一个 CNI plugin,底层利用 Linux TC 子系统中的 TBF, 所以最后转化成的是 TC 限速规则,加在容器的 veth pair 上(宿主机端)。
这种方式确实能实现 pod 的限速功能,但也存在很严重的问题,我们来分别看一下出向和入向的工作机制。
在进入下文之前,有两点重要说明:
- 限速只能在出向(egress)做。为什么?可参考 《Linux 高级路由与流量控制手册(2012)》第九章:用 tc qdisc 管理 Linux 网络带宽;
- veth pair 宿主机端的流量方向与 pod 的流量方向完全相反,也就是 pod 的 ingress 对应宿主机端 veth 的 egress,反之亦然。
译注。
对于 pod ingress 限速,需要在宿主机端 veth 的 egress 路径上设置规则。
例如,对于入向 kubernetes.io/ingress-bandwidth="50M"
的声明,会落到 veth 上的 TBF qdisc 上:
TBF(Token Bucket Filter)是个令牌桶,所有连接/流量都要经过单个队列排队处理,如下图所示:
在设计上存在的问题:
出向工作原理:
存在的问题:
pfifo_fast/fq_codel/noqueue
),现在又多了一层 ifb 设备排队,缓冲区膨胀(bufferbloat);总结起来:
因此不适用于生产环境;
这一节是介绍 Google 的基础性工作,作者引用了 Evolving from AFAP: Teaching NICs about time (Netdev, 2018) 中的一些内容;之前我们已翻译,见 流量控制(TC)五十年:从基于缓冲队列(Queue)到基于时间戳(EDT)的演进(Google, 2018), 因此一些内容不再赘述,只列一下要点。
译注。
Fig. 根据排队论,实际带宽接近瓶颈带宽时,延迟将急剧上升
两点核心转变:
Fig. 传统基于 queue 的流量整形器 vs. 新的基于 EDT 的流量整形器
有了这些技术基础,我们接下来看如何应用到 K8s。
Cilium 的 bandwidth manager,
在之前的分享 为 K8s workload 引入的一些 BPF datapath 扩展(LPC, 2021) 中已经有比较详细的介绍,这里在重新整理一下。
Cilium attach 到宿主机的物理网卡(或 bond 设备),在 BPF 程序中为每个包设置 timestamp, 然后通过 earliest departure time 在 fq 中实现限速,下图:
注意:容器限速是在物理网卡上做的,而不是在每个 pod 的 veth 设备上。这跟之前基于 ifb 的限速方案有很大不同。
Fig. Cilium 基于 BPF+EDT 的容器限速方案(逻辑架构)
从上到下三个步骤:
如果宿主机使用了 bond,那么根据 bond 实现方式的不同,FQ 的数量会不一样, 可通过
tc -s -d qdisc show dev {bond}
查看实际状态。具体来说,
- Linux bond 默认支持多队列(multi-queue),会默认创建 16 个 queue, 每个 queue 对应一个 FQ,挂在一个 MQ 下面,也就是上面图中画的;
- OVS bond 不支持 MQ,因此只有一个 FQ(v2.3 等老版本行为,新版本不清楚)。
bond 设备的 TXQ 数量,可以通过
ls /sys/class/net/{dev}/queues/
查看。 物理网卡的 TXQ 数量也可以通过以上命令看,但ethtool -l {dev}
看到的信息更多,包括了最大支持的数量和实际启用的数量。译注。
先复习下 Cilium datapath,细节见 2020 年的分享:
egress 限速工作流程:
skb->sk
不会丢失;skb->tstamp
;skb->tstamp
调度发包。过程中用到了 bpf map 存储 aggregate 信息。
netperf 压测。
同样限速 100M,延迟下降:
同样限速 100M,TPS:
主机内的问题解决了,那更大范围 —— 即公网带宽 —— 管理呢?
别着急,EDT 还能支持 BBR。
想完整了解 BBR 的设计,可参考 (论文) BBR:基于拥塞(而非丢包)的拥塞控制(ACM, 2017)。 译注。
CUBIC + fq_codel:
BBR + FQ (for EDT):
效果非常明显。
skb->tstamp
要被重置BBR 能不能用到 k8s 里面呢?
问题如下图所示,
下面介绍一些背景,为什么这个 ts 会被重置。
几种时间规范:https://www.cl.cam.ac.uk/~mgk25/posix-clocks.html
对于包的时间戳 skb->tstamp
,内核根据包的方向(RX/TX)不同而使用的两种时钟源:
如果不重置,将包从 RX 转发到 TX 会导致包在 FQ 中被丢弃,因为
超过 FQ 的 drop horizon。
FQ horizon
默认是 10s。
horizon
是 FQ 的一个配置项,表示一个时间长度, 在 net_sched: sch_fq: add horizon attribute 引入,QUIC servers would like to use SO_TXTIME, without having CAP_NET_ADMIN, to efficiently pace UDP packets. As far as sch_fq is concerned, we need to add safety checks, so that a buggy application does not fill the qdisc with packets having delivery time far in the future. This patch adds a configurable horizon (default: 10 seconds), and a configurable policy when a packet is beyond the horizon at enqueue() time: - either drop the packet (default policy) - or cap its delivery time to the horizon.
简单来说,如果一个包的时间戳离现在太远,就直接将这个包 丢弃,或者将其改为一个上限值(cap),以便节省队列空间;否则,这种 包太多的话,队列可能会被塞满,导致时间戳比较近的包都无法正常处理。 内核代码如下:
static bool fq_packet_beyond_horizon(const struct sk_buff *skb, const struct fq_sched_data *q) { return unlikely((s64)skb->tstamp > (s64)(q->ktime_cache + q->horizon)); }
译注。
另外,现在给定一个包,我们无法判断它用的是哪种 timestamp,因此只能用这种 reset 方式。
skb->tstamp
统一到同一种时钟吗?其实最开始,TCP EDT 用的也是 CLOCK_TAI 时钟。 但有人在邮件列表 里反馈说,某些特殊的嵌入式设备上重启会导致时钟漂移 50 多年。所以后来 EDT 又回到了 monotonic 时钟,而我们必须跨 netns 时 reset。
我们做了个原型验证,新加一个 bit skb->tstamp_base
来解决这个问题,
然后,
skb_set_tstamp_{mono,tai}(skb, ktime)
helper 来获取这个值,fq_enqueue()
先检查 timestamp 类型,如果不是 MONO,就 reset skb->tstamp
此外,
skb->tstamp = 0
都可以删掉了net_timestamp_check()
必须推迟到 tc ingress 之后执行我们和 Facebook 的朋友合作,已经解决了这个问题,在跨 netns 时保留时间戳,
patch 并合并到了 kernel 5.18+
。
因此 BBR+EDT 可以工作了,
K8s/Cilium backed video streaming service: CUBIC vs. BBR
如果同一个环境(例如数据中心)同时启用了 BBR 和 CUBIC,那使用 BBR 的机器会强占更多的带宽,造成不公平(unfaireness);
BBR 会触发更高的 TCP 重传速率,这源自它更加主动或激进的探测机制 (higher TCP retransmission rate due to more aggressive probing);
BBRv2 致力于解决以上问题。
Cilium 的原生带宽限速功能(v1.12 GA)
Cilium 的限速功能我们 在 v1.10 就在用了,但是使用下来发现两个问题,到目前(2022.11)社区还没有解决,
启用 bandwidth manager 之后,Cilium 会 hardcode somaxconn、netdev_max_backlog 等内核参数,覆盖掉用户自己的内核调优;
例如,如果 node netdev_max_backlog=8192
,那 Cilium 启动之后,
就会把它强制覆盖成 1000,导致在大流量场景因为宿主机这个配置太小而出现丢包。
启用 bandwidth manager 再禁用之后,并不会恢复到原来的 qdisc 配置,MQ/FQ 是残留的,导致大流量容器被限流(throttle)。
例如,如果原来物理网卡使用的默认 pfifo_fast
qdisc,或者 bond 设备默认使用
的 noqueue
,那启用再禁用之后,并不会恢复到原来的 qdisc 配置。残留 FQ 的一
个副作用就是大流量容器的偶发网络延迟,因为 FQ 要保证 flow
级别的公平(而实际上很多场景下并不需要这个公平,总带宽不超就行了)。
查看曾经启用 bandwidth manager,但现在已经禁用它的 node,可以看到 MQ/FQ 还在,
$ tc qdisc show dev bond0
qdisc mq 8042: root
qdisc fq 0: parent 8042:10 limit 10000p flow_limit 100p buckets 1024 quantum 3028 initial_quantum 15140
qdisc fq 0: parent 8042:f limit 10000p flow_limit 100p buckets 1024 quantum 3028 initial_quantum 15140
...
qdisc fq 0: parent 8042:b limit 10000p flow_limit 100p buckets 1024 quantum 3028 initial_quantum 15140
是否发生过限流可以在 tc qdisc 统计中看到:
$ tc -s -d qdisc show dev bond0
qdisc fq 800b: root refcnt 2 limit 10000p flow_limit 100p buckets 1024 orphan_mask 1023 quantum 3028 initial_quantum 15140 refill_delay 40.0ms
Sent 1509456302851808 bytes 526229891 pkt (dropped 176, overlimits 0 requeues 0)
backlog 3028b 2p requeues 0
15485 flows (15483 inactive, 1 throttled), next packet delay 19092780 ns
2920858688 gc, 0 highprio, 28601458986 throttled, 6397 ns latency, 176 flows_plimit
6 too long pkts, 0 alloc errors
要恢复原来的配置,目前我们只能手动删掉 MQ/FQ。根据内核代码分析及实际测试,删除 qdisc 的操作是无损的,
$ tc qdisc del dev bond0 root
$ tc qdisc show dev bond0
qdisc noqueue 0: root refcnt 2
qdisc clsact ffff: parent ffff:fff1