網絡介紹:Kubernetes設計文檔

模型和動機

Kubernetes從Docker默認的網絡模型中獨立出來形成一套自己的網絡模型。該網絡模型的目標是:每一個pod都擁有一個扁平化共享網絡命名空間的IP,通過該IP,pod就能夠跨網絡與其它物理機和容器進行通信。一個pod一個IP模型創建了一個乾淨、反向兼容的模型,在該模型中,從端口分配、網絡、域名解析、服務發現、負載均衡、應用配置和遷移等角度,pod都能夠被看成虛擬機或物理機。

另一方面,動態端口分配需要以下方面的支持:

固定端口(例如:用於外部可訪問服務)和動態分配端口;
分割集中分配和本地獲取的動態端口;  不過,這不但使調度複雜化(因爲端口是一種稀缺資源),而且應用程序的配置也將變得複雜,具體表現爲端口衝突、重用和耗盡;
使用非標準方法進行域名解析(例如:etcd而不是DNS);
對使用標準域名/地址解析機制的程序(例如:web瀏覽器)使用代理和/或重定向;
除了監控和緩存實例的非法地址/端口變化外,還要監控用戶組成員變化以及阻止容器/pod遷移(例如:使用CRIU)。
NAT將地址空間分段的做法引入了額外的複雜性,這將帶來諸如破壞自注冊機制等問題。

在一個pod一個IP模型中,從網絡角度看,在一個pod中的所有用戶容器都像是在同一臺宿主機中那樣。它們能夠在本地訪問其它用戶容器的端口。暴露給主機網卡的端口是通過普通Docker方式實現的。所有pod中的容器能夠通過他們“10”網段(10.x.x.x)的IP地址進行通信。

除了能夠避免上述動態端口分配帶來的問題,該方案還能使應用平滑地從非容器環境(物理機或虛擬機)遷移到同一個pod內的容器環境。在同一臺宿主機上運行應用程序棧這種場景已經找到避免端口衝突的方法(例如:通過配置環境變量)並使客戶端能夠找到這些端口。

該方案確實降低了pod中容器之間的隔離性——儘管端口可能存在衝突而且也不存在pod內跨容器的私有端口,但是對於需要自己的端口範圍的應用程序可以運行在不同的pod中,而對於需要進行私有通信的進程則可以運行在同一個容器內。另外,該方案假定的前提條件是:在同一個pod中的容器共享一些資源(例如:磁盤卷、處理器、內存等),因此損失部分隔離性也在可接受範圍之內。此外,儘管用戶能夠指定不同容器歸屬到同一個pod,但一般情況下不能指定不同pod歸屬於同一臺主機。

當任意一個容器調用SIOCGIFADDR(發起一個獲取網卡IP地址的請求)時,它所獲得的IP和與之通信的容器看到的IP是一樣的——每個pod都有一個能夠被其它pod識別的IP。通過無差別地對待容器和pdo內外部的IP和端口,我們創建了一個非NAT的扁平化地址空間。”ip addr show”能夠像預期那樣正常工作。該方案能夠使所有現有的域名解析/服務發現機制:包括自注冊機制和分配IP地址的應用程序在容器外能夠正常運行(我們應該用etcd、Euraka(用於Acme Air)或Consul等軟件測試該方案)。對pod之間的網絡通信,我們應該持樂觀態度。在同一個pod中的容器之間更傾向於通過內存卷(例如:tmpfs)或IPC(進程間通信)的方式進行通信。

該模型與標準Docker模型不同。在該模型中,每個容器會得到一個“172”網段(172.x.x.x)的IP地址,而且通過SIOCGIFADDR也只能看到一個“172”網段(172.x.x.x)的IP地址。如果這些容器與其它容器連接,對方容器看到的IP地址與該容器自己通過SIOCGIFADDR請求獲取的IP地址不同。簡單地說,你永遠無法在容器中註冊任何東西或服務,因爲一個容器不可能通過其私有IP地址被外界訪問到。

我們想到的一個解決方案是增加額外的地址層:以pod爲中心的一個容器一個IP模式。每個容器只擁有一個本地IP地址,且只在pod內可見。這將使得容器化應用程序能夠更加容易地從物理/虛擬機遷移到pod,但實現起來很複雜(例如:要求爲每個pod創建網橋,水平分割/虛擬私有的 DNS),而且可以預見的是,由於新增了額外的地址轉換層,將破壞現有的自注冊和IP分配機制。

當前實現

Google計算引擎(GCE)集羣配置了高級路由,使得每個虛擬機都有額外的256個可路由IP地址。這些是除了分配給虛擬機的通過NAT用於訪問互聯網的“主”IP之外的IP。該實現在Docker外部創建了一個叫做cbr0的網橋(爲了與docker0網橋區別開),該網橋只負責對從容器內部流向外部的網絡流量進行NAT轉發。

目前,從“主”IP(即互聯網,如果制定了正確的防火牆規則的話)發到映射端口的流量由Docker的用戶模式進行代理轉發。未來,端口轉發應該由Kubelet或Docker通過iptables進行:Issue #15。

啓動Docker時附加參數:DOCKER_OPTS=”–bridge cbr0 –iptables=false”。

並用SaltStack在每個node中創建一個網橋,代碼見container_bridge.py:

cbr0:
container_bridge.ensure:
– cidr: {{ grains[‘cbr-cidr’] }}
…
grains:
roles:
– kubernetes-pool
cbr-cidr: $MINION_IP_RANGE

在GCE中,我們讓以下IP地址可路由:

gcloud compute routes add “${MINION_NAMES[$i]}” \
–project “${PROJECT}” \
–destination-range “${MINION_IP_RANGES[$i]}” \
–network “${NETWORK}” \
–next-hop-instance “${MINION_NAMES[$i]}” \
–next-hop-instance-zone “${ZONE}” &

以上代碼中的MINION_IP_RANGES是以10.開頭的24位網絡號IP地址空間(10.x.x.x/24)。

儘管如此,GCE本身並不知道這些IP地址的任何信息。

