1、为何会起CNI?

CNI是Container Network
Interface的缩写,简单地游说,就是一个正式的,通用的接口。已领略我们现起各个各个的容器平台:docker,kubernetes,mesos,我们吧有各式各个的器皿网络解决方案:flannel,calico,weave,并且还有各个新的缓解方案以不断涌现。假如各国起一个初的解决方案,大家都急需针对两端举办适配,那么通过拉动的工作量必然是伟的,而且为是更与非必要之。事实上,我们要提供一个专业的接口,更准之说凡是均等种植协议,就会圆地化解上述问题。一旦闹新的大网方案出现,只要它可以满意这标准的商事,那么她便会为同一知足该谋的享有容器平台供网络效率,而CNI正是这么的一个标准接口协议。

 

2、什么是CNI?

浅地开口,CNI是一个接口协议,用于连接容器管理网及网络插件。前者提供一个器皿所于的network
namespace(从网络的角度来拘禁,network
namespace和容器是一点一滴等价格的),后者负责将network interface插入该network
namespace中(比如veth的同等端),并且以宿主机做有必不可少之布置(例如将veth的其他一样端在bridge中),最后对namespace中之interface举办IP和路由的布。那么CNI的工作其实倘使自从容器管理网处于落运行时信息,包括network
namespace的路线,容器ID以及network interface
name,再从容器网络的布置文件被加载网络布局新闻,再将那个信传送让相应的插件,由插件举行具体的大网布局工作,并将部署的结果更回去到容器管理网中。

最终,需要专注的凡,在事先的CNI版本中,网络部署文件只可以描述一个network,这也即便标志了一个器皿只好参预一个容器网络。然而以新兴之CNI版本中,我们得在布置文件被定义一个所谓的NetworkList,事实上便是概念一个network序列,CNI会依次调用各样network的插件对容器举行对应的布局,从而允许一个器皿可以进入四只容器网络。

 

3、怎么用CNI?

当更加研讨CNI在此以前,我认为首先来看望CNI是怎么利用的,先对CNI有一个直观的认,是挺有必不可少之,这对准咱随后的喻啊将坏有协助。现在,大家以逐条执行如下步骤来演示如何用CNI,并对各样一样步的操作举行必要之辨证。

(1)、编译安装CNI的官插件

当今法定提供了二种档次的插件:main,meta和ipam。其中main类型的插件重要提供某种网络功用,比如我们在示范中校用的brdige,以及loopback,ipvlan,macvlan等等。meta类型的插件无法当做独立的插件使用,它常常用调用其他插件,例如flannel,或者配合其他插件使用,例如portmap。最终ipam类型的插件其实是针对性具备CNI插件共有的IP管理一些的纸上谈兵,从而减弱插件编写过程被的还工作,官方提供的发出dhcp和host-local两栽类型。

继执行如下命令,完成插件的下载,编译,安装工作:

$ mkdir -p $GOPATH/src/github.com/containernetworking/plugins

$ git clone https://github.com/containernetworking/plugins.git  $GOPATH/src/github.com/containernetworking/plugins

$ cd $GOPATH/src/github.com/containernetworking/plugins

$ ./build.sh

  

最后具备的插件都用为可执行文件的花样有于目$GOPATH/src/github.com/containernetworking/plugins/bin之下。

 

(2)、创立布局文件,对所创设的纱展开描述

工作目录”/etc/cni/net.d”是CNI默认的大网布局文件目录,当没有专门指定时,CNI就会默认对拖欠目录举办查找,从中加载配置文件举行容器网络的创立。至于对安排文件相继字段的详细描述,我以在持续章节举行表达。

现今我们仅仅待履行如下命令,描述一个我们记挂使制造的容器网络”mynet”即可。为了简单起见,我们的NetworkList中特只有”mynet”这多少个network。

$ mkdir -p /etc/cni/net.d

