当前位置: 首页 > 工具软件 > cni > 使用案例 >

CNI插件之CNI插件最简实现之macvlan plugin

经景辉
2023-12-01

CNI插件之CNI插件最简实现之macvlan plugin

Kubernetes的CNI插件非常智能,通过几个yaml文件就可以完成整套安装。这是kubernet的优势,但是对于想要了解原理的同学,太智能了,反而感觉无从下手。

当我开始学习CNI插件的时候也有这般感受。当时我的第一个想法是找一个最最最简单的CNI插件,快速的了解CNI的原理。后来反复对比,选择了macvlan。一是macvlan plugin由containernetworking官方 提供,较可靠;二是macvlan使用了最简单的命令操作,这些命令我都比较熟悉;选择macvlan之后,也还是遇到了小插曲,可能是由于macvlan太简单,反而网上并没有了整理完整的安装步骤。好吧,那一切只能靠自己拼凑。等macvlan顺利运行起来后,感觉此刻对kubernet cni插件的原理立刻就打通了。写这篇博客,把我的实验过程整理出来。如果你也习惯通过一个简单的例子,解开复杂的流程,那么以下内容会非常适合你。

接下来本文将介绍:

  • 什么是CNI插件
  • macvlan插件简单实验配置示例
  • CNI插件接口方法
  • macvlan插件源码下载与编译方法
  • macvlan原理及macvlan实验示例
  • macvlan插件代码实现
  • 编写一个简单插件实验
  • CNI插件调试方法
  • macvlan总结与讨论

本文假定用户已经安装Kubernetes环境。

什么是CNI插件

CNI(container network interface),是一个接口规范,这个规范定义了输入、输出的标准和调用的接口,只要调用CNI插件的实体遵守这个规范,就能从CNI拿到满足网络互通条件的网络参数(如IP地址、网关、路由、DNS等),这些网络参数可以配置container实例。

macvlan插件简单实验配置示例

macvlan 插件正确运行需要两个前提:

  • 插件二进制正确安装
  • 提供正确的配置文件

我这边环境kubernetes版本是1.14.3,已正确安装kubernetes-cni-0.7.5-0.x86_64内部含有macvlan插件。

配置

配置文件路径:/etc/cni/net.d/10-maclannet.conf
内容如下:

{
	"cniVersion":"0.3.1",
	"name": "macvlannet",
	"type": "macvlan",
	"master": "ens3",
	"mode": "bridge",
	"isGateway": true,
	"ipMasq": false,
	"ipam": {
		"type": "host-local",
		"subnet": "192.168.1.0/24",
		"rangeStart": "192.168.1.100",
		"rangeEnd": "192.168.1.200",
		"gateway": "192.168.1.1",
		"routes": [
			{ "dst": "0.0.0.0/0" }
			]
	}
}

配置文件路径:/etc/cni/net.d/99-loopback.conf
内容如下:

{
	"cniVersion": "0.3.1",
	"name": "lo",
	"type": "loopback"
}

字段说明:

  • cniVersion:版本
  • name: 网络方案名称
  • type: 这里指定用macvlan网络方案
  • master: 指定要在哪个网口上通过macvlan虚拟出不同mac/ip的网络接口
  • mode: 桥接方式
  • ipam: ip分配方案参数
    1. type: 指定使用哪种ip分配方案,这里是host-local
    2. subnet: 分配的子网范围
    3. rangeStart:开始的地址
    4. rangeEnd:结束的地址
    5. gateway: 容器内部网络的网关
    6. routes: 路由

这里仅仅展示Kubernetes集群master上的配置,其它node节点上的配置,可以从以下链接获取:集群macvlan配置文件

在刚安装完Kubernetes集群时,通过命令kubectl get nodes -A -o wide可以看到集群节点还是NotReady的状态。

在确认完macvlan二进制已经安装、内核支持macvlan虚拟化、配置正确的放在对应的位置后,通过上述同样的命令,我们可以看到集群节点已经变成Ready状态。

另外这里还需要特别注意,Kubernetes初始化的时候指定的pod-network-cidr需要包含这里的subnet子网

CNI插件接口方法

CNI插件的接口方法一般包括:

  • ADD
  • CHECK
  • DEL
  • VERSION

实际使用中,将一个容器添加进一个网络,调用的是ADD接口方法。将一个容器从网络中删除,则是调用DEL接口方法。

调用接口方法的参数又可以分成两类:输入参数,环境变量

我们这里罗列出常用的接口方法的输入参输,环境变量,输出参数,这些参数的获取方法详见:cni插件输入输出获取方法

  • 输入参数:
    这里的输入参数,实际上就是macvlan配置文件
{
	"cniVersion":"0.3.1",
	"name": "macvlannet",
	"type": "macvlan",
	"master": "ens3",
	"mode": "bridge",
	"ipam": {
		"type": "host-local",
		"subnet": "192.168.122.0/24",
		"rangeStart": "192.168.122.21",
		"rangeEnd": "192.168.122.99",
		"gateway": "192.168.122.14",
		"routes": [
			{ "dst": "0.0.0.0/0" }
		]
	}
}
  • 环境变量:
    环境变量包括:
    1. CNI_ARGS:含有POD的NAMESPACE(K8S_POD_NAMESPACE),POD名字(K8S_POD_NAME),POD的infra容器ID(K8S_POD_INFRA_CONTAINER_ID)等信息
    2. CNI_COMMAND:包含接口调用的方法,比如ADD/DEL/CHECK/VERSION等
    3. CNI_IFNAME:包含网络接口名称
    4. CNI_NETNS:包含隔离空间路径
    5. CNI_CONTAINERID: 容器ID
    6. CNI_PATH: CNI插件二进制所在目录
CNI_ARGS=IgnoreUnknown=1;K8S_POD_NAMESPACE=kube-system;K8S_POD_NAME=nginx-controller-dxxfg;K8S_POD_INFRA_CONTAINER_ID=b76473e8fe8dd4bba26b73fef7be002a59da050f0c96d55d293848b0f891e302
CNI_COMMAND=ADD
CNI_IFNAME=eth0
CNI_NETNS=/proc/19504/ns/net
CNI_CONTAINERID=b76473e8fe8dd4bba26b73fef7be002a59da050f0c96d55d293848b0f891e302
CNI_PATH=/opt/cni/bin
  • 输出参数:
    包括:网口名称,mac地址,隔离空间路径,ipv4版本,网口ip,默认网关等信息
{
    "cniVersion": "0.3.1",
    "interfaces": [
        {
            "name": "eth0",
            "mac": "5e:e0:14:f3:31:61",
            "sandbox": "/proc/19504/ns/net"
        }
    ],
    "ips": [
        {
            "version": "4",
            "interface": 0,
            "address": "192.168.122.117/24",
            "gateway": "192.168.122.14"
        }
    ],
    "routes": [
        {
            "dst": "0.0.0.0/0"
        }
    ],
    "dns": {}
}

macvlan插件源码下载与编译方法

  • 源码下载:
git clone https://github.com/containernetworking/plugins
  • 源码编译:
    源码编译有个前提,需要先安装go环境,可以网上查找安装方法。
./build_linux.sh

如果想增加sample的编译,则可以先执行

cp -rf plugins/sample plugins/main

编译结果:

[root@k8s-new-master ~]# ls xujx/cni/plugins/bin/
bandwidth  bridge  dhcp  firewall  flannel  host-device  host-local  ipvlan  loopback  macvlan  portmap  ptp  sbr  static  tuning  vlan

可以以上内容拷贝到Kubernetes对应路径:/opt/cni/bin/

macvlan原理及macvlan实验示例

macvlan 是在 HOST 网卡上创建多个子网卡,并分配独立的 IP 地址和 MAC 地址,把子网卡分配给容器实例来实现实例与物理网络的直通,并同时保持容器实例的隔离性。Host 收到数据包后,则根据不同的 MAC 地址把数据包从转发给不同的子接口,在外界来看就相当于多台主机。macvlan 要求物理网卡支持混杂 promisc 模式并且要求 kernel 为 v3.9-3.19 和 4.0+,因为是直接通过子接口转发数据包,所以可想而知,性能比 bridge 要高,不需要经过 NAT。

macvlan支持模式:

  • private:子接口之间不允许通信,子接口能与物理网络通讯,所有数据包都经过父接口(eth0)
  • vepa(Virtual Ethernet Port Aggregator):子接口之间、子接口与物理网络允许通讯,数据包都经过父接口(eth0)进出,要求交换机支持 IEEE 802.1Q。
  • bridge:子接口之间直接通讯,不经过父接口(eth0),性能较高,但是父接口 down 之后也同样丧失通讯能力。
  • passthru:Allows a single VM to be connected directly to the physical interface. The advantage of this mode is that VM is then able to change MAC address and other interface parameters.

