Kubernetes — 爲何咱們須要Pod?

 

不過,我相信你在學習和使用 Kubernetes 項目的過程當中,已經不止一次地想要問這樣一個問題:爲何咱們會須要 Pod?html

是啊,咱們在前面已經花了不少精力去解讀 Linux 容器的原理、分析了 Docker 容器的本質,終於,「Namespace 作隔離,Cgroups 作限制,rootfs 作文件系統」這樣的「三句箴言」能夠朗朗上口了,爲何 Kubernetes 項目又忽然搞出一個 Pod 來呢?java

要回答這個問題,咱們仍是要一塊兒回憶一下我曾經反覆強調的一個問題:容器的本質究竟是什麼?node

你如今應該能夠不假思索地回答出來:容器的本質是進程。nginx

沒錯。容器,就是將來雲計算系統中的進程;容器鏡像就是這個系統裏的「.exe」安裝包。那麼 Kubernetes 呢? 你應該也能馬上回答上來:Kubernetes 就是操做系統! 很是正確。 如今,就讓咱們登陸到一臺 Linux 機器裏,執行一條以下所示的命令:web

pstree -g

  

這條命令的做用,是展現當前系統中正在運行的進程的樹狀結構。它的返回結果以下所示:docker

systemd(1)-+-accounts-daemon(1984)-+-{gdbus}(1984)
           | `-{gmain}(1984)
           |-acpid(2044)
          ...      
           |-lxcfs(1936)-+-{lxcfs}(1936)
           | `-{lxcfs}(1936)
           |-mdadm(2135)
           |-ntpd(2358)
           |-polkitd(2128)-+-{gdbus}(2128)
           | `-{gmain}(2128)
           |-rsyslogd(1632)-+-{in:imklog}(1632)
           |  |-{in:imuxsock) S 1(1632)
           | `-{rs:main Q:Reg}(1632)
           |-snapd(1942)-+-{snapd}(1942)
           |  |-{snapd}(1942)
           |  |-{snapd}(1942)
           |  |-{snapd}(1942)
           |  |-{snapd}(1942)

  

不難發現,在一個真正的操做系統裏,進程並非「孤苦伶仃」地獨自運行的,而是以進程組的方式,「有原則地」組織在一塊兒。好比,這裏有一個叫做 rsyslogd 的程序,它負責的是 Linux 操做系統裏的日誌處理。能夠看到,rsyslogd 的主程序 main,和它要用到的內核日誌模塊 imklog 等,同屬於 1632 進程組。這些進程相互協做,共同完成 rsyslogd 程序的職責。apache

注意:我在本篇中提到的「進程」,好比,rsyslogd 對應的 imklog,imuxsock 和 main,嚴格意義上來講,實際上是 Linux 操做系統語境下的「線程」。這些線程,或者說,輕量級進程之間,能夠共享文件、信號、數據內存、甚至部分代碼,從而緊密協做共同完成一個程序的職責。因此同理,我提到的「進程組」,對應的也是 Linux 操做系統語境下的「線程組」。這種命名關係與實際狀況的不一致,是 Linux 發展歷史中的一個遺留問題。設計模式

項目所作的,其實就是將「進程組」的概念映射到了容器技術中,並使其成爲了這個雲計算「操做系統」裏的「一等公民」。api

Kubernetes 項目之因此要這麼作的緣由,我在前面介紹 Kubernetes 和 Borg 的關係時曾經提到過:在 Borg 項目的開發和實踐過程當中,Google 公司的工程師們發現,他們部署的應用,每每都存在着相似於「進程和進程組」的關係。更具體地說,就是這些應用之間有着密切的協做關係,使得它們必須部署在同一臺機器上。tomcat

而若是事先沒有「組」的概念,像這樣的運維關係就會很是難以處理。

我仍是之前面的 rsyslogd 爲例子。已知 rsyslogd 由三個進程組成:一個 imklog 模塊,一個 imuxsock 模塊,一個 rsyslogd 本身的 main 函數主進程。這三個進程必定要運行在同一臺機器上,不然,它們之間基於 Socket 的通訊和文件交換,都會出現問題。

