CNI的由来

目前不论是个人还是企业,在使用k8s时,都会采用CNI作为集群网络方案实现的规范。

在早先的k8s版本中,kubelet代码里提供了networkPlugin,networkPlugin是一组GO语言的接口,实现了pod的网络配置、解除、获取,当时kubelet的代码中有一个docker_manager组件,负责容器的创建和销毁,亦会调用当前的networkPlugin,负责容器网络的操作。

随后docker提出了CNM的概念,想要基于这个概念打造容器编排系统中的网络规范,K8S社区也顺应地在networkPlugin之上推出了CNI。

CNI插件 使用CNI插件时,需要做三个配置:

kubelet启动参数中networkPlugin设置为cni 创建/etc/cni/net.d目录,并增加cni的配置文件,配置文件中可以指定需要使用的cni组件及参数 创建/opt/cni/bin目录,并将需要用到的cni组件(二进制可执行文件)放到该目录下 所有的CNI组件都支持两个命令:add和del。即配置网络和解除网络配置。【随着社区的发展,现在CNI组件还要支持GET命令,但该命令没有什么使用场景】

CNI插件的配置文件是一个json文件,不同版本的接口、以及不同的CNI组件,有着不同的配置内容结构,目前比较通用的接口版本是0.3.1的版本。

在配置文件中我们可以填入多个CNI组件,当这些CNI组件的配置以数组形式记录时,kubelet会对所有的组件进行按序链式调用,所有组件调用成功后,视为网络配置完成,过程中任何一步出现error,都会进行回滚的del操作。以保证操作流上的原子性。

几种基本的CNI插件 CNI插件按照代码中的存放目录可以分为三种:ipam、main、meta。

ipam 用于管理ip和相关网络数据,配置网卡、ip、路由等。 main 用于进行网络配置,比如创建网桥,vethpair、macvlan等。 meta 有的是用于和第三方CNI插件进行适配,如flannel,也有的用于配置内核参数和端口映射 由于官方提供的cni组件就有很多,这里我们详细介绍一些使用率较高的组件。

ipam类CNI ipam类型的cni插件,在执行add命令时会分配一个IP给调用者。执行del命令时会将调用者指定的ip放回ip池。社区开源的ipam有host-local、dhcp。

host-local 我们可以通过host-local的配置文件的数据结构来搞懂这个组件是如何管理ip的。

