Kubernetes网络详解(二) CNI原理与实现
|
1、CNI概述
CNI的全称是Container Network Interface,属于CNCF的一个子项目,它是一个完整的规范,详见 https://github.com/containernetworking/cni/blob/master/SPEC.md
本文会在一个实际的环境中讲解其基本原理,并给出一些直观的例子。
Kubernetes自身在创建和删除pod的时候不涉及网络相关的操作,它会把这些交给CNI插件来完成。具体来讲就是当Kubernetes分别在创建一个pod的时候和删除一个pod的时候,对应的cni插件要做哪些操作。
2、CNI的入口
- `cni-conf-dir`
这个参数指定了cni配置文件的目录默认是`/etc/cni/net.d`
- `cni-bin-dir`
这个参数指定了cni插件的可执行文件所在的目录`/opt/cni/bin`
CNI插件会以二进制可执行文件的形式提供,存放于`cni-bin-dir`这个目录下,kubelet会根据cni配置文件来决定具体调用哪些插件。
``` [root@node1 ~]# ls -l /opt/cni/bin/ total 36132 -rwxr-xr-x. 1 root root 2973336 Mar 25 2019 bridge -rwxr-xr-x. 1 root root 7598064 Mar 25 2019 dhcp -rwxr-xr-x. 1 root root 2110208 Mar 25 2019 flannel -rwxr-xr-x. 1 root root 2288536 Mar 25 2019 host-device -rwxr-xr-x. 1 root root 2238208 Mar 25 2019 host-local -rwxr-xr-x. 1 root root 2621472 Mar 25 2019 ipvlan -rwxr-xr-x. 1 root root 2257808 Mar 25 2019 loopback -rwxr-xr-x. 1 root root 2650160 Mar 25 2019 macvlan -rwxr-xr-x. 1 root root 2613864 Mar 25 2019 portmap -rwxr-xr-x. 1 root root 2946664 Mar 25 2019 ptp -rwxr-xr-x. 1 root root 1951880 Mar 25 2019 sample -rwxr-xr-x. 1 root root 2103456 Mar 25 2019 tuning -rwxr-xr-x. 1 root root 2617328 Mar 25 2019 vlan ```
在本文中我们会详细分析几个基础的cni plugin。
3、flannel cni plugin
flannel的DaemonSet的启动前会将一个ConfigMap中的内容copy成一个cni配置文件
``` [root@node1 ~]# cat /etc/cni/net.d/10-flannel.conflist { "cniVersion": "0.2.0", "name": "cbr0", "plugins": [ { "type": "flannel", "delegate": { "hairpinMode": true, "isDefaultGateway": true } }, { "type": "portmap", "capabilities": { "portMappings": true } } ] } ```
配置文件中的plugins参数指定了需要调用的plugin列表和对应的配置,本文中我们只关心基础的plugin,因此这里会暂时忽略portmap这个plugin。
plugin中的type参数指定具体的plugin二进制文件名称,因此kubelet会调用`/opt/cni/bin/flannel`,代码位于`https://github.com/containernetworking/plugins/tree/master/plugins/meta/flannel`
``` func main() { skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO") } ```
这个main就是flannel cni plugin的入口,当创建和删除pod时会分别调用其中的`cmdAdd`和`cmdDel`函数来执行对应的操作。
``` func cmdAdd(args *skel.CmdArgs) error { n, err := loadFlannelNetConf(args.StdinData) if err != nil { return err }
fenv, err := loadFlannelSubnetEnv(n.SubnetFile) if err != nil { return err }
if n.Delegate == nil { n.Delegate = make(map[string]interface{}) } else { if hasKey(n.Delegate, "type") && !isString(n.Delegate["type"]) { return fmt.Errorf("'delegate' dictionary, if present, must have (string) 'type' field") } if hasKey(n.Delegate, "name") { return fmt.Errorf("'delegate' dictionary must not have 'name' field, it'll be set by flannel") } if hasKey(n.Delegate, "ipam") { return fmt.Errorf("'delegate' dictionary must not have 'ipam' field, it'll be set by flannel") } }
return doCmdAdd(args, n, fenv) } ```
`cmdAdd` 中主要有三个步骤
1. 从stdin读取配置,生成一个`type NetConf struct`对象
2. 从第1步返回的`NetConf.SubnetFile`指定的路径(默认是`/run/flannel/subnet.env`)载入一个配置文件,这个文件由Flanneld DaemonSet生成,在上一篇文章中有说明
3. 调用`doCmdAdd`函数执行真正的操作
3.1、读取NetConf配置
``` type NetConf struct { types.NetConf
SubnetFile string `json:"subnetFile"` DataDir string `json:"dataDir"` Delegate map[string]interface{} `json:"delegate"` }
# types.NetConf结构体 // NetConf describes a network. type NetConf struct { CNIVersion string `json:"cniVersion,omitempty"`
Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` Capabilities map[string]bool `json:"capabilities,omitempty"` IPAM IPAM `json:"ipam,omitempty"` DNS DNS `json:"dns"`
RawPrevResult map[string]interface{} `json:"prevResult,omitempty"` PrevResult Result `json:"-"` } ```
现在回头看cni的配置文件(忽略了portmap plugin)
``` { "cniVersion": "0.2.0", "name": "cbr0", "plugins": [ { "type": "flannel", "delegate": { "hairpinMode": true, "isDefaultGateway": true } } ] } ```
对应于flannel plugin中的`NetConf`结构体,可以看出配置文件中的
``` "delegate": { "hairpinMode": true, "isDefaultGateway": true } ```
对应于`NetConf`中的`Delegate map[string]interface{}`,除了这个和name, type, version之外而其它值都为空或者默认。
3.2、读取flanneld生成的配置文件
在node1上`/run/flannel/subnet.env`这个文件的内容如下
``` [root@node1 ~]# cat /run/flannel/subnet.env FLANNEL_NETWORK=10.244.0.0/16 FLANNEL_SUBNET=10.244.1.1/24 FLANNEL_MTU=1450 FLANNEL_IPMASQ=true [root@node1 ~]# ```
3.3、执行doCmdAdd()
这是flannel cni plugin的核心
``` func doCmdAdd(args *skel.CmdArgs, n *NetConf, fenv *subnetEnv) error { n.Delegate["name"] = n.Name
if !hasKey(n.Delegate, "type") { n.Delegate["type"] = "bridge" }
if !hasKey(n.Delegate, "ipMasq") { // if flannel is not doing ipmasq, we should ipmasq := !*fenv.ipmasq n.Delegate["ipMasq"] = ipmasq }
if !hasKey(n.Delegate, "mtu") { mtu := fenv.mtu n.Delegate["mtu"] = mtu }
if n.Delegate["type"].(string) == "bridge" { if !hasKey(n.Delegate, "isGateway") { n.Delegate["isGateway"] = true } } if n.CNIVersion != "" { n.Delegate["cniVersion"] = n.CNIVersion }
n.Delegate["ipam"] = map[string]interface{}{ "type": "host-local", "subnet": fenv.sn.String(), "routes": []types.Route{ { Dst: *fenv.nw, }, }, }
return delegateAdd(args.ContainerID, n.DataDir, n.Delegate) } ```
主要完成的工作如下
- 将下一级调用的的cni plugin设置为`bridge` - 设置ipMasq和mtu,注意这里cni plugin最终的ipMasq和flanneld自身的是相反的,也就是说如果flanneld设置了,cni plugin就不再设置了 - 如果是`bridge`,且没有设置isGateway的话,将其默认设置为true - 设置ipam cni plugin为host-local,并且将subnet参数设置为上文`/run/flannel/subnet.env`中的`FLANNEL_SUBNET`,并设置路由
从这里可以看出flannel cni plugin的核心逻辑就根据当前配置生成bridge和host-local这两个cni plugin的配置参数,随后通过调用它们来实现主要的功能。
host-local cni plugin的功能是在当前节点从一个subnet中给pod分配ip地址,详细逻辑可阅读其代码来理解。这个cni plugin会被bridge cni plugin调用。
下面简述一下bridge的实现。
4、bridge cni plugin
bridge插件负责从pod到veth到cni0的整个流程。
核心功能同样在cmdAdd和cmdDel这两个函数中,下面看cmdAdd中的逻辑。
总体上分为二层和三层两部分
4.1、二层处理
第一步会先处理网桥设备
``` br, brInterface, err := setupBridge(n) if err != nil { return err } ```
`setupBridge`负责创建cni0这个bridge,并配置它的mtu、混杂模式等等,最后将这个网桥设备up起来。详细的代码如下
``` func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) { // create bridge if necessary br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode) if err != nil { return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err) } ... }
func ensureBridge(brName string, mtu int, promiscMode bool) (*netlink.Bridge, error) { br := &netlink.Bridge{ LinkAttrs: netlink.LinkAttrs{ Name: brName, MTU: mtu, TxQLen: -1, }, }
err := netlink.LinkAdd(br) ... if promiscMode { ... if err := netlink.LinkSetUp(br); err != nil { ... } ```
第二步会处理veth pair接口
``` netns, err := ns.GetNS(args.Netns) if err != nil { return fmt.Errorf("failed to open netns %q: %v", args.Netns, err) } defer netns.Close()
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode) if err != nil { return err } ```
`setupVeth`函数会在pod所在的network namespace中创建一对veth接口,并将其中的一端移到host中,然后设置它的mac地址,并将其挂载到cni0这个网桥上,如果需要的话还会设置这个接口的hairpin模式。代码如下
``` func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) (*current.Interface, *current.Interface, error) { contIface := ¤t.Interface{} hostIface := ¤t.Interface{}
err := netns.Do(func(hostNS ns.NetNS) error { // create the veth pair in the container and move host end into host netns hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, hostNS) if err != nil { return err } contIface.Name = containerVeth.Name contIface.Mac = containerVeth.HardwareAddr.String() contIface.Sandbox = netns.Path() hostIface.Name = hostVeth.Name return nil }) if err != nil { return nil, nil, err }
// need to lookup hostVeth again as its index has changed during ns move hostVeth, err := netlink.LinkByName(hostIface.Name) if err != nil { return nil, nil, fmt.Errorf("failed to lookup %q: %v", hostIface.Name, err) } hostIface.Mac = hostVeth.Attrs().HardwareAddr.String()
// connect host veth end to the bridge if err := netlink.LinkSetMaster(hostVeth, br); err != nil { return nil, nil, fmt.Errorf("failed to connect %q to bridge %v: %v", hostVeth.Attrs().Name, br.Attrs().Name, err) }
// set hairpin mode if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil { return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs().Name, err) }
return hostIface, contIface, nil } ```
4.2、三层处理
``` isLayer3 := n.IPAM.Type != ""
...
if isLayer3 { .. } ```
三层网络是否需要处理取决于ipam cni plugin是否已经配置,在我们的环境中,这个字段已经由flannel cni plugin配置成host-local了,因此这里需要处理三层的逻辑。处理的详细逻辑如下
``` // run the IPAM plugin and get back the config to apply r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData) if err != nil { return err }
// release IP in case of failure defer func() { if !success { os.Setenv("CNI_COMMAND", "DEL") ipam.ExecDel(n.IPAM.Type, args.StdinData) os.Setenv("CNI_COMMAND", "ADD") } }()
// Convert whatever the IPAM result was into the current Result type ipamResult, err := current.NewResultFromResult(r) if err != nil { return err }
result.IPs = ipamResult.IPs result.Routes = ipamResult.Routes
if len(result.IPs) == 0 { return errors.New("IPAM plugin returned missing IP config") } ```
这里bridge plugin会调用host-local这个ipmi plugin来为当前处理的pod分配一个ip地址。
``` // Gather gateway information for each IP family gwsV4, gwsV6, err := calcGateways(result, n) if err != nil { return err }
```
接着会根据这个地址计算出bridge设备cni0的ip地址。
``` // Configure the container hardware address and IP address(es) if err := netns.Do(func(_ ns.NetNS) error { contVeth, err := net.InterfaceByName(args.IfName)
... // Add the IP to the interface if err := ipam.ConfigureIface(args.IfName, result); err != nil { return err }
// Send a gratuitous arp for _, ipc := range result.IPs { if ipc.Version == "4" { _ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth) } } return nil }); err != nil { return err } ```
这段代码的含义是在pod所在的network namespace中配置之前添加进来的那个veth接口,包括将设备设置为up状态,配置ip地址和路由信息,最后发送gratuitous arp广播。
``` if n.IsGW { var firstV4Addr net.IP // Set the IP address(es) on the bridge and enable forwarding for _, gws := range []*gwInfo{gwsV4, gwsV6} { for _, gw := range gws.gws { if gw.IP.To4() != nil && firstV4Addr == nil { firstV4Addr = gw.IP }
err = ensureBridgeAddr(br, gws.family, &gw, n.ForceAddress) if err != nil { return fmt.Errorf("failed to set bridge addr: %v", err) } }
if gws.gws != nil { if err = enableIPForward(gws.family); err != nil { return fmt.Errorf("failed to enable forwarding: %v", err) } } } } ```
接下来会给cni0这个bridge配置ip地址,并执行类似于`echo 1 > /proc/sys/net/ipv4/ip_forward`的操作来启用数据包转发功能
``` if n.IPMasq { chain := utils.FormatChainName(n.Name, args.ContainerID) comment := utils.FormatComment(n.Name, args.ContainerID) for _, ipc := range result.IPs { if err = ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment); err != nil { return err } } } ```
最后如果cni plugin设置ipmasq则需要进行相关ipmasq相关的设置,在当前环境中ipmasq是由Flanneld Daemon完成的,因此这里的cni plugin不会进行设置。
至此,详细分析了bridge cni plugin的cmAdd()主要流程,其核心功能总结如下
- 新建并配置网桥设备cni0 - 在pod所在的namespace中创建一对veth接口,并将其中的一端移到host中并将其挂载到网桥上 - 调用ipmi cni plugin给pod中的一端veth接口分配ip地址并将其配置到接口上 - 给cni0配置ip地址并开启数据包转发功能
参考文献
-https://github.com/containernetworking/cni/blob/master/SPEC.md -https://github.com/containernetworking/cni -https://github.com/containernetworking/plugins
|



有话要说