使用 Cilium 给 K8s 数据平面提供强大的带宽管理功能
❝本文转自赵亚楠的博客,原文:https://arthurchiao.art/blog/better-bandwidth-management-with-ebpf-zh/,版权归原作者所有。欢迎投稿,投稿请添加微信好友:cloud-native-yang本文翻译自 KubeCon+CloudNativeCon Europe 2022 的一篇分享:Better Bandwidth Managemen
❝本文转自赵亚楠的博客,原文:https://arthurchiao.art/blog/better-bandwidth-management-with-ebpf-zh/,版权归原作者所有。欢迎投稿,投稿请添加微信好友:cloud-native-yang
本文翻译自 KubeCon+CloudNativeCon Europe 2022 的一篇分享:Better Bandwidth Management with eBPF[1]。
作者 Daniel Borkmann, Christopher, Nikolay 都来自 Isovalent(Cilium 母公司)。翻译时补充了一些背景知识、代码片段和链接,以方便理解。
由于译者水平有限,本文不免存在遗漏或错误之处。如有疑问,请查阅原文。
以下是译文。
问题描述
容器部署密度与(CPU、内存)资源管理
下面两张图来自 Sysdig 2022 的一份调研报告,
左图是容器的部署密度分布,比如 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 的资源管理机制还是不错的, 那么,网络方面有没有类似的机制呢?
网络资源管理:带宽控制模型
先回顾下基础的网络知识。下图是往返时延(Round-Trip)与 TCP 拥塞控制效果之间的关系,
结合 流量控制(TC)五十年:从基于缓冲队列(Queue)到基于时间戳(EDT)的演进(Google, 2018)[2], 这里只做几点说明:
TCP 的发送模型是尽可能快(As Fast As Possible, AFAP)
网络流量主要是靠网络设备上的出向队列(device output queue)做整形(shaping)
队列长度(queue length)和接收窗口(receive window)决定了传输中的数据速率(in-flight rate)
“多快”(how fast)取决于队列的 drain rate
现在回到我们刚才提出的问题(k8s 网络资源管理), 在 K8s 中,有什么机制能限制 pod 的网络资源(带宽)使用量吗?
K8s 中的 pod 带宽管理
Bandwidth meta plugin
K8s 自带了一个限速(bandwidth enforcement)机制,但到目前为止还是 experimental 状态;实现上是通过第三方的 bandwidth meta plugin,它会解析特定的 pod annotation,
kubernetes.io/ingress-bandwidth=XX
kubernetes.io/egress-bandwidth=XX
然后转化成对 pod 的具体限速规则,如下图所示,
bandwidth meta plugin 是一个 CNI plugin,底层利用 Linux TC 子系统中的 TBF, 所以最后转化成的是 TC 限速规则,加在容器的 veth pair 上(宿主机端)。
这种方式确实能实现 pod 的限速功能,但也存在很严重的问题,我们来分别看一下出向和入向的工作机制。
❝在进入下文之前,有两点重要说明:
限速只能在出向(egress)做。为什么?可参考 《Linux 高级路由与流量控制手册(2012)》第九章:用 tc qdisc 管理 Linux 网络带宽[3];
veth pair 宿主机端的流量方向与 pod 的流量方向完全相反,也就是 pod 的 ingress 对应宿主机端 veth 的 egress,反之亦然。
译注。
入向(ingress)限速存在的问题
对于 pod ingress 限速,需要在宿主机端 veth 的 egress 路径上设置规则。例如,对于入向 kubernetes.io/ingress-bandwidth="50M"
的声明,会落到 veth 上的 TBF qdisc 上:
TBF(Token Bucket Filter)是个令牌桶,所有连接/流量都要经过单个队列排队处理,如下图所示:
在设计上存在的问题:
TBF qdisc 所有 CPU 共享一个锁(著名的 qdisc root lock),因此存在锁竞争;流量越大锁开销越大;
veth pair 是单队列(single queue)虚拟网络设备,因此物理网卡的 多队列(multi queue,不同 CPU 处理不同 queue,并发)优势到了这里就没用了, 大家还是要走到同一个队列才能进到 pod;
在入向排队是不合适的(no-go),会占用大量系统资源和缓冲区开销(bufferbloat)。
出向(egress)限速存在的问题
出向工作原理:
Pod egress 对应 veth 主机端的 ingress,ingress 是不能做整形的,因此加了一个 ifb 设备;
所有从 veth 出来的流量会被重定向到 ifb 设备,通过 ifb TBF qdisc 设置容器限速。
存在的问题:
原来只需要在物理网卡排队(一般都会设置一个默认 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 逻辑。
Bandwidth meta plugin 问题总结
总结起来:
扩展性差,性能无法随 CPU 线性扩展(root qdisc lock 被所有 CPU 共享导致);
导致额外延迟;
占用额外资源,缓冲区膨胀。
因此不适用于生产环境;
解决思路
❝这一节是介绍 Google 的基础性工作,作者引用了 Evolving from AFAP: Teaching NICs about time (Netdev, 2018)[4] 中的一些内容;之前我们已翻译,见 流量控制(TC)五十年:从基于缓冲队列(Queue)到基于时间戳(EDT)的演进(Google, 2018)[5], 因此一些内容不再赘述,只列一下要点。
译注。
回归源头:TCP “尽可能快”发送模型存在的缺陷
思路转变:不再基于排队(queue),而是基于时间戳(EDT)
两点核心转变:
每个包(skb)打上一个最早离开时间(Earliest Departure Time, EDT),也就是最早可以发送的时间戳;
用时间轮调度器(timing-wheel scheduler)替换原来的出向缓冲队列(qdisc queue)
EDT/timing-wheel 应用到 K8s
有了这些技术基础,我们接下来看如何应用到 K8s。
Cilium 原生 pod 限速方案
整体设计:基于 BPF+EDT 实现容器限速
Cilium 的 bandwidth manager,
基于 eBPF+EDT,实现了无锁 的 pod 限速功能;
在物理网卡(或 bond 设备)而不是 veth 上限速,避免了 bufferbloat,也不会扰乱 TCP TSQ 功能。
不需要进入协议栈,Cilium 的 BPF host routing 功能,使得 FIB lookup 等过程完全在 TC eBPF 层完成,并且能直接转发到网络设备。
在物理网卡(或 bond 设备)上添加 MQ/FQ,实现时间轮调度。
工作流程
在之前的分享 为 K8s workload 引入的一些 BPF datapath 扩展(LPC, 2021)[6] 中已经有比较详细的介绍,这里在重新整理一下。
Cilium attach 到宿主机的物理网卡(或 bond 设备),在 BPF 程序中为每个包设置 timestamp, 然后通过 earliest departure time 在 fq 中实现限速,下图:
❝注意:容器限速是在物理网卡上做的,而不是在每个 pod 的 veth 设备上。这跟之前基于 ifb 的限速方案有很大不同。
从上到下三个步骤:
BPF 程序:管理(计算和设置) skb 的 departure timestamp;
TC qdisc (multi-queue) 发包调度;
物理网卡的队列。
❝如果宿主机使用了 bond,那么根据 bond 实现方式的不同,FQ 的数量会不一样, 可通过
tc -s -d qdisc show dev {bond}
查看实际状态。具体来说,
Linux bond 默认支持多队列(multi-queue),会默认创建 16 个 queue[7], 每个 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 限速工作流程:
Pod egress 流量从容器进入宿主机,此时会发生 netns 切换,但 socket 信息
skb->sk
不会丢失;Host veth 上的 BPF 标记(marking)包的 aggregate(queue_mapping),见 Cilium 代码[8];
物理网卡上的 BPF 程序根据 aggregate 设置的限速参数,设置每个包的时间戳
skb->tstamp
;FQ+MQ 基本实现了一个 timing-wheel 调度器,根据
skb->tstamp
调度发包。
过程中用到了 bpf map 存储 aggregate 信息。
性能对比:Cilium vs. Bandwidth meta plugin
netperf 压测。
同样限速 100M,延迟下降:
同样限速 100M,TPS:
小结
主机内的问题解决了,那更大范围 —— 即公网带宽 —— 管理呢?
别着急,EDT 还能支持 BBR。
公网传输:Cilium 基于 BBR 的带宽管理
BBR 基础
❝想完整了解 BBR 的设计,可参考 (论文) BBR:基于拥塞(而非丢包)的拥塞控制(ACM, 2017)。译注。
设计初衷
性能对比:bbr vs. cubic
CUBIC + fq_codel:
BBR + FQ (for EDT):
效果非常明显。
BBR + K8s/Cilium
存在的问题:跨 netns 时,skb->tstamp
要被重置
BBR 能不能用到 k8s 里面呢?
BBR + FQ 机制上是能协同工作的;但是,
内核在 skb 离开 pod netns 时,将 skb 的时间戳清掉了,导致包进入 host netns 之后没有时间戳,FQ 无法工作
问题如下图所示,
为什么会被重置
下面介绍一些背景,为什么这个 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[9]。FQ horizon
默认是 10s[10]。
❝
horizon
是 FQ 的一个配置项,表示一个时间长度, 在 net_sched: sch_fq: add horizon attribute[11] 引入,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),以便节省队列空间;否则,这种 包太多的话,队列可能会被塞满,导致时间戳比较近的包都无法正常处理。内核代码[12]如下:
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 时钟。但有人在邮件列表[13] 里反馈说,某些特殊的嵌入式设备上重启会导致时钟漂移 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,就 resetskb->tstamp
此外,
转发逻辑中所有
skb->tstamp = 0
都可以删掉了skb_mstamp_ns union 也可能删掉了
在 RX 方向,
net_timestamp_check()
必须推迟到 tc ingress 之后执行
解决
我们和 Facebook 的朋友合作,已经解决了这个问题,在跨 netns 时保留时间戳, patch 并合并到了 kernel 5.18+
。因此 BBR+EDT 可以工作了,
Demo(略)
K8s/Cilium backed video streaming service: CUBIC vs. BBR
BBR 使用注意事项
1️⃣ 如果同一个环境(例如数据中心)同时启用了 BBR 和 CUBIC,那使用 BBR 的机器会强占更多的带宽,造成不公平(unfaireness);
2️⃣ BBR 会触发更高的 TCP 重传速率,这源自它更加主动或激进的探测机制 (higher TCP retransmission rate due to more aggressive probing);
BBRv2 致力于解决以上问题。
总结及致谢
问题回顾与总结
K8s 带宽限速功能可以做地更好;
Cilium 的原生带宽限速功能(v1.12 GA)
基于 BPF+EDT 的高效实现
第一个支持 Pod 使用 BBR (及 socket pacing)的 CNI 插件
特别说明:要实现这样的架构,只能用 eBPF(realizing such architecture only possible with eBPF)
致谢
Van Jacobson
Eric Dumazet
Vytautas Valancius
Stanislav Fomichev
Martin Lau
John Fastabend
Cilium, eBPF & netdev kernel community
Cilium 限速方案存在的问题(译注)
Cilium 的限速功能我们[14] 在 v1.10 就在用了,但是使用下来发现两个问题,到目前(2022.11)社区还没有解决,
1️⃣ 启用 bandwidth manager 之后,Cilium 会 hardcode[15] somaxconn、netdev_max_backlog 等内核参数,覆盖掉用户自己的内核调优;
例如,如果 node netdev_max_backlog=8192
,那 Cilium 启动之后, 就会把它强制覆盖成 1000,导致在大流量场景因为宿主机这个配置太小而出现丢包。
2️⃣ 启用 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
引用链接
[1]
Better Bandwidth Management with eBPF: https://kccnceu2022.sched.com/event/ytsQ/better-bandwidth-management-with-ebpf-daniel-borkmann-christopher-m-luciano-isovalent
[2]流量控制(TC)五十年:从基于缓冲队列(Queue)到基于时间戳(EDT)的演进(Google, 2018): http://arthurchiao.art/blog/traffic-control-from-queue-to-edt-zh/
[3]《Linux 高级路由与流量控制手册(2012)》第九章:用 tc qdisc 管理 Linux 网络带宽: http://arthurchiao.art/blog/lartc-qdisc-zh/
[4]Evolving from AFAP: Teaching NICs about time (Netdev, 2018): https://www.youtube.com/watch?v=MAni0_lN7zE
[5]流量控制(TC)五十年:从基于缓冲队列(Queue)到基于时间戳(EDT)的演进(Google, 2018): http://arthurchiao.art/blog/traffic-control-from-queue-to-edt-zh/
[6]为 K8s workload 引入的一些 BPF datapath 扩展(LPC, 2021): http://arthurchiao.art/blog/bpf-datapath-extensions-for-k8s-zh/
[7]默认支持多队列(multi-queue),会默认创建 16 个 queue: https://www.kernel.org/doc/Documentation/networking/bonding.txt
[8]Cilium 代码: https://github.com/cilium/cilium/blob/v1.10/bpf/lib/edt.h
[9]超过 FQ 的 drop horizon: https://github.com/torvalds/linux/blob/v5.10/net/sched/sch_fq.c#L463
[10]默认是 10s: https://github.com/torvalds/linux/blob/v5.10/net/sched/sch_fq.c#L950
[11]net_sched: sch_fq: add horizon attribute: https://github.com/torvalds/linux/commit/39d010504e6b
[12]内核代码: https://github.com/torvalds/linux/blob/v5.10/net/sched/sch_fq.c#L436
[13]邮件列表: https://lore.kernel.org/netdev/2185d09d-90e1-81ef-7c7f-346eeb951bf4@gmail.com/
[14]我们: http://arthurchiao.art/blog/trip-large-scale-cloud-native-networking-and-security-with-cilium-ebpf/
[15]hardcode: https://github.com/cilium/cilium/blob/v1.12/pkg/bandwidth/bandwidth.go#L114
你可能还喜欢
点击下方图片即可阅读
云原生是一种信仰 🤘
关注公众号
后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!
点击 "阅读原文" 获取更好的阅读体验!
发现朋友圈变“安静”了吗?
更多推荐
所有评论(0)