假已經(jīng)到來,為普及更多安全知識,1月22日(本周六)晚上8點,惠州市教育局、惠州市消防救援支隊、南方報業(yè)傳媒集團惠州分社將聯(lián)合推出寒假安全教育“云課堂”。惠州市惠城區(qū)消防救援大隊工程師張蓬將為大家講解防火用電安全知識和遇火逃生技巧。希望通過這節(jié)“云課堂”,師生和家長朋友可以學到更多消防安全知識,增強安全意識和自救能力,過一個平安快樂的寒假。
安全課收看方式>>>
https://static.nfapp.southcn.com/apptpl/liveToShare.html?id=341692
【溫馨提示】
南方+視頻直播支持電視投屏。您可以下載南方+客戶端,根據(jù)下圖指引進行操作就可以在電視大屏上收看安全課啦!
2.【點播】如果不能在晚上8時準時收看班會課直播,可以選擇點播觀看:
電腦點播▼
http://pc.nfapp.southcn.com/25450/6149731.html
手機點播▼
https://static.nfapp.southcn.com/content/202201/19/c6149731.html
【策劃/統(tǒng)籌】羅銳 周歡 鄒晟紅 張蓬 何志遠 林麗麗
【文字】謝志清
【拍攝/后期】王昌輝
【作者】 謝志清;王昌輝
【來源】 南方報業(yè)傳媒集團南方+客戶端
來源:南方+ - 創(chuàng)造更多價值
文需要讀者熟悉 Ethernet(以太網(wǎng))的基本原理和 Linux 系統(tǒng)的基本網(wǎng)絡命令,以及 TCP/IP 協(xié)議族并了解傳統(tǒng)的網(wǎng)絡模型和協(xié)議包的流轉(zhuǎn)原理。文中涉及到 Linux 內(nèi)核的具體實現(xiàn)時,均以內(nèi)核 v4.19.215 版本為準。
1 從網(wǎng)卡到內(nèi)核協(xié)議棧
如圖[1],網(wǎng)絡包到達 NC(Network Computer,本文指物理機)時,由 NIC(Network Interface Controller,網(wǎng)絡接口控制器,俗稱網(wǎng)卡)設備處理,NIC 以中斷的方式向內(nèi)核傳遞消息。Linux 內(nèi)核的中斷處理分為上半部(Top Half)和下半部(Bottom Half)。上半部需要盡快處理掉和硬件相關的工作并返回,下半部由上半部激活來處理后續(xù)比較耗時的工作。
具體到 NIC 的處理流程如下:當 NIC 收到數(shù)據(jù)時,會以 DMA 方式將數(shù)據(jù)拷貝到 Ring Buffer (接收隊列) 里描述符指向的映射內(nèi)存區(qū)域,拷貝完成后會觸發(fā)中斷通知 CPU 進行處理。這里可以使用 ethtool -g {設備名,如eth0} 命令查看 RX/TX (接收/發(fā)送)隊列的大小。CPU 識別到中斷后跳轉(zhuǎn)到 NIC 的中斷處理函數(shù)開始執(zhí)行。此時要區(qū)分 NIC 的工作模式,在早先的非 NAPI(New API)[2]模式下,中斷上半部更新相關的寄存器信息,查看接收隊列并分配 sk_buff 結構指向接收到的數(shù)據(jù),最后調(diào)用 netif_rx() 把 sk_buff 遞交給內(nèi)核處理。在 netif_rx() 的函數(shù)的流程中,這個分配的 sk_buff 結構被放入 input_pkt_queue隊列后,會把一個虛擬設備加入poll_list 輪詢隊列并觸發(fā)軟中斷 NET_RX_SOFTIRQ 激活中斷下半部。此時中斷上半部就結束了,詳細的處理流程可以參見 net/core/dev.c 的 netif_rx() -> netif_rx_internal() -> enqueue_to_backlog() 過程。下半部 NET_RX_SOFTIRQ 軟中斷對應的處理函數(shù)是 net_rx_action(),這個函數(shù)會調(diào)用設備注冊的 poll() 函數(shù)進行處理。非 NAPI 的情況下這個虛擬設備的 poll() 函數(shù)固定指向 process_backlog() 函數(shù)。這個函數(shù)將 sk_buff 從 input_pkt_queue 移動到 process_queue 中,調(diào)用 __netif_receive_skb() 函數(shù)將其投遞給協(xié)議棧,最后協(xié)議棧相關代碼會根據(jù)協(xié)議類型調(diào)用相應的接口進行后續(xù)的處理。特別地,這里的 enqueue_to_backlog() 以及 process_backlog() 函數(shù)也用于和啟用了 RPS 機制后的相關邏輯。
非 NAPI(New API)模式下每個網(wǎng)絡包的到達都會觸發(fā)一次中斷處理流程,這么做降低了整體的處理能力,已經(jīng)過時了。現(xiàn)在大多數(shù) NIC 都支持 NAPI 模式了。NAPI 模式下在首包觸發(fā) NIC 中斷后,設備就會被加入輪詢隊列進行輪詢操作以提升效率,輪詢過程中不會產(chǎn)生新的中斷。為了支持 NAPI,每個 CPU 維護了一個叫 softnet_data 的結構,其中有一個 poll_list 字段放置所有的輪詢設備。此時中斷上半部很簡單,只需要更新 NIC 相關的寄存器信息,以及把設備加入poll_list 輪詢隊列并觸發(fā)軟中斷 NET_RX_SOFTIRQ就結束了。中斷下半部的處理依舊是 net_rx_action() 來調(diào)用設備驅(qū)動提供的 poll() 函數(shù)。只是 poll() 此時指向的就是設備驅(qū)動提供的輪詢處理函數(shù)了(而不是非 NAPI 模式下的內(nèi)核函數(shù) process_backlog())。這個設備驅(qū)動提供的輪詢 poll() 函數(shù)最后也會調(diào)用 __netif_receive_skb() 函數(shù)把 sk_buff 提交給協(xié)議棧處理。
非 NAPI 模式和 NAPI 模式下的流程對比如下(其中灰色底色是設備驅(qū)動要實現(xiàn)的,其他都是內(nèi)核自身的實現(xiàn)):
關于 NAPI 模式網(wǎng)絡設備驅(qū)動的實現(xiàn)以及詳細的 NAPI 模式的處理流程,這里提供一篇文章和其譯文作為參考[3](強烈推薦)。這篇文章很詳細的描述了 Intel Ethernet Controller I350 這個 NIC 設備的收包和處理細節(jié)(其姊妹篇發(fā)包處理過程和譯文[4])。另外收包這里還涉及到多網(wǎng)卡的 Bonding 模式(可以在/proc/net/bonding/bond0 里查看模式)、網(wǎng)絡多隊列(sudo lspci -vvv 查看 Ethernet controller 的 Capabilities信息里有 MSI-X: Enable+ Count=10 字樣說明 NIC 支持,可以在 /proc/interrupts 里查看中斷綁定情況)等機制。這些本文都不再贅述,有興趣的話請參閱相關資料[5]。
2 內(nèi)核協(xié)議棧網(wǎng)絡包處理流程
前文說到 NIC 收到網(wǎng)絡包構造出的 sk_buff 結構最終被 __netif_receive_skb() 提交給了內(nèi)核協(xié)議棧解析處理。這個函數(shù)首先進行 RPS[5] 相關的處理,數(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() 進行下一階段的處理。這個處理函數(shù)主要的功能有:
截至目前,數(shù)據(jù)包仍舊在數(shù)據(jù)鏈路層的處理流程中。這里復習下 OSI 七層模型與 TCP/IP 五層模型:
在網(wǎng)絡分層模型里,后一層即為前一層的數(shù)據(jù)部分,稱之為載荷(Payload)。一個完整的 TCP/IP 應用層數(shù)據(jù)包的格式如下[6]:
__netif_receive_skb_core() 的處理邏輯中需要關注的是網(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() 進行后續(xù)的處理(如果包沒被 Netfilter 丟棄)。經(jīng)過路由子系統(tǒng)檢查處理后,如果包是屬于本機的,那么會調(diào)用 ip_local_deliver() 將數(shù)據(jù)包繼續(xù)往上層協(xié)議轉(zhuǎn)發(fā)。這個函數(shù)類似之前的邏輯,依舊是呈遞給 Netfilter 框架并指定函數(shù) ip_local_deliver_finish() 進行后續(xù)的處理,這個函數(shù)最終會檢查和選擇對應的上層協(xié)議接口進行處理。
常見的上層協(xié)議比如 TCP 或者 UDP 協(xié)議的流程不在本文討論的范圍內(nèi),僅 TCP 的流程所需要的篇幅足以超過本文所有的內(nèi)容。這里給出 TCP 協(xié)議(v4)的入口函數(shù) tcp_v4_rcv() 以及 UDP 協(xié)議的入口函數(shù) udp_rcv() 作為指引自行研究,也可以閱讀其他的資料進行進一步的了解[9]。
3 Netfilter/iptables 與 NAT(網(wǎng)絡地址轉(zhuǎn)換)
關于 Netfilter 框架需要稍微著重的強調(diào)一下,因為后文要提到的網(wǎng)絡策略和很多服務透出的實現(xiàn)都要使用 Netfilter 提供的機制。
Netfilter 是內(nèi)核的包過濾框架(Packet Filtering Framework)的實現(xiàn)。簡單說就是在協(xié)議棧的各個層次的包處理函數(shù)中內(nèi)置了很多的 Hook 點來支持在這些點注冊回調(diào)函數(shù)。
圖片來自 Wikimedia,可以點開參考文獻[8]查看大圖(svg 矢量圖,可以調(diào)大網(wǎng)頁顯示百分比繼續(xù)放大)。
Linux 上最常用的防火墻 iptables 即是基于 Netfilter 來實現(xiàn)的(nftables 是新一代的防火墻)。iptables 基于表和鏈(Tables and Chains)的概念來組織規(guī)則。注意這里不要被“防火墻”這個詞誤導了,iptables 所能做的不僅僅是對包的過濾(Filter Table),還支持對包進行網(wǎng)絡地址轉(zhuǎn)換(NAT Table)以及修改包的字段(Mangle Table)。在網(wǎng)絡虛擬化里,用的最多的便是 NAT 地址轉(zhuǎn)換功能。通常此類功能一般在網(wǎng)關網(wǎng)絡設備或是負載均衡設備中很常見。當 NC 需要在內(nèi)部進行網(wǎng)絡相關的虛擬化時,也是一個類似網(wǎng)關以及負載均衡設備了。
在設置 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 開關(可能需要 modprobe br_netfilter)。bridge-nf-call-iptables 從上面的源碼分析就能理解,網(wǎng)橋的轉(zhuǎn)發(fā)處理是在 Netfilter 規(guī)則之前的。所以默認情況下二層網(wǎng)橋的轉(zhuǎn)發(fā)是不會受到三層 iptables 的限制的,但是很多虛擬化網(wǎng)絡的實現(xiàn)需要 Netfilter 規(guī)則生效,所以內(nèi)核也支持了讓網(wǎng)橋的轉(zhuǎn)發(fā)邏輯也調(diào)用一下 Netfilter 的規(guī)則。這個特性默認情況不開啟,所以需要檢查開關。至于具體的 iptables 命令,可以參考這篇文章和其譯文[10]進行了解,本文不再討論。
這里強調(diào)下,Netfilter 的邏輯運行在內(nèi)核軟中斷上下文里。如果 Netfilter 添加了很多規(guī)則,必然會造成一定的 CPU 開銷。下文在提到虛擬化網(wǎng)絡的性能降低時,很大一部分開銷便是源自這里。
在傳統(tǒng)的網(wǎng)絡認知里,網(wǎng)絡就是由帶有一個或多個 NIC 的一組 NC 使用硬件介質(zhì)和 switch(交換機)、Router(路由器)所組成的一個通信集合(圖片來自 [11],下同):
網(wǎng)絡虛擬化作為 SDN(Software?Defined?Network,軟件定義網(wǎng)絡)的一種實現(xiàn),無非就是虛擬出 vNIC(虛擬網(wǎng)卡)、vSwitch(虛擬交換機)、vRouter(虛擬路由器)等設備,配置相應的數(shù)據(jù)包流轉(zhuǎn)規(guī)則而已。其對外的接口必然也是符合其所在的物理網(wǎng)絡協(xié)議規(guī)范的,比如 Ethernet 和 TCP/IP 協(xié)議族。
隨著 Linux 網(wǎng)絡虛擬化技術的演進,有了若干種虛擬化網(wǎng)絡設備,在虛擬機和虛擬容器網(wǎng)絡中得到了廣泛的應用。典型的有 Tap/Tun/Veth、Bridge 等:
虛擬機和容器的網(wǎng)絡在傳輸流程上有些區(qū)別,前者比如 KVM 一般是使用 Tap 設備將虛擬機的 vNIC 和宿主機的網(wǎng)橋 Bridge 連接起來。而容器的 Bridge 網(wǎng)絡模式是將不同 Namespace 里的 Veth Pair 連接網(wǎng)橋 Bridge 來實現(xiàn)通信(其他方式下文討論)。
Linux Bridge 配合橋接或者 NAT 模式很容易可以實現(xiàn)同主機或跨主機的虛擬機/容器之間通信,而且 Bridge 本身也支持 VLAN 的配置,可以實現(xiàn)一些三層交換機的能力。但是很多廠商都在研發(fā)功能更豐富的虛擬交換機,流行的有 Cisco Nexus 1000V、 VMware Virtual Switch 以及廣泛使用的開源的 Open vSwitch[12] 等。利用 vSwitch,可以構建出支持更多封裝協(xié)議、更高級的虛擬網(wǎng)絡:
1 Linux Bridge + Veth Pair 轉(zhuǎn)發(fā)
VRF(Virtual Routing and Forwarding,虛擬路由轉(zhuǎn)發(fā))在網(wǎng)絡領域中是個很常見的術語。上世紀九十年代開始,很多二層交換機上就能創(chuàng)建出 4K 的 VLAN 廣播域了。4K 是因為 VLAN 標簽的格式遵循 802.1q 標準,其中定義的 VLAN ID 是 12 位的緣故(802.1q in 802.1q 可以做到 4094*4094 個,0 和 4095 保留)。如今 VRF 概念被引入三層,單個物理設備上也可以有多個虛擬路由/轉(zhuǎn)發(fā)實例了。Linux 的 VRF 實現(xiàn)了對三層的網(wǎng)絡協(xié)議棧的虛擬,而 Network Namespace(以下簡稱 netns)虛擬了整個網(wǎng)絡棧。一個 netns 的網(wǎng)絡棧包括:網(wǎng)卡(Network Interface)、回環(huán)設備(Loopback Device)、路由表(Routing Table)和 iptables 規(guī)則。本文使用 netns 進行演示(畢竟在討論容器),下文使用 ip[14] 命令創(chuàng)建和管理 netns 以及 Veth Pair 設備。
創(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í)行結果如圖(刪除先不執(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í)行結果如下:
圖片
這個新創(chuàng)建的 netns 里一貧如洗,只有一個孤獨的 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 設備
# 分別創(chuàng)建 2 對名為 veth-1-a/veth-1-b 和 veth-2-a/veth-2-b 的 Veth Pair 設備
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 設備,此時它們都沒有分配 IP 地址且是 DOWN 狀態(tài)。
將 Veth Pair 設備加入 netns
# 將 veth-1-a 設備加入 qianyi-test-1 這個 netns
ip link set veth-1-a netns qianyi-test-1
# 將 veth-1-b 設備加入 qianyi-test-2 這個 netns
ip link set veth-1-b netns qianyi-test-2
# 為設備添加 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 命令,即可看到設備已經(jīng)存在,且路由表(route 或 ip route 命令)也被默認創(chuàng)建了:
這里操作和查看設備都必須采用 ip netns exec {...} 的方式進行,如果命令很多,也可以把執(zhí)行的命令換成 bash,這樣可以方便的在這個 shell 里對該 netns 進行操作。
現(xiàn)在通過 veth-1-a/veth-1-b 這對 Veth Pair 聯(lián)通了 qianyi-test-1 和 qianyi-test-2 這兩個 netns,這兩個 netns 之間就可以通過這兩個 IP 地址相互訪問了。
ping 的同時在 101 上抓包的結果如下:
可以很清楚的看到,eth-1-a(10.0.0.101)先通過 ARP (Address Resolution Protocol,地址解析協(xié)議)詢問 10.0.0.102 的 MAC 地址。得到回應后,就以 ICMP (Internet Control Message Protocol,Internet 報文控制協(xié)議) request 和 reply 了,這也正是 ping 使用的協(xié)議。
ARP 解析的緩存信息也可以通過 arp 命令查看:
此時的網(wǎng)絡連接模式是這樣的:
這種連接模式,就很像是現(xiàn)實中把兩個帶有 NIC 的設備用網(wǎng)線連接起來,然后配置了相同網(wǎng)段的 IP 后彼此就可以通信了。那如果超過一個設備需要建立互聯(lián)呢?現(xiàn)實中就需要交換機等網(wǎng)絡設備了。還記得前文中說的 Linux 自帶的 Bridge 么?接下來就使用 Bridge 機制建立網(wǎng)絡。
進行下面的試驗前需要把 veth-1-a/veth-1-b 這對 Veth Pair 從 qianyi-test-1 和 qianyi-test-2 移動回宿主機的 netns 里,恢復初始的環(huán)境。
# 宿主機的 netns id 是 1(有些系統(tǒng)可能不是,請查詢相關系統(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)絡
# 創(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,實際上 brctl show 命令顯示的結果更易懂,可以很清楚的看到 veth-1-b 和 veth-2-b 連接在了網(wǎng)橋的接口上。當 Veth Pair 的一端連接在網(wǎng)橋上時,就會從“網(wǎng)卡”退化成一個“水晶頭”。
當下模式抓包的結果并沒有什么區(qū)別,但網(wǎng)絡連接模式不同:
按照這個模式,如果有更多的 Network Namespace 和 Veth Pair 的話,使用相同的方式添加就可以水平擴展了。
但是嘗試從 qianyi-test-1 中 ping 宿主機自然是不通的,因為沒有任何網(wǎng)絡規(guī)則可以訪問到宿主機的網(wǎng)絡:
上面的截圖中有個 docker0 的網(wǎng)橋。當機器上安裝了 Docker 之后會被自動設置好這個網(wǎng)橋供 Docker 使用。可能你注意到了,這個名為 docker0 的網(wǎng)橋居然是有 IP 地址的。現(xiàn)實中的網(wǎng)橋自然是沒有 IP 的,但是 Linux Bridge 這個虛擬設備是可以設置的。當 Bridge 設置 IP 之后,就可以將其設置成這個內(nèi)部網(wǎng)絡的網(wǎng)關(Gateway),再配合路由規(guī)則就可以實現(xiàn)最簡單的虛擬網(wǎng)絡跨機通信了(類似現(xiàn)實中的三層交換機)。
下面繼續(xù)給 br0 網(wǎng)橋創(chuàng)建地址并在 veth-1-a 和 veth-2-a 上設置其為默認的網(wǎng)關地址:
# 確認路由轉(zhuǎn)發(fā)開啟
echo "1" > /proc/sys/net/ipv4/ip_forward
# 為 br0 設置 IP 地址
ip addr add local 10.0.0.1/24 dev br0
# 為 veth-1-a 和 veth-2-a 設置默認網(wǎng)關
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
此時就能成功的訪問宿主機地址了(宿主機上的路由表在 ip link set br0 up 這一步自動創(chuàng)建了):
網(wǎng)絡模型進一步變成了這樣:
如果此時,另一臺宿主機上也存在另一個網(wǎng)段的網(wǎng)橋和若干個 netns 的話,怎么讓他們互通呢?分別在兩邊宿主機上配置一條到目的宿主機的路由規(guī)則就好了。假設另一臺宿主機的 IP 地址是 10.97.212.160,子網(wǎng)是 10.0.1.0/24 的話,那么需要在當前機器上加一條 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ī)則,會很復雜。這就一個簡單的 Underlay 模式的容器通信方案了。缺點也很明顯,要求對宿主機底層網(wǎng)絡有修改權,且比較難和底層網(wǎng)絡解耦。那如果能在物理網(wǎng)絡上構建出一個橫跨所有宿主機的虛擬網(wǎng)橋,把所有相關的 netns 里面的設備都連接上去,不就可以解耦了么。這就是 Overlay Network(覆蓋網(wǎng)絡)方案,下文會進行闡述。至于本節(jié)其他虛擬網(wǎng)絡設備的安裝和配置(比如 Open vSwitch)也是比較類似的,這里不再贅述,有興趣的話可以自行查看文檔并測試。
2 Overlay 網(wǎng)絡方案之 VXLAN
VXLAN(Virtual eXtensible Local Area Network,虛擬可擴展局域網(wǎng),RFC7348)[16],VLAN 的擴展協(xié)議,是由 IETF 定義的 NVO3(Network Virtualization over Layer 3)標準技術之一(其他有代表性的還有 NVGRE、STT)。但是 VXLAN 和 VLAN 想要解決的問題是不一樣的。VXLAN 本質(zhì)上是一種隧道封裝技術,它將數(shù)據(jù)鏈路層(L2)的以太網(wǎng)幀(Ethernet frames)封裝成傳輸層(L4)的 UDP 數(shù)據(jù)報(Datagrams),然后在網(wǎng)絡層(L3)中傳輸。效果就像數(shù)據(jù)鏈路層(L2)的以太網(wǎng)幀在一個廣播域中傳輸一樣,即跨越了三層網(wǎng)絡卻感知不到三層的存在。因為是基于 UDP 封裝,只要是 IP 網(wǎng)絡路由可達就可以構建出龐大的虛擬二層網(wǎng)絡。也因為是基于高層協(xié)議再次封裝,性能會比傳統(tǒng)的網(wǎng)絡低 20%~30% 左右(性能數(shù)據(jù)隨著技術發(fā)展會有變化,僅代表當前水平)。
這里簡要介紹下 VXLAN 的 2 個重要概念:
VXLAN 的報文格式如圖[17]:
Linux kernel v3.7.0 版本開始支持 VXLAN 網(wǎng)絡。但為了穩(wěn)定性和其他功能,請盡量選擇 kernel v3.10.0 及之后的版本。下面我們使用 10.97.212.159 和 11.238.151.74 這兩臺機器創(chuàng)建一個測試的 VXLAN 網(wǎng)絡。
# 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 設備
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 設備
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
這一長串的命令和之前的步驟完全一致,構建了一個如下圖所示的網(wǎng)絡環(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 上操作(本機有多個地址時可以用 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 上操作(本機有多個地址時可以用 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 設備都連接上去了:
然后從 10.0.0.101 上就可以 ping 通 10.0.0.103 了:
在 10.0.0.101 上抓的包來看,就像是二層互通一樣:
直接查看 arp 緩存,也是和二層互通一模一樣:
使用 arp -d 10.0.0.103 刪掉這個緩存項目,在宿主機上重新抓包并保存文件,然后用 WireShark 打開看看(因為上面設置的不是 VXLAN 默認端口 4789,還需要設置 WireShark 把抓到的 UDP 解析為 VXLAN 協(xié)議):
我們得到了預期的結果。此時的網(wǎng)絡架構如圖所示:
那么問題來了,這里使用 UDP 協(xié)議能實現(xiàn)可靠通信嗎?當然可以,可靠性不是這一層考慮的事情,而是里層被包裹的協(xié)議需要考慮的。完整的通信原理其實也并不復雜,兩臺機器上各自有 VTEP(VXLAN Tunnel Endpoints,VXLAN 隧道端點)設備,監(jiān)聽著 9527 端口上發(fā)送的 UDP 數(shù)據(jù)包。在收到數(shù)據(jù)包后拆解通過 Bridge 傳遞給指定的設備。那 VETP 這個虛擬設備怎么知道類似 10.0.0.3 這樣的地址要發(fā)送給哪臺機器上的 VETP 設備呢?這可是虛擬的二層網(wǎng)絡,底層網(wǎng)絡上可不認識這個地址。事實上在 Linux Bridge 上維護著一個名為 FDB(Forwarding Database entry)的二層轉(zhuǎn)發(fā)表,用于保存遠端虛擬機/容器的 MAC 地址、遠端 VTEP 的 IP,以及 VNI 的映射關系,可以通過 bridge fdb 命令來對 FDB 表進行操作:
# 新增條目
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
上面這個簡單的實驗就 2 臺機器,使用了命令的方式直接指定了彼此的 VTEP 地址,當 fdb 表查不到信息時發(fā)給對方就行了,這是最簡單的互聯(lián)模式。大規(guī)模 VXLAN 網(wǎng)絡下,就需要考慮如何發(fā)現(xiàn)網(wǎng)絡中其他的 VETP 地址了。解決這個問題一般有 2 種方式:一是使用組播/多播( IGMP, Internet Group Management Protocol),把節(jié)點組成一個虛擬的整體,包不清楚發(fā)給誰的話就廣播給整個組了(上述實驗中的創(chuàng)建 VETH 設備的命令修改為組播/多播地址比如 224.1.1.1 就行,remote 關鍵字也要改成 group,具體請參閱其他資料);二是通過外部的分布式控制中心來收集 FDB 信息并分發(fā)給同一個 VXLAN 網(wǎng)絡的所有節(jié)點。組播/多播受限于底層網(wǎng)絡的支持情況和大規(guī)模下的的性能問題,比如很多云網(wǎng)絡上不一定允許這么做。所以下文在討論和研究 K8s 的網(wǎng)絡方案時會看到很多網(wǎng)絡插件的實現(xiàn)采用的都是類似后者的實現(xiàn)方式。
這節(jié)就介紹到這里了。當然 Overlay 網(wǎng)絡方案也不止 VXLAN 這一種方式,只是目前很多主流的方案都采用了這種方式。其他的 Overlay 模式看上去眼花繚亂,其實說白了,無非就是 L2 over L4,L2 over L3,L3 over L3 等等各種包裝方式罷了,懂了基本原理之后都沒什么大不了的。網(wǎng)絡虛擬化的設備和機制也有很多[18],細說的話一天都說不完,但是基本的網(wǎng)絡原理掌握之后,無非是各種協(xié)議包的流轉(zhuǎn)罷了。
1 K8s 的網(wǎng)絡模型
每一個 Pod 都有它自己的 IP 地址,這就意味著你不需要顯式地在每個 Pod 之間創(chuàng)建鏈接, 你幾乎不需要處理容器端口到主機端口之間的映射。這將創(chuàng)建一個干凈的、向后兼容的模型,在這個模型里,從端口分配、命名、服務發(fā)現(xiàn)、 負載均衡、應用配置和遷移的角度來看,Pod 可以被視作虛擬機或者物理主機。
Kubernetes 對所有網(wǎng)絡設施的實施,都需要滿足以下的基本要求(除非有設置一些特定的網(wǎng)絡分段策略):
備注:僅針對那些支持 Pods 在主機網(wǎng)絡中運行的平臺(比如:Linux):
這個模型不僅不復雜,而且還和 Kubernetes 的實現(xiàn)廉價的從虛擬機向容器遷移的初衷相兼容, 如果你的工作開始是在虛擬機中運行的,你的虛擬機有一個 IP,這樣就可以和其他的虛擬機進行通信,這是基本相同的模型。
Kubernetes 的 IP 地址存在于 Pod 范圍內(nèi) - 容器共享它們的網(wǎng)絡命名空間 - 包括它們的 IP 地址和 MAC 地址。這就意味著 Pod 內(nèi)的容器都可以通過 localhost 到達各個端口。這也意味著 Pod 內(nèi)的容器都需要相互協(xié)調(diào)端口的使用,但是這和虛擬機中的進程似乎沒有什么不同, 這也被稱為“一個 Pod 一個 IP”模型。
這幾段話引用自 K8s 的官方文檔[19],簡單概括下就是一個 Pod 一個獨立的 IP 地址,所有的 Pod 之間可以不通過 NAT 通信。這個模型把一個 Pod 的網(wǎng)絡環(huán)境近似等同于一個 VM 的網(wǎng)絡環(huán)境。
2 K8s 的主流網(wǎng)絡插件實現(xiàn)原理
K8s 中的網(wǎng)絡是通過插件方式實現(xiàn)的,其網(wǎng)絡插件有 2 種類型:
圖片來自[20],本文只關注 CNI 接口插件。主流的 K8s 網(wǎng)絡插件有這些[21],本文選出 github star 數(shù)在千以上的幾個項目分析下:
Flannel
CNI 是由 CoreOS 提出的規(guī)范,那就先看下 CoreOS 自己的 Flannel 項目的設計。Flannel 會在每臺機宿主機上部署一個名為 flanneld 的代理進程,網(wǎng)段相關的數(shù)據(jù)使用 Kubernetes API/Etcd 存儲。Flannel 項目本身是一個框架,真正為我們提供容器網(wǎng)絡功能的,是 Flannel 的后端實現(xiàn)。
目前的 Flannel 有下面幾種后端實現(xiàn):VXLAN、host-gw、UDP 以及阿里云和其他大廠的支持后端(云廠商都是實驗性支持),還有諸如 IPIP、IPSec 等一些隧道通信的實驗性支持。按照官方文檔的建議是優(yōu)先使用 VXLAN 模式,host-gw 推薦給經(jīng)驗豐富且想要進一步提升性能的用戶(云環(huán)境通常不能用,原因后面說),UDP 是 Flannel 最早支持的一種性能比較差的方案,基本上已經(jīng)棄用了。
下文分別對這三種模式進行分析。
1)VXLAN
使用 Linux 內(nèi)核 VXLAN 封裝數(shù)據(jù)包的方式和原理上文已經(jīng)介紹過了,F(xiàn)lannel 創(chuàng)建了一個名為 flannel.1 的 VETH 設備。因為 flanneld 進程的存在,所以注冊和更新新的 VETH 設備關系的任務就依靠這個進程了。所以好像也沒什么需要說的了,就是每個新的 K8s Node 都會創(chuàng)建 flanneld 這個 DeamonSet 模式的守護進程。然后注冊、更新新的 VETH 設備就變得非常自然了,全局的數(shù)據(jù)自然也是保存在 Etcd 里了。這種方式圖已經(jīng)畫過了,無非是設備名字不一樣(VETH 叫 flannel.1,網(wǎng)橋叫 cni0)而已。
2)host-gw
顧名思義,host-gw 就是把宿主機 Host 當做 Gateway 網(wǎng)關來處理協(xié)議包的流動。這個方式上文其實也演示過了,至于節(jié)點的變化和路由表的增刪也是依靠 flanneld 在做的。這個方案優(yōu)缺點都很明顯,最大的優(yōu)點自然是性能,實打?qū)嵉闹苯愚D(zhuǎn)發(fā)(性能整體比宿主機層面的通信低 10%,VXLAN 可是20% 起步,甚至 30%)。缺點也很明顯,這種方式要求宿主機之間是二層連通的,還需要對基礎設施有掌控權(編輯路由表),這個在云服務環(huán)境一般較難實現(xiàn),另外規(guī)模越來越大時候的路由表規(guī)模也會隨之增大。這種方式原理上比較簡單,圖不畫了。
3)UDP
每臺宿主機上的 flanneld 進程會創(chuàng)建一個默認名為 flannel0 的 Tun 設備。Tun 設備的功能非常簡單,用于在內(nèi)核和用戶應用程序之間傳遞 IP 包。內(nèi)核將一個 IP 包發(fā)送給 Tun 設備之后,這個包就會交給創(chuàng)建這個設備的應用程序。而進程向 Tun 設備發(fā)送了一個 IP 包,那么這個 IP 包就會出現(xiàn)在宿主機的網(wǎng)絡棧中,然后根據(jù)路由表進行下一跳的處理。在由 Flannel 管理的容器網(wǎng)絡里,一臺宿主機上的所有容器都屬于該宿主機被分配的一個“子網(wǎng)”。這個子網(wǎng)的范圍信息,所屬的宿主機 IP 地址都保存在 Etcd 里。flanneld 進程均監(jiān)聽著宿主機上的 UDP 8285 端口,相互之間通過 UDP 協(xié)議包裝 IP 包給目的主機完成通信。之前說過這個模式性能差,差在哪里?這個方案就是一個在應用層模擬實現(xiàn)的 Overlay 網(wǎng)絡似得(像不像一個用戶態(tài)實現(xiàn)的 VETH 設備?),數(shù)據(jù)包相比內(nèi)核原生支持的 VXLAN 協(xié)議在用戶態(tài)多了一次進出(flanneld 進程封包/拆包過程),所以性能上損失要大一些。
Calico
Calico 是個挺有意思的項目,基本思想是把宿主機完全當成路由器,不使用隧道或 NAT 來實現(xiàn)轉(zhuǎn)發(fā),把所有二三層流量轉(zhuǎn)換成三層流量,并通過宿主機上的路由配置完成包的轉(zhuǎn)發(fā)。
Calico 和之前說的 Flannel 的 host-gw 模式區(qū)別是什么?首先 Calico 不使用網(wǎng)橋,而是通過路由規(guī)則在不同的 vNiC 間轉(zhuǎn)發(fā)數(shù)據(jù)。另外路由表也不是靠 Etcd 存儲和通知更新,而是像現(xiàn)實環(huán)境一樣直接用 BGP(Border Gateway Protocol, 邊界網(wǎng)關協(xié)議)進行路由表數(shù)據(jù)交換。BGP 挺復雜的,詳細的闡述這個協(xié)議有點費勁(而且我也不是很懂),在本文里只需要知道這個協(xié)議使得路由設備可以相互發(fā)送和學習對方的路由信息來充實自己就可以了,有興趣的話請查閱其他資料進一步了解。回到 Calico,宿主機上依舊有個名為 Felix 的守護進程和一個名為 BIRD的 BGP 客戶端。
上文說過,F(xiàn)lannel 的 host-gw 模式要求宿主機二層是互通的(在一個子網(wǎng)),在 Calico 這里依然有這個要求。但是 Calico 為不在一個子網(wǎng)的環(huán)境提供了 IPIP 模式的支持。開啟這個模式之后,宿主機上會創(chuàng)建一個 Tun 設備以 IP 隧道(IP tunnel)的方式通信。當然用了這個模式之后,包又是L3 over L3 的 Overlay 網(wǎng)絡模式了,性能也和 VXLAN 模式相當。
全路由表的通信方式也沒有額外組件,配置好 IP 路由轉(zhuǎn)發(fā)規(guī)則后全靠內(nèi)核路由模塊的流轉(zhuǎn)來做。IPIP 的架構圖也是大同小異的,也不畫了。
Cilium
eBPF-based Networking, Security, and Observability
光從這個介紹上就看出來 Cilium 散發(fā)出的那種與眾不同的氣息。這個項目目前的 Github Star 數(shù)字快過萬了,直接力壓前面兩位。Cilium 部署后會在宿主機上創(chuàng)建一個名為 cilium-agent 的守護進程,這個進程的作用就是維護和部署 eBPF 腳本來實現(xiàn)所有的流量轉(zhuǎn)發(fā)、過濾、診斷的事情(都不依賴 Netfilter 機制,kenel > v4.19 版本)。從原理圖的角度畫出來的架構圖很簡單(配圖來自 github 主頁):
Cilium 除了支持基本的網(wǎng)絡連通、隔離與服務透出之外,依托 eBPF 所以對宿主機網(wǎng)絡有更好的觀測性和故障排查能力。這個話題也很大,本文就此收住。這里給兩幾篇寫的很好的文章何其譯文可以進一步了解22。
3 K8s 容器內(nèi)訪問隔離
上文介紹了網(wǎng)絡插件的機制和實現(xiàn)原理,最終可以構建出一個二層/三層連通的虛擬網(wǎng)絡。默認情況下 Pod 間的任何網(wǎng)絡訪問都是不受限的,但是內(nèi)部網(wǎng)絡中經(jīng)常還是需要設置一些訪問規(guī)則(防火墻)的。
針對這個需求,K8s 抽象了一個名為 NetworkPolicy 的機制來支持這個功能。網(wǎng)絡策略通過網(wǎng)絡插件來實現(xiàn),要使用網(wǎng)絡策略就必須使用支持 NetworkPolicy 的網(wǎng)絡解決方案。為什么這么說?因為不是所有的網(wǎng)絡插件都支持 NetworkPolicy 機制,比如 Flannel 就不支持。至于 NetworkPolicy 的底層原理,自然是使用 iptables 配置 netfilter 規(guī)則來實現(xiàn)對包的過濾的。NetworkPolicy 配置的方法和 iptables/Netfilter 的原理細節(jié)不在本文范圍內(nèi),請參閱其他資料進行了解24。
4 K8s 容器內(nèi)服務透出
在一個 K8s 集群內(nèi)部,在網(wǎng)絡插件的幫助下,所有的容器/進程可以相互進行通信。但是作為服務提供方這是不夠的,因為很多時候,服務的使用方不會在同一個 K8s 集群內(nèi)的。那么就需要一種機制將這個集群內(nèi)的服務對外透出。K8s 使用 Service 這個對象來完成這個能力的抽象。Service 在 K8s 里是個很重要的對象,即使在 K8s 內(nèi)部進行訪問,往往也是需要 Service 包裝的(一來 Pod 地址不是永遠固定的,二來總是會有負載均衡的需求)。
一句話概括 Service 的原理就是:Service = kube-proxy + iptables 規(guī)則。當一個 Service 創(chuàng)建時,K8s 會為其分配一個 Cluster IP 地址。這個地址其實是個 VIP,并沒有一個真實的網(wǎng)絡對象存在。這個 IP 只會存在于 iptables 規(guī)則里,對這個 VIP:VPort 的訪問使用 iptables 的隨機模式規(guī)則指向了一個或者多個真實存在的 Pod 地址(DNAT),這個是 Service 最基本的工作原理。那 kube-proxy 做什么?kube-proxy 監(jiān)聽 Pod 的變化,負責在宿主機上生成這些 NAT 規(guī)則。這個模式下 kube-proxy 不轉(zhuǎn)發(fā)流量,kube-proxy 只是負責疏通管道。
K8s 官方文檔比較好的介紹了 kube-proxy 支持的多種模式和基本的原理[26]。早先的 userspace 模式基本上棄用了,上面所述的 iptables 隨機規(guī)則的做法在大規(guī)模下也不推薦使用了(想想為什么)。現(xiàn)在最推薦的當屬 IPVS 模式了,相對于前兩種在大規(guī)模下的性能更好。如果說 IPVS 這個詞比較陌生的話,LVS 這個詞恐怕是我們耳熟能詳?shù)摹T谶@個模式下,kube-proxy 會在宿主機上創(chuàng)建一個名為 kube-ipvs0 的虛擬網(wǎng)卡,然后分配 Service VIP 作為其 IP 地址。最后 kube-proxy 使用內(nèi)核的 IPVS 模塊為這個地址設置后端 POD 的地址(ipvsadm 命令可以查看)。其實 IPVS 在內(nèi)核中的實現(xiàn)也是用了 Netfilter 的 NAT 機制。不同的是,IPVS 不會為每一個地址設置 NAT 規(guī)則,而是把這些規(guī)則的處理放到了內(nèi)核態(tài),保證了 iptables 規(guī)則的數(shù)量基本上恒定,比較好的解決了之前的問題。
上面說的只是解決了負載均衡的問題,還沒提到服務透出。K8s 服務透出的方式主要有 NodePort、LoadBalancer 類型的 Service(會調(diào)用 CloudProvider 在公有云上為你創(chuàng)建一個負載均衡服務)以及 ExternalName(kube-dns 里添加 CNAME)的方式。對于第二種類型,當 Service 繁多但是又流量很小的情況下,也可以使用 Ingress 這個 Service 的 Service 來收斂掉[27]。Ingress 目前只支持七層 HTTP(S) 轉(zhuǎn)發(fā)(Service 目前只支持四層轉(zhuǎn)發(fā)),從這個角度猜猜 Ingress 怎么實現(xiàn)的?來張圖看看吧[28](當然還有很多其他的 Controller[29]):
對于這部分,本文不再進行詳細闡述了,無非就是 NAT,NAT 多了就想辦法收斂 NAT 條目。按照慣例,這里給出一篇特別好的 kube-proxy 原理闡述的文章供進一步了解[30]。
網(wǎng)絡虛擬化是個很大的話題,很難在一篇文章中完全講清楚。盡管這篇文章盡量想把重要的知識節(jié)點編織清楚,但受限于作者本人的精力以及認知上的限制,可能存在疏忽甚至錯漏。如有此類問題,歡迎在評論區(qū)討論/指正。參考文獻里給出了很多不錯的資料值得進一步去學習(部分地址受限于網(wǎng)絡環(huá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)絡棧監(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)絡棧監(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)絡地址轉(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 實現(xiàn) K8s Service 負載均衡 (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)絡模型:自己實現(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)載。
多編程語言都有一個稱為null的非值。它指示一個變量當前不指向一個對象,例如,當它還沒有初始化的時候。
相比之下,JavaScript有兩個這樣的非值:undefined和null。在這篇博文中,我們將研究它們有什么不同,以及如何最好地使用或避免它們。
這兩種值非常相似,經(jīng)常互換使用。因此,它們的區(qū)別是微妙的。
ECMAScript語言規(guī)范對其描述如下。
在JavaScript中擁有兩個非值現(xiàn)在被認為是一個設計錯誤(甚至被JavaScript的創(chuàng)造者Brendan Eich認為)。
那為什么不從 JavaScript 中刪除其中的一個值呢?JavaScript 的一個核心原則是絕不破壞向后兼容。這個原則有很多好處。它最大的缺點是,設計上的錯誤無法被刪除。
在Java中(它啟發(fā)了JavaScript的許多方面),初始化值取決于變量的靜態(tài)類型。
在JavaScript中,每個變量都可以容納對象值和基本類型值。因此,如果null意味著 "不是一個對象",那么JavaScript也需要一個初始化值,意味著 "既不是一個對象也不是一個原始值"。這個初始化值就是 undefined。
如果一個變量myVar還沒有被初始化,它的值是undefined。
let myVar;
assert.equal(myVar, undefined);
如果一個屬性.unknownProp丟失,訪問該屬性會產(chǎn)生undefined的值。
const obj = {};
assert.equal(obj.unknownProp, undefined);
如果一個函數(shù)有一個沒有參數(shù)的返回語句,該函數(shù)隱含地返回undefined。
function myFunc() {
return;
}
assert.equal(myFunc(), undefined);
如果一個參數(shù)x被省略了,語言將該參數(shù)初始化為undefined。
function myFunc() {
return;
}
assert.equal(myFunc(), undefined);
myFunc();
通過obj?.someProp的可選鏈路,如果obj未定義或為空,則返回未定義。
> undefined?.someProp
undefined
> null?.someProp
undefined
一個對象的原型要么是一個對象,要么在原型鏈的末端是null。Object.prototype沒有原型。
> Object.getPrototypeOf(Object.prototype)
null
如果我們將一個正則表達式(如/a/)與一個字符串(如'x')相匹配,我們要么得到一個帶有匹配數(shù)據(jù)的對象(如果匹配成功),要么得到空(如果匹配失敗)。
> /a/.exec('x')
null
JSON數(shù)據(jù)格式不支持undefined,只支持null。
> JSON.stringify({a: undefined, b: null})
'{"b":null}'
在以下情況下,將使用參數(shù)默認值。
例如:
function myFunc(arg='abc') {
return arg;
}
assert.equal(myFunc('hello'), 'hello');
assert.equal(myFunc(), 'abc');
assert.equal(myFunc(undefined), 'abc');
undefined也觸發(fā)了參數(shù)的默認值,這說明它是一個元值。
下面的例子說明了這一點的用處。
function concat(str1='', str2='') {
return str1 + str2;
}
function twice(str) { // (A)
return concat(str, str);
}
在A行,我們沒有為str指定一個參數(shù)默認值。當這個參數(shù)缺失時,我們把這個狀態(tài)轉(zhuǎn)發(fā)給concat(),讓它選擇一個默認值。
解構中的缺省值與參數(shù)缺省值的工作原理類似--如果一個變量在數(shù)據(jù)中沒有匹配,或者它與undefined的變量匹配,就會使用它們。
const [a='a'] = [];
assert.equal(a, 'a');
const [b='b'] = [undefined];
assert.equal(b, 'b');
const {prop: c='c'} = {};
assert.equal(c, 'c');
const {prop: d='d'} = {prop: undefined};
assert.equal(d, 'd');
當通過 value?.prop 進行可選的鏈接時
function getProp(value) {
// optional static property access
return value?.prop;
}
assert.equal(
getProp({prop: 123}), 123);
assert.equal(
getProp(undefined), undefined);
assert.equal(
getProp(null), undefined);
下面兩個操作的效果類似。
obj?.[?expr?] // optional dynamic property access
func?.(?arg0?, ?arg1?) // optional function or method call
nullish 合并算子??如果一個值undefined或為null,則允許我們使用默認值:
> undefined ?? 'default value'
'default value'
> null ?? 'default value'
'default value'
> 0 ?? 'default value'
0
> 123 ?? 'default value'
123
> '' ?? 'default value'
''
> 'abc' ?? 'default value'
'abc'
null合并賦值操作符??=:
function setName(obj) {
obj.name ??= '(Unnamed)';
return obj;
}
assert.deepEqual(
setName({}),
{name: '(Unnamed)'}
);
assert.deepEqual(
setName({name: undefined}),
{name: '(Unnamed)'}
);
assert.deepEqual(
setName({name: null}),
{name: '(Unnamed)'}
);
assert.deepEqual(
setName({name: 'Jane'}),
{name: 'Jane'}
);
下面分享一下我們自己的代碼中處理undefined 和null的最常見方法。
舉例,我們可能希望一個屬性file.title永遠存在,并且永遠是一個字符串。有兩種常見的方法來實現(xiàn)這一點。
這里,只檢查undefined和null,而不檢查一個值是否是字符串。你必須自己決定是否要把它作為一個額外的安全措施來實施。
如下所示
function createFile(title) {
if (title === undefined || title === null) {
throw new Error('`title` must not be nullish');
}
// ···
}
為什么選擇這種方法?
我們想把undefined和null當作一樣的東西,因為JavaScript代碼經(jīng)常這樣做--比如說。
// 檢測一個屬性是否存在
if (!obj.requiredProp) {
obj.requiredProp = 123;
}
const myValue = myParameter ?? 'some default';
如果我們的代碼中出現(xiàn)了問題,出現(xiàn)了undefined或null,我們希望它盡可能快地失敗。
如下所示
function createFile(title) {
title ??= '(Untitled)';
// ···
}
我們不能在這里使用參數(shù)缺省值,因為它只由undefined 觸發(fā)。相反,我們依靠nullish 合并賦值運算符??=。
為什么選擇這種方法?
例如,我們可能希望一個屬性file.title是一個字符串或 "關閉"(文件沒有標題)。有幾種方法可以實現(xiàn)這一點。
function createFile(title) {
if (title === undefined) {
throw new Error('`title` must not be undefined');
}
return {title};
}
另外,undefined可以觸發(fā)一個默認值。
function createFile(title = '(Untitled)') {
return {title};
}
為什么選擇這種方法?
function createFile(title) {
if (title === null) {
throw new Error('`title` must not be null');
}
return {title};
}
為什么選擇這種方法?
當接收一個值時,把undefined和null都當作 "不是一個值 "是有意義的。然而,當我們創(chuàng)建值時,我們希望不含糊,以便處理這些值時保持簡單。
這指向了一種不同的方法。如果我們需要一個 "關閉"的值,但又不想使用undefined或null作為這樣的值,怎么辦?請往下看。
我們可以創(chuàng)建一個特殊的值,每當屬性.title被關閉時,我們就使用這個值。
const UNTITLED = Symbol('UNTITLED');
const file = {
title: UNTITLED,
};
空對象模式來自于面向?qū)ο蟮木幊獭?/span>
在下面的例子中,UntitledFile實現(xiàn)了 "null" 模式。
// 抽象的超類
class File {
constructor(content) {
if (new.target === File) {
throw new Error('Can’t instantiate this class');
}
this.content = content;
}
}
class TitledFile extends File {
constructor(content, title) {
super(content);
this.title = title;
}
getTitle() {
return this.title;
}
}
class UntitledFile extends File {
constructor(content) {
super(content);
}
getTitle() {
return '(Untitled)';
}
}
const files = [
new TitledFile('Dear diary!', 'My Diary'),
new UntitledFile('Reminder: pick a title!'),
];
assert.deepEqual(
files.map(f => f.getTitle()),
[
'My Diary',
'(Untitled)',
]);
我們也可以只對標題使用空對象模式(而不是對整個文件對象)。
Maybe 類型是一種函數(shù)編程技術。
function getTitle(file) {
switch (file.title.kind) {
case 'just':
return file.title.value;
case 'nothing':
return '(Untitled)';
default:
throw new Error();
}
}
const files = [
{
title: {kind: 'just', value: 'My Diary'},
content: 'Dear diary!',
},
{
title: {kind: 'nothing'},
content: 'Reminder: pick a title!',
},
];
assert.deepEqual(
files.map(f => getTitle(f)),
[
'My Diary',
'(Untitled)',
]);
我們可以通過Arrays對 "just "和 "nothing "進行編碼。我們的方法的好處是,它得到了TypeScript的良好支持(通過判別性聯(lián)合)。
我不喜歡用undefined作為 "關閉 "的值,有三個原因。
因此,如果我需要一個特殊的值,我會使用以下兩種方法中的一種。
~ 完,我是小智,刷碗去了,現(xiàn)在唯有刷碗才能體驗到生活中的一點樂趣,逃 ~
作者:Michael Thiessen 譯者:前端小智 來源:dev
原文:https://2ality.com/2021/01/undefined-null-revisited.html
*請認真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。