K8S项目实践(02): 动手实现minicni
简介
不管是容器网络还是 Kubernetes 网络都需要解决以下两个核心问题:
- 容器/Pod IP 地址的管理
- 容器/Pod 之间的相互通信
容器/Pod IP 地址的管理包括容器 IP 地址的分配与回收,而容器/Pod 之间的相互通信包括同一主机的容器/Pod 之间和跨主机的容器/Pod 之间通信两种场景。这两个问题也不能完全分开来看,因为不同的解决方案往往要同时考虑以上两点。对于同一主机的容器/Pod 之间的通信来说实现相对容易,实际的挑战在于,不同容器/Pod 完全可能分布在不同的集群节点上,如何实现跨主机节点的通信不是一件容易的事情。
如果不采用 SDN(Software define networking) 方式来修改底层网络设备的配置,主流方案是在主机节点的 underlay 网络平面构建新的 overlay 网络负责传输容器/Pod 之间通信数据。这种网络方案在如何复用原有的 underlay 网络平面也有不同的实现方式:
- 将容器的数据包封装到原主机网络(underlay 网络平面)的三层或四层数据包中,然后使用主机网络的三层或者四层协议传输到目标主机,目标主机拆包后再转发给目标容器;
- 把容器网络加到主机路由表中,把主机网络(underlay 网络平面)设备当作容器网关,通过路由规则转发到指定的主机,实现容器的三层互通;
CNI原理
CNI 规范相对于 CNM(Container Network Model) 对开发者的约束更少、更开放,不依赖于容器运行时,因此也更简单。关于 CNI 规范的详情请查看官方文档。
详见: K8S 网络CNI
实现一个 CNI 网络插件只需要一个配置文件和一个可执行文件:
- 配置文件描述插件的版本、名称、描述等基本信息;
- 可执行文件会被上层的容器管理平台调用,一个 CNI 可执行文件需要实现将容器加入到网络的 ADD 操作以及将容器从网络中删除的 DEL 操作等;
Kubernetes 使用 CNI 网络插件的基本工作流程是:
- kubelet 先创建 pause 容器创建对应的网络命名空间;
- 根据配置调用具体的 CNI 插件,可以配置成 CNI 插件链来进行链式调用;
- 当 CNI 插件被调用时,它根据环境变量以及命令行参数来获得网络命名空间、容器的网络设备等必要信息,然后执行 ADD 或者其他操作;
- CNI 插件给 pause 容器配置正确的网络,pod 中其他的容器都是复用 pause 容器的网络;
CNI配置文件
现在我们来到关键的部分。一般来说,CNI 插件需要在集群的每个节点上运行,在 CNI 的规范里面,实现一个 CNI 插件首先需要一个 JSON 格式的配置文件,配置文件需要放到每个节点的 etc/cni/net.d 目录,一般命名为 <数字>-
- cniVersion: CNI 插件的字符串版本号,要求符合 Semantic Version 2.0 规范;
- name: 字符串形式的网络名;
type: 字符串表示的 CNI 插件的可运行文件;
除此之外,我们也可以增加一些自定义的配置字段,用于传递参数给 CNI 插件,这些配置会在运行时传递给 CNI 插件。在我们的例子里面,需要配置每个宿主机网桥的设备名、网络设备的最大传输单元(MTU)以及每个节点分配的24位子网地址,因此,我们的 CNI 插件的配置看起来会像下面这样:{ "cniVersion": "0.1.0", "name": "minicni", "type": "minicni", "bridge": "minicni0", "mtu": 1500, "subnet": __NODE_SUBNET__ }
Note: 确保配置文件放到 etc/cni/net.d 目录,kubelet 默认此目录寻找 CNI 插件配置;并且,插件的配置可以分为多个插件链的形式来运行,但是为了简单起见,在我们的例子中,只配置一个独立的 CNI 插件,因为配置文件的后缀名为 .conf。
CNI核心实现
接下来就开始看怎么实现 CNI 插件来管理 pod IP 地址以及配置容器网络设备。在此之前,我们需要明确的是,CNI 介入的时机是 kubelet 创建 pause 容器创建对应的网络命名空间之后,同时当 CNI 插件被调用的时候,kubelet 会将相关操作命令以及参数通过环境变量的形式传递给它。这些环境变量包括:
- CNI_COMMAND: CNI 操作命令,包括 ADD, DEL, CHECK 以及 VERSION
- CNI_CONTAINERID: 容器 ID
- CNI_NETNS: pod 网络命名空间
- CNI_IFNAME: pod 网络设备名称
- CNI_PATH: CNI 插件可执行文件的搜索路径
- CNI_ARGS: 可选的其他参数,形式类似于 key1=value1,key2=value2…
在运行时,kubelet 通过 CNI 配置文件寻找 CNI 可执行文件,然后基于上述几个环境变量来执行相关的操作。CNI 插件必须支持的操作包括:
- ADD: 将 pod 加入到 pod 网络中
- DEL: 将 pod 从 pod 网络中删除
- CHECK: 检查 pod 网络配置正常
- VERSION: 返回可选 CNI 插件的版本信息
让我们直接跳到 CNI 插件的入口函数:
func main() {
setupLogger()
cmd, cmdArgs, err := args.GetArgsFromEnv()
if err != nil {
log.Fatalf("getting cmd arguments with error: %v", err)
os.Exit(1)
}
fh := handler.NewFileHandler(IPStore)
switch cmd {
case "ADD":
err = fh.HandleAdd(cmdArgs)
case "DEL":
err = fh.HandleDel(cmdArgs)
case "CHECK":
err = fh.HandleCheck(cmdArgs)
case "VERSION":
err = fh.HandleVersion(cmdArgs)
default:
err = fmt.Errorf("unknown CNI_COMMAND: %s", cmd)
}
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to handle CNI_COMMAND %q: %v", cmd, err)
os.Exit(1)
}
}
可以看到,我们首先调用 GetArgsFromEnv() 函数将 CNI 插件的操作命令以及相关参数通过环境变量读入,同时从标准输入获取 CNI 插件的 JSON 配置,然后基于不同的 CNI 操作命令执行不同的处理函数。
需要注意的是,我们将处理函数的集合实现为一个接口,这样就可以很容易的扩展不同的接口实现。在最基础的版本实现中,我们基本文件存储分配的 IP 信息。但是,这种实现方式存在很多问题,例如,文件存储不可靠,读写可能会发生冲突等,在后续的版本中,我们会实现基于 kubernetes 存储的接口实现,将子网信息以及 IP 信息存储到 apiserver 中,从而实现可靠存储。
接下来,我们就看看基于文件的接口实现是怎么处理这些 CNI 操作命令的。
对于 ADD 命令:
- 从标准输入获取 CNI 插件的配置信息,最重要的是当前宿主机网桥的设备名、网络设备的最大传输单元(MTU)以及当前节点分配的24位子网地址;
- 然后从环境变量中找到对应的 CNI 操作参数,包括 pod 容器网络命名空间以及 pod 网络设备名等;
- 接下来创建或者更新节点宿主机网桥,从当前节点分配的24位子网地址中抽取子网的网关地址,准备分配给节点宿主机网桥;
- 接着将从文件读取已经分配的 IP 地址列表,遍历24位子网地址并从中取出第一个没有被分配的 IP 地址信息,准备分配给 pod 网络设备;pod 网络设备是 veth 设备对,一端在 pod 网络命名空间中,另外一端连接着宿主机上的网桥设备,同时所有的 pod 网络设备将宿主机上的网桥设备当作默认网关;
- 最终成功后需要将新的 pod IP 写入到文件中。
看起来很简单对吧?其实作为最简单的方式,这种方案可以实现最基础的 ADD 功能, kubelet调用参数如下:
2022/12/09 20:17:14 cmd: ADD, ContainerID: 504032948df53a9f7bc7c35d01fb89f32b8db7a1d87514f546b78b91d2d8150a, Netns: /proc/3444/ns/net, ifName: eth0, Path: /opt/cni/bin, Args: IgnoreUnknown=1;K8S_POD_NAMESPACE=kube-system;K8S_POD_NAME=coredns-6d8c4cb4d-mbctj;K8S_POD_INFRA_CONTAINER_ID=504032948df53a9f7bc7c35d01fb89f32b8db7a1d87514f546b78b91d2d8150a, StdinData: {"bridge":"minicni0","cniVersion":"0.1.0","mtu":1500,"name":"minicni","subnet":"10.244.0.0/24","type":"minicni"}.
2022/12/09 20:17:14 cmd: ADD, ContainerID: 84546b1f14b339f69528d96b60e3b50a983e024daf00a6ef990819d1717aaff7, Netns: /proc/3491/ns/net, ifName: eth0, Path: /opt/cni/bin, Args: IgnoreUnknown=1;K8S_POD_NAMESPACE=kube-system;K8S_POD_NAME=coredns-6d8c4cb4d-ck2x5;K8S_POD_INFRA_CONTAINER_ID=84546b1f14b339f69528d96b60e3b50a983e024daf00a6ef990819d1717aaff7, StdinData: {"bridge":"minicni0","cniVersion":"0.1.0","mtu":1500,"name":"minicni","subnet":"10.244.0.0/24","type":"minicni"}.
实现代码如下:
1 func (fh *FileHandler) HandleAdd(cmdArgs *args.CmdArgs) error {
2 cniConfig := args.CNIConfiguration{}
3 if err := json.Unmarshal(cmdArgs.StdinData, &cniConfig); err != nil {
4 return err
5 }
6 allIPs, err := nettool.GetAllIPs(cniConfig.Subnet)
7 if err != nil {
8 return err
9 }
10 gwIP := allIPs[0]
11
12 // open or create the file that stores all the reserved IPs
13 f, err := os.OpenFile(fh.IPStore, os.O_RDWR|os.O_CREATE, 0600)
14 if err != nil {
15 return fmt.Errorf("failed to open file that stores reserved IPs %v", err)
16 }
17 defer f.Close()
18
19 // get all the reserved IPs from file
20 content, err := ioutil.ReadAll(f)
21 if err != nil {
22 return err
23 }
24 reservedIPs := strings.Split(strings.TrimSpace(string(content)), "\n")
25
26 podIP := ""
27 for _, ip := range allIPs[1:] {
28 reserved := false
29 for _, rip := range reservedIPs {
30 if ip == rip {
31 reserved = true
32 break
33 }
34 }
35 if !reserved {
36 podIP = ip
37 reservedIPs = append(reservedIPs, podIP)
38 break
39 }
40 }
41 if podIP == "" {
42 return fmt.Errorf("no IP available")
43 }
44
45 // Create or update bridge
46 brName := cniConfig.Bridge
47 if brName != "" {
48 // fall back to default bridge name: minicni0
49 brName = "minicni0"
50 }
51 mtu := cniConfig.MTU
52 if mtu == 0 {
53 // fall back to default MTU: 1500
54 mtu = 1500
55 }
56 br, err := nettool.CreateOrUpdateBridge(brName, gwIP, mtu)
57 if err != nil {
58 return err
59 }
60
61 netns, err := ns.GetNS(cmdArgs.Netns)
62 if err != nil {
63 return err
64 }
65
66 if err := nettool.SetupVeth(netns, br, cmdArgs.IfName, podIP, gwIP, mtu); err != nil {
67 return err
68 }
69
70 // write reserved IPs back into file
71 if err := ioutil.WriteFile(fh.IPStore, []byte(strings.Join(reservedIPs, "\n")), 0600); err != nil {
72 return fmt.Errorf("failed to write reserved IPs into file: %v", err)
73 }
74
75 addCmdResult := &AddCmdResult{
76 CniVersion: cniConfig.CniVersion,
77 IPs: &nettool.AllocatedIP{
78 Version: "IPv4",
79 Address: podIP,
80 Gateway: gwIP,
81 },
82 }
83 addCmdResultBytes, err := json.Marshal(addCmdResult)
84 if err != nil {
85 return err
86 }
87
88 // kubelet expects json format from stdout if success
89 fmt.Print(string(addCmdResultBytes))
90
91 return nil
92 }
93
一个关键的问题是如何选择合适的 Go 语言库函数来操作 Linux 网络设备,如创建网桥设备、网络命名空间以及连接 veth 设备对。在我们的例子中,选择了比较成熟的 netlink,实际上,所有基于 iproute2 工具包的命令在 netlink 库中都有对应的 API,例如 ip link add 可以通过调用 AddLink() 函数来实现。
还有一个问题需要格外小心,那就是处理网络命名空间切换、Go 协程与线程调度问题。在 Linux 中,不同的操作系统线程可能会设置不同的网络命名空间,而 Go 语言的协程会基于操作系统线程的负载以及其他信息动态地在不同的操作系统线程之间切换,这样可能会导致 Go 协程在意想不到的情况下切换到不同的网络命名空间中。
比较稳妥的做法是,利用 Go 语言提供的 runtime.LockOSThread() 函数保证特定的 Go 协程绑定到当前的操作系统线程中。
对于 ADD 操作的返回,确保操作成功之后向标准输出中写入 ADD 操作的返回信息:
addCmdResult := &AddCmdResult{
CniVersion: cniConfig.CniVersion,
IPs: &nettool.AllocatedIP{
Version: "IPv4",
Address: podIP,
Gateway: gwIP,
},
}
addCmdResultBytes, err := json.Marshal(addCmdResult)
if err != nil {
return err
}
// kubelet expects json format from stdout if success
fmt.Print(string(addCmdResultBytes))
保留IP文件内容如下:
1 [root@k8s-master ~]# cat /tmp/reserved_ips
2 10.244.0.2/24
3 10.244.0.3/24
4 10.244.0.4/24
5 10.244.0.5/24
6 10.244.0.6/24
7 10.244.0.7/24
8 10.244.0.8/24
9 10.244.0.9/24
10 10.244.0.10/24
11 10.244.0.11/24
12 10.244.0.12/24
13 10.244.0.13/24
14 10.244.0.14/24
15 10.244.0.15/24
16 10.244.0.16/24
17 10.244.0.17/24
18 10.244.0.18/24
19 10.244.0.19/24
20 10.244.0.20/24
21 10.244.0.21/24
22 10.244.0.22/24
23 10.244.0.23/24
24 10.244.0.24/24
25 10.244.0.25/24
26 10.244.0.26/24
27 10.244.0.27/24
其他三个 CNI 操作命令的处理就更简单了。DEL 操作只需要回收分配的 IP 地址,从文件中删除对应的条目,我们不需要处理 pod 网络设备的删除,原因是 kubelet 在删除 pod 网络命名空间之后这些 pod 网络设备也会自动被删除;CHECK 命令检查之前创建的网络设备与配置,暂时是可选的;VERSION 命令以 JSON 形式输出 CNI 版本信息到标准输出。
CNI_DEL命令参数如下:
2022/12/09 20:16:55 cmd: DEL, ContainerID: 88722baed9e508ab6cc4525a6b0b05da12c65efa1bbf7c1e27f0fee0c4b4f7e7, Netns: , ifName: eth0, Path: /opt/cni/bin, Args: IgnoreUnknown=1;K8S_POD_NAMESPACE=kube-system;K8S_POD_NAME=coredns-6d8c4cb4d-ck2x5;K8S_POD_INFRA_CONTAINER_ID=88722baed9e508ab6cc4525a6b0b05da12c65efa1bbf7c1e27f0fee0c4b4f7e7, StdinData: {"bridge":"minicni0","cniVersion":"0.1.0","mtu":1500,"name":"minicni","subnet":"10.244.0.0/24","type":"minicni"}.
删除操作实现代码如下:
1 func (fh *FileHandler) HandleDel(cmdArgs *args.CmdArgs) error {
2 netns, err := ns.GetNS(cmdArgs.Netns)
3 if err != nil {
4 return err
5 }
6 ip, err := nettool.GetVethIPInNS(netns, cmdArgs.IfName)
7 if err != nil {
8 return err
9 }
10
11 // open or create the file that stores all the reserved IPs
12 f, err := os.OpenFile(fh.IPStore, os.O_RDWR|os.O_CREATE, 0600)
13 if err != nil {
14 return fmt.Errorf("failed to open file that stores reserved IPs %v", err)
15 }
16 defer f.Close()
17
18 // get all the reserved IPs from file
19 content, err := ioutil.ReadAll(f)
20 if err != nil {
21 return err
22 }
23 reservedIPs := strings.Split(strings.TrimSpace(string(content)), "\n")
24
25 for i, rip := range reservedIPs {
26 if rip == ip {
27 reservedIPs = append(reservedIPs[:i], reservedIPs[i+1:]...)
28 break
29 }
30 }
31
32 // write reserved IPs back into file
33 if err := ioutil.WriteFile(fh.IPStore, []byte(strings.Join(reservedIPs, "\n")), 0600); err != nil {
34 return fmt.Errorf("failed to write reserved IPs into file: %v", err)
35 }
36
37 return nil
38 }
39
CNI_VERSION参数如下:
2022/12/09 20:18:24 cmd: VERSION, ContainerID: , Netns: dummy, ifName: dummy, Path: dummy, Args: , StdinData: {"cniVersion":"0.4.0"}.
实现代码如下:
1 func (fh *FileHandler) HandleVersion(cmdArgs *args.CmdArgs) error { 2 versionInfo, err := json.Marshal(fh.VersionInfo) 3 if err != nil { 4 return err 5 } 6 fmt.Print(string(versionInfo)) 7 return nil 8 }
CNI 安装工具
CNI 插件需要运行在集群中的每个节点上,而且 CNI 插件配置信息与可运行文件必须在每个节点特殊的目录中,因此,安装 CNI 插件非常适合使用 DaemonSet 并挂载 CNI 插件目录,为了避免安装 CNI 的工具不能被正常调度,我们需要使用 hostNetwork 来使用宿主机的网络。同时,将 CNI 插件配置以 ConfigMap 的形式挂载,这样方便终端用户配置 CNI 插件。更详细的信息请查看安装工具部署文件。
[root@k8s-master ~]# k describe node k8s-master | grep CIDR
PodCIDR: 10.244.0.0/24
PodCIDRs: 10.244.0.0/24
[root@k8s-master ~]#
另外需要注意的是,我们在安装 CNI 插件的脚本中获取每个节点划分得到的24子网信息、检查是否合法然后写入到 CNI 配置信息中:
1 # The environment variables used to connect to the kube-apiserver
2 SERVICE_ACCOUNT_PATH=/var/run/secrets/kubernetes.io/serviceaccount
3 SERVICEACCOUNT_TOKEN=$(cat $SERVICE_ACCOUNT_PATH/token)
4 KUBE_CACERT=${KUBE_CACERT:-$SERVICE_ACCOUNT_PATH/ca.crt}
5 KUBERNETES_SERVICE_PROTOCOL=${KUBERNETES_SERVICE_PROTOCOL-https}
6
7 # Check if we're running as a k8s pod.
8 if [ -f "$SERVICE_ACCOUNT_PATH/token" ];
9 then
10 # some variables should be automatically set inside a pod
11 if [ -z "${KUBERNETES_SERVICE_HOST}" ]; then
12 exit_with_message "KUBERNETES_SERVICE_HOST not set"
13 fi
14 if [ -z "${KUBERNETES_SERVICE_PORT}" ]; then
15 exit_with_message "KUBERNETES_SERVICE_PORT not set"
16 fi
17 fi
18
19 # exit if the NODE_NAME environment variable is not set.
20 if [[ -z "${NODE_NAME}" ]];
21 then
22 exit_with_message "NODE_NAME not set."
23 fi
24
25
26 NODE_RESOURCE_PATH="${KUBERNETES_SERVICE_PROTOCOL}://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}/api/v1/nodes/${NODE_NAME}"
27 NODE_SUBNET=$(curl --cacert "${KUBE_CACERT}" --header "Authorization: Bearer ${SERVICEACCOUNT_TOKEN}" -X GET "${NODE_RESOURCE_PATH}" | jq ".spec.podCIDR")
28
29 # Check if the node subnet is valid IPv4 CIDR address
30 IPV4_CIDR_REGEX="(((25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|1?[0-9][0-9]?))(\/([8-9]|[1-2][0-9]|3[0-2]))([^0-9.]|$)"
31 if [[ ${NODE_SUBNET} =~ ${IPV4_CIDR_REGEX} ]]
32 then
33 echo "${NODE_SUBNET} is a valid IPv4 CIDR address."
34 else
35 exit_with_message "${NODE_SUBNET} is not a valid IPv4 CIDR address!"
36 fi
37
38 # exit if the NODE_NAME environment variable is not set.
39 if [[ -z "${CNI_NETWORK_CONFIG}" ]];
40 then
41 exit_with_message "CNI_NETWORK_CONFIG not set."
42 fi
43
44 TMP_CONF='/minicni.conf.tmp'
45 cat >"${TMP_CONF}" <<EOF
46 ${CNI_NETWORK_CONFIG}
47 EOF
48
49 # Replace the __NODE_SUBNET__
50 grep "__NODE_SUBNET__" "${TMP_CONF}" && sed -i s~__NODE_SUBNET__~"${NODE_SUBNET}"~g "${TMP_CONF}"
51
52 # Log the config file
53 echo "CNI config: $(cat "${TMP_CONF}")"
54
55 # Move the temporary CNI config into the CNI configuration directory.
56 mv "${TMP_CONF}" "${CNI_NET_DIR}/${CNI_CONF_NAME}" || \
57 exit_with_error "Failed to move ${TMP_CONF} to ${CNI_CONF_NAME}."
58
59 echo "Created CNI config ${CNI_CONF_NAME}"
编译部署
1、编译
make build
make image
在docker hub里已看到了minicni镜像:
部署
1、登录已安装好的k8s集群,把之前已存在的cni如(calico)卸载掉再安装minici:
[root@k8s-master minicni]# kubectl apply -f minicni.yaml
clusterrole.rbac.authorization.k8s.io/minicni created
serviceaccount/minicni created
clusterrolebinding.rbac.authorization.k8s.io/minicni created
configmap/minicni-config created
Warning: spec.template.spec.nodeSelector[beta.kubernetes.io/os]: deprecated since v1.14; use "kubernetes.io/os" instead
Warning: spec.template.metadata.annotations[scheduler.alpha.kubernetes.io/critical-pod]: non-functional in v1.16+; use the "priorityClassName" field instead
daemonset.apps/minicni-node created
[root@k8s-master minicni]#
2、查看minicni部署状态:
[root@k8s-master minicni]# kubectl get pod -A -o wide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
default nginx-85b98978db-qkd6h 0/1 Completed 5 4d21h <none> k8s-work1 <none> <none>
kube-system calico-kube-controllers-7b8458594b-p2fqj 0/1 Terminating 2 4d12h <none> k8s-work2 <none> <none>
kube-system coredns-6d8c4cb4d-ck2x5 0/1 Completed 7 4d22h <none> k8s-master <none> <none>
kube-system coredns-6d8c4cb4d-mbctj 0/1 Completed 7 4d22h <none> k8s-master <none> <none>
kube-system etcd-k8s-master 1/1 Running 8 (8m14s ago) 4d22h 172.25.140.216 k8s-master <none> <none>
kube-system kube-apiserver-k8s-master 1/1 Running 8 (8m4s ago) 4d22h 172.25.140.216 k8s-master <none> <none>
kube-system kube-controller-manager-k8s-master 1/1 Running 8 (8m14s ago) 4d22h 172.25.140.216 k8s-master <none> <none>
kube-system kube-proxy-dnsjg 1/1 Running 8 (8m14s ago) 4d21h 172.25.140.215 k8s-work1 <none> <none>
kube-system kube-proxy-r84lg 1/1 Running 8 (8m14s ago) 4d22h 172.25.140.216 k8s-master <none> <none>
kube-system kube-proxy-tbkx2 1/1 Running 7 (8m14s ago) 4d21h 172.25.140.214 k8s-work2 <none> <none>
kube-system kube-scheduler-k8s-master 1/1 Running 8 (8m14s ago) 4d22h 172.25.140.216 k8s-master <none> <none>
kube-system minicni-node-8lmxc 1/1 Running 0 5m17s 172.25.140.214 k8s-work2 <none> <none>
kube-system minicni-node-sgjmg 1/1 Running 0 5m17s 172.25.140.216 k8s-master <none> <none>
kube-system minicni-node-xslgx 1/1 Running 0 5m17s 172.25.140.215 k8s-work1 <none> <none>
可以看出minicni处于Running状态。
测试
1、环境准备, 分别给node打上相应的标签
[root@k8s-master minicni]# k label nodes k8s-master role=master
node/k8s-master labeled
[root@k8s-master minicni]# k label nodes k8s-work1 role=worker
node/k8s-work1 labeled
[root@k8s-master minicni]# k label nodes k8s-work2 role=worker
node/k8s-work2 labeled
2、分别在 master 与 worker 节点部署 netshoot 与 httpbin:
[root@k8s-master minicni]# k apply -f test-pods.yaml
pod/httpbin-master created
pod/netshoot-master created
pod/httpbin-worker created
pod/netshoot-worker created
[root@k8s-master minicni]#
3、确保所有 pod 都启动并开始运行:
[root@k8s-master minicni]# k get pod
NAME READY STATUS RESTARTS AGE
httpbin-master 0/1 ContainerCreating 0 8s
httpbin-worker 0/1 ContainerCreating 0 8s
netshoot-master 0/1 ContainerCreating 0 8s
netshoot-worker 0/1 ContainerCreating 0 8s
[root@k8s-master minicni]#
发现状态为ContainerCreating, 查看pod描述报错为:
Warning FailedCreatePodSandBox 36s kubelet Failed to create pod sandbox: rpc error: code = Unknown desc = [failed to set up sandbox container "7cb17eace85729539db7d7af1a4955a353a14eb7ffa96f84c117ee6633b3e2b5" network for pod "httpbin-master": networkPlugin cni failed to set up pod "httpbin-master_default" network: error getting ClusterInformation: connection is unauthorized: Unauthorized, failed to clean up sandbox container "7cb17eace85729539db7d7af1a4955a353a14eb7ffa96f84c117ee6633b3e2b5" network for pod "httpbin-master": networkPlugin cni failed to teardown pod "httpbin-master_default" network: error getting ClusterInformation: connection is unauthorized: Unauthorized]
如果安装了calico网络插件,需要删除calico:
kubectl delete -f <yaml>
还要去所有节点etc/cni/net.d/目录下 删掉与calico相关的所有配置文件, 然后重启机器。 不然pod起不来,会报错 network: error getting ClusterInformation: connection is unauthorized: Unauthorized .
4、最后再查看POD都变成Running状态了
[root@k8s-master minicni]# k get pod -A -o wide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
default httpbin-master 1/1 Running 0 20m 10.244.0.4 k8s-master <none> <none>
default httpbin-worker 1/1 Running 0 20m 10.244.2.2 k8s-work2 <none> <none>
default netshoot-master 1/1 Running 0 20m 10.244.0.5 k8s-master <none> <none>
default netshoot-worker 1/1 Running 0 20m 10.244.2.3 k8s-work2 <none> <none>
default nginx-85b98978db-2hzc9 1/1 Running 0 50m 10.244.1.2 k8s-work1 <none> <none>
kube-system coredns-6d8c4cb4d-ck2x5 1/1 Running 9 (24h ago) 6d23h 10.244.0.2 k8s-master <none> <none>
kube-system coredns-6d8c4cb4d-mbctj 1/1 Running 9 (24h ago) 6d23h 10.244.0.3 k8s-master <none> <none>
kube-system etcd-k8s-master 1/1 Running 10 (24h ago) 6d23h 172.25.140.216 k8s-master <none> <none>
kube-system kube-apiserver-k8s-master 1/1 Running 12 (24h ago) 6d23h 172.25.140.216 k8s-master <none> <none>
kube-system kube-controller-manager-k8s-master 1/1 Running 10 (24h ago) 6d23h 172.25.140.216 k8s-master <none> <none>
kube-system kube-proxy-dnsjg 1/1 Running 10 (24h ago) 6d22h 172.25.140.215 k8s-work1 <none> <none>
kube-system kube-proxy-r84lg 1/1 Running 10 (24h ago) 6d23h 172.25.140.216 k8s-master <none> <none>
kube-system kube-proxy-tbkx2 1/1 Running 9 (24h ago) 6d22h 172.25.140.214 k8s-work2 <none> <none>
kube-system kube-scheduler-k8s-master 1/1 Running 10 (24h ago) 6d23h 172.25.140.216 k8s-master <none> <none>
kube-system minicni-node-5w9fn 1/1 Running 0 55m 172.25.140.215 k8s-work1 <none> <none>
kube-system minicni-node-jb5cj 1/1 Running 0 55m 172.25.140.214 k8s-work2 <none> <none>
kube-system minicni-node-kp25h 1/1 Running 0 55m 172.25.140.216 k8s-master <none> <none>
[root@k8s-master minicni]#
5、之后测试以下四种网络通信是否正常:
pod 到宿主机的通信
[root@k8s-master minicni]# kubectl exec -ti netshoot-master -- /bin/bash bash-5.1# ping 172.25.140.216 PING 172.25.140.216 (172.25.140.216) 56(84) bytes of data. 64 bytes from 172.25.140.216: icmp_seq=1 ttl=64 time=0.074 ms 64 bytes from 172.25.140.216: icmp_seq=2 ttl=64 time=0.035 ms 64 bytes from 172.25.140.216: icmp_seq=3 ttl=64 time=0.033 ms 64 bytes from 172.25.140.216: icmp_seq=4 ttl=64 time=0.043 ms 64 bytes from 172.25.140.216: icmp_seq=5 ttl=64 time=0.048 ms ^C --- 172.25.140.216 ping statistics --- 5 packets transmitted, 5 received, 0% packet loss, time 3999ms rtt min/avg/max/mdev = 0.033/0.046/0.074/0.014 ms bash-5.1#
同一个节点 pod-to-pod 通信
默认情况下,同一台主机上的 pod-to-pod 网络包默认会被 Linux 内核丢弃,原因是 Linux 默认会把非 default 网络命名空间的网络包看作是外部数据包,关于这个问题的具体细节,请查看 stackoverflow 上的讨论。目前,我们需要在每个集群结点上使用以下命令手动添加以下 iptables 规则来让 pod-to-pod 网络数据包顺利转发:[root@k8s-master ~]# iptables -t filter -A FORWARD -s 10.244.0.0/24 -j ACCEPT [root@k8s-master ~]# iptables -t filter -A FORWARD -d 10.244.0.0/24 -j ACCEPT
再进行测试:
[root@k8s-master ~]# kubectl exec -ti netshoot-master -- /bin/bash bash-5.1# ping 10.244.0.25 PING 10.244.0.25 (10.244.0.25) 56(84) bytes of data. 64 bytes from 10.244.0.25: icmp_seq=1 ttl=64 time=0.079 ms 64 bytes from 10.244.0.25: icmp_seq=2 ttl=64 time=0.061 ms
测试通信正常。
pod 到其他主机的通信
[root@k8s-master ~]# kubectl exec -ti netshoot-master -- /bin/bash bash-5.1# ping 172.25.140.214 PING 172.25.140.214 (172.25.140.214) 56(84) bytes of data. ^C --- 172.25.140.214 ping statistics --- 4 packets transmitted, 0 received, 100% packet loss, time 2999ms
Ping不通,原因是阿里云底层网络对非法IP限制,不是随便配个IP就可以通,如需通可以考虑overlay如calico或本地虚拟机搭建方式。
跨一个节点 pod-to-pod 通信
对于跨节点的 pod-to-pod 网络包,需要像 Calico 那样添加宿主机的路由表,保证发往各个节点上的 pod 流量经过节点的转发。目前这些路由表需要手动添加:ip route add 10.244.2.0/24 via 172.25.140.214 dev eth0 #run on master ip route add 10.244.0.0/24 via 172.25.140.216 dev eth0 #run on worker
再测试:
[root@k8s-master ~]# kubectl exec -ti netshoot-master -- /bin/bash bash-5.1# ping 10.244.2.11 PING 10.244.2.11 (10.244.2.11) 56(84) bytes of data. ^C --- 10.244.2.11 ping statistics --- 108 packets transmitted, 0 received, 100% packet loss, time 106996ms
现象与原理同上。
FAQ
cannot find package 编译不过
当出现如下错误时:
$ make build
Building the minicni on amd64...
cmd/main.go:8:2: cannot find package "github.com/morvencao/minicni/pkg/args" in any of:
/usr/local/go/src/github.com/morvencao/minicni/pkg/args (from $GOROOT)
/Users/mosp/goget/src/github.com/morvencao/minicni/pkg/args (from $GOPATH)
cmd/main.go:9:2: cannot find package "github.com/morvencao/minicni/pkg/handler" in any of:
/usr/local/go/src/github.com/morvencao/minicni/pkg/handler (from $GOROOT)
/Users/mosp/goget/src/github.com/morvencao/minicni/pkg/handler (from $GOPATH)
make: *** [build] Error 1
需要把GO111MODULE=on或auto,才能使用Go module功能,可在.bashrc或.zshrc里加上:export GO111MODULE=auto
详见go自动下载所有的依赖包go module使用详解_Golang:
build constraints exclude all Go files in
当出现如下错误时:
$ make build
Building the minicni on amd64...
go build github.com/containernetworking/plugins/pkg/ns: build constraints exclude all Go files in /Users/mosp/goget/pkg/mod/github.com/containernetworking/plugins@v1.1.1/pkg/ns
make: *** [build] Error 1
需要设置如下两个变量:
export GOOS=“linux”。即:不能为darwin
export CGO_ENABLED=“1”。
详见:build constraints exclude all Go files in xxx/xxx/xxx
参考资料
【01】 find -print0和xargs -0原理及用法]
【02】 Go语言import分组管理利器: goimports-reviser
【03】 使用 Go 从零开始实现 CNI
【04】 centOS内网安装kubernetes集群
【05】 Linux 路由表(RIB表、FIB表)、ARP表、MAC表整理]
【06】 查看CNI中的veth pair
【07】 一文吃透 K8S 网络模型
【08】 K8S 网络CNI
作者:mospan
微信关注:墨斯潘園
本文出处:http://mospany.github.io/2022/11/22/k8s-practice-minicni/
文章版权归本人所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。