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