以上四种模式,子接口都不能与父接口通信

从linux man ip link可以看到还有一种source模式,这里没有列出

实验示例

  • 确定父接口, 我们的环境上用ens3
  • 创建两个隔离空间
	[root@k8s-new-master ~]# ip netns add net1
	[root@k8s-new-master ~]# ip netns add net2
  • 创建macvlan接口:mac1 ens3表示父接口 mac1表示创建的子网口名称,注意没有指定mode的时候,默认mode是vepa
	[root@k8s-new-master ~]# ip link add link ens3 mac1 type macvlan mode bridge
	[root@k8s-new-master ~]# ip link
	2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
	    link/ether 88:4f:d5:25:80:12 brd ff:ff:ff:ff:ff:ff
	6: mac1@ens3: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
	    link/ether ae:08:c6:45:4b:0c brd ff:ff:ff:ff:ff:ff
  • 将子网口mac1放进隔离空间net1里面,重命名网口名称为eth0,设置ip为192.168.88.1/24
	[root@k8s-new-master ~]# ip link set mac1 netns net1
	[root@k8s-new-master ~]# ip netns exec net1 ip link
	1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
	    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
	6: mac1@if2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
	    link/ether ae:08:c6:45:4b:0c brd ff:ff:ff:ff:ff:ff link-netnsid 0
	[root@k8s-new-master ~]# ip netns exec net1 ip link set mac1 name eth0
	[root@k8s-new-master ~]# ip netns exec net1 ip link
	1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1000
	    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
	6: eth0@if2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
	    link/ether ae:08:c6:45:4b:0c brd ff:ff:ff:ff:ff:ff link-netnsid 0
	[root@k8s-new-master ~]# ip netns exec net1 ip addr add 192.168.88.1/24 dev eth0
	[root@k8s-new-master ~]# ip netns exec net1 ip link set eth0 up
  • 同理创建mac2子网口,并放进隔离空间net2里面,重命名网口名称为eth0, 设置ip为192.168.88.2/24
	[root@k8s-new-master ~]# ip link add link ens3 mac2 type macvlan mode bridge
	[root@k8s-new-master ~]# ip link set mac2 netns net2
	[root@k8s-new-master ~]# ip netns exec net2 ip link set mac2 name eth0
	[root@k8s-new-master ~]# ip netns exec net2 ip addr add 192.168.88.2/24 dev eth0
	[root@k8s-new-master ~]# ip netns exec net2 ip link set eth0 up
  • 测试连通性
	[root@k8s-new-master ~]# ip netns exec net2 ping 192.168.88.1
	PING 192.168.88.1 (192.168.88.1) 56(84) bytes of data.
	64 bytes from 192.168.88.1: icmp_seq=1 ttl=64 time=0.701 ms
	64 bytes from 192.168.88.1: icmp_seq=2 ttl=64 time=0.065 ms

macvlan插件代码实现

main函数到调用接口ADD/CHECK/DEL/DEL/VERSION的流程。
这里我们主要介绍两个常用的接口cmdAdd和cmdDel

main
  |=> skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("macvlan"))  //源码位置containernetworking\cni\pkg\skel\skel.go
    |=> PluginMainWithError(cmdAdd, cmdCheck, cmdDel func(_ *CmdArgs) error, versionInfo version.PluginInfo, about string)
      |=> (&dispatcher{Getenv: os.Getenv,Stdin:  os.Stdin,Stdout: os.Stdout,Stderr: os.Stderr,}).pluginMain(cmdAdd, cmdCheck, cmdDel, versionInfo, about)
        |=> cmd == "ADD":
          |=> t.checkVersionAndCall(cmdArgs, versionInfo, cmdAdd)
            |=> cmdAdd()
	|=> cmd == "CHECK"
          |=> version.GreaterThanOrEqualTo(pluginVersion, configVersion)
        |=> cmd == "DEL"
          |=> t.checkVersionAndCall(cmdArgs, versionInfo, cmdDel)
            |=> cmdDel()
        |=> cmd == "VERSION"
          |=> versionInfo.Encode(t.Stdout)

cmdAdd

