Docker與k8s的恩怨情仇(五)——Kubernetes的創新

葡萄城技術團隊發表於2021-07-21

轉載請註明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。

上節中我們提到了社群生態的發展使得Kubernetes得到了良性的發展和傳播。比起相對封閉的Docker社群開放的CNCF社群獲得了更大成功,但僅僅是社群的活力對比還不足以讓Docker這麼快的敗下陣來,其根本原因是Kubernetes的對容器編排技術的理解比起Docker更勝一籌。這種優勢幾乎是壓到性的降維打擊,Docker毫無還手之力。

接下來便為大家介紹在這場容器大戰之中,Kubernetes如何佔據優勢地位。

容器編排

所謂容器編排,其實就是處理容器和容器之間的關係,在一個分散式的大型系統裡,不可能是以多個單一個體存在的,它們可能是一個與多個,一群與一群這樣相互交織著。

圖片8.png

Docker的容器編排功能

Docker構建的是以Docker容器為最核心的PaaS生態,包括以Docker Compose為主的簡單容器關係編排,以及以Docker Swarm為主的線上運維平臺。使用者可以通過Docker Compose處理自己叢集中容器之間的關係,並且通過Docker Swarm管理運維自己的叢集,可以看到這一切其實就是當初Cloud Foundry的PaaS功能,所主打的就是和Docker容器的無縫整合。

Docker Compose做到的是為多個有互動關係建立一種“連線”,把它們全部編寫在一個docker-compose.yaml檔案中,然後統一發布(我後面說到的組裡的ELK功能就是這樣做的),這樣做也有優點,就是對於簡單的幾個容器之間的互動來說非常便利。但是對於大型的叢集來說卻有些杯水車薪,並且這種每出現一種新需求就需要單獨做一項新功能的開發模式,將使後期程式碼維護變得十分困難。

Kubernetes如果要和Docker對抗,肯定不能僅僅只做Docker容器管理這種Docker本身就已經支援的功能了,這樣的話別說分庭抗禮,可能連Docker的基本使用者都吸引不到。因此在Kubernetes設計之初,就確定了不以Docker為核心依賴的設計理念。在Kubernetes中,Docker僅是容器執行時實現的一個可選項,使用者可以依據自己的喜好任意調換自己需要的容器內容且Kubernetes為這些容器都提供了介面。此外,Kubernetes準確的抓住了Docker容器的一個致命性的弱點進行了自身創新。

接下來就讓我們一起來了解,這個給Docker造成降維打擊的內容究竟是什麼?

Kubernetes的容器編排功能

圖片9.png

與Docker這種站在容器視角上只能處理容器之間的關係所不同,Kubernetes所做的是從軟體工程的設計理念出發,將關係進行了不同類的劃分,定義了緊密關係(Pod之間)和互動關係(Service之間)的概念,然後再對不同的關係進行特定的編排實現。

乍一聽你可能是一頭霧水。這裡舉個不太實際但是一看就懂的例子:如果把容器之間的關係比作人之間的關係,Docker能處理的是僅僅是站在單一個體的角度上,處理人與人之間的人際關係;而Kubernetes確是上帝,站在上帝視角不僅能處理人與人之間的人際關係,還能處理狗與狗之間的狗際關係,最主要的是能處理人與狗之間的交往關係。

圖8.png

而實現上述緊密關係的原理,就是Kubernetes創新的Pod。

Pod是Kubernetes所創新的一個概念,其原型是Borg中的Alloc,是Kubernetes執行應用的最小執行單元,由一個或者多個緊密協作的容器組合而成,其出現的原因是針對容器的一個致命性弱點——單一程式這問題的擴充套件,讓容器有了程式組的概念。通過第一節,我們知道了容器的本質是一個程式,其本身就是超級程式,其他程式都必須是它的子程式,因此在容器中,沒有程式組的概念,而在日常的程式執行中,程式組是常常配合使用的。

使用Pod處理緊密關係

