You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
$ mount | grep cgroup
cgroup2 on /sys/fs/cgroup/unified type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
...
static inline struct cgroup *sock_cgroup_ptr(struct sock_cgroup_data *skcd)
{
-#if defined(CONFIG_CGROUP_NET_PRIO) || defined(CONFIG_CGROUP_NET_CLASSID)
- unsigned long v;
-
- /*
- * @skcd->val is 64bit but the following is safe on 32bit too as we
- * just need the lower ulong to be written and read atomically.
- */
- v = READ_ONCE(skcd->val);
-
- if (v & 3)
- return &cgrp_dfl_root.cgrp;
-
- return (struct cgroup *)(unsigned long)v ?: &cgrp_dfl_root.cgrp;
-#else
- return (struct cgroup *)(unsigned long)skcd->val;
-#endif
+ return skcd->cgroup;
}
TCP’s congestion control mechanisms can lead to bursty traffic flows
on modern high-speednetworks, with a negative impact on overall
network efficiency. A pro-posed solution to this problem is to
evenly space, or “pace”, data sent intothe network over an entire round-trip time,
so that data is not sent in aburst. In this paper, we quantitatively evaluate this approach.
TCP pacing 对于有 idle time 的 flow 来说比较有用,因为拥塞窗口允许
TCP stack 将可能非常多的包一次性插入队列。
This removes the ‘slow start after idle’ choice, badly
hitting large BDP (Bandwidth-delay product) flows and applications delivering chunks of
data such as video streams.
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:
[译] 为 K8s workload 引入的一些 BPF datapath 扩展(LPC, 2021)
https://ift.tt/yUS56sD
[译] 为 K8s workload 引入的一些 BPF datapath 扩展(LPC, 2021)
Published at 2021-11-24 | Last Update 2022-10-17
译者序
本文翻译自 LPC 2021 的一篇分享: BPF datapath extensions for K8s workloads。
作者 Daniel Borkmann 和 Martynas Pumputis 都是 Cilium 的核心开发。 翻译时补充了一些背景知识、代码片段和链接,以方便理解。
翻译已获得作者授权。
由于译者水平有限,本文不免存在遗漏或错误之处。如有疑问,请查阅原文。
以下是译文。
今天分享一些我们在开发 Cilium datapath 过程中遇到的有趣问题。
0 引言
0.1 Cilium datapath 基础
Cilium BPF datapath 的设计与实现我们在过去几年已分享过多次,这里不再赘述。 有需要请参考:
接下来重点看下过去一年的几个新变化。
0.2 Cilium datapath 几个新变化
如下图所示,最近一年 Cilium 有一些新的变化:
Cilium Service LB
支持通过 IPIP 封装转发 DSR 流量。
总体来说,Cilium BPF datapath 的核心设计理念是:
0.3 本文提纲
本文接下来将介绍以下内容:
1 cgroup v1/v2 干扰问题
很多 Linux 发行版上默认同时启用了 cgroup v1/v2,导致一些干扰问题。
1.1 普通节点:v1/v2 同时挂载没问题
对于一台普通节点,同时挂载 cgroup v1/v2 后,它们在系统中的典型布局(layout)将如下:
图中标出了哪些是 v1 挂载点,哪些是 v2 挂载点。简单来说:
v2 是以进程维度挂载的单一层级树(unified hierarchy),因此顶层只有一个挂载点(
/sys/fs/cgroup/unified
)。只有 v2 支持 attach bpf 程序,例如 hook
connect
、bind
等系统调用;因此 socket-level service LB 之类的代码,只能 attach 到 cgroup v2。这种普通节点上,v1/v2 同时挂载是没问题的。下面再看一种特殊节点。
1.2 嵌套虚拟化节点
1.2.1 KIND (K8s-In-Docker)
KIND 是一个将 k8s 完全跑在容器里的项目 —— 包括 worker node —— 也就是说:
显然,这个项目的好处是只需要一台真实 node(物理机或虚拟机), 就能搭建一个多 node k8s 集群,方便测试和开发。
1.2.2 KIND-worker-node cgroup layout
为方便讨论,先对两种 node 做一下名字上的区分,简单起见:
有了以上区分,我们再来看 cgroup 的挂载情况:
可以看到,
/sys/fs/cgroup/
挂载点;而同时,由于 k8s-node 都是容器,因此它们的文件路径又都会映射到 bm-node 上;
具体来说,图中两个 k8s-node 的挂载点
/sys/fs/cgroup/
,将分别映射到 bm-node 的以下路径:/sys/fs/cgroup/docker-node-a/
/sys/fs/cgroup/docker-node-b/
这种会导致什么问题呢?
1.2.3 带来的问题
考虑到:
/sys/fs/cgroup/
)那么,
/sys/fs/cgroup/docker-node-a/
和/sys/fs/cgroup/docker-node-b/
路径下;因此,在这种嵌套虚拟化的场景下,我们的 BPF datapath 就失效了,如下图所示:
1.3 问题分析:历史代码假设 v1/v2 不会同时启用
先给一下背景介绍。
cgroup v1 中某些控制器有 tagging 功能。例如,
而在 v2 中,每个 sock 是关联到创建这个 sock 时所在的 cgroup, 在网络层直接 match cgroup(而不是 socket 本身)。
引入 v2 之后,socket cgroup 结构体
struct sock_cgroup_data
增加了另一个指针,指向 v2 object 对象。 为避免结构体膨胀以及出于性能考虑,当时将这个结构体改造成了 union,节省了 8 个字节, 代码 diff 如下:以上改动的假设是:v1 和 v2 不会同时使用。 一台机器要么使用 v1,要么使用 v2。但今天的实际情况是:v1 和 v2 同时挂载了。 那么,在 fast path 上看起来是什么样的逻辑呢?
当执行 bpf 程序时,例如
connect
系统调用,socket bpf helper 会获取相应的 cgroup v2 对象,helper 最终会调用到
sock_cgroup_ptr()
,注意这个函数只会被 cgroup v2 调用:@skcd->val is 64bit but the following is safe on 32bit too as we
just need the lower ulong to be written and read atomically.
*/
v = READ_ONCE(skcd->val);
if (v & 3) // 如果这个 socket 上使用了 cgroup v1 tagging,则
return &cgrp_dfl_root.cgrp; // fallback 到 cgroup v2 default root
return (struct cgroup )(unsigned long)v ?: &cgrp_dfl_root.cgrp;
#else
return (struct cgroup )(unsigned long)skcd->val;
#endif
}
如果有 cgroup v1 tag,就会 fallback 到 cgroup v2 default root。 如果 v1/v2 不同时使用,那没问题,但同时使用了之后,会怎么样了呢?
因为必须 fallback 到 v2。在 bm-node 上,对应的就是
/sys/fs/cgroup/docker-node-a/
/sys/fs/cgroup/docker-node-b/
等目录。而 bm-node 上的 cgroup v2 hook 是监听在
/sys/fs/cgroup/
下面的。这意味着 k8s-node 内的路径会被 bypass。或者说,像 cilium agent 这样 attach 到 root 的行 为,在 k8s-node 内做不了任何事情的。可以看到,管理 v2 是非常复杂和脆弱的,例如,
对 cgroup namespaces 或 non-root cgroup paths 的不兼容
attach 到 root 就会遇到这个问题。
v2 不可靠的唤醒机制(unreliable v2 invocation)使 bpf 程序的普及遇到问题
1.4 解决方案:v1/v2 字段拆开
struct sock_cgroup_data
2 TCP Pacing
2.0 基础
本小节为译注,方便大家理解后面的内容。有基础的可以跳过。
2.0.1 TCP Pacing(在每个 RTT 窗口内均匀发送数据)
2.0.2 TCP BBR 算法
Google 提出的一种 TCP 流控算法。Linux 内核已经支持。
2.0.3
tc
FQ (Fair Queue)内容来自 tc-fq(8) manpage。
FQ (Fair Queue) 是一个 classless packet scheduler,设计主要用于本地生成的流量。
使用方式:
setsockopt(SO_MAX_PACING_RATE)
来指定最大 pacing 速率。内部设计:
TC_PRIO_CONTROL
priority)包,预留了一个特殊的 FIFO queue,确保包永远会先被 dequeue。FQ is 不是 work-conserving 类型的。更多信息可参考 (译) 流量控制(TC)五十年:从基于缓冲队列(Queue)到基于时间戳(EDT)的演进(Google, 2018)。
TCP pacing 对于有 idle time 的 flow 来说比较有用,因为拥塞窗口允许 TCP stack 将可能非常多的包一次性插入队列。 This removes the ‘slow start after idle’ choice, badly hitting large BDP (Bandwidth-delay product) flows and applications delivering chunks of data such as video streams.
例子:
下面回到原作者分享内容。
2.1 K8s pod 限速
K8s 模型中可以通过给 pod 打上 ingress/egress bandwidth annotation 对容器进行限速,
怎么实现由插件自己决定,例如:
2.2 Cilium 中 pod egress 限速的实现
设计原理
Cilium attach 到宿主机的物理网卡(或 bond 设备),在 BPF 程序中为每个包设置 timestamp, 然后通过 earliest departure time 在 fq 中实现限速,下图:
Fig. Cilium 基于 BPF+EDT 的容器限速方案(逻辑架构)
从上到下三个步骤:
工作流程
先复习下 Cilium datapath,细节见去年的分享:
egress 限速工作流程:
skb->sk
不会丢失;skb->tstamp
;skb->tstamp
调度发包。过程中用到了 bpf map 存储 aggregate 信息。
2.3 下一步计划:支持 TCP Pacing & BBR
以上流程是没问题的。接下来我们想做的是,
想在物理网卡上默认 netns 实现这个功能。 但这个功能目前还是做不到的。
2.3.1 目前无法支持的原因:跨 netns 导致 skb 时间戳被重置
如下图所示:
在切换 netns 时,skb->tstamp 会被重置,因此物理网卡上的 FQ 看不到时间戳,无法做限速(无法计算状态)。 下面是设置 4Gbps 限速所做的测试,会发现完全不稳定:
我们做了个 POC 来保持 egress timestamp ,在切 netns 时不要重置它, 然后就非常稳定了:
2.3.2 为什么跨 netns 时,
skb->tstamp
要被重置下面介绍一些背景,为什么这个 ts 会被重置。
对于包的时间戳
skb->tstamp
,内核根据包的方向(RX/TX)不同而使用的两种时钟源:如果不重置,将包从 RX 转发到 TX 会导致包在 FQ 中被丢弃,因为 超过 FQ 的 drop horizon。 FQ
horizon
默认是 10s。另外,现在给定一个包,我们无法判断它用的是哪种 timestamp,因此只能用这种 reset 方式。
2.3.3 能将
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,就 resetskb->tstamp
此外,
skb->tstamp = 0
都可以删掉了net_timestamp_check()
必须推迟到 tc ingress 之后执行2.4 中场 Q&A 环节
问题 1:
net_timestamp_check()
功能是什么?检查硬件是否设置了时间戳,如果没有就加上?是的。
流量跨 netns 从 pod 出去后,就重新进入了 RX 路径,其中会执行主 receive 方法,后者也会调用这个函数,就会将时间戳覆盖掉。
为了保留 skb 上的 monotonic clock,以便将它从 tc ingress 一路带给给物理网卡(FQ 依据这个做限速), 我们就必须在 tc ingress 之后的位置调用这个函数。
问题 2:这个时间戳相比于包从容器发出的时刻是有偏差的?
理论上是的。
(Daniel 好像走神了,没回答。)
问题 3:用一个 bit 表示时间戳类型是否够?
理解。
这一点很好,我之前没想到过,后面我会关注一下,也许会放到 issues 列表。 但据我所知目前没有这样的转换方式,也没有办法将一个 monotonic clock 转换 TAI。
是的。
问题 4:能否让 BPF 程序处理推迟 reset timestamp 的操作?
这个问题也很好,我最开始也是这么做(hack)的。不过我觉得这个改动无法合并到内核,因为太丑陋了, 你仍然需要一些方式来避免在 scrub skb 时清掉 timestamp,例如在切换 netns 时就会遇到这种情况。 因此彻底解决这个问题就需要一种不是那么 hacky 但又有效的方式。
从我个人来说,我避免在 pod namespace 内管理任何事情,因此我不希望在容器内 attach bpf 程序。 我希望无需两个 netns 的任何协作这件事情就能完成,或者说宿主机侧自己就能完成这件事情。
问题 5:能否在 veth 加一个比特,让我们能知道自己在处理 ingress 还是 egress 路径?
对的。
这也是一种方式。
但我认为这种方式太丑陋了,因为你要如何配置这个东西呢?而且这里涉及了太多实现细节, 我们真的要将如此细节的东西(要不要清除一个 bit)暴露出来吗?我认为这种方式不够简洁。
时间有限,我们先继续下面的内容,其他问题可以会后再继续讨论。
3 自维护邻居(managed neighbor)与 FIB 扩展
3.1 Cilium L4LB 处理逻辑
Cilium L4LB 或其他基于 XDP 的负载均衡器,
LB 收到的流量通常目的地址都是 ServiceIP,
两种情况下,都是
bpf_fib_lookup()
helper 函数顺便解析 neighbor 地址以上转发,需要用到后端的 IP 和 MAC 地址信息,因此涉及到 neighbor/fib 管理。
3.2 邻居表的管理
3.2.1 XDP 场景下的邻居解析
首先需要知道,XDP 中是无法做邻居解析的,因此
3.2.2 当前的解析和管理方式
当前的邻居解析是由 cilium-agent 来做的。但这里是我们的一个痛点,如下图所示:
NUD_PERMANENT
)插入到邻居表。需要定期解析,以便即使删除不可用的表项:
3.2.3 当前管理方式存在的问题
逻辑上存在 bug:
3.2.4 解决方案:设想
设想还是让控制平面(这里就是 cilium agent)做这个事情,要求:
然后,
3.2.5 解决方案:调研
NTF_USE | NTF_EXT_LEARNED
这两个 neigh flag 大体上能帮我们实现以上设想。我之前其实并不知道这些 flag,也是看代码才发现。NTF_USE
先来看第一个 flag
NTF_USE
。可以看到,指定这个 flag 之后,将一条邻居表项加到内核时,将触发
neigh_event_send()
执行,后者会做一次邻居解析。 如果你一条 entry 加入到内核,它会在内核做 neighbor 解析,后面这条表项过期时,如果有 inbount 流量进来,或者有 outbound 流量需要这个表项 (从而再触发一次解析),它会重新更新到 reachable 状态。
NTF_EXT_LEARNED
带这个 flag 表示这是一条外部学习(externally learned)到并插入内核 (而非内核自己维护)的表项,从而 确保了这个 entry 不会进入 GC 列表;这已经使我们非常接近最终想实现的效果了。
但 NTF_EXT_LEARNED 的不足是:
ip neighbor xxx
命令之后看不到相应字段的状态(Daniel 的 patch),3.2.6 解决方案:引入一个新 flag
NUD_MANAGED
因此,我们决定添加一个创建 neighbor entry 时用的新 flag NUD_MANAGED:
neigh_event_send()
,即触发邻居解析;触发频率BASE_REACHABLE_TIME/2
;基于 iproute2 的例子:指定
nud managed
创建一条邻居表项:3.3 FIB extensions: SNAT 时的 SRC_IP 选择
关于邻居表项的管理告一段落,接下来往上走一层,来看某些情况下 cilium datapath 中的 fib 查找问题。
3.3.1 Node 有多个 IP:SNAT/Masquerade 时的源地址选择问题
来看下面这个例子。
172.16.0.1/24
,无法被外部网络主动访问192.168.0.1/24
和10.0.0.0/24
网段10.0.0.100/32
由于 Pod IP 对外不可直接访问,因此 Pod 出向流量需要做源地址转换(入向做相反转换)。 我们在 tc ingress 上 attach 了一段 bpf 程序来做这件事情(masquerade,动态版 SNAT)。
Node IP 有多个,那执行地址转换时选哪个呢? 目前的做法是在 BPF 中根据某些逻辑来选一个地址,然后将其 hardcode 到代码中,如上图所示。
但这里有个问题:还是以上图为例,虽然宿主机有
192.168.0.1/24
和10.0.0.0/24
两个网段的 IP 地址,但实际上连接到的只有10.0.0.0/24
网络。这种情况下, 如果我们用192.168.0.1/24
做 SNAT,应答流量就回不来了。也就是说,这里涉及到如何选择真实可用的 Node IP 做 masquerade。
3.3.2 解决方式
这个信息其实已经在 FIB 表中了。
因此,我们首先要做的是使用
bpf_{xdp,skb}_fib_lookup()
来动态选择源 IP。 这需要对 BPF helper 函数做一些修改。其次,给内核 引入 一个新 flag
BPF_FIB_LOOKUP_SET_SRC
,在bpf_ipv{4,6}_fib_lookup()
查询邻居表项时,自动将正确的源 IP 一起带出来,这个 patch 很快将合并到上游。此外,有了这种方式,我们也不需要在 BPF 程序中 hardcode IP 了。
效果如下图所示:
3.4 L4LB 节点多网卡:Service 转发时 egress 网卡的选择问题
3.4.1 问题描述
FIB lookup 相关的另一个问题是 multi-home 网络。 如下图所示,一个有三张网卡的 Cilium L4LB 节点在处理 Service 转换,将请求 DNAT 到特定的 backend。
那么,这里就会涉及到选择哪张网卡将流量转发出去的问题。 目前的做法是,在多个网卡的 datapath 中都重复了一些 fib lookup 逻辑。
3.4.2 解决方案
这个信息(转发表项对应的是哪个 ifindex,即网卡)其实也已经在 FIB 表中了。 因此我们希望再次通过动态 fib lookup 解决这个问题,即(
bpf_{xdp,skb}_fib_lookup()
)把这个信息顺便带出来。深入查看了相关代码之后,我们发现这个逻辑已经在了,只是 BPF helper 实现上有点问题, 因此这里我们做了一点 改动,也会合并到上游内核。
最终效果如下:
4 查询 BPF map 时的通配符匹配问题
4.1 PCAP recorder 当前使用场景:Cilium XDP L4LB
Cilium LB 节点上提供了一个灵活的 traffic recorder,
fabric -> L4LB -> L7 proxy/backend
的整条流量路径。遗憾的是今天这里不能播放 gif,只能提供两条命令供大家参考:
下面介绍一下它的内部实现。
4.2 PCAP recorder 原理
下图从 flow 的角度展示它是如何工作的:
判断入向流量。如果是需要抓取的流量,就提取基本信息存储到一个 per-cpu cache。 将原始包放到 perf ring buffer。
判断出向流量:如果 ring buffer 中记录了对应的 ingress flow,就抓取该 egress flow。
以上二者都会调用到
cilium_capture()
,它会这些抓取到流量经过隧道封装之后发往 backend。
4.3 PCAP recorder 匹配规则
4.3.1 Recorder 组成
一个 Recorder 由下面几部分组成:
4.3.2 Agent 职责
4.4 匹配规则:当前的代码实现
根据 tuple 信息和 mask 信息计算掩码之后的 key:
4.5 当前实现的问题:Mask 集合不能太大,否则开销太大
总体上来说,这是一种穷人的 wildcard match 方式。
这里的一个基本前提是 mask 集合不会很大,这个假设对我们当前来说是可接受的。 但有一些缺点:
O(n)
;4.6 原生支持通配符匹配的 BPF map
理想情况下,有内核原生的 BPF map 来避免开销非常高的 code regeneration:
这种设想最早在 2018 年 BPF + OVS 中出现过,他们想基于这种方式在 BPF 中实现 Megaflow 的匹配,但后来没进展了。
另外,我们最近也在看当前主流的包分类算法有哪些,例如 TupleMerge, 下面是论文中的截图:
即便是有大量 rules,至少论文中的仿真结果看起来非常不错:
但目前我们还没有 POC,在我们的 to-do-list 上。 这样就可以免去动态重新编译的问题,如果在座的有这方面经验,我们非常感兴趣。
5 完场 Q&A 环节
问题 6:关于 wildcard 匹配算法
还没。其实我们现在只在 5.4 上需要这个特性,用户在生产环境运行 LB,但对于其他版本,我们需要再深入研究。
总体来说,我非常同意你的观点,例如必须兼容 rcu 以及其他一些东西,不是所有东西都适合在内核。 论文中提到的一些结果都很不错,但这些终归都是仿真结果,还是需要实现一个真实的版 本来验证性能到底怎么样。
问题 7:其中一些算法是不是有专利?
这个问题我确实还没考虑到,需要回去确认一下。
问题 8:Cilium 是否已经不需要 direct interface 概念?
是的,这种自维护的状态能通过定时刷新或其他方式,保证 neighbor entry 在内核中存在且持续处于 reachable。 这样我们就能用 fib lookup 来查找邻居,后者也为 XDP datapath 提供了便利。
今天的基础设施,例如 NTF_USE flag,我认为是非常古老的代码, 它没有主动 refresh 进入 reachable 状态,除非有外部流量或内部流量事件,比如 ping node。 如果这些完全由内核的邻居子系统来管理(completely self-managed by the kernel)那自然是很好的。 这样我们就无需外部流量触发更新。
问题 9:用 libpcap 将 cbpf 编译成 ebpf 是否可以解决你们不支持 port-range 的问题?
这种方式是可行的,但我认为这种方式生成的程序将迅速膨胀。 如果你只有很少的 mask,loop unroll 不会产生问题;但如果要匹配几千个地方, 那生成的代码就会非常长。
我们想做的一件事情就是将这段代码从 LB 节点移到 CNI datapath 部分, 而后者中已经有非常复杂的 bpf 代码了,因此再加一段这样的代码将会使其进一步膨胀。 我最大的顾虑是校验器,太大或太复杂会无法通过。
6 本文翻译时,原作者特别更新
Daniel 和 Martynas 在本文翻译时非常热心地提供了以下更新:
以下是详细 patch 列表。
6.1 Merged cgroup v1/v2 patches
6.2 Merged managed neighbor entries & fixes
6.3 iproute2
6.4 go netlink lib
6.5 Cilium 1.11 neighbor rework (using managed neighbors on newer kernels)
via ArthurChiao's Blog
July 11, 2024 at 04:02PM
The text was updated successfully, but these errors were encountered: