1. 概述
進(jìn)入 K8s 的世界,會(huì)發(fā)現(xiàn)有很多方便擴(kuò)展的 Interface,包括 CNI, CSI, CRI 等挣郭,將這些接口抽象出來(lái),是為了更好的提供開(kāi)放疗韵、擴(kuò)展、規(guī)范等能力侄非。
K8s 網(wǎng)絡(luò)模型采用 CNI(Container Network Interface, 容器網(wǎng)絡(luò)接口) 協(xié)議蕉汪,只要提供一個(gè)標(biāo)準(zhǔn)的接口,就能為同樣滿足該協(xié)議的所有容器平臺(tái)提供網(wǎng)絡(luò)功能逞怨。
CNI 是 CoreOS 提出的一種容器網(wǎng)絡(luò)規(guī)范者疤,目前已被 Apache Mesos、Cloud Foundry叠赦、Kubernetes驹马、Kurma、rkt 等眾多開(kāi)源項(xiàng)目所采用除秀,同時(shí)也是一個(gè) CNCF(Cloud Native Computing Foundation) 項(xiàng)目糯累。可以預(yù)見(jiàn)册踩,CNI 將會(huì)成為未來(lái)容器網(wǎng)絡(luò)的標(biāo)準(zhǔn)泳姐。
本文將從 kubelet 啟動(dòng)、Pod 創(chuàng)建/刪除暂吉、Docker 創(chuàng)建/刪除 Container胖秒、CNI RPC 調(diào)用、容器網(wǎng)絡(luò)配置等核心流程慕的,對(duì) CSI 實(shí)現(xiàn)機(jī)制進(jìn)行了解析阎肝。
流程概覽如下:
本文及后續(xù)相關(guān)文章都基于 K8s v1.22
2. 從網(wǎng)絡(luò)模型說(shuō)起
容器的網(wǎng)絡(luò)技術(shù)日新月異,經(jīng)過(guò)多年發(fā)展肮街,業(yè)界逐漸聚焦到 Docker 的 CNM(Container Network Model, 容器網(wǎng)絡(luò)模型) 和 CoreOS 的 CNI(Container Network Interface, 容器網(wǎng)絡(luò)接口)风题。
2.1 CNM 模型
CNM 是一個(gè)被 Docker 提出的規(guī)范。現(xiàn)在已經(jīng)被 Cisco Contiv, Kuryr, Open Virtual Networking (OVN), Project Calico, VMware 和 Weave 這些公司和項(xiàng)目所采納低散。
Libnetwork 是 CNM 的原生實(shí)現(xiàn)俯邓。它為 Docker daemon 和網(wǎng)絡(luò)驅(qū)動(dòng)程序之間提供了接口。網(wǎng)絡(luò)控制器負(fù)責(zé)將驅(qū)動(dòng)和一個(gè)網(wǎng)絡(luò)進(jìn)行對(duì)接熔号。每個(gè)驅(qū)動(dòng)程序負(fù)責(zé)管理它所擁有的網(wǎng)絡(luò)以及為該網(wǎng)絡(luò)提供的各種服務(wù)稽鞭,例如 IPAM 等等。由多個(gè)驅(qū)動(dòng)支撐的多個(gè)網(wǎng)絡(luò)可以同時(shí)并存引镊。原生驅(qū)動(dòng)包括 none, bridge, overlay 以及 MACvlan朦蕴。
但是篮条,container runtime 會(huì)在不同情況下使用到不同的插件,這帶來(lái)了復(fù)雜性吩抓。另外涉茧,CNM 需要使用分布式存儲(chǔ)系統(tǒng)來(lái)保存網(wǎng)絡(luò)配置信息,例如 etcd疹娶。
- Network Sandbox:容器內(nèi)部的網(wǎng)絡(luò)棧伴栓,包括網(wǎng)絡(luò)接口、路由表雨饺、DNS 等配置的管理钳垮。Sandbox 可用 Linux 網(wǎng)絡(luò)命名空間、FreeBSD Jail 等機(jī)制進(jìn)行實(shí)現(xiàn)额港。一個(gè) Sandbox 可以包含多個(gè) Endpoint饺窿。
- Endpoint:用于將容器內(nèi)的 Sandbox 與外部網(wǎng)絡(luò)相連的網(wǎng)絡(luò)接口∫普叮可以使用 veth pair肚医、Open vSwitch 的內(nèi)部 port 等技術(shù)進(jìn)行實(shí)現(xiàn)。一個(gè) Endpoint 僅能夠加入一個(gè) Network向瓷。
- Network:可以直接互連的 Endpoint 的集合肠套。可以通過(guò) Linux bridge风罩、VLAN 等技術(shù)進(jìn)行實(shí)現(xiàn)糠排。一個(gè) Network 包含多個(gè) Endpoint。
2.2 CNI 模型
CNI 是由 CoreOS 提出的一個(gè)容器網(wǎng)絡(luò)規(guī)范超升。已采納改規(guī)范的包括 Apache Mesos, Cloud Foundry, Kubernetes, Kurma 和 rkt入宦。另外 Contiv Networking, Project Calico 和 Weave 這些項(xiàng)目也為 CNI 提供插件。
CNI 對(duì)外暴露了從一個(gè)網(wǎng)絡(luò)里面添加和剔除容器的接口室琢。CNI 使用一個(gè) json 配置文件保存網(wǎng)絡(luò)配置信息乾闰。和 CNM 不一樣,CNI 不需要一個(gè)額外的分布式存儲(chǔ)引擎盈滴。
一個(gè)容器可以被加入到被不同插件所驅(qū)動(dòng)的多個(gè)網(wǎng)絡(luò)之中涯肩。一個(gè)網(wǎng)絡(luò)有自己對(duì)應(yīng)的插件和唯一的名稱。CNI 插件需要提供兩個(gè)命令:ADD 用來(lái)將網(wǎng)絡(luò)接口加入到指定網(wǎng)絡(luò)巢钓,DEL 用來(lái)將其移除病苗。這兩個(gè)接口分別在容器被創(chuàng)建和銷(xiāo)毀的時(shí)候被調(diào)用。
CNI支持與第三方 IPAM 的集成症汹,可以用于任何容器 runtime硫朦。CNM 從設(shè)計(jì)上就僅僅支持 Docker。由于 CNI 簡(jiǎn)單的設(shè)計(jì)背镇,許多人認(rèn)為編寫(xiě) CNI 插件會(huì)比編寫(xiě) CNM 插件來(lái)得簡(jiǎn)單咬展。
3. CNI 插件
CNI 插件是二進(jìn)制可執(zhí)行文件泽裳,會(huì)被 kubelet 調(diào)用。啟動(dòng) kubelet --network-plugin=cni, --cni-conf-dir 指定 networkconfig 配置破婆,默認(rèn)路徑是:/etc/cni/net.d涮总。另外,--cni-bin-dir 指定 plugin 可執(zhí)行文件路徑祷舀,默認(rèn)路徑是:/opt/cni/bin瀑梗。
看一個(gè) CNI Demo:
在默認(rèn)網(wǎng)絡(luò)配置目錄,配置兩個(gè) xxx.conf:一個(gè) type: "bridge" 網(wǎng)橋裳扯,另一個(gè) type: "loopback" 回環(huán)網(wǎng)卡夺克。
$ mkdir -p /etc/cni/net.d
$ cat >/etc/cni/net.d/10-mynet.conf <<EOF
{
"cniVersion": "0.2.0", // CNI Spec 版本
"name": "mynet", // 自定義名稱
"type": "bridge", // 插件類型 bridge
"bridge": "cni0", // 網(wǎng)橋名稱
"isGateway": true, // 是否作為網(wǎng)關(guān)
"ipMasq": true, // 是否設(shè)置 IP 偽裝
"ipam": {
"type": "host-local", // IPAM 類型 host-local
"subnet": "10.22.0.0/16", // 子網(wǎng)段
"routes": [
{ "dst": "0.0.0.0/0" } // 目標(biāo)路由段
]
}
}
EOF
$ cat >/etc/cni/net.d/99-loopback.conf <<EOF
{
"cniVersion": "0.2.0", // CNI Spec 版本
"name": "lo", // 自定義名稱
"type": "loopback" // 插件類型 loopback
}
EOF
CNI 插件可分為三類:
Main 插件:用來(lái)創(chuàng)建具體網(wǎng)絡(luò)設(shè)備的二進(jìn)制文件。比如嚎朽,bridge、ipvlan柬帕、loopback哟忍、macvlan、ptp(point-to-point, Veth Pair 設(shè)備)陷寝,以及 vlan锅很。如開(kāi)源的 Flannel、Weave 等項(xiàng)目凤跑,都屬于 bridge 類型的 CNI 插件爆安,在具體的實(shí)現(xiàn)中,它們往往會(huì)調(diào)用 bridge 這個(gè)二進(jìn)制文件仔引。
Meta 插件:由 CNI 社區(qū)維護(hù)的內(nèi)置 CNI 插件扔仓,不能作為獨(dú)立的插件使用,需要調(diào)用其他插件咖耘。tuning翘簇,是一個(gè)通過(guò) sysctl 調(diào)整網(wǎng)絡(luò)設(shè)備參數(shù)的二進(jìn)制文件;portmap儿倒,是一個(gè)通過(guò) iptables 配置端口映射的二進(jìn)制文件版保;bandwidth,是一個(gè)使用 Token Bucket Filter (TBF) 來(lái)進(jìn)行限流的二進(jìn)制文件夫否。
IPAM 插件:IP Address Management彻犁,它是負(fù)責(zé)分配 IP 地址的二進(jìn)制文件。比如凰慈,dhcp汞幢,這個(gè)文件會(huì)向 DHCP 服務(wù)器發(fā)起請(qǐng)求;host-local溉瓶,則會(huì)使用預(yù)先配置的 IP 地址段來(lái)進(jìn)行分配急鳄。
4. kubelet 啟動(dòng)
kubelet 在 Node 節(jié)點(diǎn)上負(fù)責(zé) Pod 的創(chuàng)建谤民、銷(xiāo)毀、監(jiān)控上報(bào)等核心流程疾宏,通過(guò) Cobra 命令行解析參數(shù)啟動(dòng)二進(jìn)制可執(zhí)行文件张足。
啟動(dòng)入口如下:
// kubernetes/cmd/kubelet/kubelet.go
func main() {
command := app.NewKubeletCommand()
// kubelet uses a config file and does its own special
// parsing of flags and that config file. It initializes
// logging after it is done with that. Therefore it does
// not use cli.Run like other, simpler commands.
code := run(command)
os.Exit(code)
}
接著,一路往下進(jìn)行初始化:
cmd -> Run -> PreInitRuntimeService -> RunKubelet -> createAndInitKubelet -> startKubelet -> Run
其中 PreInitRuntimeService 會(huì)進(jìn)一步初始化 dockershim坎藐,一方面探測(cè)環(huán)境中的網(wǎng)絡(luò)配置文件(默認(rèn)路徑為:/etc/cni/net.d/*.conf/.conflist/.json)为牍,進(jìn)行 CNI 網(wǎng)絡(luò)配置;另一方面啟動(dòng) gRPC docker server 監(jiān)聽(tīng) client 請(qǐng)求岩馍,進(jìn)行具體的操作如 PodSandbox碉咆、Container 創(chuàng)建與刪除。
當(dāng)監(jiān)聽(tīng)到 Pod 事件時(shí)蛀恩,進(jìn)行對(duì)應(yīng) Pod 的創(chuàng)建或刪除疫铜,流程如下:
Run -> syncLoop -> SyncPodCreate/Kill -> UpdatePod -> syncPod/syncTerminatingPod -> dockershim gRPC -> Pod running/teminated
5. Pod 創(chuàng)建/刪除
K8s 中 Pod 的調(diào)諧采用 channel 生產(chǎn)者-消費(fèi)者模型實(shí)現(xiàn),具體通過(guò) PLEG(Pod Lifecycle Event Generator) 進(jìn)行 Pod 生命周期事件管理双谆。
// kubernetes/pkg/kubelet/pleg/pleg.go
// 通過(guò) PLEG 進(jìn)行 Pod 生命周期事件管理
type PodLifecycleEventGenerator interface {
Start() // 通過(guò) relist 獲取所有 Pods 并計(jì)算事件類型
Watch() chan *PodLifecycleEvent // 監(jiān)聽(tīng) eventChannel壳咕,傳遞給下游消費(fèi)者
Healthy() (bool, error)
}
Pod 事件生產(chǎn)者 - 相關(guān)代碼:
// kubernetes/pkg/kubelet/pleg/generic.go
// 生產(chǎn)者:獲取所有 Pods 列表,計(jì)算出對(duì)應(yīng)的事件類型顽馋,進(jìn)行 Sync
func (g *GenericPLEG) relist() {
klog.V(5).InfoS("GenericPLEG: Relisting")
...
// 獲取當(dāng)前所有 Pods 列表
podList, err := g.runtime.GetPods(true)
if err != nil {
klog.ErrorS(err, "GenericPLEG: Unable to retrieve pods")
return
}
for pid := range g.podRecords {
allContainers := getContainersFromPods(oldPod, pod)
for _, container := range allContainers {
// 計(jì)算事件類型:running/exited/unknown/non-existent
events := computeEvents(oldPod, pod, &container.ID)
for _, e := range events {
updateEvents(eventsByPodID, e)
}
}
}
// 遍歷所有事件
for pid, events := range eventsByPodID {
for i := range events {
// Filter out events that are not reliable and no other components use yet.
if events[i].Type == ContainerChanged {
continue
}
select {
case g.eventChannel <- events[i]: // 生產(chǎn)者:發(fā)送到事件 channel谓厘,對(duì)應(yīng)監(jiān)聽(tīng)的 goroutine 會(huì)消費(fèi)
default:
metrics.PLEGDiscardEvents.Inc()
klog.ErrorS(nil, "Event channel is full, discard this relist() cycle event")
}
}
}
...
}
Pod 事件消費(fèi)者 - 相關(guān)代碼:
// kubernetes/pkg/kubelet/kubelet.go
// 消費(fèi)者:根據(jù) channel 獲取的各類事件,進(jìn)行 Pod Sync
func (kl *Kubelet) syncLoopIteration(configCh <-chan kubetypes.PodUpdate, handler SyncHandler,
syncCh <-chan time.Time, housekeepingCh <-chan time.Time, plegCh <-chan *pleg.PodLifecycleEvent) bool {
select {
...
// 消費(fèi)者:監(jiān)聽(tīng) plegCh 的事件
case e := <-plegCh:
if e.Type == pleg.ContainerStarted {
// 更新容器的最后啟動(dòng)時(shí)間
kl.lastContainerStartedTime.Add(e.ID, time.Now())
}
if isSyncPodWorthy(e) {
if pod, ok := kl.podManager.GetPodByUID(e.ID); ok {
klog.V(2).InfoS("SyncLoop (PLEG): event for pod", "pod", klog.KObj(pod), "event", e)
// 進(jìn)行相關(guān) Pod 事件的 Sync
handler.HandlePodSyncs([]*v1.Pod{pod})
} else {
// If the pod no longer exists, ignore the event.
klog.V(4).InfoS("SyncLoop (PLEG): pod does not exist, ignore irrelevant event", "event", e)
}
}
// 容器銷(xiāo)毀事件處理:清除 Pod 內(nèi)相關(guān) Container
if e.Type == pleg.ContainerDied {
if containerID, ok := e.Data.(string); ok {
kl.cleanUpContainersInPod(e.ID, containerID)
}
}
...
}
return true
}
6. Docker 忙起來(lái)
經(jīng)過(guò)上一步 Pod 事件的生產(chǎn)與消費(fèi)傳遞寸谜,PodWorkers 會(huì)將事件轉(zhuǎn)化為 gRPC client 請(qǐng)求竟稳,然后調(diào)用 dockershim gRPC server,進(jìn)行 PodSandbox熊痴、infra-container(也叫 pause 容器) 的創(chuàng)建他爸。
接著,會(huì)調(diào)用 CNI 接口 SetUpPod 進(jìn)行相關(guān)網(wǎng)絡(luò)配置與啟動(dòng)愁拭,此時(shí)建立起來(lái)的容器網(wǎng)絡(luò)讲逛,就可以直接用于之后創(chuàng)建的業(yè)務(wù)容器如 initContainers、containers 進(jìn)行共享網(wǎng)絡(luò)岭埠。
相關(guān)代碼如下:
// kubernetes/pkg/kubelet/dockershim/docker_sandbox.go
// 啟動(dòng)運(yùn)行 Pod Sandbox
func (ds *dockerService) RunPodSandbox(ctx context.Context, r *runtimeapi.RunPodSandboxRequest) (*runtimeapi.RunPodSandboxResponse, error) {
config := r.GetConfig()
// Step 1: 拉取基礎(chǔ)鏡像(infra-container: k8s.gcr.io/pause:3.6)
image := defaultSandboxImage
if err := ensureSandboxImageExists(ds.client, image); err != nil {
return nil, err
}
// Step 2: 創(chuàng)建 Sandbox 容器
createConfig, err := ds.makeSandboxDockerConfig(config, image)
if err != nil {
return nil, fmt.Errorf("failed to make sandbox docker config for pod %q: %v", config.Metadata.Name, err)
}
createResp, err := ds.client.CreateContainer(*createConfig)
if err != nil {
createResp, err = recoverFromCreationConflictIfNeeded(ds.client, *createConfig, err)
}
// Step 3: 創(chuàng)建 Sandbox 檢查點(diǎn)(用于記錄當(dāng)前執(zhí)行到哪一步了)
if err = ds.checkpointManager.CreateCheckpoint(createResp.ID, constructPodSandboxCheckpoint(config)); err != nil {
return nil, err
}
// Step 4: 啟動(dòng) Sandbox 容器
err = ds.client.StartContainer(createResp.ID)
if err != nil {
return nil, fmt.Errorf("failed to start sandbox container for pod %q: %v", config.Metadata.Name, err)
}
// Step 5: 對(duì) Sandbox 容器進(jìn)行網(wǎng)絡(luò)配置
err = ds.network.SetUpPod(config.GetMetadata().Namespace, config.GetMetadata().Name, cID, config.Annotations, networkOptions)
if err != nil {
// 如果網(wǎng)絡(luò)配置失敗盏混,則回滾:刪除建立起來(lái)的 Pod 網(wǎng)絡(luò)
err = ds.network.TearDownPod(config.GetMetadata().Namespace, config.GetMetadata().Name, cID)
if err != nil {
errList = append(errList, fmt.Errorf("failed to clean up sandbox container %q network for pod %q: %v", createResp.ID, config.Metadata.Name, err))
}
// 停止容器運(yùn)行
err = ds.client.StopContainer(createResp.ID, defaultSandboxGracePeriod)
...
}
return resp, nil
}
流程圖小結(jié)如下:
根據(jù)社區(qū)討論(https://kubernetes.io/blog/2020/12/02/dockershim-faq/),dockershim 相關(guān)代碼將會(huì)在 2021 底左右移出 K8s 主干代碼惜论,之后將統(tǒng)一使用 CRI(Container Runtime Interface, 容器運(yùn)行時(shí)接口) 進(jìn)行容器生命周期管理许赃。
7. CNI RPC 接口
CNI 標(biāo)準(zhǔn)規(guī)范接口,包含了添加馆类、檢查混聊、驗(yàn)證、刪除網(wǎng)絡(luò)等接口乾巧,并提供了按列表或單個(gè)進(jìn)行網(wǎng)絡(luò)配置的兩組接口句喜,方便用戶靈活使用预愤。
CNI 從容器管理系統(tǒng)(dockershim) 處獲取運(yùn)行時(shí)信息(Container Runtime),包括 network namespace 的路徑咳胃,容器 ID 以及 network interface name植康,再?gòu)娜萜骶W(wǎng)絡(luò)的配置文件中加載網(wǎng)絡(luò)配置信息,再將這些信息傳遞給對(duì)應(yīng)的插件展懈,由插件進(jìn)行具體的網(wǎng)絡(luò)配置工作销睁,并將配置的結(jié)果再返回到容器管理系統(tǒng)中。
用戶若要編寫(xiě)自己的 CNI 插件存崖,則可專注于實(shí)現(xiàn)圖中這些 RPC 接口即可冻记,然后可以與官方維護(hù)的三類基礎(chǔ)插件自由組合,形成多種多樣的容器網(wǎng)絡(luò)解決方案来惧。
8. 小結(jié)
本文通過(guò)分析 K8s 中 kubelet 啟動(dòng)冗栗、Pod 創(chuàng)建/刪除、Docker 創(chuàng)建/刪除 Container供搀、CNI RPC 調(diào)用贞瞒、容器網(wǎng)絡(luò)配置等核心流程,對(duì) CNI 實(shí)現(xiàn)機(jī)制進(jìn)行了解析趁曼,通過(guò)源碼、圖文方式說(shuō)明了相關(guān)流程邏輯棕洋,以期更好的理解 K8s CNI 運(yùn)行流程挡闰。
K8s 網(wǎng)絡(luò)模型采用 CNI(Container Network Interface, 容器網(wǎng)絡(luò)接口) 協(xié)議,只要提供一個(gè)標(biāo)準(zhǔn)的接口掰盘,就能為同樣滿足該協(xié)議的所有容器平臺(tái)提供網(wǎng)絡(luò)功能摄悯。CNI 目前已被眾多開(kāi)源項(xiàng)目所采用,同時(shí)也是一個(gè) CNCF(Cloud Native Computing Foundation) 項(xiàng)目愧捕∩菅保可以預(yù)見(jiàn),CNI 將會(huì)成為未來(lái)容器網(wǎng)絡(luò)的標(biāo)準(zhǔn)次绘。
PS: 更多內(nèi)容請(qǐng)關(guān)注 k8s-club