追蹤 Kubernetes 中的數(shù)據(jù)包
網(wǎng)絡(luò)和操作系統(tǒng)內(nèi)核,對我來說是既陌生又滿是吸引,希望能夠撥開層層迷霧找到背后的真相。
在 ??上一篇文章?? 中我深入探討了 Kubernetes 網(wǎng)絡(luò)模型,這次我想更深入一點(diǎn):了解數(shù)據(jù)包在 Kubernetes 中的傳輸,為學(xué)習(xí) Kubernetes 的 eBPF 網(wǎng)絡(luò)加速做準(zhǔn)備,加深對網(wǎng)絡(luò)和操作系統(tǒng)內(nèi)核的理解。 文中可能有疏漏之處,還望大家賜教。
在開始之前,我可以用一句話來總結(jié)我的學(xué)習(xí)成果:數(shù)據(jù)包的流轉(zhuǎn)其實(shí)就是一個網(wǎng)絡(luò)套接字描述符(Socket File Descriptor,中文有點(diǎn)冗長,以下簡稱 socket fd)的尋址過程。 它不是簡單的指 socket fd 的內(nèi)存地址,還包括它的網(wǎng)絡(luò)地址。
在 Unix 和類 Unix 系統(tǒng)中,一切皆文件,也可以通過文件描述符來操作 socket。
基礎(chǔ)知識
數(shù)據(jù)包
既然要討論數(shù)據(jù)包的流轉(zhuǎn),先看看什么是數(shù)據(jù)包。
網(wǎng)絡(luò)數(shù)據(jù)包(network packet),也稱為網(wǎng)絡(luò)數(shù)據(jù)報(network datagram)或網(wǎng)絡(luò)幀(Network frame),是通過計(jì)算機(jī)網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)單位。拿最常見的 TCP 數(shù)據(jù)包來看包含如下幾個部分:
- Ethernet header:鏈路層信息,主要包括目的 MAC 地址和源 MAC 地址,以及報文的格式,這里是 IP 包。
- IP header:網(wǎng)絡(luò)層信息,主要包括長度、源 IP 地址和目的 IP 地址以及報文的格式,當(dāng)然這里必須是 TCP 包。
- TCP header:傳輸層信息,包括源端口和目的端口。
- 數(shù)據(jù):一般是第 7 層的數(shù)據(jù),比如 HTTP 等。
這里沒有介紹的 checksum 和 FCS 通常是用來檢查數(shù)據(jù)包在傳輸過程中是否被篡改或者發(fā)生了錯誤。

應(yīng)用程序使用 socket 向網(wǎng)絡(luò)發(fā)送數(shù)據(jù)的過程可以簡單理解為使用頭信息封裝數(shù)據(jù)的過程:TCP 數(shù)據(jù)包、IP 數(shù)據(jù)包、Ethernet 數(shù)據(jù)包;反過來,從網(wǎng)絡(luò)接收以太網(wǎng)數(shù)據(jù)包到應(yīng)用程序可以處理的數(shù)據(jù),就是解包的過程。封包和解包的過程是由內(nèi)核網(wǎng)絡(luò)協(xié)議棧來完成的。
下面分別說一下 socket 和內(nèi)核網(wǎng)絡(luò)協(xié)議棧的處理。
socket 套接字
Socket 是一種在計(jì)算機(jī)網(wǎng)絡(luò)中使用的編程接口,位于用戶空間(用戶應(yīng)用程序運(yùn)行的空間)和內(nèi)核網(wǎng)絡(luò)協(xié)議棧(內(nèi)核中對數(shù)據(jù)進(jìn)行封包和解包的組件)之間。

作為編程接口,socket 提供了如下操作(只列出部分):
- socket
- connect
- bind
- listen
- accept
- 數(shù)據(jù)傳輸
- send
- sendto
- sendmsg
- recv
- recvfrom
- recvmsg
- getsockname
- getpeername
- getsockopt? 、setsockopt 獲取或設(shè)置 socket 層或協(xié)議層選項(xiàng)
- close
通過下面的圖,可以直觀感受各個操作的作用:

