云原生系列「三」容器网络
Docker的網絡實現
?標準的Docker支持以下4類網絡模式。
◎ host模式:使用--net=host指定。
◎ container模式:使用--net=container:NAME_or_ID指定。
◎ none模式:使用--net=none指定。
◎ bridge模式:使用--net=bridge指定,為默認設置。
? ? ? 在Kubernetes管理模式下通常只會使用bridge模式,所以本節只介紹在bridge模式下Docker是如何支持網絡的。
? ? ? 在bridge模式下,Docker Daemon第1次啟動時會創建一個虛擬的網橋,默認的名稱是docker0,然后按照RPC1918的模型在私有網絡空間中給這個網橋分配一個子網。針對由Docker創建的每一個容器,都會創建一個虛擬的以太網設備(Veth設備對),其中一端關聯到網橋上,另一端使用Linux的網絡命名空間技術,映射到容器內的eth0設備,然后從網橋的地址段內給eth0接口分配一個IP地址。
? ? 如下圖所示就是Docker的默認橋接網絡模型:
?
? ? ?其中ip1是網橋的IP地址,Docker Daemon會再幾個備選地址段里給它選一個地址,通常是以172開頭的一個地址。這個地址和主機的IP地址是不重疊的。ip2是Docker在啟動容器時,在這個地址段選擇一個沒有使用的IP地址分配給容器。相應的MAC地址也根據這個IP地址,在02:42:ac:11:00:00和02:42:ac:11:ff:ff的范圍內生成,這樣做可以確保不會有ARP沖突。
? ? ?啟動后,Docker還將Veth對的名稱映射到eth0網絡接口。ip3就是主機的網卡地址。?
? ? ?在一般情況下,ip1、ip2和ip3是不同的IP段,所以在默認不做任何特殊配置的情況下,在外部是看不到ip1和ip2的。
? ? 這樣做的結果就是,在同一臺機器內的容器之間可以相互通信,不同主機上的容器不能相互通信,實際上它們甚至有可能在相同的網絡地址范圍內(不同主機上的docker0的地址段可能是一樣的)。
? ? 為了讓它們跨節點互相通信,就必須在主機的地址上分配端口,然后通過這個端口路由或代理到容器上。這種做法顯然意味著一定要在容器之間小心謹慎地協調好端口的分配,或者使用動態端口的分配技術。在不同應用之間協調好端口分配是十分困難的事情,特別是集群水平擴展時。而動態的端口分配也會帶來高度復雜性,例如:每個應用程序都只能將端口看作一個符號(因為是動態分配的,所以無法提前設置)。而且API Server要在分配完后,將動態端口插入配置的合適位置,服務也必須能互相找到對方等。這些都是Docker的網絡模型在跨主機訪問時面臨的問題。
1.查看Docker啟動后的系統情況
我們已經知道,Docker網絡在bridge模式下Docker Daemon啟動時創建docker0網橋,并在網橋使用的網段為容器分配IP。讓我們看看實際的操作。
在剛剛啟動Docker Daemon并且還沒有啟動任何容器時,網絡協議棧的配置情況如下:
?
? ? 可以看到,Docker創建了docker0網橋,并添加了iptables規則。docker0網橋和iptables規則都處于root命名空間中。通過解讀這些規則,我們發現,在還沒有啟動任何容器時,如果啟動了Docker Daemon,那么它已經做好了通信準備。對這些規則的說明如下:
(1)在NAT表中有4條記錄,前兩條匹配生效后,都會繼續執行DOCKER鏈,如果DOCKER鏈為空,前兩條只是做了一個框架,并沒有實際效果(ps:由于不是剛剛安裝的docker)。
(2)NAT表第3條的含義是,若本地發出的數據包不是發往docker0的,即是發往主機之外的設備的,則都需要進行動態地址修改(MASQUERADE),將源地址從容器的地址(172段)修改為宿主機網卡的IP地址,之后就可以發送給外面的網絡了。
(3)在FILTER表中,第3條也是一個框架,因為后繼的DOCKER鏈是空的。
(4)在FILTER表中,第5條是說,docker0發出的包,如果需要Forward到非docker0的本地IP地址的設備,則是允許的。這樣,docker0設備的包就可以根據路由規則中轉到宿主機的網卡設備,從而訪問外面的網絡。
(5)FILTER表中,第6條是說,docker0的包還可以被中轉給docker0本身,即連接在docker0網橋上的不同容器之間的通信也是允許的。
(6)FILTER表中,3條是說,如果接收到的數據包屬于以前已經建立好的連接,那么允許直接通過。這樣接收到的數據包自然又走回docker0,并中轉到相應的容器。
? ?除了這些Netfilter的設置,Linux的ip_forward功能也被Docker Daemon打開了:
?
另外,我們可以看到剛剛啟動Docker后的Route表,和啟動前沒有什么不同:
?
2.查看容器啟動后的情況(容器無端口映射)
? 此處自己操作,自己查看,不做過多解釋。
? ? 剛才查看了Docker服務啟動后的網絡情況。現在啟動一個Registry容器(不使用任何端口鏡像參數),看一下網絡堆棧部分相關的變化:
docker run --name register -d registry
ip addr
iptables-save
ip route
(1)宿主機器上的Netfilter和路由表都沒有變化,說明在不進行端口映射時,Docker的默認網絡是沒有特殊處理的。相關的NAT和FILTER這兩個Netfilter鏈還是空的。
(2)宿主機上的Veth對已經建立,并連接到容器內。
我們再次進入剛剛啟動的容器內,看看網絡棧是什么情況。容器內部的IP地址和路由如下:
docker exec -it <容器id>
進入容器后執行:
ip route
? ? ? 可以看到,默認停止的回環設備lo已經被啟動,外面宿主機連接進來的Veth設備也被命名成了eth0,并且已經配置了地址172.17.0.10。
? ? ? 路由信息表包含一條到docker0的子網路由和一條到docker0的默認路由。
3.查看容器啟動后的情況(容器有端口映射)
下面用帶端口映射的命令啟動registry:
docker run --name register -d -p 1180:5000 registry
在啟動后查看iptables的變化:
iptables-save
? ? ? 從新增的規則可以看出,Docker服務在NAT和FILTER兩個表內添加的兩個DOCKER子鏈都是給端口映射用的。在本例中我們需要把外面宿主機的1180端口映射到容器的5000端口。通過前面的分析我們知道,無論是宿主機接收到的還是宿主機本地協議棧發出的,目標地址是本地IP地址的包都會經過NAT表中的DOCKER子鏈。Docker為每一個端口映射都在這個鏈上增加了到實際容器目標地址和目標端口的轉換。
? ? ?經過這個DNAT的規則修改后的IP包,會重新經過路由模塊的判斷進行轉發。由于目標地址和端口已經是容器的地址和端口,所以數據自然就被轉發到docker0上,從而被轉發到對應的容器內部。
? ? 當然在Forward時,也需要在DOCKER子鏈中添加一條規則,如果目標端口和地址是指定容器的數據,則允許通過。
? ? 在Docker按照端口映射的方式啟動容器時,主要的不同就是上述iptables部分。而容器內部的路由和網絡設備,都和不做端口映射時一樣,沒有任何變化。
Docker的網絡局限
? ? ? ?我們從Docker對Linux網絡協議棧的操作可以看到,Docker一開始沒有考慮到多主機互聯的網絡解決方案。
? ? ? Docker一直以來的理念都是“簡單為美”,幾乎所有嘗試Docker的人都被它“用法簡單,功能強大”的特性所吸引,這也是Docker迅速走紅的一個原因。
? ? ?我們都知道,虛擬化技術中最為復雜的部分就是虛擬化網絡技術,即使是單純的物理網絡部分,也是一個門檻很高的技能領域,通常只被少數網絡工程師所掌握,所以我們可以理解結合了物理網絡的虛擬網絡技術有多難。在Docker之前,所有接觸過OpenStack的人都對其網絡問題諱莫如深,Docker明智地避開這個“雷區”,讓其他專業人員去用現有的虛擬化網絡技術解決 ? ?Docker主機的互聯問題,以免讓用戶覺得Docker太難,從而放棄學習和使用Docker。
? ? ? ?Docker成名以后,重新開始重視網絡解決方案,收購了一家Docker網絡解決方案公司—Socketplane,原因在于這家公司的產品廣受好評,但有趣的是Socketplane的方案就是以Open vSwitch為核心的,其還為Open vSwitch提供了Docker鏡像,以方便部署程序。之后,Docker開啟了一個宏偉的虛擬化網絡解決方案—Libnetwork,概念圖如下:
?
? ? 這個概念圖沒有了IP,也沒有了路由,已經顛覆了我們的網絡常識,對于不怎么懂網絡的大多數人來說,它的確很有誘惑力,未來是否會對虛擬化網絡的模型產生深遠沖擊,我們還不得而知,但它僅僅是Docker官方當前的一次“嘗試”。
? ? 針對目前Docker的網絡實現,Docker使用的Libnetwork組件只是將Docker平臺中的網絡子系統模塊化為一個獨立庫的簡單嘗試,離成熟和完善還有一段距離。
Kubernetes的網絡實現
? ? ?在實際的業務場景中,業務組件之間的關系十分復雜,特別是隨著微服務理念逐步深入人心,應用部署的粒度更加細小和靈活。為了支持業務應用組件的通信,Kubernetes網絡的設計主要致力于解決以下問題。
(1)容器到容器之間的直接通信。
(2)抽象的Pod到Pod之間的通信。
(3)Pod到Service之間的通信。
(4)集群外部與內部組件之間的通信。
? ?其中第3條、第4條在之前的章節里都有所講解,本節對更為基礎的第1條與第2條進行深入分析和講解。
1.容器到容器的通信
? ? ? ?同一個Pod內的容器(Pod內的容器是不會跨宿主機的)共享同一個網絡命名空間,共享同一個Linux協議棧。所以對于網絡的各類操作,就和它們在同一臺機器上一樣,它們甚至可以用localhost地址訪問彼此的端口。
? ? ? ?這么做的結果是簡單、安全和高效,也能減小將已經存在的程序從物理機或者虛擬機移植到容器下運行的難度。其實,在容器技術出來之前,大家早就積累了如何在一臺機器上運行一組應用程序的經驗,例如,如何讓端口不沖突,以及如何讓客戶端發現它們等。
? ? ?我們來看一下Kubernetes是如何利用Docker的網絡模型的。
?
? ? ?如上圖中的陰影部分所示,在Node上運行著一個Pod實例。在我們的例子中,容器就是圖中的容器1和容器2。容器1和容器2共享一個網絡的命名空間,共享一個命名空間的結果就是它們好像在一臺機器上運行,它們打開的端口不會有沖突,可以直接使用Linux的本地IPC進行通信(例如消息隊列或者管道)。其實,這和傳統的一組普通程序運行的環境是完全一樣的,傳統程序不需要針對網絡做特別的修改就可以移植了,它們之間的互相訪問只需要使用localhost就可以。例如,如果容器2運行的是MySQL,那么容器1使用localhost:3306就能直接訪問這個運行在容器2上的MySQL了。
2.Pod之間的通信
? ? ? ? 我們看了同一個Pod內的容器之間的通信情況,再看看Pod之間的通信情況。
? ? ? ? 每一個Pod都有一個真實的全局IP地址,同一個Node內的不同Pod之間可以直接采用對方Pod的IP地址通信,而且不需要采用其他發現機制,例如DNS、Consul或者etcd。
? ? ? ?Pod容器既有可能在同一個Node上運行,也有可能在不同的Node上運行,所以通信也分為兩類:同一個Node內Pod之間的通信和不同Node上的Pod之間的通信。
同一個Node內Pod之間的通信
? 我們看一下同一個Node內兩個Pod之間的關系,如下圖所示:
? ? ? 可以看出,Pod1和Pod2都是通過Veth連接到同一個docker0網橋上的,它們的IP地址IP1、IP2都是從docker0的網段上動態獲取的,它們和網橋本身的IP3是同一個網段的。
? ? ?另外,在Pod1、Pod2的Linux協議棧上,默認路由都是docker0的地址,也就是說所有非本地地址的網絡數據,都會被默認發送到docker0網橋上,由docker0網橋直接中轉。
? ? ?綜上所述,由于它們都關聯在同一個docker0網橋上,地址段相同,所以它們之間是能直接通信的。
不同Node上Pod之間的通信
? ? ?Pod的地址是與docker0在同一個網段的,我們知道docker0網段與宿主機網卡是兩個完全不同的IP網段,并且不同Node之間的通信只能通過宿主機的物理網卡進行,因此要想實現不同Node上Pod容器之間的通信,就必須想辦法通過主機的這個IP地址進行尋址和通信。
? ? ?另一方面,這些動態分配且藏在docker0之后的所謂“私有”IP地址也是可以找到的。Kubernetes會記錄所有正在運行的Pod的IP分配信息,并將這些信息保存在etcd中(作為Service的Endpoint)。這些私有IP信息對于Pod到Pod的通信也是十分重要的,因為我們的網絡模型要求Pod到Pod使用私有IP進行通信。所以首先要知道這些IP是什么。
? ? 之前提到,Kubernetes的網絡對Pod的地址是平面的和直達的,所以這些Pod的IP規劃也很重要,不能有沖突。只要沒有沖突,我們就可以想辦法在整個Kubernetes的集群中找到它。
綜上所述,要想支持不同Node上Pod之間的通信,就要滿足兩個條件:
(1)在整個Kubernetes集群中對Pod的IP分配進行規劃,不能有沖突;
(2)找到一種辦法,將Pod的IP和所在Node的IP關聯起來,通過這個關聯讓Pod可以互相訪問。
? ? ? 根據條件1的要求,我們需要在部署Kubernetes時對docker0的IP地址進行規劃,保證每個Node上的docker0地址都沒有沖突。我們可以在規劃后手工配置到每個Node上,或者做一個分配規則,由安裝的程序自己去分配占用。例如,Kubernetes的網絡增強開源軟件Flannel就能夠管理資源池的分配。
? ? ? 根據條件2的要求,Pod中的數據在發出時,需要有一個機制能夠知道對方Pod的IP地址掛在哪個具體的Node上。也就是說先要找到Node對應宿主機的IP地址,將數據發送到這個宿主機的網卡,然后在宿主機上將相應的數據轉發到具體的docker0上。一旦數據到達宿主機Node,則那個Node內部的docker0便知道如何將數據發送到Pod。如下圖所示:
? ? ?在上圖中:IP1對應的是Pod1,IP2對應的是Pod2。Pod1在訪問Pod2時,首先要將數據從源Node的eth0發送出去,找到并到達Node2的eth0。即先是從IP3到IP4的遞送,之后才是從IP4到IP2的遞送。
? ? ?在谷歌的GCE環境中,Pod的IP管理(類似docker0)、分配及它們之間的路由打通都是由GCE完成的。Kubernetes作為主要在GCE上面運行的框架,它的設計是假設底層已經具備這些條件,所以它分配完地址并將地址記錄下來就完成了它的工作。在實際的GCE環境中,GCE的網絡組件會讀取這些信息,實現具體的網絡打通。
? ?而在實際生產環境中,因為安全、費用、合規等種種原因,Kubernetes的客戶不可能全部使用谷歌的GCE環境,所以在實際的私有云環境中,除了需要部署Kubernetes和Docker,還需要額外的網絡配置,甚至通過一些軟件來實現Kubernetes對網絡的要求。做到這些后,Pod和Pod之間才能無差別地進行透明通信。
總結
以上是生活随笔為你收集整理的云原生系列「三」容器网络的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 云原生系列「0」容器概述
- 下一篇: 云原生系列「四」我为啥不看好Servic