轉載請注明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。
第一章:Docker與k8s的恩怨情仇(一)—成為PaaS前浪的Cloud Foundry
第二章:Docker與k8s的恩怨情仇(二)—用最簡單的技術實現“容器”
第三章:Docker與k8s的恩怨情仇(三)—后浪Docker來勢洶洶
第四章:Docker與k8s的恩怨情仇(四)-雲原生時代的閉源落幕
Docker與k8s的恩怨情仇(六)—— “容器編排”上演“終結者”大片
上節中我們提到了社區生態的發展使得Kubernetes得到了良性的發展和傳播。比起相對封閉的Docker社區開放的CNCF社區獲得了更大成功,但僅僅是社區的活力對比還不足以讓Docker這么快的敗下陣來,其根本原因是Kubernetes的對容器編排技術的理解比起Docker更勝一籌。這種優勢幾乎是壓到性的降維打擊,Docker毫無還手之力。
接下來便為大家介紹在這場容器大戰之中,Kubernetes如何占據優勢地位。
容器編排
所謂容器編排,其實就是處理容器和容器之間的關系,在一個分布式的大型系統里,不可能是以多個單一個體存在的,它們可能是一個與多個,一群與一群這樣相互交織着。
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的容器編排功能
與Docker這種站在容器視角上只能處理容器之間的關系所不同,Kubernetes所做的是從軟件工程的設計理念出發,將關系進行了不同類的划分,定義了緊密關系(Pod之間)和交互關系(Service之間)的概念,然后再對不同的關系進行特定的編排實現。
乍一聽你可能是一頭霧水。這里舉個不太實際但是一看就懂的例子:如果把容器之間的關系比作人之間的關系,Docker能處理的是僅僅是站在單一個體的角度上,處理人與人之間的人際關系;而Kubernetes確是上帝,站在上帝視角不僅能處理人與人之間的人際關系,還能處理狗與狗之間的狗際關系,最主要的是能處理人與狗之間的交往關系。
而實現上述緊密關系的原理,就是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容器是第一個啟動的:
Infra容器是一個用匯編語言編寫的、主進程是一個永遠處於“暫停”狀態的容器,其僅占用極少的資源,解壓之后也僅有100KB左右。
實例演示在Kubernetes中的Pod
介紹了一通,接下來我們在實例中為大家演示Pod長什么樣子。
我們在任意一個裝有Kubernetes的集群中通過以下的yaml文件和shell命令運行處一個Pod,這個YAML文件具體是什么意思暫時不用理會,之后我會對這個YAML做一說明,我們目前只需要明白:Kubernetes中的所有資源都可以通過以下這種YAML文件或者json文件描述的,現在我們只需要知道這是一個運行着busybox和nginx的Pod即可:
創建這個hello-pod.yaml文件之后運行以下命令:
通過上述命令,我們就成功創建了一個pod,我們可以從執行結果看到infra容器的主進程成為了此Pod的PID==1的超級進程,說明了Pod是組合而成的:
至此,我們應該要理解Pod是Kubernetes的最小調度單位這個概念了,並且也應該把Pod作為一個整體而不是多個容器的集合來看待。
我們再看看描述這個Pod的文件類型YAML。
YAML的語法定義:
YAML是一種專門編寫配置文件的語言,其簡潔且強大,在描述配置文件方面遠勝於JSON,因此在很多新興的項目比如Kubernetes和Docker Compose等都通過YAML來作為配置文件的描述語言。與HTML相同,YAML也是一個英文的縮寫:YAML Ain't Markup Language,聰明的同學已經看出來了,這是一個遞歸寫法,突出了滿滿的程序員氣息。其語法有如下特征:
- 大小寫敏感
- 使用縮進表示層級關系,類似Python
- 縮進不允許使用Tab,只允許使用空格
- 縮進的空格數目不重要,只要相同層級的元素左側對其即可
- 數組用短橫線-表示
- NULL用波浪線~表示
明確了以上概念,我們把YAML改寫成一個JSON,看看這之間的區別:
這兩種寫法在Kubernetes中是等效的,上述的JSON可以正常運行,但是Kubernetes還是更推薦使用YAML。從上面的對比中我們也能發現,在之前的使用中一直很好用的JSON現在也略顯笨拙,需要些大量的字符串標志。
看完語法,我們再來說說上述YAML中的各個節點在Kubernetes所表示的意思。Kubernetes中的有一種類似於Java語法萬物皆對象的概念,所有內部的資源,包括服務器node、服務service以及運行組Pod在kubernetes中皆是以對象的形式存儲的,其所有對象都由一下固定的部分組成:
- 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:
在這個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又是如何脫穎而出。