Kubernetes & CRI (I)

前言

前幾天探討了關於 Container 的一些基本概念,譬如相關的開放標準 Open Container Initiative 以及 Linux 下的實現方式 Namespace ..etc

接下來我們要正式的走到 kubernetes 的架構中,來探討 kubernetescontainer 這一塊是怎麼處理與設計的。

雖然 kubernetes 已發展數年,其火紅程度也席捲整個產業,然而我自己的觀察結果還是滿多人對 kubernetes 有一些誤解,認為 kubernetesdocker container 的管理工具,事實上從 kubernetes 官網 中就直接明確說明

Kubernetes (K8s) is an open-source system for automating deployment, scaling, and management of containerized applications.

kubernetes 是用來管理 containerized applications 並不是專屬於 docker 獨享,作為一個 container orchestrator 的角色, kubernetes 希望能夠管理所有容器化的應用程式。

看到這邊大概就可以想到就如同 Open Container Initiative (OCI) 一樣,為了能夠有效地銜接各式各樣不同的 Container Runtime 解決方案,勢必也需要推出相關的標準,就如同 OCI 一樣,符合標準的解決方案就能夠輕鬆的整合到 kubernetes 之中。於是 Container Runtime Interface(CRI) 標準就被設計且開發來

接下來我們將針對這個概念來細細訴說,到底什麼是 Container Runtime Interface(CRI) 以及其如何運作。

Container Runtime Interface (CRI)

對於 kubernetes 來說,希望能夠透過一個標準介面與各個 contaienr rumtime 解決方案銜接,這個銜接的接口標準就是所謂的 Container Runtime

為了更加清楚理解 CRI 的定位,我們用下列的表格來解釋
表格列出了 kubernetesCRI Runtime 各自的責任

Kubernetes CRI Runtime
Kubernetes Resources/API Pod Life Cycle (Add/Delete)
Storage (CSI) Image management
Networking (CNI) Status
Dispatcher Container Operations (attatch/exec)

基本上 CRI Runtime 很類似前述的 Containerd 一樣,能夠根據需求產生出符合 OCI 標準的容器應用程式,但是基本單位不再是 Container 而是 Pod
kubernetes 本身建築在這些基礎之上,提供更豐富的應用與 API 供使用者使用。

這兩者之間的溝通橋樑就是所謂的 CRI

CRI 所謂的標準其實非常簡單,就是所謂的 protobufAPI 介面

從官方擷取該介面資訊如下,仔細觀察你可以裡面有 Pod 也有 Container,同時也涵蓋了各式各樣的操作,如 Run, Stop, List, Remove, Start,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// Runtime service defines the public APIs for remote container runtimes
service RuntimeService {
// Version returns the runtime name, runtime version, and runtime API version.
rpc Version(VersionRequest) returns (VersionResponse) {}

// RunPodSandbox creates and starts a pod-level sandbox. Runtimes must ensure
// the sandbox is in the ready state on success.
rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}
// StopPodSandbox stops any running process that is part of the sandbox and
// reclaims network resources (e.g., IP addresses) allocated to the sandbox.
// If there are any running containers in the sandbox, they must be forcibly
// terminated.
// This call is idempotent, and must not return an error if all relevant
// resources have already been reclaimed. kubelet will call StopPodSandbox
// at least once before calling RemovePodSandbox. It will also attempt to
// reclaim resources eagerly, as soon as a sandbox is not needed. Hence,
// multiple StopPodSandbox calls are expected.
rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}
// RemovePodSandbox removes the sandbox. If there are any running containers
// in the sandbox, they must be forcibly terminated and removed.
// This call is idempotent, and must not return an error if the sandbox has
// already been removed.
rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {}
// PodSandboxStatus returns the status of the PodSandbox. If the PodSandbox is not
// present, returns an error.
rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {}
// ListPodSandbox returns a list of PodSandboxes.
rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {}

// CreateContainer creates a new container in specified PodSandbox
rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {}
// StartContainer starts the container.
rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}
// StopContainer stops a running container with a grace period (i.e., timeout).
// This call is idempotent, and must not return an error if the container has
// already been stopped.
// TODO: what must the runtime do after the grace period is reached?
rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}
// RemoveContainer removes the container. If the container is running, the
// container must be forcibly removed.
// This call is idempotent, and must not return an error if the container has
// already been removed.
rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {}
// ListContainers lists all containers by filters.
rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}
// ContainerStatus returns status of the container. If the container is not
// present, returns an error.
rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {}
// UpdateContainerResources updates ContainerConfig of the container.
rpc UpdateContainerResources(UpdateContainerResourcesRequest) returns (UpdateContainerResourcesResponse) {}
// ReopenContainerLog asks runtime to reopen the stdout/stderr log file
// for the container. This is often called after the log file has been
// rotated. If the container is not running, container runtime can choose
// to either create a new log file and return nil, or return an error.
// Once it returns error, new container log file MUST NOT be created.
rpc ReopenContainerLog(ReopenContainerLogRequest) returns (ReopenContainerLogResponse) {}

