首页 > 新闻 > 热点 > 正文

智汇华云|Kubernetes网络详解(二)CNI原理与实现

华云公众号  发表于:2020-02-27 10:04:11

原标题:智汇华云 | Kubernetes网络详解(二) CNI原理与实现

智汇华云|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

智汇华云|Kubernetes网络详解(二)CNI原理与实现

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 := &current.Interface{}

hostIface := &current.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

责任编辑:

猜你喜欢
热点排行(TOP5)
相关文章