编辑推荐: |
本文分析K8s中kubelet启动、Pod创建/删除、Docker说明创建/删除Container、CNI
RPC调用、容器网络配置等核心流程,通过对CNI实现机制进行解析,通过源码、图文方式相关流程逻辑,以期更好的理解
K8s CNI 运行流程。
本文来自于微信公众号腾讯技术工程,由Linda编辑、推荐。 |
|
1. 概述
进入K8s的世界,会发现有很多方便扩展的接口,包括CNI、CSI、CRI等,将接口抽象出来,是为了更好的提供开放、扩展、规范等能力。
K8s网络模型采用CNI(Container Network Interface)只要一个标准的网络接口,容器网络接口,提供为同样满足协议的所有容器平台的功能。
CNI 提出的一个容器网络,目前被 Apache Mesos、Cloud Foundry、Kubernetes、Kurma、rkt
等 OS 规范的项目所采用,同时也是一个 CNCF(Cloud Native Computing Foundation)
项目。可以预见,CNI 将实现成为未来涵盖网络的标准。
本文调用容器包进行网络配置启动、Po创建/删除、Docker创建/删除容器CNI RPC 、 CNI
实现机制等核心,CNI 实现机制了解析。
流程概览如下:
K8s-CNI
本文v相关文章都基于K8s1.22
2.从网络模型说起
容器的网络技术日新异,历经多年发展的接口,备受关注 Docker Docker CNM(Container
Network Model, Container Network Model) 和 CoreOS CNI(Container
Network Interface, Container)。
2.1 CNM 模型
CNM 是一个被 Docker 提出的规范。现在已经被 Cisco Contiv、Kuryr、Open
Virtual Networking (OVN)、Project Calico、VMware 和 Weave
这些公司和项目所采纳。
Libnetwork 是 CNM 的蒯实现。它为驱动和网络程序驱动之间提供了接口。网络由 Docker
守护进程负责进行网络驱动和对接。每个程序都有它负责管理的网络以及该网络提供的驱动各种服务,例如 IPAM
等。由多个支持的多个网络可以同时并存。驱动包括 none、bridge、overlay 以及 MACvlan。
包含会在不同情况下使用到不同情况的信息,这带来了复杂性。
CNM模型
Network Sandbox:容纳内部的网络栈,包括网络接口、路由表、DNS等配置的管理。Sandbox可以通过Linux网络命名空间、FreeBSD
Jail等机制进行实现。一个Sandbox可以包含多个Endpoint。
Endpoint:将容器内的Sandbox用于与外部网络的连接网络接口。可以使用veth pair、Open
vSwitch仅在内部端口等技术进行实现。一个Endpoint能够加入一个网络。
Network:可以直接互连的Endpoint的集合。可以通过Linux bridge、VLAN等技术进行。一个Network包含多个Endpoint。
2.2 CNI 模型
CNI 是由 CoreOS 提出的一个容器网络规范。已采用改规范的 Apache Mesos、Cloud
Foundry、Kubernetes、Kurma 和 rkt。另外项目包括 Contiv Networking、Project
Calico 和 Weave 也为 CNI 插件提供。
CNI 前端暴露从一个网络里面添加和配置容器的接口。CNI 使用暴露 json 保存配置信息。和
CNI 不一样,CNI 不需要一个额外的配置存储引擎。
一个容器可以被加入到不同的插件所驱动的多个网络中。一个网络有自己的插件和唯一的名称。CNI 插件需要提供两个命令:ADD
使用将网络接口加入到指定网络中,DEL将其取出。
CNI模型
NI 支持与多个 CNI 可用于从任何 IPAM 容器中集成,CNM 的运行时支持。CNM 的 Docker。简单简单的设计,插件人认为写
CNI 会比写 CN 插件来得得心应手。
3. CNI 插件
CNI插件是配置kubelet文件,会调用。启动配置--network-plugin=cni,默认指定路径是:/etc/cni/net.d。另外,-
-cni-bin-dir 指定插件运行文件路径,默认路径是:/cni/bin。
看一个 CNI Demo:在默认网络配置目录,配置两个 xxx.conf:一个类型:“bridge”网桥,另一个类型:“loopback”回环网卡。
$
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",
// 网桥名称
"isGateway": true,
// 是否作为网关
"ipMasq": true, //
是否设置 IP 伪装
"ipam": {
"type": "host-local",
// IPAM 类型 host-local
"subnet": "10.22.0.0/16",
// 子网段
"routes": [
{ "dst": "0.0.0.0/0"
} // 目标路由段
]
}
}
EOF
$ cat >/etc/cni/net.d/99-loopback.conf
<<EOF
{
"cniVersion": "0.2.0",
// CNI Spec 版本
"name": "lo",
// 自定义名称
"type": "loopback"
// 插件类型 loopback
}
EOF |
CNI 插件可分为三类:
主要使用创建具体网络设备、插件的ip、vlan文件。vlan、ptp(point-to-point
Pair 设备),以及vlan如开源的Flannel、Weave等,都属于vlan、vlan文件。
bridge 类型的 CNI ,在具体的会调用桥接实现这个插件文件。
元插件 CNI 插件通过构建 CNI 独立插件的使用插件来调整,需要是调用网络设备参数,不能通过一个文件来执行;
;带宽过滤器(来进行限流的TBF) Bucket 文件。
IP它是插件IP地址,IP地址,DHCP文件的IP地址:配置文件。比如,dh是这个文件向服务器请求管理的;主机本地地址,是负责使用该配置的IP地址段来进行的。
K8s-CNI-插件
4. kubelet 启动
kube 监控节点 负责的创建、上传文件、视频播报,等监控监 测 监 测 监 测 在 监 测 上 接
接 胸罩。
启动入口如下:
//
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)
} |
支持,一路往下进行初始化:
cmd -> 运行
-> PreInitRuntimeService -> RunKubelet ->
createAndInitKubelet -> startKubelet ->
运行 |
其中PreInInService会进一步启动dockershim,它的网络配置(默认路径为:等/中的网络配置/netd/*.conf/conflist/.json),进行CNI网络配置;启动ni文件gRPC
docker server 监听客户端请求,进行具体的操作如 PodSandbox、Container
创建与删除。
当监听到 Pod 事件时,进行 Pod 的创建或删除,流程如下:
运行 -> syncLoop
-> SyncPodCreate/Kill -> UpdatePod ->
syncPod/syncTerminatingPod -> dockershim gRPC
-> Pod 运行/终止 |
5. Pod 创建/删除
K8中Pod的具体应用者采用PLEG(Pod Lifecycle Event Generator)的生命周期事件管理实现了具体的消费者模式。
//
kubernetes/pkg/kubelet/pleg/pleg.go
// 通过 PLEG 进行 Pod 生命周期事件管理
type PodLifecycleEventGenerator
interface {
Start() // 通过 relist 获取所有 Pods
并计算事件类型
Watch() chan *PodLifecycleEvent
// 监听 eventChannel,传递给下游消费者
Healthy() (bool, error)
} |
Pod 事件生产者 - 相关代码:
//
kubernetes/pkg/kubelet/pleg/generic.go
// 生产者:获取所有 Pods 列表,计算出对应的事件类型,进行
Sync
func (g *GenericPLEG) relist()
{
klog.V(5).InfoS("GenericPLEG:
Relisting")
...
// 获取当前所有 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
{
// 计算事件类型: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]:
// 生产者:发送到事件 channel,对应监听的 goroutine 会消费
default:
metrics.PLEGDiscardEvents.Inc()
klog.ErrorS(nil, "Event
channel is full, discard this relist() cycle
event")
}
}
}
...
} |
Pod 事件消费者 - 相关代码:
//
kubernetes/pkg/kubelet/kubelet.go
// 消费者:根据 channel 获取的各类事件,进行
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 {
...
// 消费者:监听 plegCh 的事件
case e := <-plegCh:
if e.Type == pleg.ContainerStarted
{
// 更新容器的最后启动时间
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)
// 进行相关 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)
}
}
// 容器销毁事件处理:清除 Pod 内相关 Container
if e.Type == pleg.ContainerDied
{
if containerID, ok := e.Data.(string);
ok {
kl.cleanUpContainersInPod(e.ID,
containerID)
}
}
...
}
return true
} |
6. Docker 忙起来
上一步生产的消费上传,PodWorkers 与 Podbox 事件创建事件返回为 gRPC 客户端,然后调用他
gRPC 服务器,进行 PodSand、infra-container(也称为暂停容器)的。
然后,会调用 CNI 接口 SetUpPod 进行相关网络配置与启动,此时创建起来的容器网络,就可以直接用于创建之后的业务容器如
initContainers、containers 进行共享网络。
相关代码如下:
//
kubernetes/pkg/kubelet/dockershim/docker_sandbox.go
// 启动运行 Pod Sandbox
func (ds *dockerService) RunPodSandbox(ctx
context.Context, r *runtimeapi.RunPodSandboxRequest)
(*runtimeapi.RunPodSandboxResponse, error) {
config := r.GetConfig()
// Step 1: 拉取基础镜像(infra-container:
k8s.gcr.io/pause:3.6)
image := defaultSandboxImage
if err := ensureSandboxImageExists(ds.client,
image); err != nil {
return nil, err
}
// Step 2: 创建 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: 创建 Sandbox 检查点(用于记录当前执行到哪一步了)
if err = ds.checkpointManager.CreateCheckpoint(createResp.ID,
constructPodSandboxCheckpoint(config)); err
!= nil {
return nil, err
}
// Step 4: 启动 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: 对 Sandbox 容器进行网络配置
err = ds.network.SetUpPod(config.GetMetadata().Namespace,
config.GetMetadata().Name, cID, config.Annotations,
networkOptions)
if err != nil {
// 如果网络配置失败,则回滚:删除建立起来的 Pod
网络
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))
}
// 停止容器运行
err = ds.client.StopContainer(createResp.ID,
defaultSandboxGracePeriod)
...
}
return resp, nil
} |
小结如下:
K8s-CNI-流程
可知社区Dockershim Deprecation FAQ,dockershim 相关代码在2021年底左右移出K8s主干代码,之后将统一使用CRI(Container
Runtime Interface, Container运行时接口) 进行容器生命周期管理。
7. CNI RPC接口
CNI添加了、、、删除网络接口,并提供了按或每个进行网络配置规范的接口等检查,方便用户灵活使用。
CNI从这些容器管理系统(dockershim)获取处处运行时信息(Container Runtime),包括网络命名空间的路径、容器ID以及网络接口名称,再从容器网络的配置文件中加载网络配置信息,再将信息传递给的插件,由插件具体的网络配置,并将配置的结果再返回到容器系统中进行管理。
K8s-CNI-RPC
用户若要编写自己的 CNI 插件,则可专注于显示然后这些 RPC 接口启用,可以与官方维护的三类基础组合,形成多种包含网络的自由解决方案。
8.小结
本文分析K8s中kubelet启动、Pod创建/删除、Docker说明创建/删除Container、CNI
RPC调用、容器网络配置等核心流程,通过对CNI实现机制进行解析,通过源码、图文方式相关流程逻辑,以期更好的理解
K8s CNI 运行流程。
K8s只要采用CNI(Container Network Interface)协议的网络接口,已经提供一个标准的网络接口功能,既是满足该协议的所有开放平台所采用的网络接口,同时也是提供的。一个CNCF(Cloud
Native Computing)项目。可以预见,CNI未来将成为容器网络的标准。 |