1. 程式人生 > >Kubernetes設計文件 網路介紹_Kubernetes中文社群

Kubernetes設計文件 網路介紹_Kubernetes中文社群

模型和動機

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

另一方面,動態埠分配需要以下方面的支援:

  1. 固定埠(例如:用於外部可訪問服務)和動態分配埠;
  2. 分割集中分配和本地獲取的動態埠;  不過,這不但使排程複雜化(因為埠是一種稀缺資源),而且應用程式的配置也將變得複雜,具體表現為埠衝突、重用和耗盡;
  3. 使用非標準方法進行域名解析(例如:etcd而不是DNS);
  4. 對使用標準域名/地址解析機制的程式(例如:web瀏覽器)使用代理和/或重定向;
  5. 除了監控和快取例項的非法地址/埠變化外,還要監控使用者組成員變化以及阻止容器/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模型的方案:

挑戰和未來的工作

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轉換/代理。

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

  1. 容器->容器或容器<->虛擬機器。該場景需要直接使用以10.開頭的地址(10.x.x.x),並且不需要用到NAT;
  2. 容器->因特網。容器IP需要對映到主宿主機IP好讓GCE知道如何向外傳送這些流量。這裡就有兩層NAT:容器IP->內部宿主機IP->外部主機IP。第一層結合IP表發生在客戶容器上,第二層則是GCE網路的一部分。第一層(容器IP->內部宿主機IP)進行了動態埠分配,而第二層的埠對映是1:1的;
  3. 因特網->容器。該場景的流量必須通過主宿主機IP而且理想狀態下也擁有兩層NAT。但是,目前的流量路徑是:外部主機IP -> 內部主機IP -> Docker) -> (Docker -> 容器IP),即Docker是中間代理。一旦(issue #15)被解決,那麼就應該是:外部主機IP -> 內部主機IP -> 容器IP。但是,為了實現後者,就必須為每個管理的埠建立埠轉發的防火牆(iptables)。

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

IPv6

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