$ cat >/etc/cni/net.d/10-mynet.conflist <<EOF
{
        "cniVersion": "0.3.0",
        "name": "mynet",
        "plugins": [
          {
                "type": "bridge",
                "bridge": "cni0",
                "isGateway": true,
                "ipMasq": true,
                "ipam": {
                        "type": "host-local",
                        "subnet": "10.22.0.0/16",
                        "routes": [
                                { "dst": "0.0.0.0/0" }
                        ]
                }
          }
        ]
}
EOF

$ cat >/etc/cni/net.d/99-loopback.conf <<EOF
{
    "cniVersion": "0.3.0",
    "type": "loopback"
}
EOF

  

(3)、模拟CNI的行进程,创立network
namespace,出席上文中描述的器皿网络”mynet”

先是我们从github上下载、编译CNI的源码,最后以于bin目录下生成一个号称吧”cnitool”的可执行文件。事实上,能够当cnitool是一个模拟程序,大家先创建一个誉为吧ns的network
namespace,用来法一个新创制的器皿,再调用cnitool对拖欠network
namespace举行网络部署,从而模拟一个新建的容器在一个容器网络的进程。

起cnitool的举行结果来拘禁,它会合回去一个带有了interface,IP,路由等等各类消息之json串,事实上它正是CNI对容器进行网络布局后其余结果新闻,对这我们以于此起彼伏章节举行详尽的讲述。

说到底,大家得观察network
namespace内新建的网卡eth0的IP地址为10.22.0.5/16,正好包含在容器网络”mynet”的子网范围10.22.0.0/16底内,由此我们得以当容器都成功在了容器网络中,演示成功。

$ git clone https://github.com/containernetworking/cni.git $GOPATH/src/github.com/containernetworking/cni

$ cd $GOPATH/src/github.com/containernetworking/cni

$ ./build.sh

$ cd $GOPATH/src/github.com/containernetworking/cni/bin

$ export CNI_PATH=$GOPATH/src/github.com/containernetworking/plugins/bin

$ ip netns add ns

$ ./cnitool add mynet /var/run/netns/ns 
{
    "cniVersion": "0.3.0",
    "interfaces": [
        {
            "name": "cni0",
            "mac": "0a:58:0a:16:00:01"
        },
        {
            "name": "vetha418f787",
            "mac": "c6:e3:e9:1c:2f:20"
        },
        {
            "name": "eth0",
            "mac": "0a:58:0a:16:00:05",
            "sandbox": "/var/run/netns/ns"
        }
    ],
    "ips": [
        {
            "version": "4",
            "interface": 2,
            "address": "10.22.0.5/16",
            "gateway": "10.22.0.1"
        }
    ],
    "routes": [
        {
            "dst": "0.0.0.0/0"
        }
    ],
    "dns": {}
}