如今,我要把 rsyslogd 這個應用給容器化,因爲受限於容器的「單進程模型」,這三個模塊必須被分別製做成三個不一樣的容器。而在這三個容器運行的時候,它們設置的內存配額都是 1 GB。

 

再次強調一下:容器的「單進程模型」,並非指容器裏只能運行「一個」進程,而是指容器沒有管理多個進程的能力。這是由於容器裏 PID=1 的進程就是應用自己,其餘的進程都是這個 PID=1 進程的子進程。但是,用戶編寫的應用,並不可以像正常操做系統裏的 init 進程或者 systemd 那樣擁有進程管理的功能。好比,你的應用是一個 Java Web 程序(PID=1),而後你執行 docker exec 在後臺啓動了一個 Nginx 進程(PID=3)。但是,當這個 Nginx 進程異常退出的時候,你該怎麼知道呢?這個進程退出後的垃圾收集工做,又應該由誰去作呢?

 

假設咱們的 Kubernetes 集羣上有兩個節點:node-1 上有 3 GB 可用內存,node-2 有 2.5 GB 可用內存。

這時,假設我要用 Docker Swarm 來運行這個 rsyslogd 程序。爲了可以讓這三個容器都運行在同一臺機器上,我就必須在另外兩個容器上設置一個 affinity=main(與 main 容器有親密性)的約束,即:它們倆必須和 main 容器運行在同一臺機器上。

而後,我順序執行:「docker run main」「docker run imklog」和「docker run imuxsock」,建立這三個容器。

這樣,這三個容器都會進入 Swarm 的待調度隊列。而後,main 容器和 imklog 容器都前後出隊並被調度到了 node-2 上(這個狀況是徹底有可能的)。

 

但是,當 imuxsock 容器出隊開始被調度時,Swarm 就有點懵了:node-2 上的可用資源只有 0.5 GB 了,並不足以運行 imuxsock 容器;但是,根據 affinity=main 的約束,imuxsock 容器又只能運行在 node-2 上。

這就是一個典型的成組調度(gang scheduling)沒有被妥善處理的例子。

在工業界和學術界,關於這個問題的討論可謂曠日持久,也產生了不少可供選擇的解決方案。 好比,Mesos 中就有一個資源囤積(resource hoarding)的機制,會在全部設置了 Affinity 約束的任務都達到時,纔開始對它們統一進行調度。而在 Google Omega 論文中,則提出了使用樂觀調度處理衝突的方法,即:先無論這些衝突,而是經過精心設計的回滾機制在出現了衝突以後解決問題。

但是這些方法都談不上完美。資源囤積帶來了不可避免的調度效率損失和死鎖的可能性;而樂觀調度的複雜程度,則不是常規技術團隊所能駕馭的。

可是,到了 Kubernetes 項目裏,這樣的問題就迎刃而解了:Pod 是 Kubernetes 裏的原子調度單位。這就意味着,Kubernetes 項目的調度器,是統一按照 Pod 而非容器的資源需求進行計算的。

 

因此,像 imklog、imuxsock 和 main 函數主進程這樣的三個容器,正是一個典型的由三個容器組成的 Pod。Kubernetes 項目在調度時,天然就會去選擇可用內存等於 3 GB 的 node-1 節點進行綁定,而根本不會考慮 node-2。

像這樣容器間的緊密協做,咱們能夠稱爲「超親密關係」。這些具備「超親密關係」容器的典型特徵包括但不限於:互相之間會發生直接的文件交換、使用 localhost 或者 Socket 文件進行本地通訊、會發生很是頻繁的遠程調用、須要共享某些 Linux Namespace(好比,一個容器要加入另外一個容器的 Network Namespace)等等。

這也就意味着,並非全部有「關係」的容器都屬於同一個 Pod。好比,PHP 應用容器和 MySQL 雖然會發生訪問關係,但並無必要、也不該該部署在同一臺機器上,它們更適合作成兩個 Pod。

不過,相信此時你可能會有第二個疑問: 對於初學者來講,通常都是先學會了用 Docker 這種單容器的工具,纔會開始接觸 Pod。

