深入驾驭CNI【bifa365必发】

1、为啥会有CNI?

CNI是Container Network
Interface的缩写,简单地说,就是1个标准的,通用的接口。已知大家前几日有二种种种的器皿平台:docker,kubernetes,mesos,大家也有充分多彩的容器互联网化解方案:flannel,calico,weave,并且还有各类新的缓解方案在不断涌现。假使每出现四个新的化解方案,大家都须求对双方进行适配,那么通过带来的工作量必然是宏大的,而且也是重新和不要求的。事实上,大家只要提供三个正规的接口,更加准确的说是一种协议,就能圆满地缓解上述难题。一旦有新的互联网方案出现,只要它能满意那么些专业的情商,那么它就能为同样知足该协议的有着容器平台提供网络效用,而CNI就是这么的二个标准接口协议。

 

2、什么是CNI?

开首地讲,CNI是3个接口协议,用于连接容器管理种类和网络插件。前者提供二个容器所在的network
namespace(从网络的角度来看,network
namespace和容器是全然等价的),后者负责将network interface插入该network
namespace中(比如veth的1端),并且在宿主机做一些必不可缺的安顿(例如将veth的另一端参预bridge中),最终对namespace中的interface举办IP和路由的布署。那么CNI的做事实际主若是从容器管理种类处获得运转时音讯,包蕴network
namespace的门道,容器ID以及network interface
name,再从容器网络的配备文件中加载互连网计划新闻,再将那一个新闻传送给相应的插件,由插件进行实际的网络铺排工作,并将配置的结果再回来到容器管理种类中。

最终,供给注意的是,在事先的CNI版本中,网络布局文件只好描述叁个network,那也就表明了3个器皿只可以参与一个容器互联网。可是在新生的CNI版本中,我们能够在配备文件中定义二个所谓的NetworkList,事实上便是概念3个network连串,CNI会依次调用种种network的插件对容器举行对应的布局,从而允许3个器皿能够参与四个容器网络。

 

3、怎么用CNI?

在更为切磋CNI从前,笔者以为首先来看望CNI是怎么采纳的,先对CNI有2个直观的认识,是很有不可缺少的,那对我们随后的接头也将至极有帮扶。今后,我们将顺序执行如下步骤来演示如何使用CNI,并对每一步的操作进行须求的表明。

(壹)、编译安装CNI的法定插件

今天官方提供了三种类型的插件:main,meta和ipam。当中main类型的插件重要提供某种互联网成效,比如大家在示范上将使用的brdige,以及loopback,ipvlan,macvlan等等。meta类型的插件不可能当做独立的插件使用,它日常须求调用其余插件,例如flannel,或许协作别的插件使用,例如portmap。最终ipam类型的插件其实是对富有CNI插件共有的IP管理某个的虚幻,从而收缩插件编写进程中的重复工作,官方提供的有dhcp和host-local两种类型。

bifa365必发,随即执行如下命令,完毕插件的下载,编写翻译,安装工作:

$ 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

  

(三)、模拟CNI的施行进程,创制network
namespace,出席上文中描述的容器互联网”mynet”

率先大家从github上下载、编写翻译CNI的源码,最后将在bin目录下生成2个名叫”cnitool”的可执行文件。事实上,能够认为cnitool是1个模拟程序,大家先成立1个名字为ns的network
namespace,用来模拟1个新创立的器皿,再调用cnitool对该network
namespace实行互连网布署,从而模拟1个新建的容器加入叁个器皿网络的历程。

从cnitool的执行结果来看,它会回到四个分包了interface,IP,路由等等各样音信的json串,事实上它正是CNI对容器实行互连网布局后变更的结果音信,对此大家将在继续章节举行详尽的描述。

