原标题:智汇华云 | Kubernetes网络详解(二) CNI原理与实现
继上期“智汇华云”邀请到华云数据容器组资深OpenStack开发工程师郭栋为您带来“ Kubernetes网络详解(一) Flannel基本原理”之后, 本期“智汇华云”为大家带来Kubernetes网络详解的系列内容——Kubernetes网络详解(二) CNI原理与实现。
1、CNI概述
本文中 Kubernetes版本是v1.16.3。
CNI的全称是Container Network Interface,属于CNCF的一个子项目,它是一个完整的规范,详见 https://github.com/containernetworking/cni/blob/master/SPEC.md
本文 会在一个实际的环境中讲解其基本原理,并给出一些直观的例子。
Kubernetes自身在创建和删除pod的时候不涉及网络相关的操作,它会把这些交给CNI插件来完成。具体来讲就是当Kubernetes分别在创建一个pod的时候和删除一个pod的时候,对应的cni插件要做哪些操作。
2、CNI的入口
Kubernetes中创建pod的实际操作是 由node节点上的kubelet进行来完成的,它有两个重要的cni相关的命令行参数
- `cni-conf-dir`
这个参数指定了 cni配置文件的目录默认是 `/etc/cni/net.d`
- `cni-bin-dir`
这个参数指定了 cni插件的可执行文件所在的目录 `/opt/cni/bin`
CNI插件会以二进制可执行文件的形式提供,存放于 `cni-bin-dir`这个目录下,kubelet会根据cni配置文件来决定具体调用哪些插件。
```
[root@node1 ~]# ls -l /opt/cni/bin/
total 36132
```
在本文中我们会详细分析 几个基础的cni plugin。
3、flannel cni plugin
在上一篇文章中,我们 以flannel为例,讲解了其基本原理,现在接着分析cni的部分。
flannel的DaemonSet的启动前会 将一个ConfigMap中的内容copy成一个cni配置文件
```
[root@node1 ~]# cat /etc/cni/net.d/10-flannel.conflist
{
"cniVersion": "0.2.0",
"name": "cbr0",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
},
{
"type": "portmap",
"capabilities": {
"portMappings": true
}
}
]
}
```
配置文件中的plugins参数指定了需要调用的plugin列表和对应的配置,本文中我们只关心基础的plugin,因此这里会暂时忽略portmap这个plugin。
plugin中的type参数指定具体的plugin二进制文件名称,因此kubelet会调用 `/opt/cni/bin/flannel`,代码位于 `https://github.com/containernetworking/plugins/tree/master/plugins/meta/flannel`
```
func main {
skel.PluginMain(cmdAdd, cmdGet, cmdDel, version.All, "TODO")
}
```
这个main就是flannel cni plugin的入口, 当创建和删除pod时会分别调用其中的 `cmdAdd`和 `cmdDel`函数来执行对应的操作 。
```
func cmdAdd(args *skel.CmdArgs) error {
n, err := loadFlannelNetConf(args.StdinData)
if err != nil {
return err
}
fenv, err := loadFlannelSubnetEnv(n.SubnetFile)
if err != nil {
return err
}
if n.Delegate == nil {
n.Delegate = make(map[string]interface{})
} else {
if hasKey(n.Delegate, "type") && !isString(n.Delegate["type"]) {
return fmt.Errorf("'delegate' dictionary, if present, must have (string) 'type' field")
}
if hasKey(n.Delegate, "name") {
return fmt.Errorf("'delegate' dictionary must not have 'name' field, it'll be set by flannel")
}
if hasKey(n.Delegate, "ipam") {
return fmt.Errorf("'delegate' dictionary must not have 'ipam' field, it'll be set by flannel")
}
}
return doCmdAdd(args, n, fenv)
}
```
`cmdAdd`中主要有 三个步骤
1. 从stdin读取配置,生成一个 `type NetConf struct`对象
2. 从第1步返回的 `NetConf.SubnetFile`指定的路径(默认是 `/run/flannel/subnet.env`)载入一个配置文件, 这个文件由Flanneld DaemonSet生成,在上一篇文章中有说明
3. 调用 `doCmdAdd`函数执行真正的操作
3.1、读取NetConf配置
```
type NetConf struct {
types.NetConf
SubnetFile string `json:"subnetFile"`
DataDir string `json:"dataDir"`
Delegate map[string]interface{} `json:"delegate"`
}
# 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:"-"`
}
```
现在回头看 cni的配置文件(忽略了portmap plugin)
```
{
"cniVersion": "0.2.0",
"name": "cbr0",
"plugins": [
{
"type": "flannel",
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
}
]
}
```
对应于flannel plugin中的 `NetConf`结构体 ,可以看出配置文件中的
```
"delegate": {
"hairpinMode": true,
"isDefaultGateway": true
}
```
对应于 `NetConf`中的 `Delegate map[string]interface{}`, 除了这个和name, type, version之外而其它值都为空或者默认。
3.2、读取flanneld生成的配置文件
在node1上 `/run/flannel/subnet.env`这个文件的内容如下
```
[root@node1 ~]# cat /run/flannel/subnet.env
FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.1.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true
[root@node1 ~]#
```
3.3、执行doCmdAdd
这是 flannel cni plugin的核心
```
func doCmdAdd(args *skel.CmdArgs, n *NetConf, fenv *subnetEnv) error {
n.Delegate["name"] = n.Name
if !hasKey(n.Delegate, "type") {
n.Delegate["type"] = "bridge"
}
if !hasKey(n.Delegate, "ipMasq") {
// if flannel is not doing ipmasq, we should
ipmasq := !*fenv.ipmasq
n.Delegate["ipMasq"] = ipmasq
}
if !hasKey(n.Delegate, "mtu") {
mtu := fenv.mtu
n.Delegate["mtu"] = mtu
}
if n.Delegate["type"].(string) == "bridge" {
if !hasKey(n.Delegate, "isGateway") {
n.Delegate["isGateway"] = true
}
}
if n.CNIVersion != "" {
n.Delegate["cniVersion"] = n.CNIVersion
}
n.Delegate["ipam"] = map[string]interface{}{
"type": "host-local",
"subnet": fenv.sn.String,
"routes": []types.Route{
{
Dst: *fenv.nw,
},
},
}
return delegateAdd(args.ContainerID, n.DataDir, n.Delegate)
}
```
主要完成的工作如下
- 将下一级调用的的 cni plugin设置为 `bridge`
- 设置ipMasq和mtu, 注意这里cni plugin最终的ipMasq和flanneld自身的是相反的,也就是说如果flanneld设置了,cni plugin就不再设置了
- 如果是 `bridge`,且没有设置isGateway的话 ,将其默认设置为true
- 设置ipam cni plugin为host-local,并且将subnet参数设置为上文 `/run/flannel/subnet.env`中的 `FLANNEL_SUBNET`,并设置路由
从这里可以 看出flannel cni plugin的核心逻辑就根据当前配置生成bridge和host-local这两个cni plugin的配置参数,随后通过调用它们来实现主要的功能。
host-local cni plugin的功能是在当前节点从一个subnet中给pod分配ip地址,详细逻辑可阅读其代码来理解。 这个cni plugin会被bridge cni plugin调用。
下面简述一下 bridge的实现。
4、bridge cni plugin
bridge插件负责 从pod到veth到cni0的整个流程。
核心功能同样 在cmdAdd和cmdDel这两个函数中,下面看cmdAdd中的逻辑。
总体上分为二层和三层两部分
4.1、二层处理
第一步会先 处理网桥设备
```
br, brInterface, err := setupBridge(n)
if err != nil {
return err
}
```
`setupBridge`负责创建cni0这个bridge,并配置它的mtu、混杂模式等等,最后将这个网桥设备up起来。详细的代码如下
```
func setupBridge(n *NetConf) (*netlink.Bridge, *current.Interface, error) {
// create bridge if necessary
br, err := ensureBridge(n.BrName, n.MTU, n.PromiscMode)
if err != nil {
return nil, nil, fmt.Errorf("failed to create bridge %q: %v", n.BrName, err)
}
...
}
func ensureBridge(brName string, mtu int, promiscMode bool) (*netlink.Bridge, error) {
br := &netlink.Bridge{
LinkAttrs: netlink.LinkAttrs{
Name: brName,
MTU: mtu,
TxQLen: -1,
},
}
err := netlink.LinkAdd(br)
...
if promiscMode {
...
if err := netlink.LinkSetUp(br); err != nil {
...
}
```
第二步会 处理veth pair接口
```
netns, err := ns.GetNS(args.Netns)
if err != nil {
return fmt.Errorf("failed to open netns %q: %v", args.Netns, err)
}
defer netns.Close
hostInterface, containerInterface, err := setupVeth(netns, br, args.IfName, n.MTU, n.HairpinMode)
if err != nil {
return err
}
```
`setupVeth`函数会在pod所在的network namespace中创建一对veth接口,并将其中的一端移到host中,然后设置它的mac地址,并将其挂载到cni0这个网桥上,如果需要的话还会设置这个接口的hairpin模式。代码如下
```
func setupVeth(netns ns.NetNS, br *netlink.Bridge, ifName string, mtu int, hairpinMode bool) (*current.Interface, *current.Interface, error) {
contIface := ¤t.Interface{}
hostIface := ¤t.Interface{}
err := netns.Do(func(hostNS ns.NetNS) error {
// create the veth pair in the container and move host end into host netns
hostVeth, containerVeth, err := ip.SetupVeth(ifName, mtu, hostNS)
if err != nil {
return err
}
contIface.Name = containerVeth.Name
contIface.Mac = containerVeth.HardwareAddr.String
contIface.Sandbox = netns.Path
hostIface.Name = hostVeth.Name
return nil
})
if err != nil {
return nil, nil, err
}
// need to lookup hostVeth again as its index has changed during ns move
hostVeth, err := netlink.LinkByName(hostIface.Name)
if err != nil {
return nil, nil, fmt.Errorf("failed to lookup %q: %v", hostIface.Name, err)
}
hostIface.Mac = hostVeth.Attrs.HardwareAddr.String
// connect host veth end to the bridge
if err := netlink.LinkSetMaster(hostVeth, br); err != nil {
return nil, nil, fmt.Errorf("failed to connect %q to bridge %v: %v", hostVeth.Attrs.Name, br.Attrs.Name, err)
}
// set hairpin mode
if err = netlink.LinkSetHairpin(hostVeth, hairpinMode); err != nil {
return nil, nil, fmt.Errorf("failed to setup hairpin mode for %v: %v", hostVeth.Attrs.Name, err)
}
return hostIface, contIface, nil
}
```
4.2、三层处理
```
isLayer3 := n.IPAM.Type != ""
...
if isLayer3 {
..
}
```
三层网络是否需要处理取决于ipam cni plugin是否已经配置,在我们的环境中, 这个字段已经由flannel cni plugin配置成host-local了,因此这里需要处理三层的逻辑。处理的详细逻辑如下
```
// 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 {
os.Setenv("CNI_COMMAND", "DEL")
ipam.ExecDel(n.IPAM.Type, args.StdinData)
os.Setenv("CNI_COMMAND", "ADD")
}
}
// Convert whatever the IPAM result was into the current Result type
ipamResult, err := current.NewResultFromResult(r)
if err != nil {
return err
}
result.IPs = ipamResult.IPs
result.Routes = ipamResult.Routes
if len(result.IPs) == 0 {
return errors.New("IPAM plugin returned missing IP config")
}
```
这里bridge plugin 会调用host-local这个ipmi plugin来为当前处理的pod分配一个ip地址。
```
// Gather gateway information for each IP family
gwsV4, gwsV6, err := calcGateways(result, n)
if err != nil {
return err
}
```
接着会 根据这个地址计算出bridge设备cni0的ip地址。
```
// Configure the container hardware address and IP address(es)
if err := netns.Do(func(_ ns.NetNS) error {
contVeth, err := net.InterfaceByName(args.IfName)
...
// Add the IP to the interface
if err := ipam.ConfigureIface(args.IfName, result); err != nil {
return err
}
// Send a gratuitous arp
for _, ipc := range result.IPs {
if ipc.Version == "4" {
_ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth)
}
}
return nil
}); err != nil {
return err
}
```
这段代码的含义是 在pod所在的network namespace中配置之前添加进来的那个veth接口,包括将设备设置为up状态,配置ip地址和路由信息,最后发送gratuitous arp广播。
```
if n.IsGW {
var firstV4Addr net.IP
// Set the IP address(es) on the bridge and enable forwarding
for _, gws := range []*gwInfo{gwsV4, gwsV6} {
for _, gw := range gws.gws {
if gw.IP.To4 != nil && firstV4Addr == nil {
firstV4Addr = gw.IP
}
err = ensureBridgeAddr(br, gws.family, &gw, n.ForceAddress)
if err != nil {
return fmt.Errorf("failed to set bridge addr: %v", err)
}
}
if gws.gws != nil {
if err = enableIPForward(gws.family); err != nil {
return fmt.Errorf("failed to enable forwarding: %v", err)
}
}
}
}
```
接下来会给cni0这个bridge配置ip地址,并执行类似于 `echo 1 > /proc/sys/net/ipv4/ip_forward`的操作来启用数据包转发功能
```
if n.IPMasq {
chain := utils.FormatChainName(n.Name, args.ContainerID)
comment := utils.FormatComment(n.Name, args.ContainerID)
for _, ipc := range result.IPs {
if err = ip.SetupIPMasq(ip.Network(&ipc.Address), chain, comment); err != nil {
return err
}
}
}
```
最后如果cni plugin设置ipmasq则需要进行相关ipmasq相关的设置, 在当前环境中ipmasq是由Flanneld Daemon完成的,因此这里的cni plugin不会进行设置。
至此, 详细分析了bridge cni plugin的cmAdd主要流程,其核心功能总结如下
- 新建并配置网桥设备cni0
- 在pod所在的namespace中创建一对veth接口,并 将其中的一端移到host中并将其挂载到网桥上
- 调用ipmi cni plugin给pod中的 一端veth接口分配ip地址并将其配置到接口上
- 给cni0配置ip地址并开启数据包转发功能
参考文献
-https://github.com/containernetworking/cni/blob/master/SPEC.md
-https://github.com/containernetworking/cni
-https://github.com/containernetworking/plugins
责任编辑: