• CNI设计解读


    何为cni?

    kubernetes在设计网络方案的时候并没有设计统一的网络方案,只提供了统一的容器网络接口也就是所谓cni,这么做的目的就是为了遵循kubernets的核心理念OutOfTree,简单来讲就是专注于自身核心能力,将其他能力类似csi cni cri交给社区以及领域专家,这样一方面可以降低软件自身使用的复杂度,减小稳定性风险。

    flannel cni设计

    在一个pod生命周期中,cni主要调用3个方法分别是cmdAdd,cmdDel, cmdCheck,分别代表:创建容器时调用cmdAdd,销毁容器时调用cmdDel, 以及销毁前的检测cmdCheck,但是cmdCheck是在0.4.0之后添加的,对于目前常用的cni版本0.3.1来说并不支持。

    整体链路为flnnel-cni->bridge(创建设备)->host-local(申请ip)->bridge(申请到的ip写入到网卡上并配置路由)

    大致调用流程图如下:

    流程详解

    第一部分(kubelet)

    创建流程

    1. kubelet解析/etc/cni/net.d/10-flannel.conflist文件之后 根据文件里的plugins里的对象逐一执行插件并把结果传递给下一个插件继续执行 最后将结果缓存到/var/lib/cni/cache目录。

    /etc/cni/net.d/10-flannel.conflist配置如下 分别调用两个插件:

          a. flannel插件主流程依次调用bridge以及host-local 创建虚拟网卡以及路由

          b.portmap插件主要针对配置hostPort的pod 为该pod通过iptables配置端口映射

    1. {
    2. "name": "cbr0",
    3. "cniVersion": "0.3.1",
    4. "plugins": [
    5. {
    6. "type": "flannel",
    7. "delegate": {
    8. "hairpinMode": true,
    9. "isDefaultGateway": true
    10. }
    11. },
    12. {
    13. "type": "portmap",
    14. "capabilities": {
    15. "portMappings": true
    16. }
    17. }
    18. ]
    19. }
    1. func (c *CNIConfig) AddNetworkList(ctx context.Context, list *NetworkConfigList, rt *RuntimeConf) (types.Result, error) {
    2. var err error
    3. var result types.Result
    4. for _, net := range list.Plugins {
    5. result, err = c.addNetwork(ctx, list.Name, list.CNIVersion, net, result, rt)
    6. if err != nil {
    7. return nil, err
    8. }
    9. }
    10. if err = setCachedResult(result, list.Name, rt); err != nil {
    11. return nil, fmt.Errorf("failed to set network %q cached result: %v", list.Name, err)
    12. }
    13. return result, nil
    14. }

    2. 通过10-flannel.conflist我们可以看到调用的第一个插件为flannel,那么kubelet将插件目录/opt/cni/bin/flannel传递给invoke.ExecPluginWithResult函数.

    1. func (c *CNIConfig) addNetwork(ctx context.Context, name, cniVersion string, net *NetworkConfig, prevResult types.Result, rt *RuntimeConf) (types.Result, error) {
    2. c.ensureExec()
    3. pluginPath, err := c.exec.FindInPath(net.Network.Type, c.Path)
    4. if err != nil {
    5. return nil, err
    6. }
    7. newConf, err := buildOneConfig(name, cniVersion, net, prevResult, rt)
    8. if err != nil {
    9. return nil, err
    10. }
    11. return invoke.ExecPluginWithResult(ctx, pluginPath, newConf.Bytes, c.args("ADD", rt), c.exec)
    12. }

    3. invoke.ExecPluginWithResult函数里调用exec.ExecPlugin并传递参数执行相应的二进制文件

    1. func ExecPluginWithResult(ctx context.Context, pluginPath string, netconf []byte, args CNIArgs, exec Exec) (types.Result, error) {
    2. if exec == nil {
    3. exec = defaultExec
    4. }
    5. stdoutBytes, err := exec.ExecPlugin(ctx, pluginPath, netconf, args.AsEnv())
    6. if err != nil {
    7. return nil, err
    8. }
    9. // Plugin must return result in same version as specified in netconf
    10. versionDecoder := &version.ConfigDecoder{}
    11. confVersion, err := versionDecoder.Decode(netconf)
    12. if err != nil {
    13. return nil, err
    14. }
    15. return version.NewResult(confVersion, stdoutBytes)
    16. }

    三个参数如下示例:

    1. pluginPath: /opt/cni/bin/flannel
    2. NetConf: types.NetConf{
    3. CNIVersion: "0.3.1",
    4. Name: "cbr0",
    5. Type: "flannel",
    6. IPAM: types.IPAM{
    7. Type: "",
    8. },
    9. DNS: types.DNS{
    10. Nameservers: nil,
    11. Domain: "",
    12. Search: nil,
    13. Options: nil,
    14. },
    15. },
    16. Delegate: map[string]interface {}{
    17. "hairpinMode": true,
    18. "isDefaultGateway": true,
    19. },
    20. }
    21. args.AsEnv(): 设置CNI_COMMAND,CNI_CONTAINERID,CNI_NETNS,CNI_ARGS,CNI_IFNAME,CNI_PATH几个环境变量给到插件
    22. 二进制程序通过PluginMain里的getCmdArgsFromEnv函数将环境变量解析成CmdArgs之后传递给cmdAdd,cmdDel,cmdCheck

    4. 所有插件执行完成之后调用setCachedResult将结果写入到/var/lib/cni/cache/results目录下

    删除流程

    1.类似于创建流程,整体流程如下:

    TearDownPod -> plugin.deleteFromNetwork -> cniNet.DelNetworkList -> delNetwork -> invoke.ExecPluginWithoutResult

    从这里我们看到基本的流程和创建流程类似 唯一不一致的地方是我们在c.args("DEL", rt) 传入的是DEL而不是ADD。也就是说我们在这里判断我们执行插件的函数是cmdAdd还是cmdDel

    2. 最后删除/var/lib/cni/cache/results下的对应文件,/var/lib/cni/cache/results的目的是为了提供cmdcheck用于检测是否合规

    第二部分(flannel-cni)

            flannel-cni总共实现了两个方法cmdAdd以及cmdDel,通过PluginMain注册cmdAdd,cmdDel两个方法,并在PluginMain里讲传递过来的参数解析成cmdargs传递给cmdAdd,cmdDel其实现的能力如下:

    cmdAdd:

    1. 通过loadFlannelNetConf解析cmdargs里的StdinData,内容为NetConf结构体,并配置SubnetFile(/run/flannel/subnet.env)以及DataDir(/var/lib/cni/flannel)

    2. 通过loadFlannelSubnetEnv解析由flannel生成的/run/flannel/subnet.env文件

    3. 将/run/flannel/subnet.env里面的参数 渲染到由loadFlannelNetConf生成的出来的结构体中,也就是hairpinMode(发夹模式 可以让数据流量从同一个点位进出)以及isDefaultGateway(是否生成pod容器的默认网关)两个参数:

    1. types.NetConf{
    2. CNIVersion: "0.3.1",
    3. Name: "cbr0",
    4. Type: "bridge",
    5. Mtu: "1450",
    6. HairpinMode: true,
    7. IpMasq: false,
    8. IsDefaultGateway: true,
    9. IsGateway: true,
    10. IPAM: types.IPAM{
    11. "type": "host-local",
    12. "subnet": 10.244.0.1/24,
    13. "routes": []types.Route{
    14. types.Route{
    15. Dst: 10.244.0.0/16,
    16. },
    17. },
    18. }
    19. }

     4. delegateAdd通过type字段找到后续需要执行的插件名称 并通过saveScratchNetConf方法将以上结果保存到/var/lib/cni/flannel目录下并在invoke.DelegateAddDelegate里通过type字段来判断执行下一个插件的名称(bridge),并将之前的结果以及args.env传递给下一个插件(bridge),该结果通过下一个插件的StdinData获取到.

    1. func delegateAdd(cid, dataDir string, netconf map[string]interface{}) error {
    2. netconfBytes, err := json.Marshal(netconf)
    3. if err != nil {
    4. return fmt.Errorf("error serializing delegate netconf: %v", err)
    5. }
    6. // save the rendered netconf for cmdDel
    7. if err = saveScratchNetConf(cid, dataDir, netconfBytes); err != nil {
    8. return err
    9. }
    10. result, err := invoke.DelegateAdd(netconf["type"].(string), netconfBytes)
    11. if err != nil {
    12. return err
    13. }
    14. return result.Print()
    15. }

    cmdDEL:

    1.通过loadFlannelNetConf解析cmdargs里的StdinData,内容为NetConf结构体,并配置SubnetFile(/run/flannel/subnet.env)以及DataDir(/var/lib/cni/flannel)

    2. 通过CNI_ARGS获取到容器的id 通过consumeScratchNetConf函数读取/var/lib/cni/flannel/$containerId里面的配置类似,读取之后移除该文件。

    1. # cat 0e39bc1c61f18e1af93bc6f455097fcfe04872d590c313eccf4d3a4f7de224d2
    2. {"cniVersion":"0.3.1","hairpinMode":true,"ipMasq":false,"ipam":{"routes":[{"dst":"10.220.0.0/16"}],"subnet":"10.220.9.0/24","type":"host-local"},"isDefaultGateway":true,"isGateway":true,"mtu":1450,"name":"cbr0","type":"bridge"}

    3. 将从/var/lib/cni/flannel/$containerId读取出来的内容 发给下一个插件(bridge)的CmdDel函数

    第三部分(bridge cni)

    cmdAdd部分

    1. loadNetConf函数作用是读取flannel cni传递过来的StdinData 并设置BrName参数为cni0 组合成新的NetConf对象

    2. 通过setupBridge函数里的ensureBridge函数创建名称为cni0的bridge虚拟桥接网卡

    3. 通过GetNS获取该pod容器所在的网络名称空间id,args.Netns类似pid所在的net文件类似/proc/18649/ns/net

    4. 通过setupVeth在改网络namespace里创建veth网卡对,网络namespace端为eth0 宿主机端为veth***(该名称由RandomVethName函数生成,并将veth加入到宿主机端namespace下。之后配置宿主机端端veth网卡hairpin mode。

    5. 之后通过ipam.ExecAdd往下ipam(IP地址管理器)里获取ip地址,这里ipam类型为host-local,host-local的插件的规则从第四部分详解。

    6. 获取到ip的结构为:

    1. {
    2. "ip4": {
    3. "ip": "10.244.0.2",
    4. "gateway": "10.244.0.1"
    5. },
    6. "dns": {}
    7. }

    7. 通过calcGatewy来获取网关详情 这里主要以IsDefaultGW参数来判断是否给pod容器加默认网关

    8. 之后在ConfigureIface函数里进入到上述的网络namespace配置eth0网卡的ip地址并根据calcGatewy的结果增加默认路由

    9. 之后在ensureBridgeAddr函数里配置cni0网卡的ip地址,网卡mac地址,并在enableIPForward开启ipv4转发(/proc/sys/net/ipv4/ip_forward)。

    10.之后在SetupIPMasq函数里配置地址伪装的iptables规则

    CmdDEL部分

    1. 通过loadNetConf读取flannel插件传递的过来的StdinData

    2. 将flannel插件传递的过来的StdinData传递给IPAM插件去释放分配给该pod的ip地址,这里的IPAM从StdinData里可以看到 调用的是host-local插件。

    3. 切换到该容器的network namespace之后删除虚拟网卡信息,并且如果开启了地址伪装功能,删除对应的iptables规则

    第四部分(host-local cni)

    cmdAdd部分

    1.通过LoadIPAMConfig函数 将bridge传过来的StdinData以及args.env生成IPAMConfig对象

    2. 之后通过allocator.Get根据传递过来的range进行ip地址分配,这里也可以指定ip地址分配,如果指定ip地址那么会校验ip地址是否合规,如果合规将分配的地址存储到本地磁盘下(/var/lib/cni/networks/cbr0/)以ip为文件名称 内容为容器id,并把最后获取到的ip地址存储到/var/lib/cni/networks/cbr0/last_reserved_ip.0,这么做的目的是保证分配的地址不冲突。

    3. 迭代器GetIter的主要逻辑是基于LastReservedIP(这个数据保存在一个文件中)和ip range,找到下一个可分配的IP并返回,获取规则是避免LastReservedIP里的ip 在LastReservedIP的基础上+1 作为分配的ip 当然也会通过range检测分配的ip是否在合规范围内。

    4. 核心函数GetIter 生成出迭代器对象,并通过lastReservedIP来配置cur这个参数的值,Next函数根据cur的值来判断下一个可分配的ip地址

    5.之后通过Reserve方法将ip地址和容器id进行绑定存储到/var/lib/cni/networks/cbr0/并更新last_reserved_ip.0文件,最后将结果返回给bridge插件。

    1. func (a *IPAllocator) GetIter() (*RangeIter, error) {
    2. iter := RangeIter{
    3. rangeset: a.rangeset,
    4. }
    5. // Round-robin by trying to allocate from the last reserved IP + 1
    6. startFromLastReservedIP := false
    7. // We might get a last reserved IP that is wrong if the range indexes changed.
    8. // This is not critical, we just lose round-robin this one time.
    9. lastReservedIP, err := a.store.LastReservedIP(a.rangeID)
    10. if err != nil && !os.IsNotExist(err) {
    11. log.Printf("Error retrieving last reserved ip: %v", err)
    12. } else if lastReservedIP != nil {
    13. startFromLastReservedIP = a.rangeset.Contains(lastReservedIP)
    14. }
    15. // Find the range in the set with this IP
    16. if startFromLastReservedIP {
    17. for i, r := range *a.rangeset {
    18. if r.Contains(lastReservedIP) {
    19. iter.rangeIdx = i
    20. iter.startRange = i
    21. // We advance the cursor on every Next(), so the first call
    22. // to next() will return lastReservedIP + 1
    23. iter.cur = lastReservedIP
    24. break
    25. }
    26. }
    27. } else {
    28. iter.rangeIdx = 0
    29. iter.startRange = 0
    30. iter.startIP = (*a.rangeset)[0].RangeStart
    31. }
    32. return &iter, nil
    33. }
    1. func (i *RangeIter) Next() (*net.IPNet, net.IP) {
    2. r := (*i.rangeset)[i.rangeIdx]
    3. // If this is the first time iterating and we're not starting in the middle
    4. // of the range, then start at rangeStart, which is inclusive
    5. if i.cur == nil {
    6. i.cur = r.RangeStart
    7. i.startIP = i.cur
    8. if i.cur.Equal(r.Gateway) {
    9. return i.Next()
    10. }
    11. return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway
    12. }
    13. // If we've reached the end of this range, we need to advance the range
    14. // RangeEnd is inclusive as well
    15. if i.cur.Equal(r.RangeEnd) {
    16. i.rangeIdx += 1
    17. i.rangeIdx %= len(*i.rangeset)
    18. r = (*i.rangeset)[i.rangeIdx]
    19. i.cur = r.RangeStart
    20. } else {
    21. i.cur = ip.NextIP(i.cur)
    22. }
    23. if i.startIP == nil {
    24. i.startIP = i.cur
    25. } else if i.rangeIdx == i.startRange && i.cur.Equal(i.startIP) {
    26. // IF we've looped back to where we started, give up
    27. return nil, nil
    28. }
    29. if i.cur.Equal(r.Gateway) {
    30. return i.Next()
    31. }
    32. return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway
    33. }

    cmdDel部分

    1. 通过LoadIPAMConfig读取bridge传递的StdinData内容

    2. disk.New出生后存储,默认的目录是/var/lib/cni/networks/{name},后续通过store操作数据

    3. 通过ipAllocator.Release释放ip 并通过ReleaseByID函数循环读取/var/lib/cni/networks/cbr0目录下的文件,如果文件内容匹配容器id那么就移除该文件。

    1. func (s *Store) ReleaseByID(id string) error {
    2. err := filepath.Walk(s.dataDir, func(path string, info os.FileInfo, err error) error {
    3. if err != nil || info.IsDir() {
    4. return nil
    5. }
    6. data, err := ioutil.ReadFile(path)
    7. if err != nil {
    8. return nil
    9. }
    10. if strings.TrimSpace(string(data)) == strings.TrimSpace(id) {
    11. if err := os.Remove(path); err != nil {
    12. return nil
    13. }
    14. }
    15. return nil
    16. })
    17. return err
    18. }

    第五部分(portmap cni)

    cmdAdd部分

    最后flannel-cni返回给kubelet插件的结构体如下 并把结果放到prevResult里交给portmap插件 配置案例如下:

    1. {
    2. "name": "cbr0",
    3. "cniVersion": "0.3.1",
    4. "runtimeConfig": {
    5. "portMappings": [{
    6. "hostPort": 801,
    7. "containerPort": 80,
    8. "protocol": "tcp"
    9. }]
    10. },
    11. "prevResult": {
    12. "cniVersion": "1.0.0",
    13. "interfaces": [{
    14. "name": "eth0",
    15. "sandbox": "/proc/20202/ns/net"
    16. }],
    17. "ips": [{
    18. "interface": 2,
    19. "address": "10.244.0.136/24",
    20. "version": "4"
    21. }],
    22. "routes": [{
    23. "dst": "0.0.0.0/0",
    24. "gw": "10.244.0.1"
    25. }],
    26. "dns": {}
    27. }
    28. }

    2. parseConfig函数解析上面的结构体以及接口名称通常为eth0,在这里主要判断结构体内容是否合规 是否需要进行端口映射

    3. 通过forwardPorts生成并在宿主机加入iptables规则映射规则DNT以及SNAT默认情况下地址伪装是需要配置snat规则,这里配置dnat主要是为了对需要地址伪装的流量进行打标记

    cmdDEL部分

    1. 通过parseConfig解析StdinData以及ifname

    2. 获取args.ContainerID

    3. 通过unforwardPorts删除对应的iptables端口映射规则dnat以及snat

  • 相关阅读:
    Spring 04: IOC控制反转 + DI依赖注入
    基于html5的网上书店系统
    cwnd < 1 时的拥塞控制
    项目7-音乐播放器5+注册账号
    使用百度EasyDL实现明厨亮灶厨师帽识别
    LeetCode 847. Shortest Path Visiting All Nodes【状态压缩,BFS;动态规划,最短路】2200
    函数高级用法
    微信小程序案例:2-2本地生活
    【深入浅出设计模式--命令模式】
    关于电脑卡死如何开机、F8、安全模式
  • 原文地址:https://blog.csdn.net/qq_22543991/article/details/128025437