末段,大家能够观察network
namespace内新建的网卡eth0的IP地址为拾.2二.0.5/16,正好包罗在容器互连网”mynet”的子网范围拾.2贰.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,VELacrosseSION,而三种操作所要求布署的参数和结果如下:

  • 将container加入network(Add):
    • Parameters:
      • Version:CNI版本新闻
      • Container ID:
        这么些字段是可选的,然而建议利用,在容器活着的时候需要该字段全局唯一的。比如,存在IPAM的条件或者会供给各样container都分配2个单身的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和/也许IPv陆地址,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/十-mynet.conf”网络布局文件是同样的,用于描述容器了容器须求进入的互连网,上边是对内部部分重中之重字段的叙述:

  • cniVersion(string):cniVersion以Semantic Version
    贰.0的格式钦点了插件使用的CNI版本
  • name (string):Network name。那应该在全部管理域中都是绝无仅有的
  • type (string):插件类型,也意味着了CNI插件可执行文件的文书名
  • args
    (dictionary):由容器运维时提供的可选的参数。比如,能够将二个由label组成的dictionary传递给CNI插件,通过在args下扩张2个labels字段来兑现
  • ipMasqs
    (boolean):可采纳(假如插件辅助的话)。为network在宿主机创制IP
    masquerade。即便供给将宿主机作为网关,为了能够路由到容器分配的IP,那个字段是必须的
  • ipam:由特定的IPAM值组成的dictionary
    • type
      (string):IPAM插件的品种,也表示IPAM插件的可执行文件的公文名
  • dns:由特定的DNS值组成的dictionary
    • nameservers (list of
      strings):一八种对network可知的,以先行级顺序排列的DNS
      nameserver列表。列表中的每1项都带有了七个IPv四要么三个IPv6地址
    • domain (string):用于查找short hostname的当地点
    • search (list of strings):以优先级顺序排列的用来查找short
      domain的查找域。对于当先44%resolver,它的事先级比domain越来越高
    • options(list of strings):一名目繁多能够被传输给resolver的可选项

插件恐怕会定义它们本人能接到的附加的字段,可是遇到3个鲜为人知的字段或然会生出错误。例外的是args字段,它能够被用来传输壹些万分的字段,但也说不定会被插件忽略

 

5、CNI具体是怎么落到实处的?

到近日结束,大家对CNI的运用、配置和公理都早已有了主导的认识,所以也是时候基于源码来对CNI做3个不可开交的接头了。上边,大家将上述文中的以身作则实例作为线索,以模拟程序cnitool作为切入口,来对全部CNI的推行进度进展详尽的解析。

(一)、加载容器网络布局音讯

率先大家来看一下器皿互连网陈设的数据结构表示:

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,
}

  

(叁)、参与容器网络

 依据加载的器皿网络布置新闻和容器运营时消息,执行出席容器网络的操作,并将实施的结果打字与印刷输出

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举行如下三步操作:

  • 第3,调用FindInPath函数,依照newtork的门类,在插件的存放路径,也正是上文中的CNI_PATH中查找是或不是留存对应插件的可执行文件。若存在则赶回其相对路径pluginPath
  • 紧接着,调用buildOneConfig函数,从NetworkList中提取分离出脚下实践ADD操作的network的NetworkConfig结构。那Ritter别须求专注的是preResult参数,它是上1个network的操作结果,也将被编码进NetworkConfig中。要求小心的是,当我们在推行NetworkList时,必须将前3个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()函数用于营造1个Args类型的实例,在那之中第3囤积容器运行时消息,以及履行的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
}

  

在看完代码之后,大家或者某些许的失望。因为这一个理论上Infiniti基本的函数却不料的不难,它所做的做事仅仅只是exec了插件的可执行文件。话虽如此,大家照旧有以下几点需求小心:

  • 容器运营时音讯以及CNI操作命令等都是以环境变量的款式传递给插件的,那一点在上文中已经颇具聊起
  • 容器网络的布局消息是经过标准输入的款型传递给插件的
  • 插件的运维结果是以专业输出的款式重临给CNI的

到此截止,整个CNI的执行流已经11分了然了。简单地说,一个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

发表评论

电子邮件地址不会被公开。 必填项已用*标注