资讯首页 新闻资讯 云计算测评 云服务商动态 技术频道
上云无忧 > 云计算资讯  > 技术频道 > Kubernetes网络详解(二) CNI原理与实现

Kubernetes网络详解(二) CNI原理与实现

发布时间: 2020-01-26 15:51:38 |浏览量:1065| 评论: 0

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

-rwxr-xr-x. 1 root root 2973336 Mar 25  2019 bridge

-rwxr-xr-x. 1 root root 7598064 Mar 25  2019 dhcp

-rwxr-xr-x. 1 root root 2110208 Mar 25  2019 flannel

-rwxr-xr-x. 1 root root 2288536 Mar 25  2019 host-device

-rwxr-xr-x. 1 root root 2238208 Mar 25  2019 host-local

-rwxr-xr-x. 1 root root 2621472 Mar 25  2019 ipvlan

-rwxr-xr-x. 1 root root 2257808 Mar 25  2019 loopback

-rwxr-xr-x. 1 root root 2650160 Mar 25  2019 macvlan

-rwxr-xr-x. 1 root root 2613864 Mar 25  2019 portmap

-rwxr-xr-x. 1 root root 2946664 Mar 25  2019 ptp

-rwxr-xr-x. 1 root root 1951880 Mar 25  2019 sample

-rwxr-xr-x. 1 root root 2103456 Mar 25  2019 tuning

-rwxr-xr-x. 1 root root 2617328 Mar 25  2019 vlan

```


在本文中我们会详细分析几个基础的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


更多【技术频道】相关文章

有话要说

全部评论

暂无评论
官方微信
联系客服
400-826-7010
7x24小时客服热线
分享
  • QQ好友
  • QQ空间
  • 微信
  • 微博
返回顶部