這些IP地址不是外部可路由的,因此,那些有與外界通信需求的容器需要使用宿主機的網絡。如果外部流量要轉發給虛擬機,它只會被轉發給虛擬機的主IP(該IP不會被分配給任何一個pod),因此我們使用docker的-p標記把暴露的端口映射到主網卡上。該方案的帶來的副作用是不允許兩個不同的pod暴露同一個端口(更多相關討論見Issue #390)。

我們創建了一個容器用於管理pod的網絡命名空間,該網絡容器內有一個本地迴環設備和一塊虛擬以太網卡。所有的用戶容器從該pod的網絡容器那裏獲取他們的網絡命名空間。

Docker在它的“container”網絡模式下從網橋(我們爲每個節點上創建了一個網橋)那裏分配IP地址,具體步驟如下:

 使用最小鏡像創建一個普通容器(從網絡角度)並運行一條永遠阻塞的命令。這不是一個用戶定義的容器,給它一個特別的衆所周知的名字;
 創建一個新的網絡命名空間(netns)和本地迴路設備;
 創建一對新的虛擬以太網設備並將它綁定到剛剛創建的網絡命名空間;
 從docker的IP地址空間中自動分配一個IP;

在創建用戶容器時指定網絡容器的名字作爲它們的“net”參數。Docker會找到在網絡容器中運行的命令的PID並將它綁定到該PID的網絡命名空間去。

其他網絡實現例子

其他用於在GCE外部實現一個pod一個IP模型的方案:

OpenVSwitch with GRE/VxLAN
Flannel

挑戰和未來的工作

Docker API

目前,docker inspect並不展示容器的網絡配置信息,因爲它們從另一個容器提取該信息。該信息應該以某種方式暴露出來。

外部IP分配

我們希望能夠從Docker外部分配IP地址(Docker issue #6743),這樣我們就不必靜態地分配固定大小的IP範圍給每個節點,而且即使網絡容器重啓也能保持容器IP地址固定不變(Docker issue #2801),這樣就使得pod易於遷移。目前,如果網絡容器掛掉,所有的用戶容器必須關閉再重啓,因爲網絡容器重啓後其網絡命名空間會發生變化而且任何一個隨後重啓的用戶容器會加入新的網絡命名空間進而導致無法與對方容器通信。另外,IP地址的改變將會引發DNS緩存/生存時間問題。外部IP分配方案會簡化DNS支持(見下文)。

域名解析,服務發現和負載均衡

除了利用第三方的發現機制來啓用自注冊外,我們還希望自動建立DDNS(Issue #146)。hostname方法,$HOSTNAME等應該返回一個pod的名字(Issue #298),gethostbyname方法應該要能夠解析其他pod的名字。我們可能會建立一個DNS解析服務器來做這些事情(Docker issue #2267),所以我們不必動態更新/etc/hosts文件。

服務端點目前是通過環境變量獲取的。Docker鏈接兼容變量和kubernetes指定變量({NAME}_SERVICE_HOST和{NAME}_SERVICE_BAR)都是支持的,並會被解析成由服務代理打開的端口。事實上,我們並不使用docker特使模式來鏈接容器,因爲我們不需要應用程序在配置階段識別所有客戶端。雖然如今的服務都是由服務代理管理的,但是這是一個應用程序不應該依賴的實現細節,客戶端應該使用服務入口IP(以上環境變量會被解析成該IP)。然而,一個扁平化的服務命名空間無法伸縮,而且環境變量不允許動態更新,這使得通過應用隱性的順序限制進行服務部署變得複雜。我們打算在DNS中爲每個服務註冊入口IP,並且希望這種方式能夠成爲首選解決方案。

我們也希望適應其它的負載均衡方案(例如:HAProxy),非負載均衡服務(Issue #260)和其它類型的分組(例如:線程池等)。提供監測應用到pod地址的標記選擇器的能力使有效地監控用戶組成員狀態變得可能,這些監控數據將直接被服務發現機制消費或同步。用於監聽/取消監聽事件的事件鉤(Issue #140)將使上述目標更容易實現。

外部可路由性

我們希望跨節點的容器間網絡通信使用pod IP地址。假設節點A的容器IP地址空間是10.244.1.0/24,節點B的容器的IP地址空間是10.244.2.0/24,且容器A1的的IP地址是10.244.1.1,容器B1的IP地址是10.244.2.1。現在,我們希望容器A1能夠直接與容器B1通信而不通過NAT,那麼容器B1看到IP包的源IP應該是10.244.1.1而不是節點A的主宿主機IP。這意味着我們希望關閉用於容器之間(以及容器和虛擬機之間)通信的NAT。

我們也希望從外部互聯網能夠直接路由到pod。然而,我們還無法提供額外的用於直接訪問互聯網的容器IP。因此,我們不將外部IP映射到容器IP。我們通過讓終點不是內部網絡(!10.0.0.0/8)的流量經過主宿主機IP走NAT來解決這個問題,這樣與因特網通信時就能通過GCE網絡實現1:1NAT轉換。類似地,從因特網流向內部網絡的流量也能夠經宿主機IP實現NAT轉換/代理。Linux就該這麼學

因此,讓我們用三個用例場景結束上面的討論:

容器->容器或容器< ->虛擬機。該場景需要直接使用以10.開頭的地址(10.x.x.x),並且不需要用到NAT;

容器->因特網。容器IP需要映射到主宿主機IP好讓GCE知道如何向外發送這些流量。這裏就有兩層NAT:容器IP->內部宿主機IP->外部主機IP。第一層結合IP表發生在客戶容器上,第二層則是GCE網絡的一部分。第一層(容器IP->內部宿主機IP)進行了動態端口分配,而第二層的端口映射是1:1的;

因特網->容器。該場景的流量必須通過主宿主機IP而且理想狀態下也擁有兩層NAT。但是,目前的流量路徑是:外部主機IP -> 內部主機IP -> Docker) -> (Docker -> 容器IP),即Docker是中間代理。一旦(issue #15)被解決,那麼就應該是:外部主機IP -> 內部主機IP -> 容器IP。但是,爲了實現後者,就必須爲每個管理的端口建立端口轉發的防火牆(iptables)。

如果我們有辦法讓外部IP路由到集羣中的pod,我們就可以採用爲每個pod創建一個新的宿主機網卡別名的方案。該方案能夠消除使用宿主機IP地址帶來的調度限制。

IPv6

IPv6將會是一個有意思的選項,但我們還沒法使用它。支持IPv6已經被提到Docker的日程上了:Docker issue #2974,Docker issue #6923,Docker issue #6975。另外,主流雲服務提供商(例如:AWS EC2,GCE)目前也不支持直接給虛擬機綁定IPv6地址。儘管如此,我們將很高興接受在物理機上運行Kubernetes的用戶提交的代碼。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章