深入理解CNI(容器网络接口)
原文
CNI简介
容器网络的配置是一个复杂的过程,为了应对各式各样的需求,容器网络的解决方案也多种多样,例如有flannel,calico,kube-ovn,weave等。同时,容器平台/运行时也是多样的,例如有Kubernetes,Openshift,rkt等。如果每种容器平台都要跟每种网络解决方案一一对接适配,这将是一项巨大且重复的工程。当然,聪明的程序员们肯定不会允许这样的事情发生。想要解决这个问题,我们需要一个抽象的接口层,将容器网络配置方案与容器平台方案解耦。
CNI(Container Network Interface)就是这样的一个接口层,它定义了一套接口标准,提供了规范文档以及一些标准实现。采用CNI规范来设置容器网络的容器平台不需要关注网络的设置的细节,只需要按CNI规范来调用CNI接口即可实现网络的设置。
CNI最初是由CoreOS为rkt容器引擎创建的,随着不断发展,已经成为事实标准。目前绝大部分的容器平台都采用CNI标准(rkt,Kubernetes ,OpenShift等)。本篇内容基于CNI最新的发布版本v0.4.0。
值得注意的是,Docker并没有采用CNI标准,而是在CNI创建之初同步开发了CNM(Container Networking Model)标准。但由于技术和非技术原因,CNM模型并没有得到广泛的应用。
CNI是怎么工作的
CNI的接口并不是指HTTP,gRPC接口,CNI接口是指对可执行程序的调用(exec)。这些可执行程序称之为CNI插件,以K8S为例,K8S节点默认的CNI插件路径为 /opt/cni/bin
,在K8S节点上查看该目录,可以看到可供使用的CNI插件:
$ ls /opt/cni/bin/
bandwidth bridge dhcp firewall flannel host-device host-local ipvlan loopback macvlan portmap ptp sbr static tuning vlan
CNI的工作过程大致如下图所示:
CNI通过JSON格式的配置文件来描述网络配置,当需要设置容器网络时,由容器运行时负责执行CNI插件,并通过CNI插件的标准输入(stdin)来传递配置文件信息,通过标准输出(stdout)接收插件的执行结果。图中的 libcni
是CNI提供的一个go package,封装了一些符合CNI规范的标准操作,便于容器运行时和网络插件对接CNI标准。
举一个直观的例子,假如我们要调用bridge
插件将容器接入到主机网桥,则调用的命令看起来长这样:
# CNI_COMMAND=ADD 顾名思义表示创建。
# XXX=XXX 其他参数定义见下文。
# < config.json 表示从标准输入传递配置文件
CNI_COMMAND=ADD XXX=XXX ./bridge < config.json
插件入参
容器运行时通过设置环境变量以及从标准输入传入的配置文件来向插件传递参数。
环境变量
CNI_COMMAND
:定义期望的操作,可以是ADD,DEL,CHECK或VERSION。CNI_CONTAINERID
: 容器ID,由容器运行时管理的容器唯一标识符。CNI_NETNS
:容器网络命名空间的路径。(形如/run/netns/[nsname]
)。CNI_IFNAME
:需要被创建的网络接口名称,例如eth0。CNI_ARGS
:运行时调用时传入的额外参数,格式为分号分隔的key-value对,例如FOO=BAR;ABC=123
CNI_PATH
: CNI插件可执行文件的路径,例如/opt/cni/bin
。
配置文件
文件示例:
{"cniVersion": "0.4.0", // 表示希望插件遵循的CNI标准的版本。"name": "dbnet", // 表示网络名称。这个名称并非指网络接口名称,是便于CNI管理的一个表示。应当在当前主机(或其他管理域)上全局唯一。"type": "bridge", // 插件类型"bridge": "cni0", // bridge插件的参数,指定网桥名称。"ipam": { // IP Allocation Management,管理IP地址分配。"type": "host-local", // ipam插件的类型。// ipam 定义的参数"subnet": "10.1.0.0/16","gateway": "10.1.0.1"}
}
公共定义部分
配置文件分为公共部分和插件定义部分。公共部分在CNI项目中使用结构体NetworkConfig
定义:
type NetworkConfig struct {Network *types.NetConfBytes []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 IPAM `json:"ipam,omitempty"`DNS DNS `json:"dns"`RawPrevResult map[string]interface{} `json:"prevResult,omitempty"`PrevResult Result `json:"-"`
}
cniVersion
表示希望插件遵循的CNI标准的版本。name
表示网络名称。这个名称并非指网络接口名称,是便于CNI管理的一个表示。应当在当前主机(或其他管理域)上全局唯一。type
表示插件的名称,也就是插件对应的可执行文件的名称。bridge
该参数属于bridge
插件的参数,指定主机网桥的名称。ipam
表示IP地址分配插件的配置,ipam.type
则表示ipam的插件类型。
更详细的信息,可以参考官方文档。
插件定义部分
上文提到,配置文件最终是传递给具体的CNI插件的,因此插件定义部分才是配置文件的“完全体”。公共部分定义只是为了方便各插件将其嵌入到自身的配置文件定义结构体中,举bridge
插件为例:
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"`Args struct {Cni BridgeArgs `json:"cni,omitempty"`} `json:"args,omitempty"`RuntimeConfig struct {Mac string `json:"mac,omitempty"`} `json:"runtimeConfig,omitempty"`mac string
}
各插件的配置文件文档可参考官方文档。
插件操作类型
CNI插件的操作类型只有四种: ADD
, DEL
, CHECK
和 VERSION
。 插件调用者通过环境变量 CNI_COMMAND
来指定需要执行的操作。
ADD
ADD
操作负责将容器添加到网络,或对现有的网络设置做更改。具体地说,ADD
操作要么:
- 为容器所在的网络命名空间创建一个网络接口,或者
- 修改容器所在网络命名空间中的指定网络接口
例如通过 ADD
将容器网络接口接入到主机的网桥中。
其中网络接口名称由
CNI_IFNAME
指定,网络命名空间由CNI_NETNS
指定。
DEL
DEL
操作负责从网络中删除容器,或取消对应的修改,可以理解为是 ADD
的逆操作。具体地说,DEL
操作要么:
- 为容器所在的网络命名空间删除一个网络接口,或者
- 撤销
ADD
操作的修改
例如通过 DEL
将容器网络接口从主机网桥中删除。
其中网络接口名称由
CNI_IFNAME
指定,网络命名空间由CNI_NETNS
指定。
CHECK
CHECK
操作是v0.4.0加入的类型,用于检查网络设置是否符合预期。容器运行时可以通过CHECK
来检查网络设置是否出现错误,当CHECK
返回错误时(返回了一个非0状态码),容器运行时可以选择Kill掉容器,通过重新启动来重新获得一个正确的网络配置。
VERSION
VERSION
操作用于查看插件支持的版本信息。
$ CNI_COMMAND=VERSION /opt/cni/bin/bridge
{"cniVersion":"0.4.0","supportedVersions":["0.1.0","0.2.0","0.3.0","0.3.1","0.4.0"]}
链式调用
单个CNI插件的职责是单一的,比如bridge
插件负责网桥的相关配置, firewall
插件负责防火墙相关配置, portmap
插件负责端口映射相关配置。因此,当网络设置比较复杂时,通常需要调用多个插件来完成。CNI支持插件的链式调用,可以将多个插件组合起来,按顺序调用。例如先调用 bridge
插件设置容器IP,将容器网卡与主机网桥连通,再调用portmap
插件做容器端口映射。容器运行时可以通过在配置文件设置plugins
数组达到链式调用的目的:
{"cniVersion": "0.4.0","name": "dbnet","plugins": [{"type": "bridge",// type (plugin) specific"bridge": "cni0"},"ipam": {"type": "host-local",// ipam specific"subnet": "10.1.0.0/16","gateway": "10.1.0.1"}},{"type": "tuning","sysctl": {"net.core.somaxconn": "500"}}]
}
细心的读者会发现,plugins
这个字段并没有出现在上文描述的配置文件结构体中。的确,CNI使用了另一个结构体——NetworkConfigList
来保存链式调用的配置:
type NetworkConfigList struct {Name stringCNIVersion stringDisableCheck boolPlugins []*NetworkConfig Bytes []byte
}
但CNI插件是不认识这个配置类型的。实际上,在调用CNI插件时,需要将NetworkConfigList
转换成对应插件的配置文件格式,再通过标准输入(stdin)传递给CNI插件。例如在上面的示例中,实际上会先使用下面的配置文件调用 bridge
插件:
{"cniVersion": "0.4.0","name": "dbnet","type": "bridge","bridge": "cni0","ipam": {"type": "host-local","subnet": "10.1.0.0/16","gateway": "10.1.0.1"}
}
再使用下面的配置文件调用tuning
插件:
{"cniVersion": "0.4.0","name": "dbnet","type": "tuning","sysctl": {"net.core.somaxconn": "500"},"prevResult": { // 调用bridge插件的返回结果...}
}
需要注意的是,当插件进行链式调用的时候,不仅需要对NetworkConfigList
做格式转换,而且需要将前一次插件的返回结果添加到配置文件中(通过prevResult
字段),不得不说是一项繁琐而重复的工作。不过幸好libcni
已经为我们封装好了,容器运行时不需要关心如何转换配置文件,如何填入上一次插件的返回结果,只需要调用 libcni
的相关方法即可。
示例
接下来将演示如何使用CNI插件来为Docker容器设置网络。
下载CNI插件
为方便起见,我们直接下载可执行文件:
wget https://github.com/containernetworking/plugins/releases/download/v0.9.1/cni-plugins-linux-amd64-v0.9.1.tgz
mkdir -p ~/cni/bin
tar zxvf cni-plugins-linux-amd64-v0.9.1.tgz -C ./cni/bin
chmod +x ~/cni/bin/*
ls ~/cni/bin/
bandwidth bridge dhcp firewall flannel host-device host-local ipvlan loopback macvlan portmap ptp sbr static tuning vlan vrfz
如果你是在K8S节点上实验,通常节点上已经有CNI插件了,不需要再下载,但要注意将后续的 CNI_PATH
修改成/opt/cni/bin
。
示例1——调用单个插件
在示例1中,我们会直接调用CNI插件,为容器设置eth0
接口,为其分配IP地址,并接入主机网桥mynet0
。
跟docker默认使用的使用网络模式一样,只不过我们将
docker0
换成了mynet0
。
启动容器
虽然Docker不使用CNI规范,但可以通过指定 --net=none
的方式让Docker不设置容器网络。以nginx
镜像为例:
contid=$(docker run -d --net=none --name nginx nginx) # 容器ID
pid=$(docker inspect -f '{{ .State.Pid }}' $contid) # 容器进程ID
netnspath=/proc/$pid/ns/net # 命名空间路径
启动容器的同时,我们需要记录一下容器ID,命名空间路径,方便后续传递给CNI插件。容器启动后,可以看到除了lo网卡,容器没有其他的网络设置:
nsenter -t $pid -n ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00inet 127.0.0.1/8 scope host lovalid_lft forever preferred_lft forever
nsenter是namespace enter的简写,顾名思义,这是一个在某命名空间下执行命令的工具。-t表示进程ID, -n表示进入对应进程的网络命名空间。
添加容器网络接口并连接主机网桥
接下来我们使用bridge
插件为容器创建网络接口,并连接到主机网桥。创建bridge.json
配置文件,内容如下:
{"cniVersion": "0.4.0","name": "mynet","type": "bridge","bridge": "mynet0","isDefaultGateway": true,"forceAddress": false,"ipMasq": true,"hairpinMode": true,"ipam": {"type": "host-local","subnet": "10.10.0.0/16"}
}
调用bridge
插件ADD
操作:
CNI_COMMAND=ADD CNI_CONTAINERID=$contid CNI_NETNS=$netnspath CNI_IFNAME=eth0 CNI_PATH=~/cni/bin ~/cni/bin/bridge < bridge.json
调用成功的话,会输出类似的返回值:
{"cniVersion": "0.4.0","interfaces": [....],"ips": [{"version": "4","interface": 2,"address": "10.10.0.2/16", //给容器分配的IP地址"gateway": "10.10.0.1" }],"routes": [.....],"dns": {}
}
再次查看容器网络设置:
nsenter -t $pid -n ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00inet 127.0.0.1/8 scope host lovalid_lft forever preferred_lft forever
5: eth0@if40: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group defaultlink/ether c2:8f:ea:1b:7f:85 brd ff:ff:ff:ff:ff:ff link-netnsid 0inet 10.10.0.2/16 brd 10.10.255.255 scope global eth0valid_lft forever preferred_lft forever
可以看到容器中已经新增了eth0网络接口,并在ipam
插件设定的子网下为其分配了IP地址。host-local
类型的 ipam
插件会将已分配的IP信息保存到文件,避免IP冲突,默认的保存路径为/var/lib/cni/network/$NETWORK_NAME
:
ls /var/lib/cni/networks/mynet/
10.10.0.2 last_reserved_ip.0 lock
从主机访问验证
由于mynet0
是我们添加的网桥,还未设置路由,因此验证前我们需要先为容器所在的网段添加路由:
ip route add 10.10.0.0/16 dev mynet0 src 10.10.0.1 # 添加路由
curl -I 10.10.0.2 # IP换成实际分配给容器的IP地址
HTTP/1.1 200 OK
....
删除容器网络接口
删除的调用入参跟添加的入参是一样的,除了CNI_COMMAND
要替换成DEL
:
CNI_COMMAND=DEL CNI_CONTAINERID=$contid CNI_NETNS=$netnspath CNI_IFNAME=eth0 CNI_PATH=~/cni/bin ~/cni/bin/bridge < bridge.json
注意,上述的删除命令并未清理主机的
mynet0
网桥。如果你希望删除主机网桥,可以执行ip link delete mynet0 type bridge
命令删除。
示例2——链式调用
在示例2中,我们将在示例1的基础上,使用portmap
插件为容器添加端口映射。
使用cnitool
工具
前面的介绍中,我们知道在链式调用过程中,调用方需要转换配置文件,并需要将上一次插件的返回结果插入到本次插件的配置文件中。这是一项繁琐的工作,而libcni
已经将这些过程封装好了,在示例2中,我们将使用基于 libcni
的命令行工具cnitool
来简化这些操作。
示例2将复用示例1中的容器,因此在开始示例2时,请确保已删除示例1中的网络接口。
通过源码编译或go install
来安装cnitool
:
go install github.com/containernetworking/cni/cnitool@latest
配置文件
libcni
会读取.conflist
后缀的配置文件,我们在当前目录创建portmap.conflist
:
{"cniVersion": "0.4.0","name": "portmap","plugins": [{"type": "bridge","bridge": "mynet0","isDefaultGateway": true, "forceAddress": false, "ipMasq": true, "hairpinMode": true,"ipam": {"type": "host-local","subnet": "10.10.0.0/16","gateway": "10.10.0.1"}},{"type": "portmap","runtimeConfig": {"portMappings": [{"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}]}}]
}
从上述的配置文件定义了两个CNI插件,bridge
和portmap
。根据上述的配置文件,cnitool
会先为容器添加网络接口并连接到主机mynet0
网桥上(就跟示例1一样),然后再调用portmap
插件,将容器的80端口映射到主机的8080端口,就跟docker run -p 8080:80 xxx
一样。
设置容器网络
使用cnitool
我们还需要设置两个环境变量:
NETCONFPATH
: 指定配置文件(*.conflist
)的所在路径,默认路径为/etc/cni/net.d
CNI_PATH
:指定CNI插件的存放路径。
使用cnitool add
命令为容器设置网络:
CNI_PATH=~/cni/bin NETCONFPATH=. cnitool add portmap $netnspath
设置成功后,访问宿主机8080端口即可访问到容器的nginx服务。
删除网络配置
使用cnitool del
命令删除容器网络:
CNI_PATH=~/cni/bin NETCONFPATH=. cnitool del portmap $netnspath
注意,上述的删除命令并未清理主机的
mynet0
网桥。如果你希望删除主机网桥,可以执行ip link delete mynet0 type bridge
命令删除。
总结
至此,CNI的工作原理我们已基本清楚。CNI的工作原理大致可以归纳为:
- 通过JSON配置文件定义网络配置;
- 通过调用可执行程序(CNI插件)来对容器网络执行配置;
- 通过链式调用的方式来支持多插件的组合使用。
CNI不仅定义了接口规范,同时也提供了一些内置的标准实现,以及libcni
这样的“胶水层”,大大降低了容器运行时与网络插件的接入门槛。
参考
- CNI v0.4.0规范文档
- CNI master分支规范文档
- CNI内置插件文档
- cnitool 文档
- 为什么Kubernetes不使用CNM模型
- Introduction to CNI
- CNI deep dive
深入理解CNI(容器网络接口)相关推荐
- CNI:容器网络接口
CNI:容器网络接口 CNI简介 不管是 docker 还是 kubernetes,在网络方面目前都没有一个完美的.终极的.普适性的解决方案,不同的用户和企业因为各种原因会使用不同的网络方案.目前存在 ...
- 一文理解 K8s 容器网络虚拟化
简介:本文需要读者熟悉 Ethernet(以太网)的基本原理和 Linux 系统的基本网络命令,以及 TCP/IP 协议族并了解传统的网络模型和协议包的流转原理.文中涉及到 Linux 内核的具体实现 ...
- kubelet配置cni插件_从零开始入门 K8s | 理解 CNI 和 CNI 插件
原标题:从零开始入门 K8s | 理解 CNI 和 CNI 插件 作者 | 溪恒 阿里巴巴高级技术专家 本文整理自<CNCF x Alibaba 云原生技术公开课>第 26 讲,点击直达课 ...
- 理解CNI和CNI插件
理解CNI和CNI插件 本文将主要分享以下几方面的内容: CNI 是什么? Kubernetes 中如何使用 CNI? 哪个 CNI 插件适合我? 如何开发自己的 CNI 插件? 一.CNI 是什么 ...
- Spring Boot教程(7) – 直观地理解Spring容器
在你学习Spring之前,你肯定听说过"控制反转"."依赖注入"."上下文"等名词,伴随着这些名词的,是一些冗长晦涩的解释,这些解释并没有什 ...
- C#_深入理解Unity容器
C#_深入理解Unity容器 一.背景 **DIP是依赖倒置原则:**一种软件架构设计的原则(抽象概念).依赖于抽象不依赖于细节 **IOC即为控制反转(Inversion of Control):* ...
- 理解Docker容器
< 理解Docker&容器 > 理解 Docker 一.概述 " Docker 是全球领先的软件容器平台 ".开发人员利用 Docker 可以消除协作编码时&q ...
- 【k8s】理解Docker容器的进程管理(PID1进程(容器内kill命令无法杀死)、进程信号处理、僵尸进程)
文章目录 概述 1. 容器的PID namespace(名空间) 2. 如何指明容器PID1进程 3. 进程信号处理 4. 孤儿进程与僵尸进程管理 5. 进程监控 6. 总结 参考 概述 简介: Do ...
- CNI:容器网络接口详解
CNI 简介 不管是 docker 还是 kubernetes,在网络方面目前都没有一个完美的.终极的.普适性的解决方案,不同的用户和企业因为各种原因会使用不同的网络方案.目前存在网络方案 flann ...
最新文章
- 2022-2028年中国防水橡胶布行业市场发展模式及投资前景分析报告
- 两边双虚线是什么意思_【交通】这些新标识啥意思?交警教你怎么走
- GAN不只会造假:捕获数据中额外显著特征,提高表征学习可解释性,效果超越InfoGAN | IJCAI 2020...
- 计算机社团活动教学计划,社团活动教学计划(计算机平面设计).doc
- 1.封装WinMain至动态链接库
- 关于Ecllipse
- 解决-ubuntu 安装redis无法启动
- java xml注入bean_Spring实战之通过XML装配bean
- SpringBoot 使用 log4j2
- STM32 - CubeMX 的使用实例详细(01.1)- ST-LINK V2 的配置
- 【P1714】切蛋糕(单调队列)
- python关键词共现_python 共现矩阵的实现
- java游戏初始化参数过多,从头认识java-4.6 成员的初始化
- Java性能优化(详解)
- TSC打印机,使用java打印二维码
- ZYNQ系统中实现FAT32文件系统的SD卡读写 之二 VIVADO配置
- from表单的重置按钮(reset)不能重置隐藏input框的值
- eclipse 搭建ARM开发环境
- [Kerberos基础]-- kerberos认证原理---讲的非常细致,易懂
- java学习(方法)
热门文章
- 任正非的《北国之春》(zz.is2120)
- access考试素材_Access数据库基础教程素材.doc
- GitLab 解决冲突
- 几句话之Cart树、方差不纯度、基尼系数
- Tensorflow教程-曼德布洛特(Mandelbrot)集合
- HEVC Study Three(基于HM14.0平台)--GOP研究之大揭秘
- 2018 北京化工大学ACCA精英班招生简章
- python处理文件夹图片重命名问题
- 在迷茫中坚守的Neri,可能是惠普企业(HPE)的最后一位高管
- 使用VB.net将PNG图片转成icon类型图标文件