[譯] 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 的一份調研報告,

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 上;如果 pod 運行之後使用的內存超過了 memory limits,就會被操作系統以 OOM (Out Of Memory)爲由幹掉。這種針對 CPU 和 memory 的資源管理機制還是不錯的, 那麼,網絡方面有沒有類似的機制呢?

1.2 網絡資源管理:帶寬控制模型

先回顧下基礎的網絡知識。下圖是往返時延(Round-Trip)與 TCP 擁塞控制效果之間的關係,

結合 流量控制(TC)五十年:從基於緩衝隊列(Queue)到基於時間戳(EDT)的演進(Google, 2018), 這裏只做幾點說明:

現在回到我們剛纔提出的問題(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 的具體限速規則,如下圖所示,

Fig. Bandwidth meta plugin 解析 pod annotation,並通過 TC TBF 實現限速

bandwidth meta plugin 是一個 CNI plugin,底層利用 Linux TC 子系統中的 TBF, 所以最後轉化成的是 TC 限速規則,加在容器的 veth pair 上(宿主機端)。

這種方式確實能實現 pod 的限速功能,但也存在很嚴重的問題,我們來分別看一下出向和入向的工作機制。

在進入下文之前,有兩點重要說明:

  1. 限速只能在出向(egress)做。爲什麼?可參考 《Linux 高級路由與流量控制手冊(2012)》第九章:用 tc qdisc 管理 Linux 網絡帶寬;

  2. veth pair 宿主機端的流量方向與 pod 的流量方向完全相反,也就是 pod 的 ingress 對應宿主機端 veth 的 egress,反之亦然。

1.3.2 入向(ingress)限速存在的問題

對於 pod ingress 限速,需要在宿主機端 veth 的 egress 路徑上設置規則。例如,對於入向 kubernetes.io/ingress-bandwidth="50M" 的聲明,會落到 veth 上的 TBF qdisc 上:

TBF(Token Bucket Filter)是個令牌桶,所有連接 / 流量都要經過單個隊列排隊處理,如下圖所示:

在設計上存在的問題:

  1. TBF qdisc 所有 CPU 共享一個鎖(著名的 qdisc root lock),因此存在鎖競爭;流量越大鎖開銷越大;

  2. veth pair 是單隊列(single queue)虛擬網絡設備,因此物理網卡的 多隊列(multi queue,不同 CPU 處理不同 queue,併發)優勢到了這裏就沒用了, 大家還是要走到同一個隊列才能進到 pod;

  3. 在入向排隊是不合適的(no-go),會佔用大量系統資源和緩衝區開銷(bufferbloat)。

1.3.3 出向(egress)限速存在的問題

出向工作原理:

存在的問題:

  1. 原來只需要在物理網卡排隊(一般都會設置一個默認 qdisc,例如 pfifo_fast/fq_codel/noqueue),現在又多了一層 ifb 設備排隊,緩衝區膨脹(bufferbloat);

  2. 與 ingress 一樣,存在 root qdisc lock 競爭,所有 CPU 共享;

  3. 干擾 TCP Small Queues (TSQ) 正常工作;TSQ 作用是減少 bufferbloat, 工作機制是覺察到發出去的包還沒有被有效處理之後就減少發包;ifb 使得包都緩存在 qdisc 中, 使 TSQ 誤以爲這些包都已經發出去了,實際上還在主機內。

  4. 延遲顯著增加:每個 pod 原來只需要 2 個網絡設備,現在需要 3 個,增加了大量 queueing 邏輯。

#### 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 “儘可能快” 發送模型存在的缺陷

Fig. 根據排隊論,實際帶寬接近瓶頸帶寬時,延遲將急劇上升

2.2 思路轉變:不再基於排隊(queue),而是基於時間戳(EDT)

兩點核心轉變:

  1. 每個包(skb)打上一個最早離開時間(Earliest Departure Time, EDT),也就是最早可以發送的時間戳;

  2. 用時間輪調度器(timing-wheel scheduler)替換原來的出向緩衝隊列(qdisc queue)

Fig. 傳統基於 queue 的流量整形器 vs. 新的基於 EDT 的流量整形器

2.3 3 EDT/timing-wheel 應用到 K8s

有了這些技術基礎,我們接下來看如何應用到 K8s。

3 Cilium 原生 pod 限速方案

3.1 整體設計:基於 BPF+EDT 實現容器限速

Cilium 的 bandwidth manager,

### 3.2 工作流程

在之前的分享 爲 K8s workload 引入的一些 BPF datapath 擴展(LPC, 2021) 中已經有比較詳細的介紹,這裏在重新整理一下。

Cilium attach 到宿主機的物理網卡(或 bond 設備),在 BPF 程序中爲每個包設置 timestamp, 然後通過 earliest departure time 在 fq 中實現限速,下圖:

注意:容器限速是在物理網卡上做的,而不是在每個 pod 的 veth 設備上。這跟之前基於 ifb 的限速方案有很大不同。

Fig. Cilium 基於 BPF+EDT 的容器限速方案(邏輯架構)

從上到下三個步驟:

  1. BPF 程序:管理(計算和設置) skb 的 departure timestamp;

  2. TC qdisc (multi-queue) 發包調度;

  3. 物理網卡的隊列。

如果宿主機使用了 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 年的分享:

egress 限速工作流程:

1. Pod egress 流量從容器進入宿主機,此時會發生 netns 切換,但 socket 信息 skb->sk 不會丟失;

  1. Host veth 上的 BPF 標記(marking)包的 aggregate(queue_mapping),見 Cilium 代碼;

  2. 物理網卡上的 BPF 程序根據 aggregate 設置的限速參數,設置每個包的時間戳 skb->tstamp;

  3. FQ+MQ 基本實現了一個 timing-wheel 調度器,根據 skb->tstamp 調度發包。過程中用到了 bpf map 存儲 aggregate 信息。

3.4 性能對比:Cilium vs. Bandwidth meta plugin

netperf 壓測。

同樣限速 100M,延遲下降:

同樣限速 100M,TPS:

### 3.5 小結

主機內的問題解決了,那更大範圍 —— 即公網帶寬 —— 管理呢?

彆着急,EDT 還能支持 BBR。

4 公網傳輸:Cilium 基於 BBR 的帶寬管理

4.1 BBR 基礎

想完整了解 BBR 的設計,可參考 (論文) BBR:基於擁塞(而非丟包)的擁塞控制(ACM, 2017)。譯註。

4.1.1 設計初衷

4.1.2 性能對比:bbr vs. cubic

CUBIC + fq_codel:

BBR + FQ (for EDT):

效果非常明顯。

4.2 BBR + K8s/Cilium

4.2.1 存在的問題:跨 netns 時,skb->tstamp 要被重置

BBR 能不能用到 k8s 裏面呢?

問題如下圖所示,

### 4.2.2 爲什麼會被重置

下面介紹一些背景,爲什麼這個 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 方式。

4.2.3 能將 skb->tstamp 統一到同一種時鐘嗎?

其實最開始,TCP EDT 用的也是 CLOCK_TAI 時鐘。但有人在郵件列表 裏反饋說,某些特殊的嵌入式設備上重啓會導致時鐘漂移 50 多年。所以後來 EDT 又回到了 monotonic 時鐘,而我們必須跨 netns 時 reset。

我們做了個原型驗證,新加一個 bit skb->tstamp_base 來解決這個問題,

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 使用注意事項

  1. 如果同一個環境(例如數據中心)同時啓用了 BBR 和 CUBIC,那使用 BBR 的機器會強佔更多的帶寬,造成不公平(unfaireness);

2. BBR 會觸發更高的 TCP 重傳速率,這源自它更加主動或激進的探測機制 (higher TCP retransmission rate due to more aggressive probing);

BBRv2 致力於解決以上問題。

5 總結及致謝

5.1 問題回顧與總結

  1. K8s 帶寬限速功能可以做地更好;

  2. Cilium 的原生帶寬限速功能(v1.12 GA)

6 Cilium 限速方案存在的問題(譯註)

Cilium 的限速功能我們 在 v1.10 就在用了,但是使用下來發現兩個問題,到目前(2022.11)社區還沒有解決,

  1. 啓用 bandwidth manager 之後,Cilium 會 hardcode somaxconn、netdev_max_backlog 等內核參數,覆蓋掉用戶自己的內核調優;

例如,如果 node netdev_max_backlog=8192,那 Cilium 啓動之後, 就會把它強制覆蓋成 1000,導致在大流量場景因爲宿主機這個配置太小而出現丟包。

  1. 啓用 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