而若是 Pod 的設計只是出於調度上的考慮,那麼 Kubernetes 項目彷佛徹底沒有必要非得把 Pod 做爲「一等公民」吧?這不是故意增長用戶的學習門檻嗎?

 

沒錯,若是隻是處理「超親密關係」這樣的調度問題,有 Borg 和 Omega 論文珠玉在前,Kubernetes 項目確定能夠在調度器層面給它解決掉。

不過,Pod 在 Kubernetes 項目裏還有更重要的意義,那就是:容器設計模式。

爲了理解這一層含義,我就必須先給你介紹一下Pod+的實現原理。

首先,關於 Pod 最重要的一個事實是:它只是一個邏輯概念。 也就是說,Kubernetes 真正處理的,仍是宿主機操做系統上 Linux 容器的 Namespace 和 Cgroups,而並不存在一個所謂的 Pod 的邊界或者隔離環境。

那麼,Pod 又是怎麼被「建立」出來的呢?

答案是:Pod,實際上是一組共享了某些資源的容器。 具體的說:Pod 裏的全部容器,共享的是同一個 Network Namespace,而且能夠聲明共享同一個 Volume。 那這麼來看的話,一個有 A、B 兩個容器的 Pod,不就是等同於一個容器(容器 A)共享另一個容器(容器 B)的網絡和 Volume 的玩兒法麼?

這好像經過 docker run --net --volumes-from 這樣的命令就能實現嘛,好比:

docker run --net=B --volumes-from=B --name=A image-A ...

 

可是,你有沒有考慮過,若是真這樣作的話,容器 B 就必須比容器 A 先啓動,這樣一個 Pod 裏的多個容器就不是對等關係,而是拓撲關係了。

因此,在 Kubernetes 項目裏,Pod 的實現須要使用一箇中間容器,這個容器叫做 Infra 容器。在這個 Pod 中,Infra 容器永遠都是第一個被建立的容器,而其餘用戶定義的容器,則經過 Join Network Namespace 的方式,與 Infra 容器關聯在一塊兒。這樣的組織關係,能夠用下面這樣一個示意圖來表達: 

 

 如上圖所示,這個 Pod 裏有兩個用戶容器 A 和 B,還有一個 Infra 容器。很容易理解,在 Kubernetes 項目裏,Infra 容器必定要佔用極少的資源,因此它使用的是一個很是特殊的鏡像,叫做:k8s.gcr.io/pause。這個鏡像是一個用匯編語言編寫的、永遠處於「暫停」狀態的容器,解壓後的大小也只有 100~200 KB 左右。

而在 Infra 容器「Hold 住」Network Namespace 後,用戶容器就能夠加入到 Infra 容器的 Network Namespace 當中了。因此,若是你查看這些容器在宿主機上的 Namespace 文件(這個 Namespace+文件的路徑,我已經在前面的內容中介紹過),它們指向的值必定是徹底同樣的。 這也就意味着,對於 Pod 裏的容器 A 和容器 B 來講:

  • 它們能夠直接使用 localhost 進行通訊;
  • 它們看到的網絡設備跟 Infra 容器看到的徹底同樣;
  • 一個 Pod 只有一個 IP 地址,也就是這個 Pod 的 Network Namespace 對應的 IP 地址;
  • 固然,其餘的全部網絡資源,都是一個 Pod 一份,而且被該 Pod 中的全部容器共享;
  • Pod 的生命週期只跟 Infra 容器一致,而與容器 A 和 B 無關。

 

而對於同一個 Pod 裏面的全部用戶容器來講,它們的進出流量,也能夠認爲都是經過 Infra 容器完成的。這一點很重要,由於未來若是你要爲 Kubernetes 開發一個網絡插件時,應該重點考慮的是如何配置這個 Pod 的 Network Namespace,而不是每個用戶容器如何使用你的網絡配置,這是沒有意義的。