// ExecSync runs a command in a container synchronously.
rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse) {}
// Exec prepares a streaming endpoint to execute a command in the container.
rpc Exec(ExecRequest) returns (ExecResponse) {}
// Attach prepares a streaming endpoint to attach to a running container.
rpc Attach(AttachRequest) returns (AttachResponse) {}
// PortForward prepares a streaming endpoint to forward ports from a PodSandbox.
rpc PortForward(PortForwardRequest) returns (PortForwardResponse) {}

// ContainerStats returns stats of the container. If the container does not
// exist, the call returns an error.
rpc ContainerStats(ContainerStatsRequest) returns (ContainerStatsResponse) {}
// ListContainerStats returns stats of all running containers.
rpc ListContainerStats(ListContainerStatsRequest) returns (ListContainerStatsResponse) {}

// UpdateRuntimeConfig updates the runtime configuration based on the given request.
rpc UpdateRuntimeConfig(UpdateRuntimeConfigRequest) returns (UpdateRuntimeConfigResponse) {}

// Status returns the status of the runtime.
rpc Status(StatusRequest) returns (StatusResponse) {}
}

由於 CRI 的標準就是一些相關的介面,這意味只要任何 CRI Runtime 有實作這些介面,都可以跟 kubernetes 銜接來處理所有跟 Pod 有關的操作。

剩下的一個問題就是, 之前所探討過的 docker 運作流程

docker client -> docker engine -> docker-containerd -> docker-containerd-shim -> runc -> container

這個架構要怎麼跟 kubernetes & CRI 整合?

Docker & Kubernetes

解釋 Dockerkubernetes 的最好方法就是閱讀官方部落格的文章 kubernetes-containerd-integration-goes-ga。 為了節省讀者的時間,接下來就幫大家導讀一下這篇文章,以下的圖片都來自於上述的官方部落格)

上面提到 CRI 本身是個溝通介面,這代表溝通的兩方都需要根據這個界面去實現與滿足。 對於 kubernetes 來說,kubelet 自己維護與開發的,要支援 CRI 本身就不是什麼困難的事情。

但是另外一端如果要使用 docker 的話,那到底要怎麼辦?
docker 背後也是有公司再經營,也不是說改就改,這種情況下到底要如何將 docker 給整合進來?

最直觀的想法就是如果沒有辦法使得 docker 本身支援 CRI 的標準,那就額外撰寫一個轉接器,其運作在 kubeletDocker,該應用程式上承 CRIkubernetes 溝通,下承 Docker APIDocker Engine 溝通

早期的 kubernetes 採取了這種做法,kuberlet 內建相關了 dockershim 的程式碼來處理這段邏輯。這種做法可行,但是其實效能大大則扣,同時也把整體架構帶到了更複雜的境界,引進愈來愈多的元件會對開發與除錯帶來更大的成本。

可以參考下圖中的上半部份,而圖中的下半部分則是後來的改變之處


(圖片擷取自:kubernetes blog kubernetes-containerd-integration-goes-ga)

反正最後都是透過 containerd 進行操作,而本身也不太需要 docker 自己的功能,那是否就直接將 dockershim 溝通的角色從 docker engine 轉移到 containerd 即可。 因此後來又有所謂的 CRI-Containerd 的出現。

到這個階段,已經減少了一個溝通的 Daemon, 也就是 docker engine。 但是這樣並不能滿足想要最佳化的心情。

伴隨者 Containerd 本身的功能開發,提供了 Plugin 這種外掛功能的特色後,將 CRI-Containerd 的功能直接整合到該 Plugin 內就可以直接再次減少一個元件,直接讓 kubeletcontainerd 直接溝通來創建相關的 container.

相關的演進可以參考下圖

(圖片擷取自:kubernetes blog kubernetes-containerd-integration-goes-ga)

同時根據該篇文章內關於效能的評比,可以看到目前這個整合的版本不論是 CPU 或是 Memory 等系統資源的消耗都遠比過往還來得少。


(圖片擷取自:kubernetes blog kubernetes-containerd-integration-goes-ga)


(圖片擷取自:kubernetes blog kubernetes-containerd-integration-goes-ga)

這種架構下,使用者可以在一台伺服器中同時安裝 kubernetesdocker, 同時彼此會共用 containerd 來管理自己所需要的 container.

架構如下圖,有趣的一點在於這種情況下要如何確保 docker 的指令不會看到 kubernetes 所要求創建的 container, 反之亦然。
兩者都是透過 containerd 來創建 Container, 幸好有鑒於 containerd 本身提供的 namespace 的功能,可以確保不同的客戶端 docekrd, CRI-plugin 都可以有自己的 namespace,所以用 docker ps 就不會互相影響到彼此的運作。

(圖片擷取自:kubernetes blog kubernetes-containerd-integration-goes-ga)

不過上述的假設是 啟用 containerdkubernetes cluster 內才會有這個效果。

根據 這篇官方文章 install-kubeadm,目前預設的情況下都還是採用第一種方案, dockershim 的方式來使用,若需要使用 containerd 則必須要先安裝 containerd 到系統之中並且安裝 kubernetes 時設定特定的參數來切換過去。

下一篇文章就會跟大家分享要如何透過創建一個可以使用 containerdlocal kubernetes cluster, 並且使用 CRI 的工具 crictl 來操作相關的容器創建。

參考