$ ip netns exec ns ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.22.0.5  netmask 255.255.0.0  broadcast 0.0.0.0
        inet6 fe80::646e:89ff:fea6:f9b5  prefixlen 64  scopeid 0x20<link>
        ether 0a:58:0a:16:00:05  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 8  bytes 648 (648.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

  

4、怎么配CNI?

自打上文中我们得以知晓,CNI只辅助三栽操作:ADD,
DEL,VERSION,而二种植操作所用配备的参数和结果如下:

  • 将container加入network(Add):
    • Parameters:
      • Version:CNI版本消息
      • Container ID:
        这一个字段是可选的,可是提出利用,在容器在在的早晚要求该字段全局唯一的。比如,存在IPAM的环境可能会合要求每个container都分配一个独的ID,这样各种一个IP的分红还是能与一个一定的容器相关联。在appc
        implementations中,container ID其实就是pod ID
      • Network namespace path:这多少个字段表示要投入的network
        namespace的路径。例如,/proc/[pid]/ns/net或者对该目录的bind-mount/link。
      • Network configuration:
        这是一个JSON文件用于描述container可以入的network,具体内容在下文中描述
      • Extra
        arguments:该字段提供了同种而选机制,从而允许基于每个容器进行CNI插件的简易布置
      • Name of the interface inside the
        container:该字段提供了当container (network
        namespace)中之interface的名;因而,它为亟须符合Linux对于网命名的限制
    • Result:
      • Interface list:按照插件的不等,这些字段可以概括sandbox
        (container or hypervisor) interface的name,以及host
        interface的name,每个interface的hardware
        address,以及interface所在的sandbox(假设是的话)的信息。
      • IP configuration assigned to each
        interface:IPv4和/或者IPv6地址,gateways以及为sandbox或host
        interfaces中添加的路由
      • DNS inormation:包含nameservers,domains,search
        domains和options的DNS information的字典
  •  将container从network中删除(Delete):
    • Parameter:
      • Version:CNI版本信息
      • ContainerID:定义跟齐
      • Network namespace path:定义和齐
      • Network configuration:定义跟齐
      • Extra argument:定义及齐
      • Name of the interface inside the container:定义跟齐
  • 版本音信
    • Parameter:无
    • Result:再次来到插件扶助的有CNI版本  

 

以上文的讲述中我们简要了针对性Network
configuration的描述。事实上,它的内容与上文演示实例中的”/etc/cni/net.d/10-mynet.conf”网络布局文件是同等的,用于描述容器了容器需要在的大网,下边是针对性其中一些最首要字段的叙说:

  • cniVersion(string):cniVersion以Semantic Version
    2.0底格式指定了插件使用的CNI版本
  • name (string):Network name。这该以全路管理域中如故唯一的
  • type (string):插件类型,也表示了CNI插件可执行文件的公文称
  • args
    (dictionary):由容器运行时供的可选的参数。比如,可以以一个由于label组成的dictionary传递让CNI插件,通过在args下增添一个labels字段来落实
  • ipMasqs
    (boolean):可采取(假诺插件扶助的言辞)。为network在宿主机创设IP
    masquerade。假若用将宿主机作为网关,为了可以路由于至容器分配的IP,这么些字段是须的
  • ipam:由特定的IPAM值组成的dictionary
    • type
      (string):IPAM插件的档次,也表示IPAM插件的可执行文件的文件称
  • dns:由特定的DNS值组成的dictionary
    • nameservers (list of
      strings):一层层对network可见的,以优先级顺序排列的DNS
      nameserver列表。列表中的各个一样码都含有了一个IPv4要一个IPv6地址
    • domain (string):用于查找short hostname的仍地点
    • search (list of strings):以先行级顺序排列的用来查找short
      domain的查找域。对于大多数resolver,它的预先级比domain更胜似
    • options(list of strings):一多元可以为传给resolver的可选项

插件可能会师定义其自己能采取的附加的字段,可是遇到一个不明不白的字段可能晤面出错误。例外的凡args字段,它可以为用于传输一些额外的字段,但也恐怕会合给插件忽略

 

5、CNI具体是怎落实的?

及最近结束,大家本着CNI的运用、配置和法则都早就闹了基本的认识,所以啊是下因源码来对CNI做一个酣畅淋漓的理解了。上面,大家用上述文中的以身作则实例作为线索,以模拟程序cnitool作为切入口,来针对所有CNI的推行进程进展详尽的分析。

(1)、加载容器网络部署信息

首先大家来拘禁一下容器网络布局的数据结构表示:

type NetworkConfigList struct {
    Name       string
    CNIVersion string
    Plugins    []*NetworkConfig
    Bytes      []byte
}

type NetworkConfig struct {
    Network *types.NetConf
    Bytes   []byte
}

// 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         struct {
        Type string `json:"type,omitempty"`
    } `json:"ipam,omitempty"`
    DNS DNS `json:"dns"`
}

  

通过简单的分析以后,大家得以窥见,数据结构表示的情节跟示范实例中的json配置文件中央是平的。因而,这无异步的源码实现丰盛简短,基本流程如下:

  • 先是确定安排文件所在的目录netdir,如若无特意指定,则默认为”/etc/cni/net.d”
  • 调用netconf, err := libcni.LoadConfList(netdir,
    os.Args[2]),其中参数os.Args[2]也用户指定的感念使出席的network的名字,在示范示例中即为”mynet”。该函数首先会招来netdir中是否发盖”.conflist”作为后缀的配置文件,如若出,且布局消息遭到之”Name”和参数os.Args[2]一致,则一向用配备信息填写并返NetConfigList即可。否则,查找是否在因”.conf”或”.json”作为后缀的布文件。同样,假若存在”Name”一致的部署,则加载该配置文件。由于”.conf”或”.json”中仍旧单科的大网布局,由此待将那么些包装成仅爆发一个NetConfig的NetworkConfigList再返。到是截止,容器网络部署加载成功。

 

