追蹤 Kubernetes 中的數(shù)據(jù)包
網(wǎng)絡(luò)和操作系統(tǒng)內(nèi)核,對(duì)我來(lái)說(shuō)是既陌生又滿是吸引,希望能夠撥開(kāi)層層迷霧找到背后的真相。
在 ??上一篇文章?? 中我深入探討了 Kubernetes 網(wǎng)絡(luò)模型,這次我想更深入一點(diǎn):了解數(shù)據(jù)包在 Kubernetes 中的傳輸,為學(xué)習(xí) Kubernetes 的 eBPF 網(wǎng)絡(luò)加速做準(zhǔn)備,加深對(duì)網(wǎng)絡(luò)和操作系統(tǒng)內(nèi)核的理解。 文中可能有疏漏之處,還望大家賜教。
在開(kāi)始之前,我可以用一句話來(lái)總結(jié)我的學(xué)習(xí)成果:數(shù)據(jù)包的流轉(zhuǎn)其實(shí)就是一個(gè)網(wǎng)絡(luò)套接字描述符(Socket File Descriptor,中文有點(diǎn)冗長(zhǎng),以下簡(jiǎn)稱 socket fd)的尋址過(guò)程。 它不是簡(jiǎn)單的指 socket fd 的內(nèi)存地址,還包括它的網(wǎng)絡(luò)地址。
在 Unix 和類 Unix 系統(tǒng)中,一切皆文件,也可以通過(guò)文件描述符來(lái)操作 socket。
基礎(chǔ)知識(shí)
數(shù)據(jù)包
既然要討論數(shù)據(jù)包的流轉(zhuǎn),先看看什么是數(shù)據(jù)包。
網(wǎng)絡(luò)數(shù)據(jù)包(network packet),也稱為網(wǎng)絡(luò)數(shù)據(jù)報(bào)(network datagram)或網(wǎng)絡(luò)幀(Network frame),是通過(guò)計(jì)算機(jī)網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)單位。拿最常見(jiàn)的 TCP 數(shù)據(jù)包來(lái)看包含如下幾個(gè)部分:
- Ethernet header:鏈路層信息,主要包括目的 MAC 地址和源 MAC 地址,以及報(bào)文的格式,這里是 IP 包。
- IP header:網(wǎng)絡(luò)層信息,主要包括長(zhǎng)度、源 IP 地址和目的 IP 地址以及報(bào)文的格式,當(dāng)然這里必須是 TCP 包。
- TCP header:傳輸層信息,包括源端口和目的端口。
- 數(shù)據(jù):一般是第 7 層的數(shù)據(jù),比如 HTTP 等。
這里沒(méi)有介紹的 checksum 和 FCS 通常是用來(lái)檢查數(shù)據(jù)包在傳輸過(guò)程中是否被篡改或者發(fā)生了錯(cuò)誤。
應(yīng)用程序使用 socket 向網(wǎng)絡(luò)發(fā)送數(shù)據(jù)的過(guò)程可以簡(jiǎn)單理解為使用頭信息封裝數(shù)據(jù)的過(guò)程:TCP 數(shù)據(jù)包、IP 數(shù)據(jù)包、Ethernet 數(shù)據(jù)包;反過(guò)來(lái),從網(wǎng)絡(luò)接收以太網(wǎng)數(shù)據(jù)包到應(yīng)用程序可以處理的數(shù)據(jù),就是解包的過(guò)程。封包和解包的過(guò)程是由內(nèi)核網(wǎng)絡(luò)協(xié)議棧來(lái)完成的。
下面分別說(shuō)一下 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)核中對(duì)數(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
通過(guò)下面的圖,可以直觀感受各個(gè)操作的作用:
開(kāi)始講解內(nèi)核網(wǎng)絡(luò)協(xié)議棧之前,先說(shuō)下數(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ù)長(zhǎng)度、源和目標(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é)議棧的各個(gè)層級(jí),如數(shù)據(jù)鏈路層、網(wǎng)絡(luò)層、傳輸層等。sk_buff 數(shù)據(jù)結(jié)構(gòu)的字段很多,有 4 個(gè)重要的字段且都是指針類型。sk_buff 在不同層的使用,就是通過(guò)修改這些指針來(lái)完成的:加 header (封包)和移除 header(解包)。
這個(gè)過(guò)程操作做的是指針,數(shù)據(jù)是零拷貝的,可以極大地提升效率。
內(nèi)核網(wǎng)絡(luò)協(xié)議棧
封包
應(yīng)用程序使用 socket 的 sendmsg 操作發(fā)送數(shù)據(jù)(這里不深入講解 netfilter、traffic control、queue discipline):
- 先分配 sk_buff
- 接下來(lái)開(kāi)始網(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)
- 如果包過(guò)長(zhǎng),分段傳輸
- L2 尋址,即查找可以擁有目標(biāo) IP 地址的設(shè)備的 MAC 地址
- 設(shè)置鏈路層信息,
- 至此內(nèi)核網(wǎng)絡(luò)協(xié)議棧的操作完成
- 調(diào)用 tc(traffic control)egress(可以對(duì)包進(jìn)行重定向)
- 進(jìn)入隊(duì)列 queue discipline(qdisc)
- 寫(xiě)入 NIC(network interface controler)
- 發(fā)送到網(wǎng)絡(luò)
解包
NIC 收到網(wǎng)絡(luò)發(fā)來(lái)的數(shù)據(jù)包(這里不深入講解 direct memory access、netfilter、traffic control):
- 將數(shù)據(jù)包寫(xiě)如 DMA 中(Direct Memory Access 直接內(nèi)存訪問(wèn),不需要依賴 CPU,由 NIC 直接寫(xiě)入到內(nèi)存中)
- 分配 sk_buff,并填充元數(shù)據(jù),比如 protocol 為 Ethernet 類型,接收數(shù)據(jù)包的網(wǎng)絡(luò)接口等
- 將鏈路層信息保存在 sk_buff 的 mac_header 字段中,并“移除”數(shù)據(jù)包中的鏈路層信息(移動(dòng)指針)
- 接下來(lái)開(kāi)始網(wǎng)絡(luò)協(xié)議棧的處理
- 將網(wǎng)絡(luò)層信息保存在 network_header 字段中
- 調(diào)用 tc ingress
- “移除”網(wǎng)絡(luò)層信息
- 將傳輸層信息保存在 transport_header 字段中
- 調(diào)用 netfilter(PRE_ROUTING)
- 查找路由
- 合并多個(gè)分包
- 調(diào)用 netfilter(LOCAL_IN)
- “移除”傳輸層信息
- 查找監(jiān)聽(tīng)目標(biāo)端口的 socket,或者發(fā)送 reset
- 將數(shù)據(jù)寫(xiě)入 socket 的接收隊(duì)列中
- 發(fā)信號(hào)通知有數(shù)據(jù)寫(xiě)入隊(duì)列
- 至此內(nèi)核網(wǎng)絡(luò)協(xié)議棧的操作完成
- sk_buff 從 socket 接收隊(duì)列中出隊(duì)
- 將數(shù)據(jù)寫(xiě)入應(yīng)用程序的緩沖區(qū)
- 釋放 sk_buff
Kubernetes 的網(wǎng)絡(luò)模型
另一部分的基礎(chǔ)知識(shí)就是 Kubernetes 的網(wǎng)絡(luò)模型了,可以參考之前的那篇 深入探索 Kubernetes 網(wǎng)絡(luò)模型和網(wǎng)絡(luò)通信。
Kubernetes 中的數(shù)據(jù)包流轉(zhuǎn)
這里繼續(xù)討論之前文章中的三種通信場(chǎng)景,pod 間的通信使用 pod IP 地址。如果要討論通過(guò) Service 來(lái)訪問(wèn),則要加入 netfilter 的討論篇幅會(huì)增加不少。
同 pod 內(nèi)的容器間通信
pod 內(nèi)兩個(gè)容器間的方式通常使用回環(huán)地址 127.0.0.1?,在封包的 #4 路由過(guò)程中確定了使用回環(huán)網(wǎng)卡 lo進(jìn)行傳輸。
同節(jié)點(diǎn)上的 pod 間通信
curl? 發(fā)出的請(qǐng)求在封包 #4 過(guò)程中確定使用 eth0? 接口。然后通過(guò)與 eth0? 相連的隧道 veth1 到達(dá)節(jié)點(diǎn)的根網(wǎng)絡(luò)空間。
veth1? 通過(guò)網(wǎng)橋 cni0? 與其他 pod 相連虛擬以太接口 vethX? 相連。在封包 #10 L2 尋址中,ARP 請(qǐng)求通過(guò)網(wǎng)橋發(fā)送給所有相連的接口是否擁有原始請(qǐng)求中的目的 IP 地址(這里是 10.42.1.9)
拿到了 veth0? 的 MAC 地址后,在封包 #11 中設(shè)置數(shù)據(jù)包的鏈路層信息。數(shù)據(jù)包發(fā)出后,經(jīng)過(guò) veth0? 隧道進(jìn)入 pod httpbin? 的 eth0 接口中,然后開(kāi)始解包的過(guò)程。
解包的過(guò)程沒(méi)啥特別,確定了 httpbin 使用的 socket。
不同節(jié)點(diǎn)的 pod 間通信
這里稍微不同,就是在通過(guò) cni0? 發(fā)送 ARP 請(qǐng)求沒(méi)有收到應(yīng)答,使用根命名空間也就是主機(jī)的路由表,確定了目標(biāo)主機(jī) IP 地址后,然后通過(guò)主機(jī)的 eth0 放 ARP 請(qǐng)求并收到目標(biāo)主機(jī)的響應(yīng)。將其 MAC 地址在封包 #11 中寫(xiě)入。
數(shù)據(jù)包發(fā)送到目標(biāo)主機(jī)后,開(kāi)始解包的過(guò)程,最終進(jìn)入目標(biāo) pod。
在集群層面有一張路由表,里面存儲(chǔ)著每個(gè)節(jié)點(diǎn)的 Pod IP 網(wǎng)段(節(jié)點(diǎn)加入到集群時(shí)會(huì)分配一個(gè) 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,依次類推)。通過(guò)節(jié)點(diǎn)的 Pod IP 網(wǎng)段可以判斷出請(qǐng)求 IP 的節(jié)點(diǎn),然后請(qǐng)求被發(fā)送到該節(jié)點(diǎn)。
總結(jié)
統(tǒng)計(jì)一下在三個(gè)場(chǎng)景中,經(jīng)過(guò)內(nèi)核網(wǎng)絡(luò)協(xié)議棧的處理次數(shù)都是兩次(包括 netfilter 的處理。),即使是同 pod 或者同節(jié)點(diǎn)內(nèi)。而這兩種情況實(shí)際都發(fā)生在同一個(gè)內(nèi)核空間中。
假如同一個(gè)內(nèi)核空間中的兩個(gè) socket 可以直接傳輸數(shù)據(jù),是不是就可以省掉內(nèi)核網(wǎng)絡(luò)協(xié)議棧處理帶來(lái)的延遲?
下篇繼續(xù)。
參考資料
[1] sk_buff: https://elixir.bootlin.com/linux/latest/source/include/linux/skbuff.h#L843