這就意味着,若是你的網絡插件須要在容器裏安裝某些包或者配置才能完成的話,是不可取的:Infra 容器鏡像的 rootfs 裏幾乎什麼都沒有,沒有你隨意發揮的空間。固然,這同時也意味着你的網絡插件徹底沒必要關心用戶容器的啓動與否,而只須要關注如何配置 Pod,也就是 Infra 容器的 Network Namespace 便可。 有了這個設計以後,共享 Volume 就簡單多了:Kubernetes 項目只要把全部 Volume 的定義都設計在 Pod 層級便可。

這樣,一個 Volume 對應的宿主機目錄對於 Pod 來講就只有一個,Pod 裏的容器只要聲明掛載這個 Volume,就必定能夠共享這個 Volume 對應的宿主機目錄。好比下面這個例子

 

apiVersion: v1
kind: Pod
metadata:
  name: two-containers
spec:
  restartPolicy: Never
  volumes:
  - name: shared-data
    hostPath:      
      path: /data
  containers:
  - name: nginx-container
    image: nginx
    volumeMounts:
    - name: shared-data
      mountPath: /usr/share/nginx/html
  - name: debian-container
    image: debian
    volumeMounts:
    - name: shared-data
      mountPath: /pod-data
    command: ["/bin/sh"]
    args: ["-c", "echo Hello from the debian container > /pod-data/index.html"]

  

在這個例子中,debian-container 和 nginx-container 都聲明掛載了 shared-data 這個 Volume。而 shared-data 是 hostPath 類型。因此,它對應在宿主機上的目錄就是:/data。而這個目錄,其實就被同時綁定掛載進了上述兩個容器當中。

這就是爲何,nginx-container 能夠從它的/usr/share/nginx/html 目錄中,讀取到 debian-container 生成的 index.html 文件的緣由。

明白了 Pod 的實現原理後,咱們再來討論「容器設計模式」,就容易多了。 Pod 這種「超親密關係」容器的設計思想,實際上就是但願,當用戶想在一個容器裏跑多個功能並不相關的應用時,應該優先考慮它們是否是更應該被描述成一個 Pod 裏的多個容器。爲了可以掌握這種思考方式,你就應該儘可能嘗試使用它來描述一些用單個容器難以解決的問題。

第一個最典型的例子是:WAR 包與 Web 服務器。

 咱們如今有一個 Java Web 應用的 WAR 包,它須要被放在 Tomcat 的 webapps 目錄下運行起來。

假如,你如今只能用 Docker 來作這件事情,那該如何處理這個組合關係呢?

一種方法是,把 WAR 包直接放在 Tomcat 鏡像的 webapps 目錄下,作成一個新的鏡像運行起來。但是,這時候,若是你要更新 WAR 包的內容,或者要升級 Tomcat 鏡像,就要從新制做一個新的發佈鏡像,很是麻煩。

另外一種方法是,你壓根兒無論 WAR 包,永遠只發佈一個 Tomcat 容器。不過,這個容器的 webapps 目錄,就必須聲明一個 hostPath 類型的 Volume,從而把宿主機上的 WAR 包掛載進 Tomcat 容器當中運行起來。不過,這樣你就必需要解決一個問題,即:如何讓每一臺宿主機,都預先準備好這個存儲有 WAR 包的目錄呢?這樣來看,你只能獨立維護一套分佈式存儲系統了。

實際上,有了 Pod 以後,這樣的問題就很容易解決了。咱們能夠把 WAR 包和 Tomcat 分別作成鏡像,而後把它們做爲一個 Pod 裏的兩個容器「組合」在一塊兒。這個 Pod 的配置文件以下所示:

 

apiVersion: v1
kind: Pod
metadata:
  name: javaweb-2
spec:
  initContainers:
  - image: geektime/sample:v2
    name: war
    command: ["cp", "/sample.war", "/app"]
    volumeMounts:
    - mountPath: /app
      name: app-volume
  containers:
  - image: geektime/tomcat:7.0
    name: tomcat
    command: ["sh","-c","/root/apache-tomcat-7.0.42-v2/bin/start.sh"]
    volumeMounts:
    - mountPath: /root/apache-tomcat-7.0.42-v2/webapps
      name: app-volume
    ports:
    - containerPort: 8080
      hostPort: 8001 
  volumes:
  - name: app-volume
    emptyDir: {}

  

 在這個 Pod 中,咱們定義了兩個容器,第一個容器使用的鏡像是 geektim/sample:v2,這個鏡像裏只有一個 WAR 包(sample.war)放在根目錄下。而第二個容器則使用的是一個標準的 Tomcat 鏡像。