(2)、配置容器运行时音信

同等,大家事先来拘禁一下容器运行时信息的数据结构:

type RuntimeConf struct {
    ContainerID string
    NetNS       string
    IfName      string
    Args        [][2]string
    // A dictionary of capability-specific data passed by the runtime
    // to plugins as top-level keys in the 'runtimeConfig' dictionary
    // of the plugin's stdin data.  libcni will ensure that only keys
    // in this map which match the capabilities of the plugin are passed
    // to the plugin
    CapabilityArgs map[string]interface{}
}

  

内最为根本之字段无疑是”NetNS”,它指定了特需在容器网络的network
namespace路径。而Args字段和CapabilityArgs字段都是可选的,用于传递额外的布局消息。具体的内容参见上文中的布置表明。在上文的以身作则实例中,我们连从未指向Args和CapabilityArgs举行其他的布,为了简单起见,大家能够一向看它也空。由此,cnitool对RuntimeConf的部署也就算颇为简约了,只需要拿参数指定的netns赋值给NetNS字段,而ContainerID和IfName字段随意赋值即可,默认将它分别赋值为”cni”和”eth0″,具体代码如下:

rt := &libcni.RuntimeConf{
    ContainerID:    "cni",
    NetNS:          netns,
    IfName:         "eth0",
    Args:           cniArgs,
    CapabilityArgs: capabilityArgs,
}

  

(3)、插足容器网络

 依据加载的器皿网络部署音信以及容器运行时新闻,执行参加容器网络的操作,并将推行之结果打印输出

switch os.Args[1] {
case CmdAdd:
    result, err := cninet.AddNetworkList(netconf, rt)
    if result != nil {
        _ = result.Print()
    }
    exit(err)
    ......
}

  

  

连接下我们登AddNetworkList函数中

// AddNetworkList executes a sequence of plugins with the ADD command
func (c *CNIConfig) AddNetworkList(list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
    var prevResult types.Result
    for _, net := range list.Plugins {
        pluginPath, err := invoke.FindInPath(net.Network.Type, c.Path)
                .....
        newConf, err := buildOneConfig(list, net, prevResult, rt)
                ......
        prevResult, err = invoke.ExecPluginWithResult(pluginPath, newConf.Bytes, c.args("ADD", rt))
                ......
    }

    return prevResult, nil
}

  

于函数上方之笺注大家虽能够领悟及,该函数的意就是是按部就班梯次对NetworkList中之次第network执行ADD操作。该函数的行进程为生彰着,利用一个巡回遍历NetworkList中的逐一network,并对准每个network举办如下三步操作:

  • 首先,调用FindInPath函数,按照newtork的花色,在插件的存放路径,也就是是上文中的CNI_PATH中搜索是否留存对应插件的可执行文件。若有则归其相对路径pluginPath
  • 随即,调用buildOneConfig函数,从NetworkList中领取分离有当下实施ADD操作的network的NetworkConfig结构。这里特别需要小心的是preResult参数,它是齐一个network的操作结果,也以让编码进NetworkConfig中。需要留意的凡,当我们当履行NetworkList时,必须用前方一个network的施行结果作参数传递给当下在举行实践之network。并且于buildOneConfig函数构建每个NetworkConfig时会合默认将中的”name”和”cniVersion”和NetworkList中的布置保持一致,从而避免争辩。
  • 末段,调用invoke.ExecPluginWithResult(pluginPath, netConf.Bytes,
    c.args(“ADD”,
    rt))真正举行network的ADD操作。这里我们要留意的凡netConf.Bytes和c.args(“ADD”,
    rt)这一点儿个参数。其中netConf.Bytes用于存放NetworkConfig中的NetConf结构和诸如上文中的prevResult举办json编码形成的字节流。而c.args()函数用于构建一个Args类型的实例,其中首要囤积容器运行时信息,以及履行的CNI操作的音,例如”ADD”或”DEL”,和插件的积存路径。