输入参数:args.StdinData, 也就是插件的标准输入参数
输出参数:result结构体涵IPs(分配的ip信息), Routes(分配的路由信息)等信息
函数流程:

func cmdAdd(args *skel.CmdArgs) error {
        n, cniVersion, err := loadConf(args.StdinData, args.Args) //加载输入参数
        if err != nil {
                return err
        }

        isLayer3 := n.IPAM.Type != "" //获取置文件ipam选项

        netns, err := ns.GetNS(args.Netns)  //获取容器隔离空间名称
        if err != nil {
                return fmt.Errorf("failed to open netns %q: %v", netns, err)
        }
        defer netns.Close()

        macvlanInterface, err := createMacvlan(n, args.IfName, netns)  //创建macvlan网络设备
        if err != nil {
                return err
        }

        // Delete link if err to avoid link leak in this ns
        defer func() {
                if err != nil {
                        netns.Do(func(_ ns.NetNS) error {
                                return ip.DelLinkByName(args.IfName)
                        })
                }
        }()

        // Assume L2 interface only
        result := &current.Result{CNIVersion: cniVersion, Interfaces: []*current.Interface{macvlanInterface}}

        if isLayer3 {
                // run the IPAM plugin and get back the config to apply
                r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData) //从ipam 获取ip,路由信息
                if err != nil {
                        return err
                }

                // Invoke ipam del if err to avoid ip leak
                defer func() {
                        if err != nil {
                                ipam.ExecDel(n.IPAM.Type, args.StdinData)
                        }
                }()

                // Convert whatever the IPAM result was into the current Result type
                ipamResult, err := current.NewResultFromResult(r)
                if err != nil {
                        return err
                }

                if len(ipamResult.IPs) == 0 {
                        return errors.New("IPAM plugin returned missing IP config")
                }

                result.IPs = ipamResult.IPs
                result.Routes = ipamResult.Routes

                for _, ipc := range result.IPs {
                        // All addresses apply to the container macvlan interface
                        ipc.Interface = current.Int(0)
                }
                err = netns.Do(func(_ ns.NetNS) error {
                        if err := ipam.ConfigureIface(args.IfName, result); err != nil { //配置ip地址,路由等信息到网络接口上
                                return err
                        }

                        contVeth, err := net.InterfaceByName(args.IfName)
                        if err != nil {
                                return fmt.Errorf("failed to look up %q: %v", args.IfName, err)
                        }

                        for _, ipc := range result.IPs {
                                if ipc.Version == "4" {
                                        _ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
                                }
                        }
                        return nil
                })
                if err != nil {
                        return err
                }
        } else {
                // For L2 just change interface status to up
                err = netns.Do(func(_ ns.NetNS) error {
                        macvlanInterfaceLink, err := netlink.LinkByName(args.IfName)
                        if err != nil {
                                return fmt.Errorf("failed to find interface name %q: %v", macvlanInterface.Name, err)
                        }

                        if err := netlink.LinkSetUp(macvlanInterfaceLink); err != nil {
                                return fmt.Errorf("failed to set %q UP: %v", args.IfName, err)
                        }

                        return nil
                })
                if err != nil {
                        return err
                }
        }

        result.DNS = n.DNS

        return types.PrintResult(result, cniVersion)
}

createMacvlan:
输入参数: 网络配置, 接口名称, 网络隔离参数
输出参数:网络接口结构体
函数流程:

func createMacvlan(conf *NetConf, ifName string, netns ns.NetNS) (*current.Interface, error) {
        macvlan := &current.Interface{}

        mode, err := modeFromString(conf.Mode) //获取配置的mode信息
        if err != nil {
                return nil, err
        }

        m, err := netlink.LinkByName(conf.Master)  //通过master(parent)接口名称获取相关信息
        if err != nil {
                return nil, fmt.Errorf("failed to lookup master %q: %v", conf.Master, err)
        }

        // due to kernel bug we have to create with tmpName or it might
        // collide with the name on the host and error out
        tmpName, err := ip.RandomVethName()
        if err != nil {
                return nil, err
        }

        linkAttrs := netlink.LinkAttrs{ //填充netlink.Attrs属性
                MTU:         conf.MTU,
                Name:        tmpName, //创建的子网络接口名称
                ParentIndex: m.Attrs().Index,  //接口index参数
                Namespace:   netlink.NsFd(int(netns.Fd())), //隔离空间参数
        }

        if conf.Mac != "" {  //是否输入参数含有mac地址
                addr, err := net.ParseMAC(conf.Mac) //解析mac地址
                if err != nil {
                        return nil, fmt.Errorf("invalid args %v for MAC addr: %v", conf.Mac, err)
                }
                linkAttrs.HardwareAddr = addr //netlink.Attrs属性添加mac信息
        }
       mv := &netlink.Macvlan{ //mode, linkAttrs赋值给netlink.Macvlan
                LinkAttrs: linkAttrs,
                Mode:      mode,
        }

        if err := netlink.LinkAdd(mv); err != nil { //根据上述设置的netlink信息,执行创建子网络接口的动作
                return nil, fmt.Errorf("failed to create macvlan: %v", err)
        }

        err = netns.Do(func(_ ns.NetNS) error {
                // TODO: duplicate following lines for ipv6 support, when it will be added in other places
                ipv4SysctlValueName := fmt.Sprintf(IPv4InterfaceArpProxySysctlTemplate, tmpName)
                if _, err := sysctl.Sysctl(ipv4SysctlValueName, "1"); err != nil {
                        // remove the newly added link and ignore errors, because we already are in a failed state
                        _ = netlink.LinkDel(mv)
                        return fmt.Errorf("failed to set proxy_arp on newly added interface %q: %v", tmpName, err)
                }

                err := ip.RenameLink(tmpName, ifName) //重命名子网络接口名称
                if err != nil {
                        _ = netlink.LinkDel(mv)
                        return fmt.Errorf("failed to rename macvlan to %q: %v", ifName, err)
                }
                macvlan.Name = ifName  //输出参数macvlan设置接口名称

                // Re-fetch macvlan to get all properties/attributes
                contMacvlan, err := netlink.LinkByName(ifName)
                if err != nil {
                        return fmt.Errorf("failed to refetch macvlan %q: %v", ifName, err)
                }
                macvlan.Mac = contMacvlan.Attrs().HardwareAddr.String() //输出参数获取mac地址
                macvlan.Sandbox = netns.Path() //输出参数设置Sandbox(隔离空间路径信息)

                return nil
        })
     if err != nil {
                return nil, err
        }

        return macvlan, nil //返回输出参数macvlan结构体
}

cmdDel

输入参数: 网络配置, 接口名称, 网络隔离参数
输出参数: 无
代码流程:

func cmdDel(args *skel.CmdArgs) error {
        n, _, err := loadConf(args.StdinData, args.Args) //加载输入参数
        if err != nil {
                return err
        }

        isLayer3 := n.IPAM.Type != ""

        if isLayer3 {
                err = ipam.ExecDel(n.IPAM.Type, args.StdinData) //从ipam 删除ip,路由等信息
                if err != nil {
                        return err
                }
        }

        if args.Netns == "" {
                return nil
        }

        // There is a netns so try to clean up. Delete can be called multiple times
        // so don't return an error if the device is already removed.
        err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error {
                if err := ip.DelLinkByName(args.IfName); err != nil { //删除子网络接口
                        if err != ip.ErrLinkNotFound {
                                return err
                        }
                }
                return nil
        })

        return err
}

ipam

ipam主要用于ip地址、网关的分配,函数调用顺序是从上面的macvlan插件开始:
cmdAdd=>…=>ipam.ExecAdd
cmd=>…=>ipam.ExecDel

|=> ipam.ExecAdd //源码位置 pkg/ipam/ipam.go
  |=> invoke.DelegateAdd(context.TODO(), plugin, netconf, nil) //cni/pkg/invoke/delegate.go, 配置文件制定host-local
    |=> ExecPluginWithResult(ctx, pluginPath, netconf, delegateArgs("ADD"), realExec)
      |=> exec.ExecPlugin(ctx, pluginPath, netconf, args.AsEnv())
      ...
        |=> main  //host-local插件的主函数, 这里假设我们配置文件里面配置ipam的type为host-local
|=> ipam.ExecDel
  |=> invoke.DelegateDel(context.TODO(), plugin, netconf, nil)
    ...  //同上

ipam.cmdAdd:
输入参数: stdin_data标准输入等信息
输出参数: ip、路由信息
代码流程:

func cmdAdd(args *skel.CmdArgs) error {
	ipamConf, confVersion, err := allocator.LoadIPAMConfig(args.StdinData, args.Args) //加载IPAM的配置和版本号
	if err != nil {
		return err
	}

	result := &current.Result{}

	if ipamConf.ResolvConf != "" {  //若ipamConf.ResolvConf不为""
		dns, err := parseResolvConf(ipamConf.ResolvConf) //解析ResolvConf信息
		if err != nil {
			return err
		}
		result.DNS = *dns // 设置DNS信息
	}

	store, err := disk.New(ipamConf.Name, ipamConf.DataDir)  // 存储路径示例 /var/lib/cni/networks/<cni name>/
	if err != nil {
		return err
	}
	defer store.Close()

	// Keep the allocators we used, so we can release all IPs if an error
	// occurs after we start allocating
	allocs := []*allocator.IPAllocator{}

	// Store all requested IPs in a map, so we can easily remove ones we use
	// and error if some remain
	requestedIPs := map[string]net.IP{} //net.IP cannot be a key

	for _, ip := range ipamConf.IPArgs {
		requestedIPs[ip.String()] = ip
	}

	for idx, rangeset := range ipamConf.Ranges {
		allocator := allocator.NewIPAllocator(&rangeset, store, idx)

		// Check to see if there are any custom IPs requested in this range.
		var requestedIP net.IP
		for k, ip := range requestedIPs {
			if rangeset.Contains(ip) {
				requestedIP = ip
				delete(requestedIPs, k)
				break
			}
		}

		ipConf, err := allocator.Get(args.ContainerID, args.IfName, requestedIP) //获取一个ip
		if err != nil {
			// Deallocate all already allocated IPs
			for _, alloc := range allocs {
				_ = alloc.Release(args.ContainerID, args.IfName)
			}
			return fmt.Errorf("failed to allocate for range %d: %v", idx, err)
		}

		allocs = append(allocs, allocator)

		result.IPs = append(result.IPs, ipConf)
	}

	// If an IP was requested that wasn't fulfilled, fail
	if len(requestedIPs) != 0 {
		for _, alloc := range allocs {
			_ = alloc.Release(args.ContainerID, args.IfName)
		}
		errstr := "failed to allocate all requested IPs:"
		for _, ip := range requestedIPs {
			errstr = errstr + " " + ip.String()
		}
		return fmt.Errorf(errstr)
	}

	result.Routes = ipamConf.Routes //设置路由信息

	return types.PrintResult(result, confVersion)
}

ipam.cmdDel
输入参数: stdin_data标准输入等信息
输出参数: nil or err
代码流程:

func cmdDel(args *skel.CmdArgs) error {
        ipamConf, _, err := allocator.LoadIPAMConfig(args.StdinData, args.Args) //加载IPAM的配置
        if err != nil {
                return err
        }

        store, err := disk.New(ipamConf.Name, ipamConf.DataDir)
        if err != nil {
                return err
        }
        defer store.Close()

        // Loop through all ranges, releasing all IPs, even if an error occurs
        var errors []string
        for idx, rangeset := range ipamConf.Ranges {
                ipAllocator := allocator.NewIPAllocator(&rangeset, store, idx)

                err := ipAllocator.Release(args.ContainerID, args.IfName) //删除已经分配的ip
                if err != nil {
                        errors = append(errors, err.Error())
                }
        }

        if errors != nil {
                return fmt.Errorf(strings.Join(errors, ";"))
        }
        return nil
}

编写一个简单插件

我们这里通过一个shell 脚本编写的bash-cni插件,实现容器ip地址分配与设置及容器网关的设置

bash-cni插件、配置详见超链接

bash-cni插件配置

{
        "cniVersion": "0.3.1",		//版本
        "name": "mynet",			//网络名词
        "type": "bash-cni",			//使用的插件
        "network": "192.168.0.0/16",	//集群POD 地址区间
        "subnet": "192.168.1.0/24"		//本节点 POD 地址区间
}

bash-cni插件简单解析

shell脚本大家应该都比较熟悉,这里就只注释最核心的几个命令

ADD 方法

输入参数,输出参数详见上面的cmdAdd