type IPAMConfig struct { _Range Name string Type string json:"type" Routes []_types.Route json:"routes"//交付的ip对应的路由 DataDir string json:"dataDir"//本地ip池的数据库目录 ResolvConf string json:"resolvConf"//交付的ip对应的dns Ranges []RangeSet json:"ranges"//交付的ip所属的网段,网关信息 IPArgs []net.IP json:"-" // Requested IPs from CNI_ARGS and args }

配置文件范例:

{ "cniVersion": "0.3.1", "name": "mynet", "type": "ipvlan", "master": "foo0", "ipam": { "type": "host-local", "resolvConf": "/home/here.resolv", "dataDir": "/home/cni/network", "ranges": [ [ { "subnet": "10.1.2.0/24", "rangeStart": "10.1.2.9", "rangeEnd": "10.1.2.20", "gateway": "10.1.2.30" }, { "subnet": "10.1.4.0/24" } ], [{ "subnet": "11.1.2.0/24", "rangeStart": "11.1.2.9", "rangeEnd": "11.1.2.20", "gateway": "11.1.2.30" }] ] } } 从上面的配置我们可以清楚:

host-local组件通过在配置文件中指定的subnet进行网络划分 host-local在本地通过指定目录(默认为/var/lib/cni/networks)记录当前的ip pool数据 host-local将IP分配并告知调用者时,还可以告知dns、路由等配置信息。这些信息通过配置文件和对应的resolv文件记录。 host-local的应用范围比较广,kubenet、bridge、ptp、ipvlan等cni network插件都被用来和host-local配合进行ip管理。

dhcp 社区的cni组件中就包含了dhcp这个ipam,但并没有提供一个可以参考的案例,翻看了相关的源码,大致逻辑是:

向dhcp申请ip时,dhcp会使用rpc访问本地的socket(/run/cni/dhcp.sock)申请一个ip的租约。然后将IP告知调用者。 向dhcp删除IP时,dhcp同样通过rpc请求,解除该IP的租约。 main(network)类CNI main类型的cni组件做的都是一些核心功能,比如配置网桥、配置各种虚拟化的网络接口(veth、macvlan、ipvlan等)。这里我们着重讲使用率较高的bridge和ptp。

bridge brige模式,即网桥模式。在node上创建一个linux bridge,并通过vethpair的方式在容器中设置网卡和IP。只要为容器配置一个二层可达的网关:比如给网桥配置IP,并设置为容器ip的网关。容器的网络就能建立起来。

如下是bridge的配置项数据结构:

type NetConf struct { types.NetConf BrName string json:"bridge" //网桥名 IsGW bool json:"isGateway" //是否将网桥配置为网关 IsDefaultGW bool json:"isDefaultGateway" // ForceAddress bool json:"forceAddress"//如果网桥已存在且已配置了其他IP,通过此参数决定是否将其他ip除去 IPMasq bool json:"ipMasq"//如果true,配置私有网段到外部网段的masquerade规则 MTU int json:"mtu" HairpinMode bool json:"hairpinMode" PromiscMode bool json:"promiscMode" } 我们关注其中的一部分字段,结合代码可以大致整理出bridge组件的工作内容。首先是ADD命令:

执行ADD命令时,brdige组件创建一个指定名字的网桥,如果网桥已经存在,就使用已有的网桥; 创建vethpair,将node端的veth设备连接到网桥上; 从ipam获取一个给容器使用的ip数据,并根据返回的数据计算出容器对应的网关; 进入容器网络名字空间,修改容器中网卡名和网卡ip,以及配置路由,并进行arp广播(注意我们只为vethpair的容器端配置ip,node端是没有ip的); 如果IsGW=true,将网桥配置为网关,具体方法是:将第三步计算得到的网关IP配置到网桥上,同时根据需要将网桥上其他ip删除。最后开启网桥的ip_forward内核参数; 如果IPMasq=true,使用iptables增加容器私有网网段到外部网段的masquerade规则,这样容器内部访问外部网络时会进行snat,在很多情况下配置了这条路由后容器内部才能访问外网。(这里代码中会做exist检查,防止生成重复的iptables规则); 配置结束,整理当前网桥的信息,并返回给调用者。 其次是DEL命令:

根据命令执行的参数,确认要删除的容器ip,调用ipam的del命令,将IP还回IP pool; 进入容器的网络名字空间,根据容器IP将对应的网卡删除; 如果IPMasq=true,在node上删除创建网络时配置的几条iptables规则。 ptp ptp其实是bridge的简化版。但是它做的网络配置其实看上去倒是更复杂了点。并且有一些配置在自测过程中发现并没有太大用处。它只创建vethpair,但是会同时给容器端和node端都配置一个ip。容器端配置的是容器IP,node端配置的是容器IP的网关(/32),同时,容器里做了一些特殊配置的路由,以满足让容器发出的arp请求能被vethpair的node端响应。实现内外的二层连通。

ptp的网络配置步骤如下:

从ipam获取IP,根据ip类型(ipv4或ipv6)配置响应的内核ip_forward参数; 创建一对vethpair;一端放到容器中; 进入容器的网络namespace,配置容器端的网卡,修改网卡名,配置IP,并配置一些路由。假如容器ip是10.18.192.37/20,所属网段是10.18.192.0/20,网关是10.18.192.1,我们这里将进行这样的配置: 配置IP后,内核会自动生成一条路由,形如:10.18.192.0/20 dev eth0 scope link,我们将它删掉:ip r d ** 配置一条私有网到网关的真实路由:ip r a 10.18.192.0/20 via 10.18.192.1 dev eth0 配置一条到网关的路由:10.18.192.1/32 dev eth0 scope link 退出到容器外,将vethpair的node端配置一个IP(ip为容器ip的网关,mask=32); 配置外部的路由:访问容器ip的请求都路由到vethpair的node端设备去。 如果IPMasq=true,配置iptables 获取完整的网卡信息(vethpair的两端),返回给调用者。 与bridge不同主要的不同是:ptp不使用网桥,而是直接使用vethpair+路由配置,这个地方其实有很多其他的路由配置可以选择,一样可以实现网络的连通性,ptp配置的方式只是其中之一。万变不离其宗的是:

只要容器内网卡发出的arp请求,能被node回复或被node转发并由更上层的设备回复,形成一个二层网络,容器里的数据报文就能被发往node上;然后通过node上的路由,进行三层转发,将数据报文发到正确的地方,就可以实现网络的互联。

bridge和ptp其实是用了不同方式实现了这个原则中的“二层网络”:

bridge组件给网桥配置了网关的IP,并给容器配置了到网关的路由。实现二层网络 ptp组件给vethpair的对端配置了网关的IP,并给容器配置了单独到网关IP的路由,实现二层网络 ptp模式的路由还存在一个问题:没有配置default路由,因此容器不能访问外部网络,要实现也很简单,以上面的例子,在容器里增加一条路由:default via 10.18.192.1 dev eth0

host-device 相比前面两种cni main组件,host-device显得十分简单因为他就只会做两件事情:

收到ADD命令时,host-device根据命令参数,将网卡移入到指定的网络namespace(即容器中)。 收到DEL命令时,host-device根据命令参数,将网卡从指定的网络namespace移出到root namespace。 细心的你肯定会注意到,在bridge和ptp组件中,就已经有“将vethpair的一端移入到容器的网络namespace”的操作。那这个host-device不是多此一举吗?

并不是。host-device组件有其特定的使用场景。假设集群中的每个node上有多个网卡,其中一个网卡配置了node的IP。而其他网卡都是属于一个网络的,可以用来做容器的网络,我们只需要使用host-device,将其他网卡中的某一个丢到容器里面就行。

host-device模式的使用场景并不多。它的好处是:bridge、ptp等方案中,node上所有容器的网络报文都是通过node上的一块网卡出入的,host-device方案中每个容器独占一个网卡,网络流量不会经过node的网络协议栈,隔离性更强。缺点是:在node上配置数十个网卡,可能并不好管理;另外由于不经过node上的协议栈,所以kube-proxy直接废掉。k8s集群内的负载均衡只能另寻他法了。

macvlan 有关macvlan的实践可以参考这篇文章。这里做一个简单的介绍:macvlan是linux kernal的特性,用于给一个物理网络接口(parent)配置虚拟化接口,虚拟化接口与parent网络接口拥有不同的mac地址,但parent接口上收到发给其对应的虚拟化接口的mac的包时,会分发给对应的虚拟化接口,有点像是将虚拟化接口和parent接口进行了'桥接'。给虚拟化网络接口配置了IP和路由后就能互相访问。

macvlan省去了linux bridge,但是配置macvlan后,容器不能访问parent接口的IP。

ipvlan ipvlan与macvlan有点类似,但对于内核要求更高(3.19),ipvlan也会从一个网络接口创建出多个虚拟网络接口,但他们的mac地址是一样的, 只是IP不一样。通过路由可以实现不同虚拟网络接口之间的互联。

使用ipvlan也不需要linux bridge,但容器一样不能访问parent接口的IP。 关于ipvlan的内容可以参考这篇文章

meta 类CNI meta组件通常进行一些额外的网络配置(tuning),或者二次调用(flannel)。

tuning 用于进行内核网络参数的配置。并将调用者的数据和配置后的内核参数返回给调用者。

有时候我们需要配置一些虚拟网络接口的内核参数,可以通过这个组件进行配置。比如:

accept_redirects send_redirects proxy_delay accept_local arp_filter 可以在这里查看可配置的网络参数和释义。

portmap 用于在node上配置iptables规则,进行SNAT,DNAT和端口转发。

portmap组件通常在main组件执行完毕后执行,因为它的执行参数仰赖之前的组件提供

flannel cni plugins中的flannel是开源网络方案flannel的“调用器”。这也是flannel网络方案适配CNI架构的一个产物。为了便于区分,以下我们称cni plugins中的flannel 为flanenl cni。

我们知道flannel是一个容器的网络方案,通常使用flannel时,node上会运行一个daemon进程:flanneld,这个进程会返回该node上的flannel网络、subnet,MTU等信息。并保存到本地文件中。

如果对flannel网络方案有一定的了解,会知道他在做网络接口配置时,其实干的事情和bridge组件差不多。只不过flannel网络下的bridge会跟flannel0网卡互联,而flannel0网卡上的数据会被封包(udp、vxlan下)或直接转发(host-gw)。

而flannel cni做的事情就是:

执行ADD命令时,flannel cni会从本地文件中读取到flanneld的配置。然后根据命令的参数和文件的配置,生成一个新的cni配置文件(保存在本地,文件名包含容器id以作区分)。新的cni配置文件中会使用其他cni组件,并注入相关的配置信息。之后,flannel cni根据这个新的cni配置文件执行ADD命令。 执行DEL命令时,flannel cni从本地根据容器id找到之前创建的cni配置文件,根据该配置文件执行DEL命令。 也就是说flannel cni此处是一个flannel网络模型的委托者,falnnel网络模型委托它去调用其他cni组件,进行网络配置。通常调用的是bridge和host-local。

CNI的使用 k8s中Pod是最小的逻辑业务单元,我们知道Pod中可以有多个container,这些container会共享同一个网络namespace。也就是说他们的IP、路由等网络配置是共享的。

kubelet为每个Pod都会启动一个容器,作为该Pod的Sandbox,这个容器使用一个不做任何事情的镜像,启动后kubelet调用CNI插件的ADD子命令,给这个容器进行网络配置。成功后,在创建其他容器时,网络namespace就直接引用Sandbox的。这样实现了Pod的网络。如果ADD失败了,这个Sandbox容器会被销毁(销毁时要执行DEL),然后kubelet会再次重试创建Sandbox容器并配置网络。

每当kubelet定时自检发现有pod需要被删除、或有Sandbox需要清理时,kubelet就会找到该pod的Sandbox容器,进行网络清理。清理时执行CNI插件的DEL子命令,如果DEL失败,kubelet会等待1min后进行重试。

Copyright © 温玉 2021 | 浙ICP备2020032454号 all right reserved,powered by Gitbook该文件修订时间: 2021-04-22 09:40:34

results matching ""

    No results matching ""