[譯] Cilium:基於 BPF-EDT-FQ-BBR 更好地帶寬網絡管理
譯者序
本文翻譯自 KubeCon+CloudNativeCon Europe 2022 的一篇分享:Better Bandwidth Management with eBPF。
作者 Daniel Borkmann, Christopher, Nikolay 都來自 Isovalent(Cilium 母公司)。翻譯時補充了一些背景知識、代碼片段和鏈接,以方便理解。
由於譯者水平有限,本文不免存在遺漏或錯誤之處。如有疑問,請查閱原文。
以下是譯文。
1 問題描述
1.1 容器部署密度與(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 的資源管理機制還是不錯的, 那麼,網絡方面有沒有類似的機制呢?
1.2 網絡資源管理:帶寬控制模型
先回顧下基礎的網絡知識。下圖是往返時延(Round-Trip)與 TCP 擁塞控制效果之間的關係,
-
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 的具體限速規則,如下圖所示,
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 上:
-
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 設置容器限速。
-
原來只需要在物理網卡排隊(一般都會設置一個默認 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 邏輯。
總結起來:
擴展性差,性能無法隨 CPU 線性擴展(root qdisc lock 被所有 CPU 共享導致);導致額外延遲;佔用額外資源,緩衝區膨脹。因此不適用於生產環境;
2 解決思路
這一節是介紹 Google 的基礎性工作,作者引用了 Evolving from AFAP: Teaching NICs about time (Netdev, 2018) 中的一些內容;之前我們已翻譯,見 流量控制(TC)五十年:從基於緩衝隊列(Queue)到基於時間戳(EDT)的演進(Google, 2018), 因此一些內容不再贅述,只列一下要點。
譯註。
2.1 迴歸源頭:TCP “儘可能快” 發送模型存在的缺陷
2.2 思路轉變:不再基於排隊(queue),而是基於時間戳(EDT)
兩點核心轉變:
-
每個包(skb)打上一個最早離開時間(Earliest Departure Time, EDT),也就是最早可以發送的時間戳;
-
用時間輪調度器(timing-wheel scheduler)替換原來的出向緩衝隊列(qdisc queue)
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,實現時間輪調度。
在之前的分享 爲 K8s workload 引入的一些 BPF datapath 擴展(LPC, 2021) 中已經有比較詳細的介紹,這裏在重新整理一下。
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, 每個 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 年的分享:
-
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,延遲下降:
主機內的問題解決了,那更大範圍 —— 即公網帶寬 —— 管理呢?
4 公網傳輸:Cilium 基於 BBR 的帶寬管理
4.1 BBR 基礎
想完整了解 BBR 的設計,可參考 (論文) BBR:基於擁塞(而非丟包)的擁塞控制(ACM, 2017)。譯註。
4.1.1 設計初衷
4.2 BBR + K8s/Cilium
4.2.1 存在的問題:跨 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。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 可以工作了,
4.3 Demo(略)
K8s/Cilium backed video streaming service: CUBIC vs. BBR
4.4 BBR 使用注意事項
- 如果同一個環境(例如數據中心)同時啓用了 BBR 和 CUBIC,那使用 BBR 的機器會強佔更多的帶寬,造成不公平(unfaireness);
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
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/6OdPfFK5zFRCfolDd7jU0Q