CNI开发的基本套路

背景

CNI 这东西搞容器的运维想必都不陌生,要知道,kubernetes的设计之初是不包含网络的,网络这玩意每家公司有每家公司自己的玩法,对各个公司来说,没有那种大一统的完美方案,只有最适合自己的方案,所以,kubernetes在设计的时候,没有设计统一的网络方案,只提供了统一的容器网络接口,Container Network Interface,也就是所谓的CNI。

CNI 和 IPAM

一般说的CNI都是包含IPAM的,但其实2个功能是分开实现的。

CNI用于实现网络构建(network部分,网络接口创建,VLAN划分,端口打开关闭等等)。

IPAM用于实现IP地址,DNS,网关信息的分配。

这两者是可以*组合的,比如你可以用flannel的CNI,IPAM却用DHCP或者HOSTLOCAL,甚至如果不想用IPAM的接口,自己在CNI里也可以实现对于的代码逻辑。


CNI创建流程

创建流程可以参考官方的源码,以官方的bridge的CNI代码进行分析,在一个POD的生命周期中,CNI主要有3个方法进行调用,分别是:

  • cmdadd
  • cmdcheck
  • cmddel
func main() {

    skel.PluginMain(cmdAdd, cmdCheck, cmdDel, version.All, bv.BuildString("bridge"))

}

看方法名字大概能猜到干了啥吧,创建容器调用cmdadd, 销毁cmddel, cmdcheck是0.4.0之后新加的,比如粗暴的del,所以在del前会做一些check,check内容可以根据需要自己定义.

先画一下基本的创建流程

graph TD;
 kubelet调用CNI-->获取创建基本参数;
 获取创建基本参数-->解析参数;
 解析参数-->获取ns,podname;
 获取ns,podname-->解析net.d下配置文件;
 解析net.d下配置文件-->netlink配置端口;
 netlink配置端口-->调用ipam;
 调用ipam-->分配IP,DNS,GW;
 分配IP,DNS,GW-->传值CNI;
  传值CNI-->配置IP;
  配置IP-->返回kubelet;

大致流程如上,每个CNI插件基本都会遵循以上流程进行网络的创建,只是不同插件会在上述流程上添加不同的动作,比如bridge会基于netlink创建网桥,vlan会分vlanID等等

代码分析(以bridge为例):

解析参数:cni创建网络一般会从2个渠道获取参数,一个是创建时master传过来的args.Args,是个很长的字符串,一般会包含namespace信息,podname信息等,一个是配置文件传入的args.StdinData,基于配置文件,结构体如下:

type NetConf struct {
    types.NetConf
    BrName       string `json:"bridge"`
    IsGW         bool   `json:"isGateway"`
    IsDefaultGW  bool   `json:"isDefaultGateway"`
    ForceAddress bool   `json:"forceAddress"`
    IPMasq       bool   `json:"ipMasq"`
    MTU          int    `json:"mtu"`
    HairpinMode  bool   `json:"hairpinMode"`
    PromiscMode  bool   `json:"promiscMode"`
    Vlan         int    `json:"vlan"`
}

对比配置文件:

{
    "cniVersion": "0.3.1",    "name": "mynet",    "type": "bridge",    "bridge": "mynet0",    "isDefaultGateway": true,    "forceAddress": false,    "ipMasq": true,    "hairpinMode": true,    "ipam": {
        "type": "host-local",        "subnet": "10.10.0.0/16"
    }}

再看一下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:"-"`
}

type IPAM struct {
    Type string `json:"type,omitempty"`
}

// DNS contains values interesting for DNS resolvers
type DNS struct {
    Nameservers []string `json:"nameservers,omitempty"`
    Domain      string   `json:"domain,omitempty"`
    Search      []string `json:"search,omitempty"`
    Options     []string `json:"options,omitempty"`
}

// Result is an interface that provides the result of plugin execution
type Result interface {
    // The highest CNI specification result version the result supports
    // without having to convert
    Version() string
    // Returns the result converted into the requested CNI specification
    // result version, or an error if conversion failed
    GetAsVersion(version string) (Result, error)
    // Prints the result in JSON format to stdout
    Print() error
    // Prints the result in JSON format to provided writer
    PrintTo(writer io.Writer) error
}

其中类似IsGW,IPMasq之类的均是自定义的变量,暂时可以忽略,主要需要注意的是cniVersion,name,IPAM之类的通用变量.

创建网络:以bridge为例,基本都是调用netlink 进行创建

func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) {
    vlanFiltering := false
    if n.Vlan != 0 {
        vlanFiltering = true
    }
    // create bridge if necessary
    br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode, vlanFiltering)
    if err != nil {
        return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err)
    }
    return br, &current.Interface{
        Name: br.Attrs().Name,
        Mac:  br.Attrs().HardwareAddr.String(),
    }, nil
}

IP分配: ip分配基于ipam进行分配,和cni 一样,同样有cmdadd, cmddel, cmdcheck等方法,ipam之后再分析,这边只要知道ipam分配出IP即可。

// 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 {
                ipam.ExecDel(n.IPAM.Type, args.StdinData)
            }
        }()

IP配置: CNI的工作除了在Node节点上创建适配的网络以外,主要还要在pod所在的pasue container上进行IP的分配,DNS以及路由的配置,由于pause container 享有独立的namespace,所以需要需要在指定的ns下进行IP配置, 官方提供了netns .Do 可以帮助进行操作

pod 由于需要经常销毁,创建,对于网络来说arp刷新很频繁,一般交换机都有mac 表的老化时间,所以在配置IP后,使用arping 手动刷新arp 表。

// Send a gratuitous arp
        if err := netns.Do(func(_ ns.NetNS) error {
            contVeth, err := net.InterfaceByName(args.IfName)
            if err != nil {
                return err
            }
            for _, ipc := range result.IPs {
                if ipc.Version == "4" {
                    _ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
                }
            }
            return nil
        }); err != nil {
            return err
        }

所以编写一个自定义的CNI并不困难,遵循基本的套路,然后再套路里加上自己的业务逻辑,就可以实现符合自己业务需求的K8S网络模型。


上一篇:Shell脚本,正则符号()的保留功能,将内容复制并使用\数字进行内容访问


下一篇:k8s安装记录