Kubernetes入門(三) - 網絡
這是我在國外的博客上看到的一系列關于Docker和Kubernetes網絡分析的文章,感覺描述得比較清晰,便于剛接觸Docker和 Kubernetes的朋友了解相關的知識。在看的同時順便就翻譯了,方便和網友一起交流探討。但我不僅僅是翻譯,還按照文章中描述的進行了實際測試,對 于測試中出現的問題和由于環境不同或者軟件版本更新帶來的變更也在譯者注中給出說明。
我并沒有按原文一字一句的翻譯,對于理解這些知識點沒有幫助的語句我一般都忽略了。
我的測試環境和筆者不同,我使用的是單臺物理機,用Vagrant模擬多臺VM Host,VM Host使用和物理機相同網段的public網絡,操作系統是CentOS 7.0,Docker版本1.6.2。Kubernetes網絡的要求是不同主機上的容器間要能夠互通。
本文使用了MLS來打通不同機器Docker私網的通訊。我嘗試用家用路由器的靜態路由完成,但是因為Vagrant網絡設置原因,一直沒有成 功。我還在分析,叢當前情況看是Vagrant每個Vagrant虛擬機有兩個網絡接口,其中一個管理接口每臺機器的IP是一樣的,我估計這個產生了干 擾,因為我抓的不同機器的IP包都轉換成為了物理機器的IP地址而不是各自虛擬機的IP地址。所以在我的實驗中,我使用了Flannel來打通不同主機間 容器網絡的連接問題。這個對很多場景是更好的選擇,很多情況下你很難在為新增一個Kubernetes的主機就在交換機上去增加一條路由。而且牽涉到虛擬 機和虛擬網絡時,難定位的問題會更多。
我對Docker和其相關技術如此感興趣的一個原因是因為隨之而來的新的網絡模式。Kubernetes有一個獨特的(并且相當棒)方式來處理這 些網絡挑戰,但是他第一眼看上去有些難以理解。我這個帖子目的是帶你通過部署幾個Kubernetes的construct來分析Kubernetes在 網絡層做了什么來達成這點。不過讓我們從部署一個Pod的基礎開始。我們將使用第一個帖子(Kubernetes入門(一) - 構建)構建的實驗環境和在第二個帖子(Kubernetes入門(二) - Constructs)創建的一些配置文件例子。
注意:我必須在這里指出這個實驗環境是基于裸機硬件的。在這個類型的實驗環境的網絡模型和你見到的云提供商提供的相比有些許不同。盡管如此,Kubernetes在網絡層面所做的事的背后的機制是一樣的。
我們的實驗環境是這樣的:
之前我們提到了Pod IP地址的話題,讓我們提供一些背景信息,對情況有一些基本的了解。Kubernetes的網絡模型決定了Kubernetes的每個節點可以被路由。回 憶一下docker網絡模型,他提供了一個IP地址段為172.17.0.0/16的docker0網橋。每個容器可以從這個子網獲得IP地址并用 docker0網橋的IP(172.17.42.1)做為其默認網關。值得注意的是網絡不需要知道172.17.0.0/16如何到達,因為docker 主機為從容器發出的流量在真實的NIC的IP地址背后做了一個IP masquerade(或者hide NAT)。這就是說網絡將任何從容器來的流量看做是從主機物理IP地址發出的。
注意:我們在這個帖子里面使用網絡總是指連接主機的物理網絡。
Docker的網絡模型對使用方便是有意義的,但并不理想。這個模型需要對各種端口進行映射并且一般會限制Docker主機的能力。在 Kubernetes模型中,每個Docker主機的docker0網橋都是可以路由的。 那就是說,當一個Pod部署后,集群其他主機能夠不在物理主機上做端口映射就可以直接訪問Pod。有了這種說法,從網絡觀點來看,你可以將 Kubernetes節點當做路由器。我們將我們的實驗環境圖變為一個網絡圖,他看起來就像這樣。
多層交換機(MLS)有兩個3層分段。一個支持10.20.30.0/24網絡,另一個支持192.168.10.0/24網絡。此外上面還有路 由告訴如何到達每個掛在路由器(Kubernetes節點)上的子網。這意味著任何節點上產生的容器會使用節點(docker0網橋IP)做為他們的默認 網關,接著節點使用MLS做為他的默認網關。我有點和這個概念糾纏,不過他非常重要。網絡管理員喜歡邊緣三層網絡。
現在讓我們轉到一些例子來看在不同情況下Kubernetes在網絡側是如何做的...
部署一個Pod
id: "webpod" kind: "Pod" apiVersion: "v1beta1" desiredState: manifest: version: "v1beta1" id: "webpod" containers: - name: "webpod80" image: "jonlangemak/docker:web_container_80" cpu: 100 ports: - containerPort: 80 hostPort: 80 - name: "webpod8080" image: "jonlangemak/docker:web_container_8080" cpu: 100 ports: - containerPort: 8080 hostPort: 8080 labels: name: "web"
譯者注: 帖子是筆者2015年初寫的,所以這個construct的語法已經過時了,在我測試使用的比較新的v0.19.0版本上已經不能運行,我重新用最新的construct語法重寫了一下:
kind: "Pod" apiVersion: "v1" metadata: labels: name: "web" name: webpod spec: containers: - name: "webpod80" image: "jonlangemak/docker:web_container_80" resources: limits: cpu: "0.5" ports: - containerPort: 80 hostPort: 80 - name: "webpod8080" image: "jonlangemak/docker:web_container_8080" resources: limites: cpu: "0.5" cpu: 100 ports: - containerPort: 8080 hostPort: 8080
讓我們假設我們在一個空白的master上進行工作。我清理了上個帖子用到的所有Replication controller、Pods和Service...
讓我們找一個節點檢查一下這個點的運行情況,現在讓我們選擇kubminion1。
現在沒有任何容器在運行,我只想指出網絡配置符合預期。我們有一個docker0網橋接口和minions本地IP接口。讓我們回到master,部署我們上面配置的pod,看看會發生什么...
一些有趣的事情發生了,Kubernetes指派10.20.30.62(kubminion1)這臺主機運行Pod。注意Pod也分配了一個IP地址,這個地址正位于kubmini1的docker0網橋。讓我們拎出kubminion1來看看發生了什么。
kuminion1現在運行了3個容器。我們的Pod規格里只定義了2個,那么第三個從哪里來的?第3個運行的容器鏡像 叫"kubernetes/pause:go"。注意到這個容器有所有端口映射。為什么會這樣?我們深入觀察一下容器看看為什么會這樣。我們使用 Docker的"inspect"命令看看每個容器的一些信息。也就是說看每個部署的容器的網絡模式。
譯者注:現在這個鏡像默認是放在GCE的鏡像倉庫里了,gcr.io/google_containers/puase:0.8.0, 不幸的是Google的服務器都被墻了,要么KX上網,要么將這個鏡像重新指向到Docker公共倉庫。這個鏡像在公共倉庫里就是 kubernetes/puase:0.8.0。在kubelet的啟動參數里面指定:
有趣,如果我們檢查每個容器的網絡模式,我們可以看到一個非常有趣的配置。第一個運行"kubernetes/pause:go"的容器使用的是 默認的網絡模式。我們觀察的第二和第三個容器是在我們pod中定義的運行"web_container_80" 和"web_container_8080"鏡像的容器。注意每個pod容器都有一個非默認網絡配置。特別是每個pod容器使用mapped container mode并指定運行"Kubernetes/pause:go"鏡像的容器為目標容器。
譯者注:mapped container mode參見Docker網絡入門(三) - 鏡像容器
讓我們想一想,為什么他們這樣做?首先,所有在一個pod內的容器需要共享相同的IP地址。這使得鏡像容器模式必不可少。既然如此,為什么他們不 啟動第一個pod容器,并將第二個pod容器連接到第一個?我想這個問題的答案有兩個方面。第一,連接一個有兩個以上容器的pod很痛苦(譯者注:這句話 有些不太清楚,我覺得作者可能的意思是一個pod有多個容器,你不知道哪個是第一個容器,有一個pause容器就確定了)。其次,你對被連接的第一個容器 產生了依賴。如果第二個容器連接到第一個容器并且第一個容器掛掉,第二個容器的網絡棧也掛了。運行一個基礎容器,將其他的pod容器連接到這個容器更加容 易(也更加聰明)。這也簡化了端口映射,我們只需要將端口映射規則應用到pause容器。
于是我們的pod網絡圖看上去就像這樣...
所以真實網絡對于pod IP流量的目的地就是這個pause容器。上面這個圖有一些欺騙性因為他將pause容器顯示為將80和8080端口轉發到相關容器。pause容器并沒 有真的做這個轉發,這是邏輯上像這樣做了因為兩個容器直接監聽了兩個端口并與pause容器共享了相同的網絡棧。這就是為什么所以對真實容器的端口映射都 顯示為在pause容器上的映射。我們通過docker port命令檢查。
所以pause容器真的只是持有pod的網絡端點(Endpoint) ,沒有做任何其他事情。那么主機做了什么?他做了什么事情將流量導入到pause容器嗎?讓我們看一下iptables規則:
這里有一些規則,但是沒有任何規則是作用于我們剛才定義的pod的。就像我在上一篇帖子提到的,在每一個Kubernetes節點上 Kubernetes提供了一些默認服務。那就是我們在上面輸出中看到的。關鍵點是我們沒有看見任何masquerade規則或者任何為 pod10.10.10.2做的入站端口映射。
部署一個服務
我們已經看到Kubernetes如何處理連接到他的最基本的構件,讓我討論一下他如何處理服務。就像我們在上個 帖子討論的那樣,服務允許你抽象在pod里托管的服務。此外服務允許你通過提供跨托管服務的多個Pod的負載均衡機制來提供服務的水平擴展能力。讓我再次 將我們剛才創建的pod刪除重置實驗環境以確保從空白開始現在讓我們嘗試我們在上個帖子中定義的服務并再次對其審視。 這里是我們稱為"myfirstservice“的服務配置文件
id: "webfrontend" kind: "Service" apiVersion: "v1beta1" port: 80 containerPort: 80 selector: name: "web" labels: name: "webservice"
為了讓事情解釋得更清晰,我們將服務做一些輕微的改變。
id: "webfrontend" kind: "Service" apiVersion: "v1beta1" port: 80 containerPort: 8080 selector: name: "web8080" labels: name: "webservice"
相同的配置,我們只是將容器端口變為了8080。讓我們在Kubernetes集群定義這個服務。
譯者注: 帖子是筆者2015年初寫的,所以這個construct的語法已經過時了,在我測試使用的比較新的v0.19.0版本上已經不能運行,我重新用最新的construct語法重寫了一下:
apiVersion: v1 kind: Service spec: ports: - port: 80 containerPort: 8080 selector: name: web8080 metadata: labels: name: webservice name: webfrontend
注意:我不記得我以前是否提到過,但是服務必需在符合服務的selector的pod之前構建。這個能保證服務相關的變量能存在于容器中。
譯者注:Kubernetes容器訪問服務有兩種方式:
1.一種是通過環境變量訪問,服務的IP和端口在容器啟動時做為環境變量傳遞進入容器,容器的應用可以根據這些環境變量訪問服務,例如訪問redis或者 mysql。這樣就要求服務先啟動,否則服務的相關信息就沒法傳入Pod(因為此時服務還沒有創建,這些信息都還沒有)。這是一種比較糟糕的方法,對順序 有依賴且要求應用要適配環境變量。其中環境變量的規則是"<服務名稱(大寫)>PORT<服務端口>TCP_ADDR"這個環境 變量的值是服務的IP,"<服務名稱(大寫)>_PORT<服務端口>_TCP_PORT"這個環境變量是服務端口。例如訪問上 面的服務就用WEBFRONTEND_PORT_8080_TCP_ADDR和WEBFRONTEND_PORT_8080_TCP_ADDR兩個環境變 量
2.另一種是通過dns訪問,這顯然是比較好的辦法,在后面的帖子中會提到如何在Kubernetes集群中部署用于服務訪問的本地DNS。這樣服務直接 就可以通過服務名訪問,由DNS將服務名轉變為服務的IP,不再需要依賴環境變量,只要依賴服務名稱即可。如: curl -L http://webfrontend
服務的創建符合預期。我們檢查可用的服務時會發現集群給服務指定了一個IP地址。這個IP地址被分配到了Kubernetes所說 的"Portal Netwokr”范圍內。如果你還記得,當我們在kubmasta上構建API服務時,我們定義的一個標識就是"PortalNet"
[Unit] Description=Kubernetes API Server After=etcd.service Wants=etcd.service [Service] ExecStart=/opt/kubernetes/kube-apiserver \ --address=0.0.0.0 \ --port=8080 \ --etcd_servers=http://127.0.0.1:4001 \ --portal_net=10.100.0.0/16 \ --logtostderr=true Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target
他可以是任意子網,只要不和物理主機或者docker0的子網重合即可。他可以是任意子網的原因是他永遠不會路由到網絡上。Portal net只對本地節點有意義,他只是一個將流量從容器流出并導向默認網關(docker0網橋)的方法。在下一步動作之前,讓我們檢查一下 kubminion1看看我們定義了服務定義后發生了什么改變。我們從檢查netfilter規則開始:
這些規則做了什么?第一行告訴主機匹配10.100.87.105:80的TCP流。如果他看到一個流匹配到這個規范,他會將流量重定向到本地的 39770端口。第二個行告訴節點以不同的方式做同樣的事情,因為覆蓋的是從主機而不是容器產生的流量。這兩條規則不相同的原因是REDIRECT只對通 過主機的流量生效(產生于容器,通過主機)。從主機產生的流量需要用DNAT規則進行處理。長話短說,他們用不同的方式完成同樣的事情,于是所有流出節點 指向10.100.87.105:80的流量會被重定向到主機的39770端口
于是我們知道所有去向服務IP和端口的流量會重定向到本機的39770端口。但那又什么效果呢?這是kubernetes-proxy服務開始起 作用的地方。proxy服務為每個新創建的服務隨機分配了一個端口并創建了一個服務內創建一個負載均衡對象并監聽指定的端口。在這個場景里,端口敲好是 39770。如果當我們創建服務時我們查看了kubminion1上kubernetes-service的日志(譯者注:感覺是kubernetes- proxy service),我們會看到像這樣的條目:
現在指向服務的流量被重定向到了proxy。為了完成負載均衡,我們啟動我們上個帖子的一個replication controller以便于我們查看其運行情況。我將為我的replication controller用這個配置:
id: web-controller-2 apiVersion: v1beta1 kind: ReplicationController desiredState: replicas: 4 replicaSelector: name: web8080 podTemplate: desiredState: manifest: version: v1beta1 id: webpod containers: - name: webpod image: jonlangemak/docker:web_container_8080 ports: - containerPort: 8080 labels: name: web8080
譯者注
新版的construct應該這樣寫:
apiVersion: v1 kind: ReplicationController metadata: labels: name: web8080 name: web8080 spec: replicas: 4 selector: name: web8080 template: metadata: labels: name: web8080 spec: containers: - name: web8080 image: jonlangemak/docker:web_container_8080 ports: - containerPort: 8080 name: web8080
讓我們將其加載到集群并讓所有pod啟動 </blockquote>
看上去不錯。現在我們讓所有的pod運行起來了,服務會選擇匹配標簽"web8080"的pod進行負載均衡。因為replication controller selector通過標簽"web8080"匹配所有的pod,我們將有4個pod用于負載均衡。在這一點上,我認為我們的實驗環境看起來像這樣:
Kubernetes proxy被描述為一種墊片,他只是運行在節點上的另外一個服務。我們上面看到的規則讓Kubernetes proxy成為了流量指向服務IP地址的墊片。
看看他的運行情況,我們使用tcpdump進行一系列包捕捉。 為了做這件事,我們需要在kubminion1上安裝tcpdump,我們使用這個命令進行安裝:
yum -y install tcpdump
完成安裝后,我們打開三個kubminion1的SSH會話。第一個窗口我們運行如下tcpdump指令
tcpdump -nn -q -i ens18 port 8080
注意:在這個例子里我們會捕捉物理接口的包。在我的例子里,他叫"ens18" (譯者注:譯者使用的環境是通過flannel來打通跨主機容器網絡的連接,所以是對flannel0網橋進行抓包)
在第二個窗口我們運行另外一個tcpdump,但是我們需要首先獲得一些信息。換句話說我們需要獲得容器連接到docker0網橋的虛擬接口的名稱。假設你在主機上運行了一個webpod容器,你可以完成簡單的"ifconfig",你將只有一個"Veth"接口。
拷貝接口的名字,在你的第二個窗口將其拷貝到tcpdump命令。
tcpdump -nn -q -i veth12370a6 host 10.100.87.105
同時運行兩個命令,將窗口疊在一起,這樣你可以同時查看。
你同時運行了兩個抓包后,讓我們將注意力轉移到第三個窗口。讓我們使用"docker exec"命令附著到“web_container_8080”容器(首先使用“docker ps”命令獲取容器的名稱)
docker exec -it e130a52dfae6 /bin/bash
在進入運行容器后我們嘗試通過curl命令訪問服務
curl 10.100.87.105
當我們第一次訪問服務,我們在窗口里會看到:
這說明了什么?讓我們在我們的網絡圖上畫出來,上面窗口的抓包(通過物理NIC的流量)用紅色顯示,下面窗口抓包(通過docker0網橋)用藍色顯示:
注意:我在畫kubminio3上的線的時候繞過了Kubernetes proxy。 我這樣做是因為在kubminion3上的Kubernetes proxy并沒有對這個流起作用。換句話說,proxy service攔截了服務請求直接發送到他負載均衡的pod。
讓我們先看下方的窗口,我們看到的是從容器角度出發的流量。容器企圖建立一個10.100.87.105:80的TCP連接。我們看到叢服務的 IP地址10.100.87.105返回的流量。叢容器的角度來看,整個通訊是與服務進行的。如果我們看第二個窗口(上面那個)我們能看到實際發生了什 么。我們看到叢節點的物理地址(10.20.30.62)發出的到托管在kubminion3(10.10.30.4)上的pod的TCP會話。總結一 下,Kubernetes proxy服務做為一個全代理維護了兩個不同的連接。第一個是叢container到proxy的,第二個是叢proxy到負載均衡目的地的。
如果我們清理我們的抓包信息并再次運行curl,我們會看到流量負載均衡到另一個節點。
在這個例子里,Kubernetes proxy決定將流量流到運行在kubminion2的pod。我們的網絡流圖看起來像這樣。
我想你不需要我再展示其他兩種負載均衡我們測試服務的可能的結果了。關于理解服務很重要的一點是服務能讓我們方便的擴展服務的pod。 我們可以看到結合使用replication controller一起,這會是一個強大的特性。
但是服務處理了Kubernetes集群一個重要方面的同時,他們卻只對pod訪問服務有意義。回憶一下,portal IP的空間不能叢外部網絡訪問,他只對于本地主機有意義。那么集群之外如何訪問部署在集群中的應用呢?我們在下一個帖子中說明。
來自:http://dockone.io/article/520