不過,你可能已經注意到,WAR 包容器的類型再也不是一個普通容器,而是一個 Init Container 類型的容器。 在 Pod 中,全部 Init Container 定義的容器,都會比 spec.containers 定義的用戶容器先啓動。而且,Init Container 容器會按順序逐一啓動,而直到它們都啓動而且退出了,用戶容器纔會啓動。

因此,這個 Init Container 類型的 WAR 包容器啓動後,我執行了一句"cp /sample.war /app",把應用的 WAR 包拷貝到 /app 目錄下,而後退出。

然後這個 /app 目錄,就掛載了一個名叫 app-volume 的 Volume。

接下來就很關鍵了。Tomcat 容器,一樣聲明瞭掛載 app-volume 到本身的 webapps 目錄下。

因此,等 Tomcat 容器啓動時,它的 webapps 目錄下就必定會存在 sample.war 文件:這個文件正是 WAR 包容器啓動時拷貝到這個 Volume 裏面的,而這個 Volume 是被這兩個容器共享的。

像這樣,咱們就用一種「組合」方式,解決了 WAR 包與 Tomcat 容器之間耦合關係的問題。

實際上,這個所謂的「組合」操做,正是容器設計模式裏最經常使用的一種模式,它的名字叫:sidecar。

顧名思義,sidecar 指的就是咱們能夠在一個 Pod 中,啓動一個輔助容器,來完成一些獨立於主進程(主容器)以外的工做。

好比,在咱們的這個應用 Pod 中,Tomcat 容器是咱們要使用的主容器,而 WAR 包容器的存在,只是爲了給它提供一個 WAR 包而已。因此,咱們用 Init Container 的方式優先運行 WAR 包容器,扮演了一個 sidecar 的角色。

 

第二個例子,則是容器的日誌收集

好比,我如今有一個應用,須要不斷地把日誌文件輸出到容器的 /var/log 目錄中。

這時,我就能夠把一個 Pod 裏的 Volume 掛載到應用容器的 /var/log 目錄上。 而後,我在這個 Pod 裏同時運行一個 sidecar 容器,它也聲明掛載同一個 Volume 到本身的 /var/log 目錄上。

這樣,接下來 sidecar 容器就只須要作一件事兒,那就是不斷地從本身的 /var/log 目錄裏讀取日誌文件,轉發到 MongoDB 或者 Elasticsearch 中存儲起來。這樣,一個最基本的日誌收集工做就完成了。 跟第一個例子同樣,這個例子中的 sidecar 的主要工做也是使用共享的 Volume 來完成對文件的操做。

但不要忘記,Pod 的另外一個重要特性是,它的全部容器都共享同一個 Network Namespace。這就使得不少與 Pod 網絡相關的配置和管理,也均可以交給 sidecar 完成,而徹底無須干涉用戶容器。這裏最典型的例子莫過於 Istio 這個微服務治理項目了。 Istio 項目使用 sidecar  容器完成微服務治理的原理,我在後面很快會講解到。

 備忘

Pod,其實是在扮演傳統基礎設施裏「虛擬機」的角色;而容器,則是這個虛擬機裏運行的用戶程序。

 k8s.gcr.io/pause 的存在使得一個pod中的全部鏡像能夠:

  • 它們能夠直接使用 localhost 進行通訊;
  • 它們看到的網絡設備跟 Infra 容器看到的徹底同樣;
  • 一個 Pod 只有一個 IP 地址,也就是這個 Pod 的 Network Namespace 對應的 IP 地址;
  • 固然,其餘的全部網絡資源,都是一個 Pod 一份,而且被該 Pod 中的全部容器共享;
  • Pod 的生命週期只跟 Infra 容器一致,而與容器 A 和 B 無關。
相關文章
相關標籤/搜索