開始講解內(nèi)核網(wǎng)絡(luò)協(xié)議棧之前,先說下數(shù)據(jù)包在內(nèi)存中的數(shù)據(jù)結(jié)構(gòu):sk_buff[1]。
sk_buff
sk_buff 是 Linux 內(nèi)核中用于管理網(wǎng)絡(luò)數(shù)據(jù)包的數(shù)據(jù)結(jié)構(gòu),它包含了接收和發(fā)送的網(wǎng)絡(luò)數(shù)據(jù)包的各種信息和屬性,如數(shù)據(jù)包的協(xié)議、數(shù)據(jù)長度、源和目標(biāo)地址等。sk_buff 是一種可以在網(wǎng)絡(luò)層和數(shù)據(jù)鏈路層之間傳遞的數(shù)據(jù)結(jié)構(gòu),可以被用于所有類型的網(wǎng)絡(luò)協(xié)議棧,例如 TCP/IP、UDP、ICMP 等。
sk_buff 在 Linux 內(nèi)核中廣泛應(yīng)用于網(wǎng)絡(luò)協(xié)議棧的各個層級,如數(shù)據(jù)鏈路層、網(wǎng)絡(luò)層、傳輸層等。sk_buff 數(shù)據(jù)結(jié)構(gòu)的字段很多,有 4 個重要的字段且都是指針類型。sk_buff 在不同層的使用,就是通過修改這些指針來完成的:加 header (封包)和移除 header(解包)。
這個過程操作做的是指針,數(shù)據(jù)是零拷貝的,可以極大地提升效率。

內(nèi)核網(wǎng)絡(luò)協(xié)議棧
封包
應(yīng)用程序使用 socket 的 sendmsg 操作發(fā)送數(shù)據(jù)(這里不深入講解 netfilter、traffic control、queue discipline):
- 先分配 sk_buff
- 接下來開始網(wǎng)絡(luò)協(xié)議棧的處理
- 設(shè)置傳輸層信息(這里是 TCP 頭中的源和目的端口)
- 根據(jù)目標(biāo) IP 查找路由
- 設(shè)置網(wǎng)絡(luò)層信息(源和目的 IP 地址等)
- 調(diào)用 netfilter(LOCAL_OUT)
- 設(shè)置接口(interface)和協(xié)議(protocol)
- 調(diào)用 netfilter(POST_ROUTING)
- 如果包過長,分段傳輸
- L2 尋址,即查找可以擁有目標(biāo) IP 地址的設(shè)備的 MAC 地址
- 設(shè)置鏈路層信息,
- 至此內(nèi)核網(wǎng)絡(luò)協(xié)議棧的操作完成
- 調(diào)用 tc(traffic control)egress(可以對包進(jìn)行重定向)
- 進(jìn)入隊(duì)列 queue discipline(qdisc)
- 寫入 NIC(network interface controler)
- 發(fā)送到網(wǎng)絡(luò)
解包
NIC 收到網(wǎng)絡(luò)發(fā)來的數(shù)據(jù)包(這里不深入講解 direct memory access、netfilter、traffic control):
- 將數(shù)據(jù)包寫如 DMA 中(Direct Memory Access 直接內(nèi)存訪問,不需要依賴 CPU,由 NIC 直接寫入到內(nèi)存中)
- 分配 sk_buff,并填充元數(shù)據(jù),比如 protocol 為 Ethernet 類型,接收數(shù)據(jù)包的網(wǎng)絡(luò)接口等
- 將鏈路層信息保存在 sk_buff 的 mac_header 字段中,并“移除”數(shù)據(jù)包中的鏈路層信息(移動指針)
- 接下來開始網(wǎng)絡(luò)協(xié)議棧的處理
- 將網(wǎng)絡(luò)層信息保存在 network_header 字段中
- 調(diào)用 tc ingress
- “移除”網(wǎng)絡(luò)層信息
- 將傳輸層信息保存在 transport_header 字段中
- 調(diào)用 netfilter(PRE_ROUTING)
- 查找路由
- 合并多個分包
- 調(diào)用 netfilter(LOCAL_IN)
- “移除”傳輸層信息
- 查找監(jiān)聽目標(biāo)端口的 socket,或者發(fā)送 reset
- 將數(shù)據(jù)寫入 socket 的接收隊(duì)列中
- 發(fā)信號通知有數(shù)據(jù)寫入隊(duì)列
- 至此內(nèi)核網(wǎng)絡(luò)協(xié)議棧的操作完成
- sk_buff 從 socket 接收隊(duì)列中出隊(duì)
- 將數(shù)據(jù)寫入應(yīng)用程序的緩沖區(qū)
- 釋放 sk_buff
Kubernetes 的網(wǎng)絡(luò)模型
另一部分的基礎(chǔ)知識就是 Kubernetes 的網(wǎng)絡(luò)模型了,可以參考之前的那篇 深入探索 Kubernetes 網(wǎng)絡(luò)模型和網(wǎng)絡(luò)通信。
Kubernetes 中的數(shù)據(jù)包流轉(zhuǎn)
這里繼續(xù)討論之前文章中的三種通信場景,pod 間的通信使用 pod IP 地址。如果要討論通過 Service 來訪問,則要加入 netfilter 的討論篇幅會增加不少。
同 pod 內(nèi)的容器間通信
pod 內(nèi)兩個容器間的方式通常使用回環(huán)地址 127.0.0.1?,在封包的 #4 路由過程中確定了使用回環(huán)網(wǎng)卡 lo進(jìn)行傳輸。

