译者序
本文翻译自 KubeCon+CloudNativeCon Europe 2022 的一篇分享:Better Bandwidth Management with eBPF。
作者 Daniel Borkmann, Christopher, Nikolay 都来自 Isovalent(Cilium 母公司)。翻译时补充了一些背景知识、代码片段和链接,以方便理解。
由于译者水平有限,本文不免存在遗漏或错误之处。如有疑问,请查阅原文。
以下是译文。
![c84a433cf10dd447f7d428343a7ec5d8.png](https://i-blog.csdnimg.cn/blog_migrate/e76eb5e6e78bc77e14e928801414661c.png)
![d904b9407a19b3c339e60fa8847e5e02.png](https://i-blog.csdnimg.cn/blog_migrate/323869dcedc939cfd00d6eb2896300b5.png)
1 问题描述
1.1 容器部署密度与(CPU、内存)资源管理
下面两张图来自 Sysdig 2022 的一份调研报告,
![7848ce7440343d63af26188033c258df.png](https://i-blog.csdnimg.cn/blog_migrate/d47d5dd7027c70948ba1eb991c8a5f53.png)
左图是容器的部署密度分布,比如 33% 的 k8s 用户中,每个 node 上平均会部署 16~25 个 Pod;
右图是每台宿主机上的容器中位数,可以看到过去几年明显在不断增长。
这两个图说明:容器的部署密度越来越高。这导致的 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 上;如果 pod 运行之后使用的内存超过了 memory limits,就会被操作系统以 OOM (Out Of Memory)为由干掉。这种针对 CPU 和 memory 的资源管理机制还是不错的, 那么,网络方面有没有类似的机制呢?
1.2 网络资源管理:带宽控制模型
先回顾下基础的网络知识。下图是往返时延(Round-Trip)与 TCP 拥塞控制效果之间的关系,
![2a6d6e90fca1ec56ff683f588fd87cd9.png](https://i-blog.csdnimg.cn/blog_migrate/f1bed6cf61ad3af7e4a7f7328acd1eda.png)
结合 流量控制(TC)五十年:从基于缓冲队列(Queue)到基于时间戳(EDT)的演进(Google, 2018), 这里只做几点说明:
TCP 的发送模型是尽可能快(As Fast As Possible, AFAP)
网络流量主要是靠网络设备上的出向队列(device output queue)做整形(shaping)
队列长度(queue length)和接收窗口(receive window)决定了传输中的数据速率(in-flight rate)
“多快”(how fast)取决于队列的 drain rate
现在回到我们刚才提出的问题(k8s 网络资源管理), 在 K8s 中,有什么机制能限制 pod 的网络资源(带宽)使用量吗?
1.3 K8s 中的 pod 带宽管理
1.3.1 Bandwidth meta plugin
K8s 自带了一个限速(bandwidth enforcement)机制,但到目前为止还是 experimental 状态;实现上是通过第三方的 bandwidth meta plugin,它会解析特定的 pod annotation,
kubernetes.io/ingress-bandwidth=XX
kubernetes.io/egress-bandwidth=XX
然后转化成对 pod 的具体限速规则,如下图所示,
![03d40013c2ab49b7a9fcdceea8f93048.png](https://i-blog.csdnimg.cn/blog_migrate/714d08b97e4e85af09b485a60693a508.png)
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,反之亦然。
1.3.2 入向(ingress)限速存在的问题
对于 pod ingress 限速,需要在宿主机端 veth 的 egress 路径上设置规则。例如,对于入向 kubernetes.io/ingress-bandwidth="50M" 的声明,会落到 veth 上的 TBF qdisc 上:
![cfb180c3c2a8f3e4deb94c14aeae3e2c.png](https://i-blog.csdnimg.cn/blog_migrate/c34561a4ffc7c1e1aa24d532e7272683.png)
TBF(Token Bucket Filter)是个令牌桶,所有连接/流量都要经过单个队列排队处理,如下图所示:
![94d0de55932171f8da7e368a0e1f740a.png](https://i-blog.csdnimg.cn/blog_migrate/72a27b04e4646fba3a0af095c405651f.png)
在设计上存在的问题:
TBF qdisc 所有 CPU 共享一个锁(著名的 qdisc root lock),因此存在锁竞争;流量越大锁开销越大;
veth pair 是单队列(single queue)虚拟网络设备,因此物理网卡的 多队列(multi queue,不同 CPU 处理不同 queue,并发)优势到了这里就没用了, 大家还是要走到同一个队列才能进到 pod;
在入向排队是不合适的(no-go),会占用大量系统资源和缓冲区开销(bufferbloat)。
1.3.3 出向(egress)限速存在的问题
出向工作原理:
Pod egress 对应 veth 主机端的 ingress,ingress 是不能做整形的,因此加了一个 ifb 设备;
所有从 veth 出来的流量会被重定向到 ifb 设备,通过 ifb TBF qdisc 设置容器限速。
![d5a8c64cbdc12052d5a7d93287b84bbb.png](https://i-blog.csdnimg.cn/blog_migrate/e33dee8ba58fc109a5dfa9adaadec343.png)
存在的问题:
原来只需要在物理网卡排队(一般都会设置一个默认 qdisc,例如 pfifo_fast/fq_codel/noqueue),现在又多了一层 ifb 设备排队,缓冲区膨胀(bufferbloat);
与 ingress 一样,存在 root qdisc lock 竞争,所有 CPU 共享;
干扰 TCP Small Queues (TSQ) 正常工作;TSQ 作用是减少 bufferbloat, 工作机制是觉察到发出去的包还没有被有效处理之后就减少发包;ifb 使得包都缓存在 qdisc 中, 使 TSQ 误以为这些包都已经发出去了,实际上还在主机内。
延迟显著增加:每个 pod 原来只需要 2 个网络设备,现在需要 3 个,增加了大量 queueing 逻辑。
![4dbdf7e8d2eb6b458d9ea2cc2df382d9.png](https://i-blog.csdnimg.cn/blog_migrate/1156ac4143292988709b23bade5c8fcb.png)
1.3.4 Bandwidth meta plugin 问题总结
总结起来:
扩展性差,性能无法随 CPU 线性扩展(root qdisc lock 被所有 CPU 共享导致);导致额外延迟;占用额外资源,缓冲区膨胀。因此不适用于生产环境;
2 解决思路
这一节是介绍 Google 的基础性工作,作者引用了 Evolving from AFAP: Teaching NICs about time (Netdev, 2018) 中的一些内容;之前我们已翻译,见 流量控制(TC)五十年:从基于缓冲队列(Queue)到基于时间戳(EDT)的演进(Google, 2018), 因此一些内容不再赘述,只列一下要点。
译注。
2.1 回归源头:TCP “尽可能快”发送模型存在的缺陷
![00fc7a774535dd499475d854e742132d.png](https://i-blog.csdnimg.cn/blog_migrate/496a019c92bf2bd457c4a7aebca1ab8e.png)
2.2 思路转变:不再基于排队(queue),而是基于时间戳(EDT)
两点核心转变:
每个包(skb)打上一个最早离开时间(Earliest Departure Time, EDT),也就是最早可以发送的时间戳;
用时间轮调度器(timing-wheel scheduler)替换原来的出向缓冲队列(qdisc queue)
![97be4a2cea86f477f56aaff9fdd096a6.png](https://i-blog.csdnimg.cn/blog_migrate/fd1ef4a9fdd168deaa631b16c408f3ae.png)
2.3 3 EDT/timing-wheel 应用到 K8s
有了这些技术基础,我们接下来看如何应用到 K8s。
3 Cilium 原生 pod 限速方案
3.1 整体设计:基于 BPF+EDT 实现容器限速
Cilium 的 bandwidth manager,
基于 eBPF+EDT,实现了无锁 的 pod 限速功能;
在物理网卡(或 bond 设备)而不是 veth 上限速,避免了 bufferbloat,也不会扰乱 TCP TSQ 功能。
不需要进入协议栈,Cilium 的 BPF host routing 功能,使得 FIB lookup 等过程完全在 TC eBPF 层完成,并且能直接转发到网络设备。
在物理网卡(或 bond 设备)上添加 MQ/FQ,实现时间轮调度。
![206b32e27a41151dc9847f8d7c7aa7d4.png](https://i-blog.csdnimg.cn/blog_migrate/2c96559b1ce0997e62cf050e5075ca15.png)
3.2 工作流程
在之前的分享 为 K8s workload 引入的一些 BPF datapath 扩展(LPC, 2021) 中已经有比较详细的介绍,这里在重新整理一下。
Cilium attach 到宿主机的物理网卡(或 bond 设备),在 BPF 程序中为每个包设置 timestamp, 然后通过 earliest departure time 在 fq 中实现限速,下图:
注意:容器限速是在物理网卡上做的,而不是在每个 pod 的 veth 设备上。这跟之前基于 ifb 的限速方案有很大不同。
![e3016ddf94d9734b2004450f142ab665.png](https://i-blog.csdnimg.cn/blog_migrate/456e2c1de6ca6f23f3285b1891dde8d3.png)
从上到下三个步骤:
BPF 程序:管理(计算和设置) skb 的 departure timestamp;
TC qdisc (multi-queue) 发包调度;
物理网卡的队列。
如果宿主机使用了 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} 看到的信息更多,包括了最大支持的数量和实际启用的数量。
3.3 数据包处理过程
先复习下 Cilium datapath,细节见 2020 年的分享:
![bbd5c6babb45ddeaf12aec93f89f1ed1.png](https://i-blog.csdnimg.cn/blog_migrate/bf508c3143cac040c864405b90c9494d.png)
egress 限速工作流程:
![b6b619eabe5be7af173f399d065deda0.png](https://i-blog.csdnimg.cn/blog_migrate/ffeb4e8dd312704fe0741af5d1e11bb5.png)
Pod egress 流量从容器进入宿主机,此时会发生 netns 切换,但 socket 信息 skb->sk 不会丢失;
Host veth 上的 BPF 标记(marking)包的 aggregate(queue_mapping),见 Cilium 代码;
物理网卡上的 BPF 程序根据 aggregate 设置的限速参数,设置每个包的时间戳 skb->tstamp;
FQ+MQ 基本实现了一个 timing-wheel 调度器,根据 skb->tstamp 调度发包。过程中用到了 bpf map 存储 aggregate 信息。
3.4 性能对比:Cilium vs. Bandwidth meta plugin
netperf 压测。
同样限速 100M,延迟下降:
![b7f17305091e1f89898bed886d469488.png](https://i-blog.csdnimg.cn/blog_migrate/b1ecf1f417af0348eb9c2c8ca1e01954.png)
同样限速 100M,TPS:
![be4f7ef9ee56d11f991ed91c0b453e02.png](https://i-blog.csdnimg.cn/blog_migrate/4af970956ca3a2f38fa46bab65a550fd.png)
3.5 小结
主机内的问题解决了,那更大范围 —— 即公网带宽 —— 管理呢?
![57566cf87f912d5bb397e7c748d5aaa9.png](https://i-blog.csdnimg.cn/blog_migrate/6a7e3e9bf0364f102afb5115bd5bb309.png)
别着急,EDT 还能支持 BBR。
4 公网传输:Cilium 基于 BBR 的带宽管理
4.1 BBR 基础
想完整了解 BBR 的设计,可参考 (论文) BBR:基于拥塞(而非丢包)的拥塞控制(ACM, 2017)。译注。
4.1.1 设计初衷
![b8eeaef14d4ca4cc1bf60df3ade1c198.png](https://i-blog.csdnimg.cn/blog_migrate/843afdc97bccda273b5bda2044cf6430.png)
4.1.2 性能对比:bbr vs. cubic
![8aa62888ba144ed10e19e3f1147ce1a3.png](https://i-blog.csdnimg.cn/blog_migrate/083ef217c3968f2632191b9672ee9051.png)
CUBIC + fq_codel:
![144d1173a4ddc4aa3b0f36569c438ebb.png](https://i-blog.csdnimg.cn/blog_migrate/36b8dced8afb7d905e547d1d3c197a3d.png)
BBR + FQ (for EDT):
![990828b3dd280de81e3efbb7c56661f7.png](https://i-blog.csdnimg.cn/blog_migrate/3d450861aa70d6a97d07b52070614df3.png)
效果非常明显。
4.2 BBR + K8s/Cilium
4.2.1 存在的问题:跨 netns 时,skb->tstamp 要被重置
BBR 能不能用到 k8s 里面呢?
BBR + FQ 机制上是能协同工作的;但是,
内核在 skb 离开 pod netns 时,将 skb 的时间戳清掉了,导致包进入 host netns 之后没有时间戳,FQ 无法工作.
问题如下图所示,
![60a4c85f8df77f864679b14c7e95f9a8.png](https://i-blog.csdnimg.cn/blog_migrate/6d4185e7b3ec9d8b036a77af62398103.png)
4.2.2 为什么会被重置
下面介绍一些背景,为什么这个 ts 会被重置。
几种时间规范:https://www.cl.cam.ac.uk/~mgk25/posix-clocks.html
对于包的时间戳 skb->tstamp,内核根据包的方向(RX/TX)不同而使用的两种时钟源:
Ingress 使用 CLOCK_TAI (TAI: international atomic time)
Egress 使用 CLOCK_MONOTONIC(也是 FQ 使用的时钟类型)
如果不重置,将包从 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 方式。
4.2.3 能将 skb->tstamp 统一到同一种时钟吗?
其实最开始,TCP EDT 用的也是 CLOCK_TAI 时钟。但有人在邮件列表 里反馈说,某些特殊的嵌入式设备上重启会导致时钟漂移 50 多年。所以后来 EDT 又回到了 monotonic 时钟,而我们必须跨 netns 时 reset。
我们做了个原型验证,新加一个 bit skb->tstamp_base 来解决这个问题,
0 表示使用的 TAI,
1 表示使用的 MONO, 然后,
TX/RX 通过 skb_set_tstamp_{mono,tai}(skb, ktime) helper 来获取这个值,
fq_enqueue() 先检查 timestamp 类型,如果不是 MONO,就 reset skb->tstamp 此外,
转发逻辑中所有 skb->tstamp = 0 都可以删掉了
skb_mstamp_ns union 也可能删掉了
在 RX 方向,net_timestamp_check() 必须推迟到 tc ingress 之后执行
4.2.4 解决
我们和 Facebook 的朋友合作,已经解决了这个问题,在跨 netns 时保留时间戳, patch 并合并到了 kernel 5.18+。因此 BBR+EDT 可以工作了,
![cd11ac6959622e38140b50ec187f4caa.png](https://i-blog.csdnimg.cn/blog_migrate/63e72d9da6361ed33c0a8ed9746c2208.png)
4.3 Demo(略)
K8s/Cilium backed video streaming service: CUBIC vs. BBR
4.4 BBR 使用注意事项
如果同一个环境(例如数据中心)同时启用了 BBR 和 CUBIC,那使用 BBR 的机器会强占更多的带宽,造成不公平(unfaireness);
![7d901776bb67a6a395d96ed6f939e4d5.png](https://i-blog.csdnimg.cn/blog_migrate/2b9e0d2bdbe4238863158eb7027cd17b.png)
BBR 会触发更高的 TCP 重传速率,这源自它更加主动或激进的探测机制 (higher TCP retransmission rate due to more aggressive probing);
BBRv2 致力于解决以上问题。
5 总结及致谢
5.1 问题回顾与总结
K8s 带宽限速功能可以做地更好;
Cilium 的原生带宽限速功能(v1.12 GA)
基于 BPF+EDT 的高效实现
第一个支持 Pod 使用 BBR (及 socket pacing)的 CNI 插件 -- 特别说明:要实现这样的架构,只能用 eBPF(realizing such architecture only possible with eBPF)
6 Cilium 限速方案存在的问题(译注)
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
推荐
原创不易,随手关注或者”在看“,诚挚感谢!