PouchContainer CRI的設計與實現方法

HitTwice發表於2018-06-08

  1. CRI簡介

  在每個Kubernetes節點的最底層都有一個程式負責具體的容器建立刪除工作,Kubernetes會對其介面進行呼叫,從而完成容器的編排排程。我們將這一層軟體稱之為容器執行時(Container Runtime),大名鼎鼎的Docker就是其中的代表。

  當然,容器執行時並非只有Docker一種,包括CoreOS的rkt,hyper.sh的runV,Google的gvisor,以及本文的主角PouchContainer,都包含了完整的容器操作,能夠用來建立特性各異的容器。不同的容器執行時有著各自獨特的優點,能夠滿足不同使用者的需求,因此Kubernetes支援多種容器執行時勢在必行。

  最初,Kubernetes原生內建了對Docker的呼叫介面,之後社群又在Kubernetes 1.3中整合了rkt的介面,使其成為了Docker以外,另一個可選的容器執行時。不過,此時不論是對於Docker還是對於rkt的呼叫都是和Kubernetes的核心程式碼強耦合的,這無疑會帶來如下兩方面的問題:

  新興的容器執行時,例如PouchContainer這樣的後起之秀,加入Kubernetes生態難度頗大。容器執行時的開發者必須對於Kubernetes的程式碼(至少是Kubelet)有著非常深入的理解,才能順利完成兩者之間的對接。

  Kubernetes的程式碼將更加難以維護,這也體現在兩方面:(1)將各種容器執行時的呼叫介面全部硬編碼進Kubernetes,會讓Kubernetes的核心程式碼變得臃腫不堪,(2)容器執行時介面細微的改動都會引發Kubernetes核心程式碼的修改,增加Kubernetes的不穩定性

  為了解決這些問題,社群在Kubernetes 1.5引入了CRI(Container Runtime Interface),透過定義一組容器執行時的公共介面將Kubernetes對於各種容器執行時的呼叫介面遮蔽至核心程式碼以外,Kubernetes核心程式碼只對該抽象介面層進行呼叫。而對於各種容器執行時,只要滿足了CRI中定義的各個介面就能順利接入Kubernetes,成為其中的一個容器執行時選項。方案雖然簡單,但是對於Kubernetes社群維護者和容器執行時開發者來說,都是一種解放。

  2. CRI設計概述

  如上圖所示,左邊的Kubelet是Kubernetes叢集的Node Agent,它會對本節點上容器的狀態進行監控,保證它們都按照預期狀態執行。為了實現這一目標,Kubelet會不斷呼叫相關的CRI介面來對容器進行同步。

  CRI shim則可以認為是一個介面轉換層,它會將CRI介面,轉換成對應底層容器執行時的介面,並呼叫執行,返回結果。對於有的容器執行時,CRI shim是作為一個獨立的程式存在的,例如當選用Docker為Kubernetes的容器執行時,Kubelet初始化時,會附帶啟動一個Docker shim程式,它就是Docker的CRI shime。而對於PouchContainer,它的CRI shim則是內嵌在Pouchd中的,我們將其稱之為CRI manager。關於這一點,我們會在下一節討論PouchContainer相關架構時再詳細敘述。

  CRI本質上是一套gRPC介面,Kubelet內建了一個gRPC Client,CRI shim中則內建了一個gRPC Server。Kubelet每一次對CRI介面的呼叫,都將轉換為gRPC請求由gRPC Client傳送給CRI shim中的gRPC Server。Server呼叫底層的容器執行時對請求進行處理並返回結果,由此完成一次CRI介面呼叫。

  CRI定義的gRPC介面可劃分兩類,ImageService和RuntimeService:其中ImageService負責管理容器的映象,而RuntimeService則負責對容器生命週期進行管理以及與容器進行互動(exec/attach/port-forward)。

  3. CRI Manager架構設計

  在PouchContainer的整個架構體系中,CRI Manager實現了CRI定義的全部介面,擔任了PouchContainer中CRI shim的角色。當Kubelet呼叫一個CRI介面時,請求就會透過Kubelet的gRPC Client傳送到上圖的gRPC Server中。Server會對請求進行解析,並呼叫CRI Manager相應的方法進行處理。

  我們先透過一個例子來簡單瞭解一下各個模組的功能。例如,當到達的請求為建立一個Pod,那麼CRI Manager會先將獲取到的CRI格式的配置轉換成符合PouchContainer介面要求的格式,呼叫Image Manager拉取所需的映象,再呼叫Container Manager建立所需的容器,並呼叫CNI Manager,利用CNI外掛對Pod的網路進行配置。最後,Stream Server會對互動型別的CRI請求,例如exec/attach/portforward進行處理。

  值得注意的是,CNI Manager和Stream Server是CRI Manager的子模組,而CRI Manager,Container Manager以及Image Manager是三個平等的模組,它們都位於同一個二進位制檔案Pouchd中,因此它們之間的呼叫都是最為直接的函式呼叫,並不存在例如Docker shim與Docker互動時,所需要的遠端呼叫開銷。下面,我們將進入CRI Manager內部,對其中重要功能的實現做更為深入的理解。

  4. Pod模型的實現

  在Kubernetes的世界裡,Pod是最小的排程部署單元。簡單地說,一個Pod就是由一些關聯較為緊密的容器構成的容器組。作為一個整體,這些“親密”的容器之間會共享一些東西,從而讓它們之間的互動更為高效。例如,對於網路,同一個Pod中的容器會共享同一個IP地址和埠空間,從而使它們能直接透過localhost互相訪問。對於儲存,Pod中定義的volume會掛載到其中的每個容器中,從而讓每個容器都能對其進行訪問。

  事實上,只要一組容器之間共享某些Linux Namespace以及掛載相同的volume就能實現上述的所有特性。下面,我們就透過建立一個具體的Pod來分析PouchContainer中的CRI Manager是如何實現Pod模型的:

  1、當Kubelet需要新建一個Pod時,首先會對RunPodSandbox這一CRI介面進行呼叫,而CRI Manager對該介面的實現是建立一個我們稱之為"infra container"的特殊容器。從容器實現的角度來看,它並不特殊,無非是呼叫Container Manager,建立一個映象為pause-amd64:3.0的普通容器。但是從整個Pod容器組的角度來看,它是有著特殊作用的,正是它將自己的Linux Namespace貢獻出來,作為上文所說的各容器共享的Linux Namespace,將容器組中的所有容器聯結到一起。它更像是一個載體,承載了Pod中所有其他的容器,為它們的執行提供基礎設施。而一般我們也用infra container代表一個Pod。

  2、在infra container建立完成之後,Kubelet會對Pod容器組中的其他容器進行建立。每建立一個容器就是連續呼叫CreateContainer和StartContainer這兩個CRI介面。對於CreateContainer,CRI Manager僅僅只是將CRI格式的容器配置轉換為PouchContainer格式的容器配置,再將其傳遞給Container Manager,由其完成具體的容器建立工作。這裡我們唯一需要關心的問題是,該容器如何加入上文中提到的infra container的Linux Namespace。其實真正的實現非常簡單,在Container Manager的容器配置引數中有PidMode, IpcMode以及NetworkMode三個引數,分別用於配置容器的Pid Namespace,Ipc Namespace和Network Namespace。籠統地說,對於容器的Namespace的配置一般都有兩種模式:"None"模式,即建立該容器自己獨有的Namespace,另一種即為"Container"模式,即加入另一個容器的Namespace。顯然,我們只需要將上述三個引數配置為"Container"模式,加入infra container的Namespace即可。具體是如何加入的,CRI Manager並不需要關心。對於StartContainer,CRI Manager僅僅只是做了一層轉發,從請求中獲取容器ID並呼叫Container Manager的Start介面啟動容器。

  3、最後,Kubelet會不斷呼叫ListPodSandbox和ListContainers這兩個CRI介面來獲取本節點上容器的執行狀態。其中ListPodSandbox羅列的其實就是各個infra container的狀態,而ListContainer羅列的是除了infra container以外其他容器的狀態。現在問題是,對於Container Manager來說,infra container和其他container並不存在任何區別。那麼CRI Manager是如何對這些容器進行區分的呢?事實上,CRI Manager在建立容器時,會在已有容器配置的基礎之上,額外增加一個label,標誌該容器的型別。從而在實現ListPodSandbox和ListContainers介面的時候,以該label的值作為條件,就能對不同型別的容器進行過濾。

  綜上,對於Pod的建立,我們可以概述為先建立infra container,再建立pod中的其他容器,並讓它們加入infra container的Linux Namespace。

  5. Pod網路配置

  因為Pod中所有的容器都是共享Network Namespace的,因此我們只需要在建立infra container的時候,對它的Network Namespace進行配置即可。

  在Kubernetes生態體系中容器的網路功能都是由CNI實現的。和CRI類似,CNI也是一套標準介面,各種網路方案只要實現了該介面就能無縫接入Kubernetes。CRI Manager中的CNI Manager就是對CNI的簡單封裝。它在初始化的過程中會載入目錄/etc/cni/net.d下的配置檔案,如下所示:

  其中指定了配置Pod網路會使用到的CNI外掛,例如上文中的bridge,以及一些網路配置資訊,例如本節點Pod所屬的子網範圍和路由配置。

  下面我們就透過具體的步驟來展示如何將一個Pod加入CNI網路:

  1、當呼叫container manager建立infra container時,將NetworkMode設定為"None"模式,表示建立一個該infra container獨有的Network Namespace且不做任何配置。

  2、根據infra container對應的PID,獲取其對應的Network Namespace路徑/proc/{pid}/ns/net。

  3、呼叫CNI Manager的SetUpPodNetwork方法,核心引數為步驟二中獲取的Network Namespace路徑。該方法做的工作就是呼叫CNI Manager初始化時指定的CNI外掛,例如上文中的bridge,對引數中指定的Network Namespace進行配置,包括建立各種網路裝置,進行各種網路配置,將該Network Namespace加入外掛對應的CNI網路中。

  對於大多數Pod,網路配置都是按照上述步驟操作的,大部分的工作將由CNI以及對應的CNI外掛替我們完成。但是對於一些特殊的Pod,它們會將自己的網路模式設定為"Host",即和宿主機共享Network Namespace。這時,我們只需要在呼叫Container Manager建立infra container時,將NetworkMode設定為"Host",並且跳過CNI Manager的配置即可。

  對於Pod中其他的容器,不論Pod是處於"Host"網路模式,還是擁有獨立的Network Namespace,都只需要在呼叫Container Manager建立容器時,將NetworkMode配置為"Container"模式,加入infra container所在的Network Namespace即可。

  6. IO流處理

  Kubernetes提供了例如kubectl exec/attach/port-forward這樣的功能來實現使用者和某個具體的Pod或者容器的直接互動。如下所示:

  可以看到,exec一個Pod等效於ssh登入到該容器中。下面,我們根據kubectl exec的執行流來分析Kubernetes中對於IO請求的處理,以及CRI Manager在其中扮演的角色。

  如上圖所示,執行一條kubectl exec命令的步驟如下:

  1、kubectl exec命令的本質其實是對Kubernetes叢集中某個容器執行exec命令,並將由此產生的IO流轉發到使用者的手中。所以請求將首先層層轉發到達該容器所在節點的Kubelet,Kubelet再根據配置呼叫CRI中的Exec介面。請求的配置引數如下:

  2、令人感到意外的是,CRI Manager的Exec方法並沒有直接呼叫Container Manager,對目標容器執行exec命令,而是轉而呼叫了其內建的Stream Server的GetExec方法。

  3、Stream Server的GetExec方法所做的工作是將該exec請求的內容儲存到了上圖所示的Request Cache中,並返回一個token,利用該token,我們可以重新從Request Cache中找回對應的exec請求。最後,將這個token寫入一個URL中,並作為執行結果層層返回到ApiServer。

  4、ApiServer利用返回的URL直接對目標容器所在節點的Stream Server發起一個http請求,請求的頭部包含了"Upgrade"欄位,要求將http協議升級為websocket或者SPDY這樣的streaming protocol,用於支援多條IO流的處理,本文我們以SPDY為例。

  5、Stream Server對ApiServer傳送的請求進行處理,首先根據URL中的token,從Request Cache中獲取之前儲存的exec請求配置。之後,回覆該http請求,同意將協議升級為SPDY,並根據exec請求的配置等待ApiServer建立指定數量的stream,分別對應標準輸入Stdin,標準輸出Stdout,標準錯誤輸出Stderr。

  6、待Stream Server獲取指定數量的Stream之後,依次呼叫Container Manager的CreateExec和startExec方法,對目標容器執行exec操作並將IO流轉發至對應的各個stream中。

  7、最後,ApiServer將各個stream的資料轉發至使用者,開啟使用者與目標容器的IO互動。

  事實上,在引入CRI之前,Kubernetes對於IO的處理方式和我們的預期是一致的,Kubelet會直接對目標容器執行exec命令,並將IO流轉發回ApiServer。但是這樣會讓Kubelet承載過大的壓力,所有的IO流都需要經過它的轉發,這顯然是不必要的。因此上述的處理雖然初看較為複雜,但是有效地緩解了Kubelet的壓力,並且也讓IO的處理更為高效。

  7. 總結

  本文從引入CRI的緣由而起,簡要描述了CRI的架構,重點敘述了PouchContainer對CRI各個核心功能模組的實現。CRI的存在讓PouchContainer容器加入Kubernetes生態變得更為簡單快捷。而我們也相信,PouchContainer獨有的特性必定會讓Kubernetes生態變得更加豐富多彩。

  PouchContainer CRI的設計與實現,是阿里巴巴-浙江大學前沿技術聯合研究中心的聯合研究專案,旨在幫助PouchContainer 作為一種成熟的容器執行時(container runtime),積極在生態層面擁抱 CNCF。浙江大學 SEL 實驗室的卓越技術力量,有效幫助 Pouch 完成 CRI 層面的空白,未來預計在阿里巴巴以及其他使用PouchContainer的資料中心中,創造不可估量的價值。
  
  本文作者:allencloud
  原文連結:https://yq.aliyun.com/articles/599829?spm=a2c4e.11153940.bloghomeflow.60.188e291aXn1Xeu

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31473948/viewspace-2155852/,如需轉載,請註明出處,否則將追究法律責任。

相關文章