同節(jié)點(diǎn)上的 pod 間通信
curl? 發(fā)出的請求在封包 #4 過程中確定使用 eth0? 接口。然后通過與 eth0? 相連的隧道 veth1 到達(dá)節(jié)點(diǎn)的根網(wǎng)絡(luò)空間。
veth1? 通過網(wǎng)橋 cni0? 與其他 pod 相連虛擬以太接口 vethX? 相連。在封包 #10 L2 尋址中,ARP 請求通過網(wǎng)橋發(fā)送給所有相連的接口是否擁有原始請求中的目的 IP 地址(這里是 10.42.1.9)
拿到了 veth0? 的 MAC 地址后,在封包 #11 中設(shè)置數(shù)據(jù)包的鏈路層信息。數(shù)據(jù)包發(fā)出后,經(jīng)過 veth0? 隧道進(jìn)入 pod httpbin? 的 eth0 接口中,然后開始解包的過程。
解包的過程沒啥特別,確定了 httpbin 使用的 socket。

不同節(jié)點(diǎn)的 pod 間通信
這里稍微不同,就是在通過 cni0? 發(fā)送 ARP 請求沒有收到應(yīng)答,使用根命名空間也就是主機(jī)的路由表,確定了目標(biāo)主機(jī) IP 地址后,然后通過主機(jī)的 eth0 放 ARP 請求并收到目標(biāo)主機(jī)的響應(yīng)。將其 MAC 地址在封包 #11 中寫入。
數(shù)據(jù)包發(fā)送到目標(biāo)主機(jī)后,開始解包的過程,最終進(jìn)入目標(biāo) pod。
在集群層面有一張路由表,里面存儲著每個節(jié)點(diǎn)的 Pod IP 網(wǎng)段(節(jié)點(diǎn)加入到集群時會分配一個 Pod 網(wǎng)段(Pod CIDR),比如在 k3s 中默認(rèn)的 Pod CIDR 是 10.42.0.0/16?,節(jié)點(diǎn)獲取到的網(wǎng)段是 10.42.0.0/24、10.42.1.0/24、10.42.2.0/24,依次類推)。通過節(jié)點(diǎn)的 Pod IP 網(wǎng)段可以判斷出請求 IP 的節(jié)點(diǎn),然后請求被發(fā)送到該節(jié)點(diǎn)。

總結(jié)
統(tǒng)計(jì)一下在三個場景中,經(jīng)過內(nèi)核網(wǎng)絡(luò)協(xié)議棧的處理次數(shù)都是兩次(包括 netfilter 的處理。),即使是同 pod 或者同節(jié)點(diǎn)內(nèi)。而這兩種情況實(shí)際都發(fā)生在同一個內(nèi)核空間中。
假如同一個內(nèi)核空間中的兩個 socket 可以直接傳輸數(shù)據(jù),是不是就可以省掉內(nèi)核網(wǎng)絡(luò)協(xié)議棧處理帶來的延遲?
下篇繼續(xù)。
參考資料
[1] sk_buff: https://elixir.bootlin.com/linux/latest/source/include/linux/skbuff.h#L843