ADD)
	network=$(echo "$stdin" | jq -r ".network")
	subnet=$(echo "$stdin" | jq -r ".subnet")
	subnet_mask_size=$(echo $subnet | awk -F  "/" '{print $2}')

	all_ips=$(nmap -sL $subnet | grep "Nmap scan report" | awk '{print $NF}')
	all_ips=(${all_ips[@]})
	skip_ip=${all_ips[0]}
	gw_ip=${all_ips[1]}
	reserved_ips=$(cat $IP_STORE 2> /dev/null || printf "$skip_ip\n$gw_ip\n") # reserving 10.244.0.0 and 10.244.0.1 
	reserved_ips=(${reserved_ips[@]})
	printf '%s\n' "${reserved_ips[@]}" > $IP_STORE
	container_ip=$(allocate_ip)

	mkdir -p /var/run/netns/
	ln -sfT $CNI_NETNS /var/run/netns/$CNI_CONTAINERID	# 创建进程对应的网络隔离空间软连接,之后ip netns list就可以查看到,
								# 有了这一步,之后的命令就都很熟悉了
								# 这里的别名是容器id:$CNI_CONTAINERID, 后面通过ip netns exec $CNI_CONTAINERID bash即可进入容器
								# 容器id示例: 1c2e939e5d5199d4e426e0f582af0edb905e00ab54e2f7015fb3c6f1a2d209c8

	rand=$(tr -dc 'A-F0-9' < /dev/urandom | head -c4)
	host_if_name="veth$rand"
	ip link add $CNI_IFNAME type veth peer name $host_if_name 	# 创建veth-pair 这样一对虚拟设备接口, 
									# 实际命令示例:ip link add eth0 type veth peer name veth2D2A
									# 2D2A随机生成

	ip link set $host_if_name up 					# up 接口
	ip link set $host_if_name master cni0 				# 将接口veth2D2A 添加到桥cni0上

	ip link set $CNI_IFNAME netns $CNI_CONTAINERID			# 将网络接口eth0移进隔离空间$CNI_CONTAINERID:
									# 1c2e939e5d5199d4e426e0f582af0edb905e00ab54e2f7015fb3c6f1a2d209c8
	ip netns exec $CNI_CONTAINERID ip link set $CNI_IFNAME up	# up容器中的eth0接口
	ip netns exec $CNI_CONTAINERID ip addr add $container_ip/$subnet_mask_size dev $CNI_IFNAME # 设置ip,掩码到隔离空间中的eth0
									# 示例:ip netns exec 1c2e939e5d5199d4e426e0f582af0edb905e00ab54e2f7015fb3c6f1a2d209c8 ip addr add 192.168.1.6/24 dev eth0
	ip netns exec $CNI_CONTAINERID ip route add default via $gw_ip dev $CNI_IFNAME   # 设置隔离空间网关
									# 示例: ip netns exec 1c2e939e5d5199d4e426e0f582af0edb905e00ab54e2f7015fb3c6f1a2d209c8 ip route add default via 192.168.1.1 dev eth0

	mac=$(ip netns exec $CNI_CONTAINERID ip link show eth0 | awk '/ether/ {print $2}') # 获取隔离空间中eth0对应的mac地址
	# 输出json格式的各个参数: mac/ip/gw/netns等等
echo "{
  \"cniVersion\": \"0.3.1\",
  \"interfaces\": [                                            
      {
          \"name\": \"eth0\",
          \"mac\": \"$mac\",                            
          \"sandbox\": \"$CNI_NETNS\" 
      }
  ],
  \"ips\": [
      {
          \"version\": \"4\",
          \"address\": \"$container_ip/$subnet_mask_size\",
          \"gateway\": \"$gw_ip\",          
          \"interface\": 0 
      }
  ]
}" >&3
;;

DEL 方法

输入参数,输出参数详见上面的cmdDel

DEL)
	ip=$(ip netns exec $CNI_CONTAINERID ip addr show eth0 | awk '/inet / {print $2}' | sed  s%/.*%% || echo "") # 获取隔离空间ip eth0网络接口设置的ip
	if [ ! -z "$ip" ]
	then
		sed -i "/$ip/d" $IP_STORE   # 从/tmp/reserved_ips移除该ip
	fi
	rm -f /var/run/netns/$CNI_CONTAINERID  # 删除之前创建个ip netns 命令用的软链接
;;

bash-cni插件连通性问题修复

同节点容器间不通

添加转发允许

iptables -t filter -A FORWARD -s 192.168.0.0/16 -j ACCEPT
iptables -t filter -A FORWARD -d 192.168.0.0/16 -j ACCEPT

容器内,访问互联网不通

增加不是从cni0出去的规则NAT(SNAT)规则

