共计 7280 个字符,预计需要花费 19 分钟才能阅读完成。
Overview
之前在 Kubernetes 学习笔记之 kube-proxy service 实现原理 学习到 calico 会在 worker 节点上为 pod 创立路由 route 和虚构网卡 virtual interface,并为 pod 调配 pod ip,以及为 worker 节点调配 pod cidr 网段。
咱们生产 k8s 网络插件应用 calico cni,在装置时会装置两个插件:calico 和 calico-ipam,官网装置文档 Install the plugin 也说到了这一点,而这两个插件代码在 calico.go,代码会编译出两个二进制文件:calico 和 calico-ipam。calico 插件次要用来创立 route 和 virtual interface,而 calico-ipam 插件次要用来调配 pod ip 和为 worker 节点调配 pod cidr。
重要问题是,calico 是如何做到的?
Sandbox container
kubelet 过程在开始启动时,会调用容器运行时的 SyncPod 来创立 pod 内相干容器,次要做了几件事件 L657-L856:
- 创立 sandbox container,这里会调用 cni 插件创立 network 等步骤,同时思考了边界条件,创立失败会 kill sandbox container 等等
- 创立 ephemeral containers、init containers 和一般的 containers。
这里只关注创立 sandbox container 过程,只有这一步会创立 pod network,这个 sandbox container 创立好后,其余 container 都会和其共享同一个 network namespace,所以一个 pod 内各个容器看到的网络协议栈是同一个,ip 地址都是雷同的,通过 port 来辨别各个容器。具体创立过程,会调用容器运行时服务创立容器,这里会先筹备好 pod 的相干配置数据,创立 network namespace 时也须要这些配置数据 L36-L138:
func (m *kubeGenericRuntimeManager) createPodSandbox(pod *v1.Pod, attempt uint32) (string, string, error) {
// 生成 pod 相干配置数据
podSandboxConfig, err := m.generatePodSandboxConfig(pod, attempt)
// ...
// 这里会在宿主机上创立 pod logs 目录,在 /var/log/pods/{namespace}_{pod_name}_{uid} 目录下
err = m.osInterface.MkdirAll(podSandboxConfig.LogDirectory, 0755)
// ...
// 调用容器运行时创立 sandbox container,咱们生产 k8s 这里是 docker 创立
podSandBoxID, err := m.runtimeService.RunPodSandbox(podSandboxConfig, runtimeHandler)
// ...
return podSandBoxID, "", nil
}
k8s 应用 cri(container runtime interface) 来形象出标准接口,目前 docker 还不反对 cri 接口,所以 kubelet 做了个适配模块 dockershim,代码在 pkg/kubelet/dockershim
。下面代码中的 runtimeService 对象就是 dockerService 对象,所以能够看下 dockerService.RunPodSandbox()
代码实现 L76-L197:
// 创立 sandbox container,以及为该 container 创立 network
func (ds *dockerService) RunPodSandbox(ctx context.Context, r *runtimeapi.RunPodSandboxRequest) (*runtimeapi.RunPodSandboxResponse, error) {config := r.GetConfig()
// Step 1: Pull the image for the sandbox.
// 1. 拉取镜像
image := defaultSandboxImage
podSandboxImage := ds.podSandboxImage
if len(podSandboxImage) != 0 {image = podSandboxImage}
if err := ensureSandboxImageExists(ds.client, image); err != nil {return nil, err}
// Step 2: Create the sandbox container.
// 2. 创立 sandbox container
createResp, err := ds.client.CreateContainer(*createConfig)
// ...
resp := &runtimeapi.RunPodSandboxResponse{PodSandboxId: createResp.ID}
ds.setNetworkReady(createResp.ID, false)
// Step 3: Create Sandbox Checkpoint.
// 3. 创立 checkpoint
if err = ds.checkpointManager.CreateCheckpoint(createResp.ID, constructPodSandboxCheckpoint(config)); err != nil {return nil, err}
// Step 4: Start the sandbox container.
// Assume kubelet's garbage collector would remove the sandbox later, if
// startContainer failed.
// 4. 启动容器
err = ds.client.StartContainer(createResp.ID)
// ...
// Step 5: Setup networking for the sandbox.
// All pod networking is setup by a CNI plugin discovered at startup time.
// This plugin assigns the pod ip, sets up routes inside the sandbox,
// creates interfaces etc. In theory, its jurisdiction ends with pod
// sandbox networking, but it might insert iptables rules or open ports
// on the host as well, to satisfy parts of the pod spec that aren't
// recognized by the CNI standard yet.
// 5. 这一步为 sandbox container 创立网络,次要是调用 calico cni 插件创立路由和虚构网卡,以及为 pod 调配 pod ip,为该宿主机划分 pod 网段
cID := kubecontainer.BuildContainerID(runtimeName, createResp.ID)
networkOptions := make(map[string]string)
if dnsConfig := config.GetDnsConfig(); dnsConfig != nil {
// Build DNS options.
dnsOption, err := json.Marshal(dnsConfig)
if err != nil {return nil, fmt.Errorf("failed to marshal dns config for pod %q: %v", config.Metadata.Name, err)
}
networkOptions["dns"] = string(dnsOption)
}
// 这一步调用网络插件来 setup sandbox pod
// 因为咱们网络插件都是 cni(container network interface),所以代码在 pkg/kubelet/dockershim/network/cni/cni.go
err = ds.network.SetUpPod(config.GetMetadata().Namespace, config.GetMetadata().Name, cID, config.Annotations, networkOptions)
// ...
return resp, nil
}
因为咱们网络插件都是 cni(container network interface),代码 ds.network.SetUpPod
持续追下去发现理论调用的是 cniNetworkPlugin.SetUpPod()
,代码在 pkg/kubelet/dockershim/network/cni/cni.go:
func (plugin *cniNetworkPlugin) SetUpPod(namespace string, name string, id kubecontainer.ContainerID, annotations, options map[string]string) error {
// ...
netnsPath, err := plugin.host.GetNetNS(id.ID)
// ...
// Windows doesn't have loNetwork. It comes only with Linux
if plugin.loNetwork != nil {
// 增加 loopback
if _, err = plugin.addToNetwork(cniTimeoutCtx, plugin.loNetwork, name, namespace, id, netnsPath, annotations, options); err != nil {return err}
}
// 调用网络插件创立网络相干资源
_, err = plugin.addToNetwork(cniTimeoutCtx, plugin.getDefaultNetwork(), name, namespace, id, netnsPath, annotations, options)
return err
}
func (plugin *cniNetworkPlugin) addToNetwork(ctx context.Context, network *cniNetwork, podName string, podNamespace string, podSandboxID kubecontainer.ContainerID, podNetnsPath string, annotations, options map[string]string) (cnitypes.Result, error) {
// 这一步筹备网络插件所需相干参数,这些参数最初会被 calico 插件应用
rt, err := plugin.buildCNIRuntimeConf(podName, podNamespace, podSandboxID, podNetnsPath, annotations, options)
// ...
// 这里会调用调用 cni 规范库里的 AddNetworkList 函数,最初会调用 calico 二进制命令
res, err := cniNet.AddNetworkList(ctx, netConf, rt)
// ...
return res, nil
}
// 这些参数次要包含 container id,pod 等相干参数
func (plugin *cniNetworkPlugin) buildCNIRuntimeConf(podName string, podNs string, podSandboxID kubecontainer.ContainerID, podNetnsPath string, annotations, options map[string]string) (*libcni.RuntimeConf, error) {
rt := &libcni.RuntimeConf{
ContainerID: podSandboxID.ID,
NetNS: podNetnsPath,
IfName: network.DefaultInterfaceName,
CacheDir: plugin.cacheDir,
Args: [][2]string{{"IgnoreUnknown", "1"},
{"K8S_POD_NAMESPACE", podNs},
{"K8S_POD_NAME", podName},
{"K8S_POD_INFRA_CONTAINER_ID", podSandboxID.ID},
},
}
// port mappings 相干参数
// ...
// dns 相干参数
// ...
return rt, nil
}
addToNetwork()
函数会调用 cni 规范库里的 AddNetworkList 函数。CNI 是容器网络标准接口 Container Network Interface,这个代码仓库提供了 CNI 标准接口的相干实现,所有 K8s 网络插件都必须实现该 CNI 代码仓库中的接口,K8s 网络插件如何实现标准可见 SPEC.md,咱们也可实现遵循该标准规范实现一个简略的网络插件。
所以 kubelet、cni 和 calico 的三者关系就是:kubelet 调用 cni 标准规范代码包,cni 调用 calico 插件二进制文件。cni 代码包中的 AddNetworkList 相干代码如下 AddNetworkList:
func (c *CNIConfig) addNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (types.Result, error) {c.ensureExec()
pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
// ...
// pluginPath 就是 calico 二进制文件门路,这里其实就是调用 calico ADD 命令,并传递相干参数,参数也是上文形容的曾经筹备好了的
// 参数传递也是写入了环境变量,calico 二进制文件能够从环境变量里取值
return invoke.ExecPluginWithResult(ctx, pluginPath, newConf.Bytes, c.args("ADD", rt), c.exec)
}
// AddNetworkList executes a sequence of plugins with the ADD command
func (c *CNIConfig) AddNetworkList(ctx context.Context, list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
// ...
for _, net := range list.Plugins {result, err = c.addNetwork(ctx, list.Name, list.CNIVersion, net, result, rt)
// ...
}
// ...
return result, nil
}
以上 pluginPath 就是 calico 二进制文件门路,这里 calico 二进制文件门路参数是在启动 kubelet 时通过参数 --cni-bin-dir
传进来的,可见官网 kubelet command-line-tools-reference,并且启动参数 --cni-conf-dir
蕴含 cni 配置文件门路,该门路蕴含 cni 配置文件内容相似如下:
{
"name": "k8s-pod-network",
"cniVersion": "0.3.1",
"plugins": [
{
"type": "calico",
"log_level": "debug",
"log_file_path": "/var/log/calico/cni/cni.log",
"datastore_type": "kubernetes",
"nodename": "minikube",
"mtu": 1440,
"ipam": {"type": "calico-ipam"},
"policy": {"type": "k8s"},
"kubernetes": {"kubeconfig": "/etc/cni/net.d/calico-kubeconfig"}
},
{
"type": "portmap",
"snat": true,
"capabilities": {"portMappings": true}
},
{
"type": "bandwidth",
"capabilities": {"bandwidth": true}
}
]
}
cni 相干代码是个规范骨架,外围还是须要调用第三方网络插件来实现为 sandbox 创立网络资源。cni 也提供了一些示例 plugins,代码仓库见 containernetworking/plugins,并配有文档阐明见 plugins docs,比方能够参考学习官网提供的 static IP address management plugin。
总结
总之,kubelet 在创立 sandbox container 时候,会先调用 cni 插件命令,如 calico ADD
命令并通过环境变量传递相干命令参数,来给 sandbox container 创立 network 相干资源对象,比方 calico 会创立 route 和 virtual interface,以及为 pod 调配 ip 地址,和从集群网段 cluster cidr 中为以后 worker 节点调配 pod cidr 网段,并且会把这些数据写入到 calico datastore 数据库里。
所以,关键问题,还是得看 calico 插件代码是如何做的。