事实上ExecPluginWithResult仅仅是一个封装函数,它仅仅只是调用了函数defaultPluginExec.WithResult(pluginPath,
netconf, args)之后,就直重返了。

func (e *PluginExec) WithResult(pluginPath string, netconf []byte, args CNIArgs) (types.Result, error) {
    stdoutBytes, err := e.RawExec.ExecPlugin(pluginPath, netconf, args.AsEnv())
        .....
    // Plugin must return result in same version as specified in netconf
    versionDecoder := &version.ConfigDecoder{}
    confVersion, err := versionDecoder.Decode(netconf)
        ....
    return version.NewResult(confVersion, stdoutBytes)
}

  

足看得出WithResult函数的执行流也是挺清晰的,同样为堪分成以下三步执行:

  • 第一调用e.RawExec.ExecPlugin(pluginPath, netconf,
    args.AsEnv())函数执行实际的CNI操作,对于它的具体内容,大家用于下文举行辨析。此处需要小心的凡其的老多少个参数args.AsEnv(),该函数开的劳作实际就是是沾已有的环境变量,并且将args内的信息,例如CNI操作命令,以环境变量的模式保存起来,以例如”CNI_COMMAND=ADD”的样式传输给插件。由此大家得以领会,容器运行时信息、CNI操作命令以及插件存储路径都是坐环境变量的款型传递给插件的
  • 继调用versionDecoder.Decode(netconf)从network配置中剖析出CNI版本音信
  • 最终,调用version.NewResult(confVersion,
    stdoutBytes),按照CNI版本,构建相应的回结果

终极,我们来看望e.RawExecPlugin函数是怎样操作的,代码如下所示:

func (e *RawExec) ExecPlugin(pluginPath string, stdinData []byte, environ []string) ([]byte, error) {
    stdout := &bytes.Buffer{}

    c := exec.Cmd{
        Env:    environ,
        Path:   pluginPath,
        Args:   []string{pluginPath},
        Stdin:  bytes.NewBuffer(stdinData),
        Stdout: stdout,
        Stderr: e.Stderr,
    }
    if err := c.Run(); err != nil {
        return nil, pluginErr(err, stdout.Bytes())
    }

    return stdout.Bytes(), nil
}

  

以圈罢代码之后,我们也许暴发微之失望。因为是理论及最核心的函数却奇怪之简练,它所开的劳作仅仅只是exec了插件的可执行文件。话虽如此,大家照样有以下几点需要注意:

  • 容器运行时音信以及CNI操作命令等仍然盖环境变量的款型传递给插件的,这一点在齐文中已经颇具提及
  • json,容器网络的安排消息是因而正规输入的样式传递让插件的
  • 插件的运行结果是坐标准输出的款型再次来到给CNI的

暨是结束,整个CNI的执行流已经挺了解了。简单地说,一个CNI插件就是是一个可执行文件,大家从部署文件被取network配置信息,从容器管理网处于得到运行时音讯,再从前者以正规化输入的花样,后者因为环境变量的款式传递传递给插件,最后因安排文件中定义的各种依次调用各样插件,并且用前方一个插件的推行结果包含在network配置信息被传送给下一个执行之插件,整个过程即是如此。鉴于篇幅所限,本文特只是分析了CNI的ADD操作,然则相信有了上文的底蕴之后,领会DEL操作为无会晤极其为难。

  

参照链接:

[1] CNI源码:https://github.com/containernetworking/cni

[2] CNI plugin源码:https://github.com/containernetworking/plugins

[3]
《Kubernetes指南》:https://feisky.gitbooks.io/kubernetes/network/cni/

[4]
《CNI:容器网络接口》:http://cizixs.com/2017/05/23/container-network-cni

相关文章

网站地图xml地图