iptables -t nat -A POSTROUTING -s 192.168.1.0/24 ! -o cni0 -j MASQUERADE # node1:
iptables -t nat -A POSTROUTING -s 192.168.2.0/24 ! -o cni0 -j MASQUERADE # node2:

跨节点容器间不通

添加跨网段路由

ip route add 192.168.2.0/24 via 192.168.122.16 dev ens3 # run on node1, 192.168.122.16是node2节点的宿主ip
ip route add 192.168.1.0/24 via 192.168.122.15 dev ens3 # run on node2, 192.168.122.15是node1节点的宿主ip
跨节点容器间数据流程

示例:
node1: 192.168.122.15 容器A网络ip:192.168.1.14, 容器网络网关:192.168.1.1(配置在node1的cni0桥上)
node1: 192.168.122.16 容器B网络ip:192.168.2.14, 容器网络网关:192.168.2.1(配置在node2的cni0桥上)

动作:在容器A上ping容器B

  1. 容器A将报文发给node1上的网桥cni0 //这里容器A上的eth0是veth设备,对端veth设备在cni0上
  2. node1上,经过路由表,找到网关192.168.16,使用网卡ens3
  3. 在node1上做snat,将原地址从192.168.1.14换成ens3的网卡ip 192.168.122.15
  4. 在node2上,报文forward到cni上,然后发给192.168.2.14 //同样的veth pair
  5. 回城同理

CNI调试方法

在使用Kubernetes的CNI进行配置网络的过程中,编写插件,配置插件配置文件外,实际使用中经常会关注容器将通信的问题,同节点容器间通信及跨节点容器间通信等。

通过命令kubectl exec -it -n kube-system -c kube-flannel kube-flannel-ds-amd64-frmtz /bin/sh 我们可以再主节点进相应的POD节点的相应的容器中,但这存在一定的极限:
比如部分image里面,实际上是没有我们常用的工具,比如ifconfig/ip/route等命令。

这里我们提供一个进入POD所在的网络隔离空间的方法,在隔离空间中,可以使用宿主机丰富的工具命令。脚本及说明详见超链接

比如:
执行以下命令进入coredns所在的隔离空间:

contain2ns.sh add  coredns-fb8b8dccf-gshlr
enter_ns.sh coredns-fb8b8dccf-gshlr

contain2ns.sh 脚本分析

脚本输入: POD_NAME
输出无:
代码流程:

	CONTAIN=k8s_POD_$2			# 输入POD NAME, 补充在docker 中显示的前缀:k8s_POD
	CONTAIN_ID=`docker ps |grep $CONTAIN |awk -F " " '{printf $1}'`	# 获取docker 容器列表中,含有k8s_POD_<POD_NAME>的容器id,其中docker ps可以显示容器列表, grep用于过滤名称
	PID=`docker top $CONTAIN_ID|awk -F " " '{if(NR==2)printf $2}'` # 通过docker <CONTAIN_ID> 获取容器对应你的PID等信息,然后awk获取PID,行号为2
	ln -sf /proc/$PID/ns/net "/var/run/netns/$2"  #创建进程网络隔离空间的软链接,放在/var/run/netns目录中,别名为输入的POD名称。通过ip netns list即可查看到对应的隔离空间,这里类似本篇文章中自己编写一个网络插件章节中的实现。

macvlan总结与讨论

通过使用与分析,我们知道:

  • macvlan插件在集群多节点上,要每个节点都放配置文件, 比如/etc/cni/net.d/10-maclannet.conf, 并且各个节点的子网不能冲突。
  • macvlan插件默认网关的设置上还需要考虑ip是否已经存在,插件并不会自动帮我们设置,需要手动配置。
  • macvlan插件在集群多节点上,跨节点容器间通信上还需要手动配置网段路由。
  • macvlan插件在访问外部网络上,需要再手动配置网关,之后流量部分情况下,还需要走snat规则出公网。
  • macvlan插件的容器内部接口,是基于指定的宿主机主(master)接口,容器内部接口与主(master)接口不能直接通信。
  • macvlan插件是一个underlay的网络技术,网络栈有一定的独立性,安全方面会有一定的限制,性能上接近于宿主机的网络。

同时作为一个underlay的实现,macvlan方案在性能上接近于宿主机网络。

那当前有什么方案可以直接解决上述问题呢? 下一篇《CNI网络插件之flannel》将为你详细介绍。

 类似资料: