简介

不管是容器网络还是 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 规范的详情请查看官方文档

img

详见: 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 目录,一般命名为 <数字>-.conf,而且配置文件至少需要以下几个必须的字段:

  1. cniVersion: CNI 插件的字符串版本号,要求符合 Semantic Version 2.0 规范;
  2. name: 字符串形式的网络名;
  3. 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

img

在docker hub里已看到了minicni镜像:

img

部署

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/
文章版权归本人所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。