文需要讀者熟悉 Ethernet(以太網(wǎng))的基本原理和 Linux 系統(tǒng)的基本網(wǎng)絡(luò)命令,以及 TCP/IP 協(xié)議族并了解傳統(tǒng)的網(wǎng)絡(luò)模型和協(xié)議包的流轉(zhuǎn)原理。文中涉及到 Linux 內(nèi)核的具體實(shí)現(xiàn)時,均以內(nèi)核 v4.19.215 版本為準(zhǔn)。
1 從網(wǎng)卡到內(nèi)核協(xié)議棧
如圖[1],網(wǎng)絡(luò)包到達(dá) NC(Network Computer,本文指物理機(jī))時,由 NIC(Network Interface Controller,網(wǎng)絡(luò)接口控制器,俗稱網(wǎng)卡)設(shè)備處理,NIC 以中斷的方式向內(nèi)核傳遞消息。Linux 內(nèi)核的中斷處理分為上半部(Top Half)和下半部(Bottom Half)。上半部需要盡快處理掉和硬件相關(guān)的工作并返回,下半部由上半部激活來處理后續(xù)比較耗時的工作。
具體到 NIC 的處理流程如下:當(dāng) NIC 收到數(shù)據(jù)時,會以 DMA 方式將數(shù)據(jù)拷貝到 Ring Buffer (接收隊列) 里描述符指向的映射內(nèi)存區(qū)域,拷貝完成后會觸發(fā)中斷通知 CPU 進(jìn)行處理。這里可以使用 ethtool -g {設(shè)備名,如eth0} 命令查看 RX/TX (接收/發(fā)送)隊列的大小。CPU 識別到中斷后跳轉(zhuǎn)到 NIC 的中斷處理函數(shù)開始執(zhí)行。此時要區(qū)分 NIC 的工作模式,在早先的非 NAPI(New API)[2]模式下,中斷上半部更新相關(guān)的寄存器信息,查看接收隊列并分配 sk_buff 結(jié)構(gòu)指向接收到的數(shù)據(jù),最后調(diào)用 netif_rx() 把 sk_buff 遞交給內(nèi)核處理。在 netif_rx() 的函數(shù)的流程中,這個分配的 sk_buff 結(jié)構(gòu)被放入 input_pkt_queue隊列后,會把一個虛擬設(shè)備加入poll_list 輪詢隊列并觸發(fā)軟中斷 NET_RX_SOFTIRQ 激活中斷下半部。此時中斷上半部就結(jié)束了,詳細(xì)的處理流程可以參見 net/core/dev.c 的 netif_rx() -> netif_rx_internal() -> enqueue_to_backlog() 過程。下半部 NET_RX_SOFTIRQ 軟中斷對應(yīng)的處理函數(shù)是 net_rx_action(),這個函數(shù)會調(diào)用設(shè)備注冊的 poll() 函數(shù)進(jìn)行處理。非 NAPI 的情況下這個虛擬設(shè)備的 poll() 函數(shù)固定指向 process_backlog() 函數(shù)。這個函數(shù)將 sk_buff 從 input_pkt_queue 移動到 process_queue 中,調(diào)用 __netif_receive_skb() 函數(shù)將其投遞給協(xié)議棧,最后協(xié)議棧相關(guān)代碼會根據(jù)協(xié)議類型調(diào)用相應(yīng)的接口進(jìn)行后續(xù)的處理。特別地,這里的 enqueue_to_backlog() 以及 process_backlog() 函數(shù)也用于和啟用了 RPS 機(jī)制后的相關(guān)邏輯。
非 NAPI(New API)模式下每個網(wǎng)絡(luò)包的到達(dá)都會觸發(fā)一次中斷處理流程,這么做降低了整體的處理能力,已經(jīng)過時了。現(xiàn)在大多數(shù) NIC 都支持 NAPI 模式了。NAPI 模式下在首包觸發(fā) NIC 中斷后,設(shè)備就會被加入輪詢隊列進(jìn)行輪詢操作以提升效率,輪詢過程中不會產(chǎn)生新的中斷。為了支持 NAPI,每個 CPU 維護(hù)了一個叫 softnet_data 的結(jié)構(gòu),其中有一個 poll_list 字段放置所有的輪詢設(shè)備。此時中斷上半部很簡單,只需要更新 NIC 相關(guān)的寄存器信息,以及把設(shè)備加入poll_list 輪詢隊列并觸發(fā)軟中斷 NET_RX_SOFTIRQ就結(jié)束了。中斷下半部的處理依舊是 net_rx_action() 來調(diào)用設(shè)備驅(qū)動提供的 poll() 函數(shù)。只是 poll() 此時指向的就是設(shè)備驅(qū)動提供的輪詢處理函數(shù)了(而不是非 NAPI 模式下的內(nèi)核函數(shù) process_backlog())。這個設(shè)備驅(qū)動提供的輪詢 poll() 函數(shù)最后也會調(diào)用 __netif_receive_skb() 函數(shù)把 sk_buff 提交給協(xié)議棧處理。
非 NAPI 模式和 NAPI 模式下的流程對比如下(其中灰色底色是設(shè)備驅(qū)動要實(shí)現(xiàn)的,其他都是內(nèi)核自身的實(shí)現(xiàn)):
關(guān)于 NAPI 模式網(wǎng)絡(luò)設(shè)備驅(qū)動的實(shí)現(xiàn)以及詳細(xì)的 NAPI 模式的處理流程,這里提供一篇文章和其譯文作為參考[3](強(qiáng)烈推薦)。這篇文章很詳細(xì)的描述了 Intel Ethernet Controller I350 這個 NIC 設(shè)備的收包和處理細(xì)節(jié)(其姊妹篇發(fā)包處理過程和譯文[4])。另外收包這里還涉及到多網(wǎng)卡的 Bonding 模式(可以在/proc/net/bonding/bond0 里查看模式)、網(wǎng)絡(luò)多隊列(sudo lspci -vvv 查看 Ethernet controller 的 Capabilities信息里有 MSI-X: Enable+ Count=10 字樣說明 NIC 支持,可以在 /proc/interrupts 里查看中斷綁定情況)等機(jī)制。這些本文都不再贅述,有興趣的話請參閱相關(guān)資料[5]。
2 內(nèi)核協(xié)議棧網(wǎng)絡(luò)包處理流程
前文說到 NIC 收到網(wǎng)絡(luò)包構(gòu)造出的 sk_buff 結(jié)構(gòu)最終被 __netif_receive_skb() 提交給了內(nèi)核協(xié)議棧解析處理。這個函數(shù)首先進(jìn)行 RPS[5] 相關(guān)的處理,數(shù)據(jù)包會繼續(xù)在隊列里轉(zhuǎn)一圈(一般開啟了 RSS 的網(wǎng)卡不需要開啟 RPS)。如果需要分發(fā)包到其他 CPU 去處理,則會使用 enqueue_to_backlog() 投遞給其他 CPU 的隊列,并在 process_backlog()) 中觸發(fā) IPI(Inter-Processor Interrupt,處理器間中斷,于 APIC 總線上傳輸,并不通過 IRQ)給其他 CPU 發(fā)送通知(net_rps_send_ipi()函數(shù))。
最終,數(shù)據(jù)包會由 __netif_receive_skb_core() 進(jìn)行下一階段的處理。這個處理函數(shù)主要的功能有:
截至目前,數(shù)據(jù)包仍舊在數(shù)據(jù)鏈路層的處理流程中。這里復(fù)習(xí)下 OSI 七層模型與 TCP/IP 五層模型:
在網(wǎng)絡(luò)分層模型里,后一層即為前一層的數(shù)據(jù)部分,稱之為載荷(Payload)。一個完整的 TCP/IP 應(yīng)用層數(shù)據(jù)包的格式如下[6]:
__netif_receive_skb_core() 的處理邏輯中需要關(guān)注的是網(wǎng)橋和接下來 IP 層以及 TCP/UDP 層的處理。首先看 IP 層,__netif_receive_skb_core() 調(diào)用 deliver_skb(),后者調(diào)用具體協(xié)議的 .func() 接口。對于 IP 協(xié)議,這里指向的是 ip_rcv() 函數(shù)。這個函數(shù)做了一些統(tǒng)計和檢查之后,就把包轉(zhuǎn)給了 Netfilter [7]框架并指定了函數(shù) ip_rcv_finish() 進(jìn)行后續(xù)的處理(如果包沒被 Netfilter 丟棄)。經(jīng)過路由子系統(tǒng)檢查處理后,如果包是屬于本機(jī)的,那么會調(diào)用 ip_local_deliver() 將數(shù)據(jù)包繼續(xù)往上層協(xié)議轉(zhuǎn)發(fā)。這個函數(shù)類似之前的邏輯,依舊是呈遞給 Netfilter 框架并指定函數(shù) ip_local_deliver_finish() 進(jìn)行后續(xù)的處理,這個函數(shù)最終會檢查和選擇對應(yīng)的上層協(xié)議接口進(jìn)行處理。
常見的上層協(xié)議比如 TCP 或者 UDP 協(xié)議的流程不在本文討論的范圍內(nèi),僅 TCP 的流程所需要的篇幅足以超過本文所有的內(nèi)容。這里給出 TCP 協(xié)議(v4)的入口函數(shù) tcp_v4_rcv() 以及 UDP 協(xié)議的入口函數(shù) udp_rcv() 作為指引自行研究,也可以閱讀其他的資料進(jìn)行進(jìn)一步的了解[9]。
3 Netfilter/iptables 與 NAT(網(wǎng)絡(luò)地址轉(zhuǎn)換)
關(guān)于 Netfilter 框架需要稍微著重的強(qiáng)調(diào)一下,因為后文要提到的網(wǎng)絡(luò)策略和很多服務(wù)透出的實(shí)現(xiàn)都要使用 Netfilter 提供的機(jī)制。
Netfilter 是內(nèi)核的包過濾框架(Packet Filtering Framework)的實(shí)現(xiàn)。簡單說就是在協(xié)議棧的各個層次的包處理函數(shù)中內(nèi)置了很多的 Hook 點(diǎn)來支持在這些點(diǎn)注冊回調(diào)函數(shù)。
圖片來自 Wikimedia,可以點(diǎn)開參考文獻(xiàn)[8]查看大圖(svg 矢量圖,可以調(diào)大網(wǎng)頁顯示百分比繼續(xù)放大)。
Linux 上最常用的防火墻 iptables 即是基于 Netfilter 來實(shí)現(xiàn)的(nftables 是新一代的防火墻)。iptables 基于表和鏈(Tables and Chains)的概念來組織規(guī)則。注意這里不要被“防火墻”這個詞誤導(dǎo)了,iptables 所能做的不僅僅是對包的過濾(Filter Table),還支持對包進(jìn)行網(wǎng)絡(luò)地址轉(zhuǎn)換(NAT Table)以及修改包的字段(Mangle Table)。在網(wǎng)絡(luò)虛擬化里,用的最多的便是 NAT 地址轉(zhuǎn)換功能。通常此類功能一般在網(wǎng)關(guān)網(wǎng)絡(luò)設(shè)備或是負(fù)載均衡設(shè)備中很常見。當(dāng) NC 需要在內(nèi)部進(jìn)行網(wǎng)絡(luò)相關(guān)的虛擬化時,也是一個類似網(wǎng)關(guān)以及負(fù)載均衡設(shè)備了。
在設(shè)置 iptables 的 NAT 規(guī)則前,還需要打開內(nèi)核的包轉(zhuǎn)發(fā)功能 echo "1" > /proc/sys/net/ipv4/ip_forward 才可以。另外建議也打開 echo "1" /proc/sys/net/bridge/bridge-nf-call-iptables 開關(guān)(可能需要 modprobe br_netfilter)。bridge-nf-call-iptables 從上面的源碼分析就能理解,網(wǎng)橋的轉(zhuǎn)發(fā)處理是在 Netfilter 規(guī)則之前的。所以默認(rèn)情況下二層網(wǎng)橋的轉(zhuǎn)發(fā)是不會受到三層 iptables 的限制的,但是很多虛擬化網(wǎng)絡(luò)的實(shí)現(xiàn)需要 Netfilter 規(guī)則生效,所以內(nèi)核也支持了讓網(wǎng)橋的轉(zhuǎn)發(fā)邏輯也調(diào)用一下 Netfilter 的規(guī)則。這個特性默認(rèn)情況不開啟,所以需要檢查開關(guān)。至于具體的 iptables 命令,可以參考這篇文章和其譯文[10]進(jìn)行了解,本文不再討論。
這里強(qiáng)調(diào)下,Netfilter 的邏輯運(yùn)行在內(nèi)核軟中斷上下文里。如果 Netfilter 添加了很多規(guī)則,必然會造成一定的 CPU 開銷。下文在提到虛擬化網(wǎng)絡(luò)的性能降低時,很大一部分開銷便是源自這里。
在傳統(tǒng)的網(wǎng)絡(luò)認(rèn)知里,網(wǎng)絡(luò)就是由帶有一個或多個 NIC 的一組 NC 使用硬件介質(zhì)和 switch(交換機(jī))、Router(路由器)所組成的一個通信集合(圖片來自 [11],下同):
網(wǎng)絡(luò)虛擬化作為 SDN(Software?Defined?Network,軟件定義網(wǎng)絡(luò))的一種實(shí)現(xiàn),無非就是虛擬出 vNIC(虛擬網(wǎng)卡)、vSwitch(虛擬交換機(jī))、vRouter(虛擬路由器)等設(shè)備,配置相應(yīng)的數(shù)據(jù)包流轉(zhuǎn)規(guī)則而已。其對外的接口必然也是符合其所在的物理網(wǎng)絡(luò)協(xié)議規(guī)范的,比如 Ethernet 和 TCP/IP 協(xié)議族。
隨著 Linux 網(wǎng)絡(luò)虛擬化技術(shù)的演進(jìn),有了若干種虛擬化網(wǎng)絡(luò)設(shè)備,在虛擬機(jī)和虛擬容器網(wǎng)絡(luò)中得到了廣泛的應(yīng)用。典型的有 Tap/Tun/Veth、Bridge 等:
虛擬機(jī)和容器的網(wǎng)絡(luò)在傳輸流程上有些區(qū)別,前者比如 KVM 一般是使用 Tap 設(shè)備將虛擬機(jī)的 vNIC 和宿主機(jī)的網(wǎng)橋 Bridge 連接起來。而容器的 Bridge 網(wǎng)絡(luò)模式是將不同 Namespace 里的 Veth Pair 連接網(wǎng)橋 Bridge 來實(shí)現(xiàn)通信(其他方式下文討論)。
Linux Bridge 配合橋接或者 NAT 模式很容易可以實(shí)現(xiàn)同主機(jī)或跨主機(jī)的虛擬機(jī)/容器之間通信,而且 Bridge 本身也支持 VLAN 的配置,可以實(shí)現(xiàn)一些三層交換機(jī)的能力。但是很多廠商都在研發(fā)功能更豐富的虛擬交換機(jī),流行的有 Cisco Nexus 1000V、 VMware Virtual Switch 以及廣泛使用的開源的 Open vSwitch[12] 等。利用 vSwitch,可以構(gòu)建出支持更多封裝協(xié)議、更高級的虛擬網(wǎng)絡(luò):
1 Linux Bridge + Veth Pair 轉(zhuǎn)發(fā)
VRF(Virtual Routing and Forwarding,虛擬路由轉(zhuǎn)發(fā))在網(wǎng)絡(luò)領(lǐng)域中是個很常見的術(shù)語。上世紀(jì)九十年代開始,很多二層交換機(jī)上就能創(chuàng)建出 4K 的 VLAN 廣播域了。4K 是因為 VLAN 標(biāo)簽的格式遵循 802.1q 標(biāo)準(zhǔn),其中定義的 VLAN ID 是 12 位的緣故(802.1q in 802.1q 可以做到 4094*4094 個,0 和 4095 保留)。如今 VRF 概念被引入三層,單個物理設(shè)備上也可以有多個虛擬路由/轉(zhuǎn)發(fā)實(shí)例了。Linux 的 VRF 實(shí)現(xiàn)了對三層的網(wǎng)絡(luò)協(xié)議棧的虛擬,而 Network Namespace(以下簡稱 netns)虛擬了整個網(wǎng)絡(luò)棧。一個 netns 的網(wǎng)絡(luò)棧包括:網(wǎng)卡(Network Interface)、回環(huán)設(shè)備(Loopback Device)、路由表(Routing Table)和 iptables 規(guī)則。本文使用 netns 進(jìn)行演示(畢竟在討論容器),下文使用 ip[14] 命令創(chuàng)建和管理 netns 以及 Veth Pair 設(shè)備。
創(chuàng)建、查看、刪除 Network Namespace
# 創(chuàng)建名為 qianyi-test-1 和 add qianyi-test-2 的命名 netns,可以在 /var/run/netns/ 下查看
ip netns add qianyi-test-1
ip netns add qianyi-test-2
# 查看所有的 Network Namespace
ip netns list
# 刪除 Network Namespace
ip netns del qianyi-test-1
ip netns del qianyi-test-2
執(zhí)行結(jié)果如圖(刪除先不執(zhí)行):
有興趣的話可以使用 strace 命令跟蹤這個創(chuàng)建過程看看 ip 命令是怎么創(chuàng)建的(strace ip netns add qianyi-test-1)。
在 netns 中執(zhí)行命令
# 在 qianyi-test-1 這個 netns 中執(zhí)行 ip addr 命令(甚至可以直接執(zhí)行 bash 命令得到一個 shell)
# nsenter 這個命令也很好用,可以 man nsenter 了解
ip netns exec qianyi-test-1 ip addr
執(zhí)行結(jié)果如下:
圖片
這個新創(chuàng)建的 netns 里一貧如洗,只有一個孤獨(dú)的 lo 網(wǎng)卡,還是 DOWN 狀態(tài)的。下面開啟它:
開啟 lo 網(wǎng)卡,這個很重要
ip netns exec qianyi-test-1 ip link set dev lo up
ip netns exec qianyi-test-2 ip link set dev lo up
狀態(tài)變成了 UNKOWN,這是正常的。這個狀態(tài)是驅(qū)動提供的,但是 lo 的驅(qū)動沒有做這個事情。
創(chuàng)建 Veth Pair 設(shè)備
# 分別創(chuàng)建 2 對名為 veth-1-a/veth-1-b 和 veth-2-a/veth-2-b 的 Veth Pair 設(shè)備
ip link add veth-1-a type veth peer name veth-1-b
ip link add veth-2-a type veth peer name veth-2-b
使用 ip addr 命令可以查看:
8-9,10-11 便是上面創(chuàng)建出來的 2 對 Veth Pair 設(shè)備,此時它們都沒有分配 IP 地址且是 DOWN 狀態(tài)。
將 Veth Pair 設(shè)備加入 netns
# 將 veth-1-a 設(shè)備加入 qianyi-test-1 這個 netns
ip link set veth-1-a netns qianyi-test-1
# 將 veth-1-b 設(shè)備加入 qianyi-test-2 這個 netns
ip link set veth-1-b netns qianyi-test-2
# 為設(shè)備添加 IP 地址/子網(wǎng)掩碼并開啟
ip netns exec qianyi-test-1 ip addr add 10.0.0.101/24 dev veth-1-a
ip netns exec qianyi-test-1 ip link set dev veth-1-a up
ip netns exec qianyi-test-2 ip addr add 10.0.0.102/24 dev veth-1-b
ip netns exec qianyi-test-2 ip link set dev veth-1-b up
此時我們分別在兩個 netns 中執(zhí)行 ip addr 命令,即可看到設(shè)備已經(jīng)存在,且路由表(route 或 ip route 命令)也被默認(rèn)創(chuàng)建了:
這里操作和查看設(shè)備都必須采用 ip netns exec {...} 的方式進(jìn)行,如果命令很多,也可以把執(zhí)行的命令換成 bash,這樣可以方便的在這個 shell 里對該 netns 進(jìn)行操作。
現(xiàn)在通過 veth-1-a/veth-1-b 這對 Veth Pair 聯(lián)通了 qianyi-test-1 和 qianyi-test-2 這兩個 netns,這兩個 netns 之間就可以通過這兩個 IP 地址相互訪問了。
ping 的同時在 101 上抓包的結(jié)果如下:
可以很清楚的看到,eth-1-a(10.0.0.101)先通過 ARP (Address Resolution Protocol,地址解析協(xié)議)詢問 10.0.0.102 的 MAC 地址。得到回應(yīng)后,就以 ICMP (Internet Control Message Protocol,Internet 報文控制協(xié)議) request 和 reply 了,這也正是 ping 使用的協(xié)議。
ARP 解析的緩存信息也可以通過 arp 命令查看:
此時的網(wǎng)絡(luò)連接模式是這樣的:
這種連接模式,就很像是現(xiàn)實(shí)中把兩個帶有 NIC 的設(shè)備用網(wǎng)線連接起來,然后配置了相同網(wǎng)段的 IP 后彼此就可以通信了。那如果超過一個設(shè)備需要建立互聯(lián)呢?現(xiàn)實(shí)中就需要交換機(jī)等網(wǎng)絡(luò)設(shè)備了。還記得前文中說的 Linux 自帶的 Bridge 么?接下來就使用 Bridge 機(jī)制建立網(wǎng)絡(luò)。
進(jìn)行下面的試驗前需要把 veth-1-a/veth-1-b 這對 Veth Pair 從 qianyi-test-1 和 qianyi-test-2 移動回宿主機(jī)的 netns 里,恢復(fù)初始的環(huán)境。
# 宿主機(jī)的 netns id 是 1(有些系統(tǒng)可能不是,請查詢相關(guān)系統(tǒng)的文檔)
ip netns exec qianyi-test-1 ip link set veth-1-a netns 1
ip netns exec qianyi-test-2 ip link set veth-1-b netns 1
創(chuàng)建 Linux Bridge 并配置網(wǎng)絡(luò)
# 創(chuàng)建一個名為 br0 的 bridge 并啟動(也可以使用 brctl 命令)
ip link add br0 type bridge
ip link set br0 up
# 將 veth-1-a/veth-1-b 和 veth-2-a/veth-2-b 這兩對 Veth Pair 的 a 端放入 qianyi-test-1 和 qianyi-test-2
ip link set veth-1-a netns qianyi-test-1
ip link set veth-2-a netns qianyi-test-2
# 為 veth-1-a 和 veth-2-a 配置 IP 并開啟
ip netns exec qianyi-test-1 ip addr add 10.0.0.101/24 dev veth-1-a
ip netns exec qianyi-test-1 ip link set dev veth-1-a up
ip netns exec qianyi-test-2 ip addr add 10.0.0.102/24 dev veth-2-a
ip netns exec qianyi-test-2 ip link set dev veth-2-a up
# 將 veth-1-a/veth-1-b 和 veth-2-a/veth-2-b 這兩對 Veth Pair 的 b 端接入 br0 網(wǎng)橋并開啟接口
ip link set veth-1-b master br0
ip link set dev veth-1-b up
ip link set veth-2-b master br0
ip link set dev veth-2-b up
執(zhí)行完可以查看創(chuàng)建好的網(wǎng)橋和配置好的 IP,實(shí)際上 brctl show 命令顯示的結(jié)果更易懂,可以很清楚的看到 veth-1-b 和 veth-2-b 連接在了網(wǎng)橋的接口上。當(dāng) Veth Pair 的一端連接在網(wǎng)橋上時,就會從“網(wǎng)卡”退化成一個“水晶頭”。
當(dāng)下模式抓包的結(jié)果并沒有什么區(qū)別,但網(wǎng)絡(luò)連接模式不同:
按照這個模式,如果有更多的 Network Namespace 和 Veth Pair 的話,使用相同的方式添加就可以水平擴(kuò)展了。
但是嘗試從 qianyi-test-1 中 ping 宿主機(jī)自然是不通的,因為沒有任何網(wǎng)絡(luò)規(guī)則可以訪問到宿主機(jī)的網(wǎng)絡(luò):
上面的截圖中有個 docker0 的網(wǎng)橋。當(dāng)機(jī)器上安裝了 Docker 之后會被自動設(shè)置好這個網(wǎng)橋供 Docker 使用。可能你注意到了,這個名為 docker0 的網(wǎng)橋居然是有 IP 地址的。現(xiàn)實(shí)中的網(wǎng)橋自然是沒有 IP 的,但是 Linux Bridge 這個虛擬設(shè)備是可以設(shè)置的。當(dāng) Bridge 設(shè)置 IP 之后,就可以將其設(shè)置成這個內(nèi)部網(wǎng)絡(luò)的網(wǎng)關(guān)(Gateway),再配合路由規(guī)則就可以實(shí)現(xiàn)最簡單的虛擬網(wǎng)絡(luò)跨機(jī)通信了(類似現(xiàn)實(shí)中的三層交換機(jī))。
下面繼續(xù)給 br0 網(wǎng)橋創(chuàng)建地址并在 veth-1-a 和 veth-2-a 上設(shè)置其為默認(rèn)的網(wǎng)關(guān)地址:
# 確認(rèn)路由轉(zhuǎn)發(fā)開啟
echo "1" > /proc/sys/net/ipv4/ip_forward
# 為 br0 設(shè)置 IP 地址
ip addr add local 10.0.0.1/24 dev br0
# 為 veth-1-a 和 veth-2-a 設(shè)置默認(rèn)網(wǎng)關(guān)
ip netns exec qianyi-test-1 ip route add default via 10.0.0.1
ip netns exec qianyi-test-2 ip route add default via 10.0.0.1
此時就能成功的訪問宿主機(jī)地址了(宿主機(jī)上的路由表在 ip link set br0 up 這一步自動創(chuàng)建了):
網(wǎng)絡(luò)模型進(jìn)一步變成了這樣:
如果此時,另一臺宿主機(jī)上也存在另一個網(wǎng)段的網(wǎng)橋和若干個 netns 的話,怎么讓他們互通呢?分別在兩邊宿主機(jī)上配置一條到目的宿主機(jī)的路由規(guī)則就好了。假設(shè)另一臺宿主機(jī)的 IP 地址是 10.97.212.160,子網(wǎng)是 10.0.1.0/24 的話,那么需要在當(dāng)前機(jī)器上加一條 10.0.1.0/24 via 10.97.212.160 的規(guī)則,10.97.212.160 上加一條 10.0.0.0/24 via 10.97.212.159 的規(guī)則就可以了(或者 iptables 配置 SNAT/DNAT 規(guī)則)。那如果有 N 臺呢?那就會是個 N * N 條的規(guī)則,會很復(fù)雜。這就一個簡單的 Underlay 模式的容器通信方案了。缺點(diǎn)也很明顯,要求對宿主機(jī)底層網(wǎng)絡(luò)有修改權(quán),且比較難和底層網(wǎng)絡(luò)解耦。那如果能在物理網(wǎng)絡(luò)上構(gòu)建出一個橫跨所有宿主機(jī)的虛擬網(wǎng)橋,把所有相關(guān)的 netns 里面的設(shè)備都連接上去,不就可以解耦了么。這就是 Overlay Network(覆蓋網(wǎng)絡(luò))方案,下文會進(jìn)行闡述。至于本節(jié)其他虛擬網(wǎng)絡(luò)設(shè)備的安裝和配置(比如 Open vSwitch)也是比較類似的,這里不再贅述,有興趣的話可以自行查看文檔并測試。
2 Overlay 網(wǎng)絡(luò)方案之 VXLAN
VXLAN(Virtual eXtensible Local Area Network,虛擬可擴(kuò)展局域網(wǎng),RFC7348)[16],VLAN 的擴(kuò)展協(xié)議,是由 IETF 定義的 NVO3(Network Virtualization over Layer 3)標(biāo)準(zhǔn)技術(shù)之一(其他有代表性的還有 NVGRE、STT)。但是 VXLAN 和 VLAN 想要解決的問題是不一樣的。VXLAN 本質(zhì)上是一種隧道封裝技術(shù),它將數(shù)據(jù)鏈路層(L2)的以太網(wǎng)幀(Ethernet frames)封裝成傳輸層(L4)的 UDP 數(shù)據(jù)報(Datagrams),然后在網(wǎng)絡(luò)層(L3)中傳輸。效果就像數(shù)據(jù)鏈路層(L2)的以太網(wǎng)幀在一個廣播域中傳輸一樣,即跨越了三層網(wǎng)絡(luò)卻感知不到三層的存在。因為是基于 UDP 封裝,只要是 IP 網(wǎng)絡(luò)路由可達(dá)就可以構(gòu)建出龐大的虛擬二層網(wǎng)絡(luò)。也因為是基于高層協(xié)議再次封裝,性能會比傳統(tǒng)的網(wǎng)絡(luò)低 20%~30% 左右(性能數(shù)據(jù)隨著技術(shù)發(fā)展會有變化,僅代表當(dāng)前水平)。
這里簡要介紹下 VXLAN 的 2 個重要概念:
VXLAN 的報文格式如圖[17]:
Linux kernel v3.7.0 版本開始支持 VXLAN 網(wǎng)絡(luò)。但為了穩(wěn)定性和其他功能,請盡量選擇 kernel v3.10.0 及之后的版本。下面我們使用 10.97.212.159 和 11.238.151.74 這兩臺機(jī)器創(chuàng)建一個測試的 VXLAN 網(wǎng)絡(luò)。
# 10.97.212.159 上操作
# 創(chuàng)建名為 qianyi-test-1 和 add qianyi-test-2 的命名 netns,可以在 /var/run/netns/ 下查看
ip netns add qianyi-test-1
ip netns add qianyi-test-2
# 開啟 lo 網(wǎng)卡,這個很重要
ip netns exec qianyi-test-1 ip link set dev lo up
ip netns exec qianyi-test-2 ip link set dev lo up
# 創(chuàng)建一個名為 br0 的 bridge 并啟動(也可以使用 brctl 命令)
ip link add br0 type bridge
ip link set br0 up
# 分別創(chuàng)建 2 對名為 veth-1-a/veth-1-b 和 veth-2-a/veth-2-b 的 Veth Pair 設(shè)備
ip link add veth-1-a type veth peer name veth-1-b
ip link add veth-2-a type veth peer name veth-2-b
# 將 veth-1-a/veth-1-b 和 veth-2-a/veth-2-b 這兩對 Veth Pair 的 a 端放入 qianyi-test-1 和 qianyi-test-2
ip link set veth-1-a netns qianyi-test-1
ip link set veth-2-a netns qianyi-test-2
# 為 veth-1-a 和 veth-2-a 配置 IP 并開啟
ip netns exec qianyi-test-1 ip addr add 10.0.0.101/24 dev veth-1-a
ip netns exec qianyi-test-1 ip link set dev veth-1-a up
ip netns exec qianyi-test-2 ip addr add 10.0.0.102/24 dev veth-2-a
ip netns exec qianyi-test-2 ip link set dev veth-2-a up
# 將 veth-1-a/veth-1-b 和 veth-2-a/veth-2-b 這兩對 Veth Pair 的 b 端接入 br0 網(wǎng)橋并開啟接口
ip link set veth-1-b master br0
ip link set dev veth-1-b up
ip link set veth-2-b master br0
ip link set dev veth-2-b up
# 11.238.151.74 上操作
# 創(chuàng)建名為 qianyi-test-3 和 add qianyi-test-4 的命名 netns,可以在 /var/run/netns/ 下查看
ip netns add qianyi-test-3
ip netns add qianyi-test-4
# 開啟 lo 網(wǎng)卡,這個很重要
ip netns exec qianyi-test-3 ip link set dev lo up
ip netns exec qianyi-test-4 ip link set dev lo up
# 創(chuàng)建一個名為 br0 的 bridge 并啟動(也可以使用 brctl 命令)
ip link add br0 type bridge
ip link set br0 up
# 分別創(chuàng)建 2 對名為 veth-3-a/veth-3-b 和 veth-4-a/veth-4-b 的 Veth Pair 設(shè)備
ip link add veth-3-a type veth peer name veth-3-b
ip link add veth-4-a type veth peer name veth-4-b
# 將 veth-3-a/veth-3-b 和 veth-4-a/veth-4-b 這兩對 Veth Pair 的 a 端放入 qianyi-test-3 和 qianyi-test-4
ip link set veth-3-a netns qianyi-test-3
ip link set veth-4-a netns qianyi-test-4
# 為 veth-3-a 和 veth-4-a 配置 IP 并開啟
ip netns exec qianyi-test-3 ip addr add 10.0.0.103/24 dev veth-3-a
ip netns exec qianyi-test-3 ip link set dev veth-3-a up
ip netns exec qianyi-test-4 ip addr add 10.0.0.104/24 dev veth-4-a
ip netns exec qianyi-test-4 ip link set dev veth-4-a up
# 將 veth-3-a/veth-3-b 和 veth-4-a/veth-4-b 這兩對 Veth Pair 的 b 端接入 br0 網(wǎng)橋并開啟接口
ip link set veth-3-b master br0
ip link set dev veth-3-b up
ip link set veth-4-b master br0
ip link set dev veth-4-b up
這一長串的命令和之前的步驟完全一致,構(gòu)建了一個如下圖所示的網(wǎng)絡(luò)環(huán)境:
這個環(huán)境里,10.0.0.101 和 10.0.0.102 是通的,10.0.0.103 和 10.0.0.104 也是通的,但是顯然 10.0.0.101/10.0.0.102 和 10.0.0.103/10.0.0.104 是無法通信的。
接下來配置 VXLAN 環(huán)境打通這四個 netns 環(huán)境:
# 10.97.212.159 上操作(本機(jī)有多個地址時可以用 local 10.97.212.159 指定)
ip link add vxlan1 type vxlan id 1 remote 11.238.151.74 dstport 9527 dev bond0
ip link set vxlan1 master br0
ip link set vxlan1 up
# 11.238.151.74 上操作(本機(jī)有多個地址時可以用 local 11.238.151.74 指定)
ip link add vxlan2 type vxlan id 1 remote 10.97.212.159 dstport 9527 dev bond0
ip link set vxlan2 master br0
ip link set vxlan2 up
使用 brctl show br0 命令可以看到兩個 VXLAN 設(shè)備都連接上去了:
然后從 10.0.0.101 上就可以 ping 通 10.0.0.103 了:
在 10.0.0.101 上抓的包來看,就像是二層互通一樣:
直接查看 arp 緩存,也是和二層互通一模一樣:
使用 arp -d 10.0.0.103 刪掉這個緩存項目,在宿主機(jī)上重新抓包并保存文件,然后用 WireShark 打開看看(因為上面設(shè)置的不是 VXLAN 默認(rèn)端口 4789,還需要設(shè)置 WireShark 把抓到的 UDP 解析為 VXLAN 協(xié)議):
我們得到了預(yù)期的結(jié)果。此時的網(wǎng)絡(luò)架構(gòu)如圖所示:
那么問題來了,這里使用 UDP 協(xié)議能實(shí)現(xiàn)可靠通信嗎?當(dāng)然可以,可靠性不是這一層考慮的事情,而是里層被包裹的協(xié)議需要考慮的。完整的通信原理其實(shí)也并不復(fù)雜,兩臺機(jī)器上各自有 VTEP(VXLAN Tunnel Endpoints,VXLAN 隧道端點(diǎn))設(shè)備,監(jiān)聽著 9527 端口上發(fā)送的 UDP 數(shù)據(jù)包。在收到數(shù)據(jù)包后拆解通過 Bridge 傳遞給指定的設(shè)備。那 VETP 這個虛擬設(shè)備怎么知道類似 10.0.0.3 這樣的地址要發(fā)送給哪臺機(jī)器上的 VETP 設(shè)備呢?這可是虛擬的二層網(wǎng)絡(luò),底層網(wǎng)絡(luò)上可不認(rèn)識這個地址。事實(shí)上在 Linux Bridge 上維護(hù)著一個名為 FDB(Forwarding Database entry)的二層轉(zhuǎn)發(fā)表,用于保存遠(yuǎn)端虛擬機(jī)/容器的 MAC 地址、遠(yuǎn)端 VTEP 的 IP,以及 VNI 的映射關(guān)系,可以通過 bridge fdb 命令來對 FDB 表進(jìn)行操作:
# 新增條目
bridge fdb add <remote_host_mac_addr> dev <vxlan_interface> dst <remote_host_ip_addr>
# 刪除條目
bridge fdb del <remote_host_mac_addr> dev <vxlan_interface>
# 替換條目
bridge fdb replace <remote_host_mac_addr> dev <vxlan_interface> dst <remote_host_ip_addr>
# 顯示條目
bridge fdb show
上面這個簡單的實(shí)驗就 2 臺機(jī)器,使用了命令的方式直接指定了彼此的 VTEP 地址,當(dāng) fdb 表查不到信息時發(fā)給對方就行了,這是最簡單的互聯(lián)模式。大規(guī)模 VXLAN 網(wǎng)絡(luò)下,就需要考慮如何發(fā)現(xiàn)網(wǎng)絡(luò)中其他的 VETP 地址了。解決這個問題一般有 2 種方式:一是使用組播/多播( IGMP, Internet Group Management Protocol),把節(jié)點(diǎn)組成一個虛擬的整體,包不清楚發(fā)給誰的話就廣播給整個組了(上述實(shí)驗中的創(chuàng)建 VETH 設(shè)備的命令修改為組播/多播地址比如 224.1.1.1 就行,remote 關(guān)鍵字也要改成 group,具體請參閱其他資料);二是通過外部的分布式控制中心來收集 FDB 信息并分發(fā)給同一個 VXLAN 網(wǎng)絡(luò)的所有節(jié)點(diǎn)。組播/多播受限于底層網(wǎng)絡(luò)的支持情況和大規(guī)模下的的性能問題,比如很多云網(wǎng)絡(luò)上不一定允許這么做。所以下文在討論和研究 K8s 的網(wǎng)絡(luò)方案時會看到很多網(wǎng)絡(luò)插件的實(shí)現(xiàn)采用的都是類似后者的實(shí)現(xiàn)方式。
這節(jié)就介紹到這里了。當(dāng)然 Overlay 網(wǎng)絡(luò)方案也不止 VXLAN 這一種方式,只是目前很多主流的方案都采用了這種方式。其他的 Overlay 模式看上去眼花繚亂,其實(shí)說白了,無非就是 L2 over L4,L2 over L3,L3 over L3 等等各種包裝方式罷了,懂了基本原理之后都沒什么大不了的。網(wǎng)絡(luò)虛擬化的設(shè)備和機(jī)制也有很多[18],細(xì)說的話一天都說不完,但是基本的網(wǎng)絡(luò)原理掌握之后,無非是各種協(xié)議包的流轉(zhuǎn)罷了。
1 K8s 的網(wǎng)絡(luò)模型
每一個 Pod 都有它自己的 IP 地址,這就意味著你不需要顯式地在每個 Pod 之間創(chuàng)建鏈接, 你幾乎不需要處理容器端口到主機(jī)端口之間的映射。這將創(chuàng)建一個干凈的、向后兼容的模型,在這個模型里,從端口分配、命名、服務(wù)發(fā)現(xiàn)、 負(fù)載均衡、應(yīng)用配置和遷移的角度來看,Pod 可以被視作虛擬機(jī)或者物理主機(jī)。
Kubernetes 對所有網(wǎng)絡(luò)設(shè)施的實(shí)施,都需要滿足以下的基本要求(除非有設(shè)置一些特定的網(wǎng)絡(luò)分段策略):
備注:僅針對那些支持 Pods 在主機(jī)網(wǎng)絡(luò)中運(yùn)行的平臺(比如:Linux):
這個模型不僅不復(fù)雜,而且還和 Kubernetes 的實(shí)現(xiàn)廉價的從虛擬機(jī)向容器遷移的初衷相兼容, 如果你的工作開始是在虛擬機(jī)中運(yùn)行的,你的虛擬機(jī)有一個 IP,這樣就可以和其他的虛擬機(jī)進(jìn)行通信,這是基本相同的模型。
Kubernetes 的 IP 地址存在于 Pod 范圍內(nèi) - 容器共享它們的網(wǎng)絡(luò)命名空間 - 包括它們的 IP 地址和 MAC 地址。這就意味著 Pod 內(nèi)的容器都可以通過 localhost 到達(dá)各個端口。這也意味著 Pod 內(nèi)的容器都需要相互協(xié)調(diào)端口的使用,但是這和虛擬機(jī)中的進(jìn)程似乎沒有什么不同, 這也被稱為“一個 Pod 一個 IP”模型。
這幾段話引用自 K8s 的官方文檔[19],簡單概括下就是一個 Pod 一個獨(dú)立的 IP 地址,所有的 Pod 之間可以不通過 NAT 通信。這個模型把一個 Pod 的網(wǎng)絡(luò)環(huán)境近似等同于一個 VM 的網(wǎng)絡(luò)環(huán)境。
2 K8s 的主流網(wǎng)絡(luò)插件實(shí)現(xiàn)原理
K8s 中的網(wǎng)絡(luò)是通過插件方式實(shí)現(xiàn)的,其網(wǎng)絡(luò)插件有 2 種類型:
圖片來自[20],本文只關(guān)注 CNI 接口插件。主流的 K8s 網(wǎng)絡(luò)插件有這些[21],本文選出 github star 數(shù)在千以上的幾個項目分析下:
Flannel
CNI 是由 CoreOS 提出的規(guī)范,那就先看下 CoreOS 自己的 Flannel 項目的設(shè)計。Flannel 會在每臺機(jī)宿主機(jī)上部署一個名為 flanneld 的代理進(jìn)程,網(wǎng)段相關(guān)的數(shù)據(jù)使用 Kubernetes API/Etcd 存儲。Flannel 項目本身是一個框架,真正為我們提供容器網(wǎng)絡(luò)功能的,是 Flannel 的后端實(shí)現(xiàn)。
目前的 Flannel 有下面幾種后端實(shí)現(xiàn):VXLAN、host-gw、UDP 以及阿里云和其他大廠的支持后端(云廠商都是實(shí)驗性支持),還有諸如 IPIP、IPSec 等一些隧道通信的實(shí)驗性支持。按照官方文檔的建議是優(yōu)先使用 VXLAN 模式,host-gw 推薦給經(jīng)驗豐富且想要進(jìn)一步提升性能的用戶(云環(huán)境通常不能用,原因后面說),UDP 是 Flannel 最早支持的一種性能比較差的方案,基本上已經(jīng)棄用了。
下文分別對這三種模式進(jìn)行分析。
1)VXLAN
使用 Linux 內(nèi)核 VXLAN 封裝數(shù)據(jù)包的方式和原理上文已經(jīng)介紹過了,F(xiàn)lannel 創(chuàng)建了一個名為 flannel.1 的 VETH 設(shè)備。因為 flanneld 進(jìn)程的存在,所以注冊和更新新的 VETH 設(shè)備關(guān)系的任務(wù)就依靠這個進(jìn)程了。所以好像也沒什么需要說的了,就是每個新的 K8s Node 都會創(chuàng)建 flanneld 這個 DeamonSet 模式的守護(hù)進(jìn)程。然后注冊、更新新的 VETH 設(shè)備就變得非常自然了,全局的數(shù)據(jù)自然也是保存在 Etcd 里了。這種方式圖已經(jīng)畫過了,無非是設(shè)備名字不一樣(VETH 叫 flannel.1,網(wǎng)橋叫 cni0)而已。
2)host-gw
顧名思義,host-gw 就是把宿主機(jī) Host 當(dāng)做 Gateway 網(wǎng)關(guān)來處理協(xié)議包的流動。這個方式上文其實(shí)也演示過了,至于節(jié)點(diǎn)的變化和路由表的增刪也是依靠 flanneld 在做的。這個方案優(yōu)缺點(diǎn)都很明顯,最大的優(yōu)點(diǎn)自然是性能,實(shí)打?qū)嵉闹苯愚D(zhuǎn)發(fā)(性能整體比宿主機(jī)層面的通信低 10%,VXLAN 可是20% 起步,甚至 30%)。缺點(diǎn)也很明顯,這種方式要求宿主機(jī)之間是二層連通的,還需要對基礎(chǔ)設(shè)施有掌控權(quán)(編輯路由表),這個在云服務(wù)環(huán)境一般較難實(shí)現(xiàn),另外規(guī)模越來越大時候的路由表規(guī)模也會隨之增大。這種方式原理上比較簡單,圖不畫了。
3)UDP
每臺宿主機(jī)上的 flanneld 進(jìn)程會創(chuàng)建一個默認(rèn)名為 flannel0 的 Tun 設(shè)備。Tun 設(shè)備的功能非常簡單,用于在內(nèi)核和用戶應(yīng)用程序之間傳遞 IP 包。內(nèi)核將一個 IP 包發(fā)送給 Tun 設(shè)備之后,這個包就會交給創(chuàng)建這個設(shè)備的應(yīng)用程序。而進(jìn)程向 Tun 設(shè)備發(fā)送了一個 IP 包,那么這個 IP 包就會出現(xiàn)在宿主機(jī)的網(wǎng)絡(luò)棧中,然后根據(jù)路由表進(jìn)行下一跳的處理。在由 Flannel 管理的容器網(wǎng)絡(luò)里,一臺宿主機(jī)上的所有容器都屬于該宿主機(jī)被分配的一個“子網(wǎng)”。這個子網(wǎng)的范圍信息,所屬的宿主機(jī) IP 地址都保存在 Etcd 里。flanneld 進(jìn)程均監(jiān)聽著宿主機(jī)上的 UDP 8285 端口,相互之間通過 UDP 協(xié)議包裝 IP 包給目的主機(jī)完成通信。之前說過這個模式性能差,差在哪里?這個方案就是一個在應(yīng)用層模擬實(shí)現(xiàn)的 Overlay 網(wǎng)絡(luò)似得(像不像一個用戶態(tài)實(shí)現(xiàn)的 VETH 設(shè)備?),數(shù)據(jù)包相比內(nèi)核原生支持的 VXLAN 協(xié)議在用戶態(tài)多了一次進(jìn)出(flanneld 進(jìn)程封包/拆包過程),所以性能上損失要大一些。
Calico
Calico 是個挺有意思的項目,基本思想是把宿主機(jī)完全當(dāng)成路由器,不使用隧道或 NAT 來實(shí)現(xiàn)轉(zhuǎn)發(fā),把所有二三層流量轉(zhuǎn)換成三層流量,并通過宿主機(jī)上的路由配置完成包的轉(zhuǎn)發(fā)。
Calico 和之前說的 Flannel 的 host-gw 模式區(qū)別是什么?首先 Calico 不使用網(wǎng)橋,而是通過路由規(guī)則在不同的 vNiC 間轉(zhuǎn)發(fā)數(shù)據(jù)。另外路由表也不是靠 Etcd 存儲和通知更新,而是像現(xiàn)實(shí)環(huán)境一樣直接用 BGP(Border Gateway Protocol, 邊界網(wǎng)關(guān)協(xié)議)進(jìn)行路由表數(shù)據(jù)交換。BGP 挺復(fù)雜的,詳細(xì)的闡述這個協(xié)議有點(diǎn)費(fèi)勁(而且我也不是很懂),在本文里只需要知道這個協(xié)議使得路由設(shè)備可以相互發(fā)送和學(xué)習(xí)對方的路由信息來充實(shí)自己就可以了,有興趣的話請查閱其他資料進(jìn)一步了解。回到 Calico,宿主機(jī)上依舊有個名為 Felix 的守護(hù)進(jìn)程和一個名為 BIRD的 BGP 客戶端。
上文說過,F(xiàn)lannel 的 host-gw 模式要求宿主機(jī)二層是互通的(在一個子網(wǎng)),在 Calico 這里依然有這個要求。但是 Calico 為不在一個子網(wǎng)的環(huán)境提供了 IPIP 模式的支持。開啟這個模式之后,宿主機(jī)上會創(chuàng)建一個 Tun 設(shè)備以 IP 隧道(IP tunnel)的方式通信。當(dāng)然用了這個模式之后,包又是L3 over L3 的 Overlay 網(wǎng)絡(luò)模式了,性能也和 VXLAN 模式相當(dāng)。
全路由表的通信方式也沒有額外組件,配置好 IP 路由轉(zhuǎn)發(fā)規(guī)則后全靠內(nèi)核路由模塊的流轉(zhuǎn)來做。IPIP 的架構(gòu)圖也是大同小異的,也不畫了。
Cilium
eBPF-based Networking, Security, and Observability
光從這個介紹上就看出來 Cilium 散發(fā)出的那種與眾不同的氣息。這個項目目前的 Github Star 數(shù)字快過萬了,直接力壓前面兩位。Cilium 部署后會在宿主機(jī)上創(chuàng)建一個名為 cilium-agent 的守護(hù)進(jìn)程,這個進(jìn)程的作用就是維護(hù)和部署 eBPF 腳本來實(shí)現(xiàn)所有的流量轉(zhuǎn)發(fā)、過濾、診斷的事情(都不依賴 Netfilter 機(jī)制,kenel > v4.19 版本)。從原理圖的角度畫出來的架構(gòu)圖很簡單(配圖來自 github 主頁):
Cilium 除了支持基本的網(wǎng)絡(luò)連通、隔離與服務(wù)透出之外,依托 eBPF 所以對宿主機(jī)網(wǎng)絡(luò)有更好的觀測性和故障排查能力。這個話題也很大,本文就此收住。這里給兩幾篇寫的很好的文章何其譯文可以進(jìn)一步了解22。
3 K8s 容器內(nèi)訪問隔離
上文介紹了網(wǎng)絡(luò)插件的機(jī)制和實(shí)現(xiàn)原理,最終可以構(gòu)建出一個二層/三層連通的虛擬網(wǎng)絡(luò)。默認(rèn)情況下 Pod 間的任何網(wǎng)絡(luò)訪問都是不受限的,但是內(nèi)部網(wǎng)絡(luò)中經(jīng)常還是需要設(shè)置一些訪問規(guī)則(防火墻)的。
針對這個需求,K8s 抽象了一個名為 NetworkPolicy 的機(jī)制來支持這個功能。網(wǎng)絡(luò)策略通過網(wǎng)絡(luò)插件來實(shí)現(xiàn),要使用網(wǎng)絡(luò)策略就必須使用支持 NetworkPolicy 的網(wǎng)絡(luò)解決方案。為什么這么說?因為不是所有的網(wǎng)絡(luò)插件都支持 NetworkPolicy 機(jī)制,比如 Flannel 就不支持。至于 NetworkPolicy 的底層原理,自然是使用 iptables 配置 netfilter 規(guī)則來實(shí)現(xiàn)對包的過濾的。NetworkPolicy 配置的方法和 iptables/Netfilter 的原理細(xì)節(jié)不在本文范圍內(nèi),請參閱其他資料進(jìn)行了解24。
4 K8s 容器內(nèi)服務(wù)透出
在一個 K8s 集群內(nèi)部,在網(wǎng)絡(luò)插件的幫助下,所有的容器/進(jìn)程可以相互進(jìn)行通信。但是作為服務(wù)提供方這是不夠的,因為很多時候,服務(wù)的使用方不會在同一個 K8s 集群內(nèi)的。那么就需要一種機(jī)制將這個集群內(nèi)的服務(wù)對外透出。K8s 使用 Service 這個對象來完成這個能力的抽象。Service 在 K8s 里是個很重要的對象,即使在 K8s 內(nèi)部進(jìn)行訪問,往往也是需要 Service 包裝的(一來 Pod 地址不是永遠(yuǎn)固定的,二來總是會有負(fù)載均衡的需求)。
一句話概括 Service 的原理就是:Service = kube-proxy + iptables 規(guī)則。當(dāng)一個 Service 創(chuàng)建時,K8s 會為其分配一個 Cluster IP 地址。這個地址其實(shí)是個 VIP,并沒有一個真實(shí)的網(wǎng)絡(luò)對象存在。這個 IP 只會存在于 iptables 規(guī)則里,對這個 VIP:VPort 的訪問使用 iptables 的隨機(jī)模式規(guī)則指向了一個或者多個真實(shí)存在的 Pod 地址(DNAT),這個是 Service 最基本的工作原理。那 kube-proxy 做什么?kube-proxy 監(jiān)聽 Pod 的變化,負(fù)責(zé)在宿主機(jī)上生成這些 NAT 規(guī)則。這個模式下 kube-proxy 不轉(zhuǎn)發(fā)流量,kube-proxy 只是負(fù)責(zé)疏通管道。
K8s 官方文檔比較好的介紹了 kube-proxy 支持的多種模式和基本的原理[26]。早先的 userspace 模式基本上棄用了,上面所述的 iptables 隨機(jī)規(guī)則的做法在大規(guī)模下也不推薦使用了(想想為什么)。現(xiàn)在最推薦的當(dāng)屬 IPVS 模式了,相對于前兩種在大規(guī)模下的性能更好。如果說 IPVS 這個詞比較陌生的話,LVS 這個詞恐怕是我們耳熟能詳?shù)摹T谶@個模式下,kube-proxy 會在宿主機(jī)上創(chuàng)建一個名為 kube-ipvs0 的虛擬網(wǎng)卡,然后分配 Service VIP 作為其 IP 地址。最后 kube-proxy 使用內(nèi)核的 IPVS 模塊為這個地址設(shè)置后端 POD 的地址(ipvsadm 命令可以查看)。其實(shí) IPVS 在內(nèi)核中的實(shí)現(xiàn)也是用了 Netfilter 的 NAT 機(jī)制。不同的是,IPVS 不會為每一個地址設(shè)置 NAT 規(guī)則,而是把這些規(guī)則的處理放到了內(nèi)核態(tài),保證了 iptables 規(guī)則的數(shù)量基本上恒定,比較好的解決了之前的問題。
上面說的只是解決了負(fù)載均衡的問題,還沒提到服務(wù)透出。K8s 服務(wù)透出的方式主要有 NodePort、LoadBalancer 類型的 Service(會調(diào)用 CloudProvider 在公有云上為你創(chuàng)建一個負(fù)載均衡服務(wù))以及 ExternalName(kube-dns 里添加 CNAME)的方式。對于第二種類型,當(dāng) Service 繁多但是又流量很小的情況下,也可以使用 Ingress 這個 Service 的 Service 來收斂掉[27]。Ingress 目前只支持七層 HTTP(S) 轉(zhuǎn)發(fā)(Service 目前只支持四層轉(zhuǎn)發(fā)),從這個角度猜猜 Ingress 怎么實(shí)現(xiàn)的?來張圖看看吧[28](當(dāng)然還有很多其他的 Controller[29]):
對于這部分,本文不再進(jìn)行詳細(xì)闡述了,無非就是 NAT,NAT 多了就想辦法收斂 NAT 條目。按照慣例,這里給出一篇特別好的 kube-proxy 原理闡述的文章供進(jìn)一步了解[30]。
網(wǎng)絡(luò)虛擬化是個很大的話題,很難在一篇文章中完全講清楚。盡管這篇文章盡量想把重要的知識節(jié)點(diǎn)編織清楚,但受限于作者本人的精力以及認(rèn)知上的限制,可能存在疏忽甚至錯漏。如有此類問題,歡迎在評論區(qū)討論/指正。參考文獻(xiàn)里給出了很多不錯的資料值得進(jìn)一步去學(xué)習(xí)(部分地址受限于網(wǎng)絡(luò)環(huán)境,可能需要特定的方式才能訪問)。
參考文獻(xiàn)
1、TCP Implementation in Linux: A Brief Tutorial, Helali Bhuiyan, Mark McGinley, Tao Li, Malathi Veeraraghavan, University of Virginia:https://www.semanticscholar.org/paper/TCP-Implementation-in-Linux-%3A-A-Brief-Tutorial-Bhuiyan-McGinley/f505e259fb0cd8cf3f75582d46cd209fd9cb1d1a
2、NAPI, linuxfoundation, https://wiki.linuxfoundation.org/networking/napi
3、Monitoring and Tuning the Linux Networking Stack: Receiving Data, Joe Damato,譯文:Linux 網(wǎng)絡(luò)棧監(jiān)控和調(diào)優(yōu):接收數(shù)據(jù)(2016):http://arthurchiao.art/blog/tuning-stack-rx-zh/
4、Monitoring and Tuning the Linux Networking Stack: Sending Data, Joe Damato, 譯文:Linux 網(wǎng)絡(luò)棧監(jiān)控和調(diào)優(yōu):發(fā)送數(shù)據(jù)(2017):http://arthurchiao.art/blog/tuning-stack-tx-zh/
5、Scaling in the Linux Networking Stack, https://github.com/torvalds/linux/blob/master/Documentation/networking/scaling.rst
6、Understanding TCP internals step by step for Software Engineers and System Designers, Kousik Nath
7、Netfilter, https://www.netfilter.org/
8、Netfilter-packet-flow, https://upload.wikimedia.org/wikipedia/commons/3/37/Netfilter-packet-flow.svg
9、Analysis TCP in Linux, https://github.com/fzyz999/Analysis_TCP_in_Linux
10、NAT - Network Address Translation, 譯文:NAT - 網(wǎng)絡(luò)地址轉(zhuǎn)換(2016):http://arthurchiao.art/blog/nat-zh/
11、Virtual networking in Linux, By M. Jones, IBM Developer:https://developer.ibm.com/tutorials/l-virtual-networking/
12、Open vSwitch, http://www.openvswitch.org/
13、Linux Namespace, https://man7.org/linux/man-pages/man7/namespaces.7.html
14、ip, https://man7.org/linux/man-pages/man8/ip.8.html
15、Veth, https://man7.org/linux/man-pages/man4/veth.4.html
16、VxLAN, https://en.wikipedia.org/wiki/Virtual_Extensible_LAN
17、QinQ vs VLAN vs VXLAN, John, https://community.fs.com/blog/qinq-vs-vlan-vs-vxlan.htm
18、Introduction to Linux interfaces for virtual networking, Hangbin Liu:https://developers.redhat.com/blog/2018/10/22/introduction-to-linux-interfaces-for-virtual-networking#
19、Cluster Networking, 英文地址https://kubernetes.io/zh/docs/concepts/cluster-administration/networking/
20、THE CONTAINER NETWORKING LANDSCAPE: CNI FROM COREOS AND CNM FROM DOCKER, Lee Calcote:https://thenewstack.io/container-networking-landscape-cni-coreos-cnm-docker/
21、CNI - the Container Network Interface, https://github.com/containernetworking/cni
22、Making the Kubernetes Service Abstraction Scale using eBPF, [譯] 利用 eBPF 支撐大規(guī)模 K8s Service (LPC, 2019):https://linuxplumbersconf.org/event/4/contributions/458/
23、基于 BPF/XDP 實(shí)現(xiàn) K8s Service 負(fù)載均衡 (LPC, 2020)https://linuxplumbersconf.org/event/7/contributions/674/
24、A Deep Dive into Iptables and Netfilter Architecture, Justin Ellingwood:https://www.digitalocean.com/community/tutorials/a-deep-dive-into-iptables-and-netfilter-architecture
25、Iptables Tutorial 1.2.2, Oskar Andreasson:https://www.frozentux.net/iptables-tutorial/iptables-tutorial.html
26、Virtual IPs and service proxies, 英文地址:https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies
27、Ingress, 英文地址:https://kubernetes.io/docs/concepts/services-networking/ingress/
28、NGINX Ingress Controller, https://www.nginx.com/products/nginx-ingress-controller/
29、Ingress Controllers, 英文地址:https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/
30、Cracking kubernetes node proxy (aka kube-proxy), [譯] 深入理解 Kubernetes 網(wǎng)絡(luò)模型:自己實(shí)現(xiàn) Kube-Proxy 的功能:https://cloudnative.to/blog/k8s-node-proxy/
原文鏈接:https://developer.aliyun.com/article/813025?utm_content=g_1000310145
本文為阿里云原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
背景
近期,閑魚核心應(yīng)用出現(xiàn)了一個比較難解決的問題。在該應(yīng)用集群中,會隨機(jī)偶現(xiàn)一兩個實(shí)例,其JVM運(yùn)行在一個掛起的狀態(tài),深入分析stack文件發(fā)現(xiàn),此時jvm中有大量的線程在等待一把沒有任何線程持有的鎖。問題實(shí)例所在的機(jī)器負(fù)載相對正常機(jī)器要輕很多,而其線程數(shù)則大幅增多。由于該問題在邏輯上的沖突,加上周邊問題的復(fù)雜性,使得研究、分析、解決該問題變得相對來說困難與曲折。本文將系統(tǒng)性地介紹如何解決這個問題,并找出問題背后的原因。
問題分析
在實(shí)際解決這個問題的時候,我們發(fā)現(xiàn)不僅問題本身顯得不合常理,其周邊環(huán)境也相對來說不友善,給問題的分析與解決帶來了較大的困難。
集群中隨機(jī)出現(xiàn)。問題隨機(jī)出現(xiàn)在該應(yīng)用集群中的一個或幾個實(shí)例中,無法提前預(yù)知其出現(xiàn)規(guī)律。
單機(jī)出現(xiàn)時間不可預(yù)知,現(xiàn)場捕捉困難,捕捉風(fēng)險大,一般發(fā)現(xiàn)已經(jīng)為事后,無現(xiàn)場第一手?jǐn)?shù)據(jù)。從單個機(jī)器,或者單個實(shí)例看,則是出現(xiàn)概率非常低,出現(xiàn)時間完全隨機(jī)。這使得蹲點(diǎn)單臺機(jī)器以捕捉這個問題的思路幾乎行不通,策略擴(kuò)大至整個集群又可能出現(xiàn)穩(wěn)定性及性能問題。
問題出現(xiàn)頻率低。出現(xiàn)頻率大概在一到兩天一次。
問題表現(xiàn)復(fù)雜。該問題的表現(xiàn)很復(fù)雜,不僅從第一眼看去不合常理,JVM內(nèi)部出現(xiàn)了大量線程在等待一把沒有任何線程持有的鎖。另外,問題機(jī)器的負(fù)載非常低,基本上在5%以內(nèi),相當(dāng)于空載,而JVM中線程數(shù)卻非常多,最多發(fā)現(xiàn)過接近4k個線程。
問題周邊環(huán)境復(fù)雜。該問題出現(xiàn)前后,應(yīng)用先后引入了rxjava、協(xié)程,應(yīng)用為早期應(yīng)用,服務(wù)結(jié)構(gòu)復(fù)雜,而log4j問題又和網(wǎng)上大量的文章情形不符。
驗證困難。理論分析完成后,無法在線上復(fù)現(xiàn)及驗證,安全性、穩(wěn)定性、數(shù)據(jù)等都不允許直接在線上驗證。
解決方案
解決這個問題的主要按照以下六步,一步步排除法,最終定位并解決問題。按照先易后難,先直接后理論,先數(shù)據(jù)后源碼的順序,總結(jié)出來以下六步,大體上投入時間逐步增加,難度也逐漸增加。
step1. 代碼bug查找
代碼問題指的是業(yè)務(wù)代碼本身邏輯問題把JVM帶入了某種故障狀態(tài)。問題的分析及排除很簡單,通過觀察應(yīng)用日志即可。
step2. 現(xiàn)場捕獲
定位了問題,問題也就解決了一半。
一般來說,定位問題主要有兩個分類,即時定位,事后定位。
前一種是指我們實(shí)時直接監(jiān)控JVM信息,在關(guān)鍵信息異常時,即發(fā)生動作。配合周期性的信息采集,基本可以對問題發(fā)生時刻前后數(shù)據(jù)精準(zhǔn)采集和對比,做法一般是采用JVM代理方式或JMS方式。JVM代理分為C語言和Java語言代理,C語言代理運(yùn)行在JVM層,可以做到即時Java代碼發(fā)生故障故障,依然可以正常采集信息。Java代理相對C語言代理來說編寫起來方便,實(shí)際上C語言部分任務(wù)還是通過JNI接口構(gòu)造Java對象執(zhí)行的。JMS方式可以實(shí)時采集各種指標(biāo),也是目前監(jiān)控主要采用等方式。缺點(diǎn)是對應(yīng)用的侵入性非常大,不適合解決問題用。
事后定位是指通過日志監(jiān)控等較緩慢的方式去對問題發(fā)生時刻定位,由于該問題的特殊性,日志無法提供需要的信息以判斷故障,另外,日志無法采集我們需要的信息,尤其是JVM內(nèi)部線程和鎖的信息。
在后續(xù)現(xiàn)象的觀察中,發(fā)現(xiàn)了一個比較普遍的現(xiàn)象,應(yīng)用由正常轉(zhuǎn)為故障需要一個漫長的時間,應(yīng)用可能在臨界區(qū)停留相當(dāng)長的時間,極端例子中應(yīng)用在線程數(shù)提升后依然能夠正常運(yùn)行接近24小時,之后發(fā)生了自恢復(fù)。另外,在和JVM組同學(xué)對接的時候,又被告知阿里jdk對C代理支持可能由于安全原因被關(guān)閉。基本上宣布這個問題的研究進(jìn)入了下一個階段。step3. io hang
考慮到大部分實(shí)例業(yè)務(wù)日志打印緩慢或者根本不再打印,可能原因是io方面出了問題,通過查看容器硬件監(jiān)控及應(yīng)用火焰圖,可以輕松將IO問題排除。
step4. 鎖分析鎖問題主要包含死鎖和丟鎖。死鎖的特點(diǎn)很明顯,一旦發(fā)生死鎖,則與該鎖相關(guān)的線程都將停止。首先這點(diǎn)和大量實(shí)例運(yùn)行緩慢不符,其次,這個問題可以輕易通過stack文件排除。丟鎖主要和協(xié)程有關(guān),和死鎖相似,考慮到協(xié)程可能在切換過程中發(fā)生丟鎖,造成的現(xiàn)象和該問題很類似,即沒有線程持有的鎖。丟鎖最主要的問題也是不可恢復(fù),一旦丟鎖,則JVM相關(guān)線程就永遠(yuǎn)不可恢復(fù),和該問題不符。另外,觀察大部分stack文件發(fā)現(xiàn),此時JVM中的協(xié)程數(shù)量并不多,線程池Worker實(shí)例也在變化。
publicvoid callAppenders(LoggingEvent event) {
int writes = 0;
for(Category c = this; c != ; c = c.parent) {
// Protected against simultaneous call to addAppender, removeAppender,...
synchronized(c) {
if(c.aai != ) {
writes += c.aai.appendLoopOnAppenders(event);
}
if(!c.additive) {
break;
}
}
}
if(writes == 0) {
repository.emitNoAppenderWarning(this);
}
}
查看HotSpot源碼,在退出臨界區(qū)時,首先要做的是把鎖狀態(tài)重置,也即對象頭重置及Montior對象當(dāng)前owner置,然后才會喚醒所有相關(guān)線程搶鎖。如果此時內(nèi)存放不下所有有關(guān)線程,隨著線程的喚醒,活躍線程會被扇出以提供內(nèi)存空間。大量的扇入和扇出使得這個過程顯得很緩慢,也就出現(xiàn)了一個沒有任何線程持有的鎖,實(shí)際上JVM此時在進(jìn)行一個艱難的搶鎖任務(wù)。
for(p = w ; p != ; p = p->_next) {
guarantee (p->TState== ObjectWaiter::TS_CXQ, "Invariant") ;
p->TState= ObjectWaiter::TS_ENTER ;
p->_prev = q ;
q = p ;
}
// Prepend the RATs to the EntryList
if(_EntryList!= ) {
q->_next = _EntryList;
_EntryList->_prev = q ;
}
在前面步驟中大致定位了一個大的方向,線程數(shù)增加導(dǎo)致內(nèi)存不足。接下來需要深入框架層去分析引起線程數(shù)增加的可能原因。先后對HSF、Modulet、Mtop、netty等框架進(jìn)行了源碼級別的分析,主要跟蹤各個框架線程分配策略。其中,HSF默認(rèn)設(shè)置的線程池模型擾動抗性很低。在HSF框架中,netty線程池將任務(wù)提交到HSF Provider線程池,HSF Provider線程池采用業(yè)務(wù)隔離設(shè)計,在一次對外服務(wù)中,HSF Provider大量調(diào)用HSF Consumer,而Consumer會被提交至Consumer線程池中執(zhí)行。在該應(yīng)用中,Provider和Consumer線程池容量比例大于200:1。
失衡的線程池結(jié)構(gòu),極容易服務(wù)發(fā)生網(wǎng)絡(luò)抖動、回環(huán)調(diào)用時使Consumer線程池服務(wù)能力下降,進(jìn)而使整個應(yīng)用實(shí)例對外服務(wù)能力下降。而有規(guī)律的故障不應(yīng)該和無規(guī)律的抖動有關(guān)。
回環(huán)與問題出現(xiàn)頻率之間讀者可以通過概率論進(jìn)行分析,假定100臺機(jī)器,則每次請求會有1/100的概率發(fā)生回環(huán),同理,每10000次請求就會發(fā)生雙回環(huán),1M次請求則是3回環(huán)。在該問題中,概率論分析和實(shí)際情況是契合的。
在研究框架層的時候,發(fā)現(xiàn)了回環(huán)調(diào)用對系統(tǒng)的危害。但是還有一個疑問需要回答,回環(huán)調(diào)用完成后,應(yīng)用應(yīng)該能恢復(fù),而線上實(shí)際情況是,自恢復(fù)是個小概率事件。結(jié)合前一節(jié)可以得出一個結(jié)論,回環(huán)調(diào)用使應(yīng)用Consumer線程池處理能力下降,進(jìn)而使上游線程池水位逐漸提升直至被打滿。而數(shù)量過度增加的線程池使得內(nèi)存資源緊張,導(dǎo)致JVM基于磁盤運(yùn)行而搶鎖困難,搶鎖過程的拉長使得沒有任何線程持有鎖這個常規(guī)狀態(tài)下的瞬時狀態(tài)被拉長,JVM服務(wù)能力大打折扣,而duct平臺由于策略原因不能應(yīng)對該問題的特殊情況導(dǎo)致其無法啟動切流,流量照常打入JVM。于是就形成了一個惡性循環(huán),線程數(shù)提升導(dǎo)致JVM進(jìn)入一種非常規(guī)狀態(tài),服務(wù)能力下降,而流量照常,導(dǎo)致線程數(shù)很難下降。于是,JVM長時間運(yùn)行在一個非常緩慢的狀態(tài),從表現(xiàn)上來看就是jvm掛起。下表為一個較有代表性的流量對比(實(shí)際上故障機(jī)狀態(tài)跨度非常大,這兩臺機(jī)器較為典型而已)
接下來,本文采用阿里PAS壓測平臺,對預(yù)發(fā)機(jī)器進(jìn)行了壓測驗證。由于線上問題復(fù)雜,無法復(fù)現(xiàn)線上的環(huán)境,只能對其誘因進(jìn)行驗證。下表為壓測過程中應(yīng)用的性能表現(xiàn)。由于壓測模式限制,所支持的最大tps在超時的情況下非常低,如表所示只有80左右,考慮到壓測環(huán)境機(jī)器數(shù)量,回環(huán)數(shù)量還要打折。
從下圖可以看出,平均RT為2500ms左右,絕大多數(shù)請求都在超時狀態(tài)。
壓測結(jié)果表明,回環(huán)不需要多高的流量,就能把應(yīng)用實(shí)際服務(wù)能力大打折扣。考慮到線上還有其他類型的請求,填充在回環(huán)之間,這會使線程池迅速打滿,并使得處理回環(huán)請求的時間加長,惡化應(yīng)用從回環(huán)調(diào)用中恢復(fù)的能力。總結(jié)和思考在JVM出現(xiàn)問題的時候,首先要閱讀業(yè)務(wù)代碼,這個雖然看似作用不大,卻有可能以相當(dāng)?shù)土拇鷥r解決問題。之后,主要思路就是捕獲現(xiàn)場,現(xiàn)場捕獲將極大程度上有助于問題的解決。如果該步驟不可行,或者成本相對較高,可以先去排查周邊原因。這主要包括IO、鎖、硬軟件資源,在執(zhí)行這些排查的時候,要留意這些方面出問題的表現(xiàn)和實(shí)際問題的表現(xiàn)契合度。比較明顯的就是一旦死鎖、丟鎖,或者IO hang,則程序無法從故障狀態(tài)恢復(fù),相關(guān)線程也不能繼續(xù)執(zhí)行。這些特點(diǎn)可以協(xié)助排除部分大的方向。最后,對資源耗盡的排查,則是基于本文所述問題的一個基本特點(diǎn),絕大多數(shù)JVM運(yùn)行緩慢而不是停止運(yùn)行。所以,資源緊張成為一個解決問題的大方向,并最終定位了問題。深入到框架層主要是從理論上分析問題產(chǎn)生的原因,然后在結(jié)合實(shí)際情況,分析整個解決思路的正確性。讀者在遇到類似JVM問題時,可參考本文所述的方法與步驟,對實(shí)際問題進(jìn)行分析與研究。
閑魚技術(shù)團(tuán)隊不僅是阿里巴巴集團(tuán)旗下閑置交易社區(qū)的創(chuàng)造者,更是移動與高并發(fā)大數(shù)據(jù)應(yīng)用新技術(shù)的引導(dǎo)者與創(chuàng)新者。我們與Google Flutter/Dart小組密切合作,為社區(qū)貢獻(xiàn)了多個高star的項目和大量PR。我們正在積極探索深度學(xué)習(xí)和視覺技術(shù)在互動、交易、社區(qū)場景的創(chuàng)新應(yīng)用。閑魚技術(shù)與集團(tuán)中間件團(tuán)隊共同打造的FaaS平臺每天支持?jǐn)?shù)以千萬級用戶的高并發(fā)訪問場景。
就是現(xiàn)在!客戶端/服務(wù)端java/架構(gòu)/前端/質(zhì)量工程師面向社會+校園招聘,base杭州阿里巴巴西溪園區(qū),一起做有創(chuàng)想空間的社區(qū)產(chǎn)品、做深度頂級的開源項目,一起拓展技術(shù)邊界成就極致!
*投喂簡歷給小閑魚→guicai.gxy@alibaba-inc.com
開源項目、峰會直擊、關(guān)鍵洞察、深度解讀
請認(rèn)準(zhǔn)閑魚技術(shù)