為了給大家介紹Pod處理緊密關係的原理,這裡舉一個程式組的例子:

Linux中有一個負責作業系統日誌處理的程式rsyslogd是由三個模組組成,分別是:imklog模組、muxsock模組以及rsyslogd自己的main函式主程式。這三個程式組一定要執行在同一臺機器上,否則它們之間的基於Socket的通訊和檔案的交換都會出現問題。

而上述的這個問題,如果出現在Docker中,就不得不使用三個不同的容器分別描述了,並且使用者還得自己模擬處理它們三個之間的通訊關係,這種複雜度可能比使用容器運維都高的多。並且對於這個問題的運維,Docker Swarm也有自己本身的問題。以上述的例子為基礎,如果三個模組分別都需要1GB的記憶體執行,如果Docker Swarm執行的叢集中有兩個node,node-1剩餘2.5GB,node-2剩餘3GB。這種情況下分別使用docker run 模組執行上述三個容器,基於Swarm的affinity=main約束,他們三個都必須要排程到同一臺機器上,但是Swarm卻很有可能先分配兩個去node-1,然後剩餘的一個由於還剩0.5GB不滿足排程而使這次排程失敗。這種典型的成組排程(gang scheduling)沒有被妥善處理的例子在Docker Swarm中經常存在。

基於上述的需求,Kubernetes有了Pod這個概念來處理這種緊密關係。在一個Pod中的容器共享相同的Cgroups和Namespace,因此它們之間並不存在邊界和隔離環境,它們可以共享同一個網路IP,使用相同的Volume處理資料等等。其中的原理就是在多個容器之間建立其共享資源的連結。但是為了解決到底是A共享B,還是B共享A,以及A和B誰先啟動這種拓撲性的問題,一個Pod其實是由一個Infra容器聯合AB兩個容器共同組成的,其中Infra容器是第一個啟動的:

圖片11.png

Infra容器是一個用匯編語言編寫的、主程式是一個永遠處於“暫停”狀態的容器,其僅佔用極少的資源,解壓之後也僅有100KB左右。

例項演示在Kubernetes中的Pod

介紹了一通,接下來我們在例項中為大家演示Pod長什麼樣子。

我們在任意一個裝有Kubernetes的叢集中通過以下的yaml檔案和shell命令執行處一個Pod,這個YAML檔案具體是什麼意思暫時不用理會,之後我會對這個YAML做一說明,我們目前只需要明白:Kubernetes中的所有資源都可以通過以下這種YAML檔案或者json檔案描述的,現在我們只需要知道這是一個執行著busybox和nginx的Pod即可:

圖片12.png

建立這個hello-pod.yaml檔案之後執行以下命令:

圖片13.png

通過上述命令,我們就成功建立了一個pod,我們可以從執行結果看到infra容器的主程式成為了此Pod的PID==1的超級程式,說明了Pod是組合而成的:

圖片17.png

至此,我們應該要理解Pod是Kubernetes的最小排程單位這個概念了,並且也應該把Pod作為一個整體而不是多個容器的集合來看待。
我們再看看描述這個Pod的檔案型別YAML。

YAML的語法定義:

YAML是一種專門編寫配置檔案的語言,其簡潔且強大,在描述配置檔案方面遠勝於JSON,因此在很多新興的專案比如Kubernetes和Docker Compose等都通過YAML來作為配置檔案的描述語言。與HTML相同,YAML也是一個英文的縮寫:YAML Ain't Markup Language,聰明的同學已經看出來了,這是一個遞迴寫法,突出了滿滿的程式設計師氣息。其語法有如下特徵:

-       大小寫敏感

-       使用縮排表示層級關係,類似Python

-       縮排不允許使用Tab,只允許使用空格

-       縮排的空格數目不重要,只要相同層級的元素左側對其即可

-       陣列用短橫線-表示

-       NULL用波浪線~表示

明確了以上概念,我們把YAML改寫成一個JSON,看看這之間的區別:

圖片14.png

這兩種寫法在Kubernetes中是等效的,上述的JSON可以正常執行,但是Kubernetes還是更推薦使用YAML。從上面的對比中我們也能發現,在之前的使用中一直很好用的JSON現在也略顯笨拙,需要些大量的字串標誌。

看完語法,我們再來說說上述YAML中的各個節點在Kubernetes所表示的意思。Kubernetes中的有一種類似於Java語法萬物皆物件的概念,所有內部的資源,包括伺服器node、服務service以及執行組Pod在kubernetes中皆是以物件的形式儲存的,其所有物件都由一下固定的部分組成:

圖片15.png

-       apiVersion:在官方文件中並沒有給出相應的解釋,但是從名字可以看出這是一個規定API版本的欄位,但是此欄位不能自定義,必須符合Kubernetes的官方約束,目前我們用到的基本都是v1穩定版

-       kind:指明當前的配置是什麼型別,比如Pod、Service、Ingress、Node等,注意這個首字母是大寫的

-       metadata:用於描述當前配置的meta資訊,比如name,label等

-       spec:指明當前配置的具體實現

所有的Kubernetes物件基本都滿足以上的格式,因此最開始Pod的YAML檔案的意思是“使用v1穩定版本的API資訊,型別是Pod,名稱是hello-pod,具體實現是開啟ProcessNamespace,有兩個容器。

知道了YAML的概念,讓我們在迴歸主題。為了解決容器單一程式問題,只建立Pod的原因之一是Google通過Pod實現了自己的容器設計模式,而Google則為Kubernetes編寫了最適合的容器設計模式。

舉個最常用的例子:

Java專案並不能像.Net Core專案那樣編譯完成後直接自宿主執行,必須要把編譯生成的war包拷貝到服務宿主程式比如Tomcat的執行目錄下才可以正常使用。但是在實際情況中越大的公司分工越明確,很大概率負責Java專案開發和服務宿主程式開發的團隊並不是同一團隊。

為了讓上述情況中的兩個團隊可以各自獨立開發並且還可以緊密合作,我們可以使用Pod解決這個問題。

下面這個yaml檔案就定義了一個滿足上述需求的Pod:

圖片16.png

在這個yaml檔案中,我們定義了一個java程式和tomcat程式的容器,並且對這兩個容器之間的容器進行了一次掛載操作:將java程式的/app路徑以及tomcat程式的/root/apache-tomcat/webapps同時掛載到了sample-volume這個掛載捲上,並且最後定了這個掛載卷就是一個記憶體資料卷。並且定義了java程式所在的容器是一個initContainer,說明此容器是在tomcat容器之前啟動的,並且啟動之後執行了一個cp的命令。

在上述Pod描述了這樣一個場景:程式執行開始執行時,Java容器啟動,把自己的war包sample.war拷貝到了自己的/app目錄下;之後tomcat容器啟動,執行啟動指令碼,執行的war包從自己的/root/apache-tomcat/webapps路徑下獲得。

可以看到通過上述的配置描述,我們既沒有改動Java程式,也沒有改動tomcat程式,卻讓它們完美的配合工作了,完成了解耦操作。這個例子就是容器設計模式中的Sidecar模式,還有很多設計模式,感興趣的同學可以去進一步自行學習。

總結

以上介紹的就是Kubernetes為了解決緊密關係而抽象出來的概念Pod的基礎內容了,需要注意的是,Pod提供的只是一種編排的思想,而不是具體的技術方案,在我們使用的Kubernetes框架中,Pod只不過是以Docker作為載體實現了而已,如果你使用的底層容器是虛擬機器,比如virtlet,那這個Pod建立時就根本不需要Infra Container,因為虛擬機器天生就支援多程式協同。

在說完了Pod的基礎的內容,在下一節中我們將會為大家介紹在接下來的容器編排戰爭之中,Kubernetes又是如何脫穎而出。

相關文章