美文网首页
cni设计简析

cni设计简析

作者: cloudFans | 来源:发表于2022-05-13 17:38 被阅读0次

    cni设计

    参考: https://github.com/containernetworking/cni/blob/main/SPEC.md

    1. 插件式使用的cni

    术语理解:

    container: 在cni这里对应linux network ns
    network: 指一组endpoint,可以用于获取到唯一的地址
    runtime: 一个负责执行CNI plugin的程序
    plugin: 负责配置网络

    这个文档会仅对runtime 和 plugin进行说明

    第一部分: 网络配置格式

    cniVersion: 版本,版本号越大支持的特性越丰富
    name: 网络名
    disableCheck: 对应cni的check (Add|Del|check)
    plugins: cni list

    plugin 配置对象

    type: 对应cni二进制的名字,比如macvlan,ipvlan
    capabilities: 稍后补充
    ipMasq: 通过host上的一个ip来伪装整个网络,所有pod用的该网段的pod ip都会使用该host ip进行地址伪装(nat)
    ipam: 地址管理字典,其内的type字段即是ipam plugin
    dns: 一个字典,包括,nameservers,domain,search,options

    
    {
      "cniVersion": "1.0.0",
      "name": "dbnet",
      "plugins": [
        {
          "type": "bridge",
          // plugin specific parameters
          "bridge": "cni0",
          "keyA": ["some more", "plugin specific", "configuration"],
          
          "ipam": {
            "type": "host-local",
            // ipam specific
            "subnet": "10.1.0.0/16",
            "gateway": "10.1.0.1",
            "routes": [
                {"dst": "0.0.0.0/0"}
            ]
          },
          "dns": {
            "nameservers": [ "10.1.0.1" ]
          }
        },
        {
          "type": "tuning",
          "capabilities": {
            "mac": true
          },
          "sysctl": {
            "net.core.somaxconn": "500"
          }
        },
        {
            "type": "portmap",
            "capabilities": {"portMappings": true}
        }
      ]
    }
    

    第二部分, 执行协议

    cni protocol协议基于容器运行时调用cni(二进制),cni定义了cni二进制脚本和容器运行时交互的协议。
    plugin分两种。

    "Interface" plugins: 常见的cni macvlan ipvlan等
    "Chained" plugins: 基于cni配置好的接口,做qos,安全组等配置,比如terway 的ipvlan和之后的cilium链式调用

    容器运行时,通过将配置和环境变量以stdin的形式传递给cni插件。cni插件配置完容器接口以及网络之后,基于stdout 反馈成功。 如果有错误,基于strerr反馈。
    stdin的配置和stdout的结果都是JSON格式。

    成功是状态码 必须是0
    失败时 非0

    容器运行时是在root network ns中执行cni plugin的。

    传递给cni的json包括:

    CNI_COMMAND: ADD, DEL, CHECK, or VERSION
    CNI_CONTAINERID: 容器id
    CNI_NETNS: 容器所在网络命名空间 /run/netns/[nsname]
    CNI_IFNAME: 容器内部的网卡
    CNI_ARGS: 额外的参数,格式为 "FOO=BAR;ABC=123"
    CNI_PATH: CNI插件所在路径

    cni 的操作

    ADD 将容器连接到网络,或者执行变更

    1. 在CNI_NETNS内部, 基于CNI_IFNAME 创建网卡
    2. 或者在CNI_NETNS内部, 基于CNI_IFNAME 调整接口的配置

    如果ADD成功,那么返回的json 也就是 prevResult 对象会包括这些内容: https://github.com/containernetworking/cni/blob/main/SPEC.md#Success

    cniVersion:
    容器网卡: name, mac, samdbox(CNI_NETNS)
    ips: ipcidr 192.168.1.3/24, gw, 网卡index
    路由: dst cidr, gw
    dns: nameservers domain search options

    如果容器内有同样的接口,肯定会报错,而且,容器运行时不会连续执行ADD两次(中间没有删除动作),仅执行一次。

    输入:

    容器运行时会提供一个json 对象作为cni的标输入:

    必须包含的字段:

    1. CNI_COMMAND
    2. CNI_CONTAINERID
    3. CNI_NETNS
    4. CNI_IFNAME

    可选字段:
    CNI_ARGS
    CNI_PATH

    DEL是add的反操作,暂不写
    CHECK 用于检查和确认,暂不写

    ipam 代理插件

    Delegated plugins (IPAM)

    ipam 插件必须反馈一个缩略的的成功对象的结构体。 比如没有接口数组以及接口ip等。

    ipam插件式cni设计的一部分,ipam和cni二级制协调工作,才能完成网络配置

    第三部分, 网络配置的执行

    容器运行时会在容器内部执行 add, del, check 等网络操作。

    1. 网络配置如何转化
    1. 转化后的网络配置如何提供给插件

    网络配置的操作统一叫做 attachment, 容器id 和 接口id, 是attachment的唯一标识

    (操作)周期 和 顺序

    1. 在调用任何插件(cni ipam)之前,容器运行时必须已经创建了一个新的网络命名空间。
    2. 对于同一个容器,容器运行时不能调用多个并行的操作。 多个容器的操作可以并行。
    3. 不同的容器,插件可以并行执行,必要的话,需要实现一定的锁机制,必须ipam 的地址分配。
    4. 容器运行时必须保证,add 和 delete的先后顺序, add 结束,delete才能执行,即使add失败,del必须成功执行。
    5. delete保证幂等,无论delete多少次,结果一致。
    6. add 和 delete 之间的时间段, 网络配置不能发生变化。
    7. 在不同的attachments之间,网络配置不能发生变化。
    8. 容器运行时负责ns的清理。

    Attachment的参数:

    1. 容器id
    2. ns
    3. 容器网卡名
    4. 原生参数, 额外参数,键值对格式,在CNI_ARGS 里面
    5. Capability Arguments, 键值对,json 格式: https://github.com/containernetworking/cni/blob/main/CONVENTIONS.md
    
     "capabilities": {"portMappings": true}
      "runtimeConfig": {
        "portMappings": [
          {"hostPort": 8080, "containerPort": 80, "protocol": "tcp"}
        ]
      }
    
    

    添加一个attachment

    #  cat /etc/cni/net.d/00-multus.conf | jq
    {
      "cniVersion": "0.4.0",
      "name": "multus-cni-network",
      "type": "multus",
      "capabilities": {
        "portMappings": true
      },
      "kubeconfig": "/etc/cni/net.d/multus.d/multus.kubeconfig",
      "delegates": [
        {
          "name": "kube-ovn",
          "cniVersion": "0.3.1",
          "plugins": [
            {
              "type": "kube-ovn",
              "server_socket": "/run/openvswitch/kube-ovn-daemon.sock"
            },
            {
              "type": "portmap",
              "capabilities": {
                "portMappings": true
              }
            }
          ]
        }
      ]
    }
    
    # 比如当前的k8s集群环境
    # plugins包含两部分 cni是multus,ipam是kube-ovn-daemon
    
    
    1. 查看type 字段,不存在,则失败,报错
    2. 基于plugin 配置,去请求配置,包括如下参数

    如果这个plugin在list中的第一位, 不需要提供之前的结果,应该指的是默认cni。
    其他的plugins, 之前的结果就是上一个plugin处理过的输出。

    这里说明,主plugin和其他plugin是有一定的顺序链式调用关系的。

    1. 执行plugin 二进制(比如macvlan cni), 基于CNI_COMMAND=ADD 参数,二进制可以通过环境变量读入参数,stdin也有包含一些配置项(ip mac dns 网关 svc localdns等)。

    2. 一旦plugin 执行出错,会立刻停止并失败,返回错误。

    容器运行时,必须存储最后一个plugin返回的结果,并持久化存储(pod的状态),用于做pod状态检查以及是否删除。

    Deleting an attachment: 是add的反向操作,暂不写

    从 plugin 配置中,获取(待)执行配置项

    网络配置的格式: 就是一个list,包含所有需要执行的plugin配置内容。

    必须被转化为单个plugin能够理解的格式,因为即使是多网卡场景,也要一个一个的执行。

    单个plugin调用时需要的配置也是一个json。

    容器运行时必须将以下配置项传递给cni二进制

    1. cniVersion: cni 版本
    2. name: 取自网络配置中的name字段
    3. runtimeConfig: json 对象, plugin中的capabilities 字段
    4. prevResult: 一个json对象,上一个运行过的plugin(add/delete/check)返回的结果

    plugin 中定义的 capabilities会被容器运行时移除。 其他field都毫无更改的直接传递。

    runtimeConfig

    然而 当CNI_ARGS 被传递给所有plugins后,没有任何指示表示这些信息会被消费。
    所以,Capability需要在plugins字段中明确定义。 那么 容器运行时,就可以基于此来确定是否一个给定的网络配置支持某些特定的capability(能力)。

    
    {
      "type": "myPlugin",
      "capabilities": {
        "portMappings": true  // 表示支持端口映射的能力
      }
    }
    
    // 将 capabilities 格式化为 runtimeConfig
    
    {
      "type": "myPlugin",
      "runtimeConfig": {
        "portMappings": [ { "hostPort": 8080, "containerPort": 80, "protocol": "tcp" } ]
      }
      ...
    }
    
    

    runtimeConfig 的内容来自于 网络配置中的capabilities域

    第四部分 插件代理(Plugin Delegation)

    有一些操作,无论出于何种原因,都不能合理地实现为一个离散的链式插件。
    更确切的说,一个CNI plugin 希望能代理别的插件执行一些功能。 一个最常用的例子就是代理执行ip 地址分配(ipam)

    作为网络配置的一部分,CNI 插件需要能够分配并维护容器内网卡的ip地址,也包括配置必要的路由。

    这让CNI插件具有极大的灵活性,但也变得更重(ipam 路由 dns 这些东西的维护)。

    很多CNI插件需要的ipam的代码几乎都是类似的,比如dhcp host-local。

    所以, 一个CNI 插件需要设计为可以用来作为其他plugin的ipam代理。

    为了让CNI更轻便,而且让IPAM 和 cni (二进制)保持正交关系(低耦合)。

    正交设计原则

    消除重复
    分离关注点
    缩小依赖范围
    向稳定的方向依赖

    ipam 被设计为一个第三方的插件,为了保证ipam被代理调用,同时设计了一个协议,用于指导代理(ipam)功能的实现。
    然而这不是容器运行时的职责,而是CNI plugin的职责: 在执行的过程中,适当的时间点,调用IPAM插件。
    ipam 插件必须定义了ip,子网,网关,路由。 并且返回这些信息给“主” 插件去应用(把这些参数在调用cni二进制的时候给过去)。
    ipam插件也有可能通过协议(dhcp)来获取这些信息。 或者数据存储在本地文件系统中。 或者网络配置的ipam部分。

    被代理的插件协议(Delegated Plugin protocol)

    和cni 插件一样,被代理的(ipam)插件也是通过调用一个可执行的(二进制)。 这些插件也在预先指定好的目录下,

    # ls -l /opt/cni/bin/
    total 177440
    -rwxr-xr-x. 1 root root  4151672 Feb  5  2021 bandwidth # 带宽限制
    -rwxr-xr-x. 1 root root  4536104 Feb  5  2021 bridge
    -rwxr-xr-x. 1 root root 10270090 Feb  5  2021 dhcp # ipam
    -rwxr-xr-x. 1 root root  4767801 Feb  5  2021 firewall # 防火墙限制
    -rwxr-xr-x. 1 root root  3357992 Feb  5  2021 flannel
    -rwxr-xr-x. 1 root root  4144106 Feb  5  2021 host-device
    -rwxr-xr-x. 1 root root  3565330 Feb  5  2021 host-local # ipam
    -rwxr-xr-x. 1 root root  4288339 Feb  5  2021 ipvlan
    -rwxr-xr-x. 1 root root 65658880 Apr 10 03:51 kube-ovn
    -rwxr-xr-x. 1 root root  3566204 Apr 10 03:51 loopback
    -rwxr-xr-x. 1 root root  4497003 Apr 10 03:51 macvlan
    -rwxr-xr-x. 1 root root 41849770 Apr 10 03:48 multus
    -rwxr-xr-x. 1 root root  3979034 Apr 10 03:51 portmap # 端口映射
    -rwxr-xr-x. 1 root root  4467317 Feb  5  2021 ptp
    -rwxr-xr-x. 1 root root  3701138 Feb  5  2021 sbr
    -rwxr-xr-x. 1 root root  3153330 Feb  5  2021 static
    -rwxr-xr-x. 1 root root  3668289 Feb  5  2021 tuning
    -rwxr-xr-x. 1 root root  4287972 Feb  5  2021 vlan
    -rwxr-xr-x. 1 root root  3759977 Feb  5  2021 vrf
    
    

    CNI_PATH: 用于指定CNI的路径

    所有被代理的cni插件收到的环境变量都是一样的,就像cni插件一样,通过stdin收到网络配置,基于stdout传出配置。

    被代理的插件(下层的)提供完整的网络配置给到(上层的)插件,(插件间按照主次顺序调用)

    被代理的插件执行流程

    当一个插件执行一个被代理的插件时,它应该:

    在CNI_PATH(环境变量)对应目录下查找二进制对象
    根据插件收到的配置和环境变量执行找到的二进制对象(执行时传给配置和环境变量)
    确保 被代理的插件的stderr被传递给调用者的stderr对象

    ADD 场景中,如果被代理的插件执行失败了,那么在返回错误前需要执行该插件的del操作。
    同样的,DEL | check 上层插件检测失败,要将stderr返回给下层。

    第五部分,返回值类型

    只有3种类型

     Success (or Abbreviated Success) 
     Error
     _Version
    
    
    5.1 Success

    所有插件的请求配置内容中都会有一个 prevResult, prevResult 作为插件自身的输出。
    任何插件都可能会对 prevResult 进行修改。 如果不修改,直接反射(传递)即可。

    ADD 操作成功后,所有插件(链式执行后)必须返回一个json对象:
    cniVersion:
    interfaces: 网卡名, mac, ns
    ips: attachment 配置好的ipcidr, 网关, 网卡index
    routes: 路由 dst cidr,gw
    dns: nameservers domain search options

    ipam (被代理的插件)

    被代理的插件可以忽略 相关的配置项。
    ipam 必须返回一个缩略的 Success 成功对象,可能不会包含 interfaces 数组,以及ips 对象内部的interface。

    错误对象暂时不了解

    版本

    插件必须返回cniVerison, 以及 supportedVersions

    
    {
        "cniVersion": "1.0.0",
        "supportedVersions": [ "0.1.0", "0.2.0", "0.3.0", "0.3.1", "0.4.0", "1.0.0" ]
    }
    
    
    

    示例

    1. CNI_COMMAND=ADD, 把如下参数传递给 bridge cni二级制 插件
    
    
    {
        "cniVersion": "1.0.0", 
        "name": "dbnet", // 网络名
        "type": "bridge", // cni 二进制的名字
        "bridge": "cni0", // 参数 网桥名
        "keyA": ["some more", "plugin specific", "configuration"], // 额外参数
        "ipam": { // 被代理调用的ipam 插件
            "type": "host-local",
            "subnet": "10.1.0.0/16",
            "gateway": "10.1.0.1"
        },
        "dns": { // 被代理调用的 dns 插件 ?
            "nameservers": [ "10.1.0.1" ]
        }
    }
    
    

    接下来 主 cni (比如multus),会调用ipam(基于ipam内的参数,执行调用host-local二进制,将subnet gw属性基于stdin传递给该二进制)

    host-local 会返回如下结果

    
    {
        "ips": [
            {
              "address": "10.1.0.5/16",  // 分配出ip
              "gateway": "10.1.0.1"      // 返回网关
            }
        ],
        "routes": [
          {
            "dst": "0.0.0.0/0"
          }
        ],
        "dns": {
          "nameservers": [ "10.1.0.1" ]
        }
    }
    
    

    然后 bridge 二进制cni文件,基于ipam的返回结果,配置容器的网络,然后返回如下结果

    
    
    {
        "ips": [
            {
              "address": "10.1.0.5/16",
              "gateway": "10.1.0.1",
              "interface": 2
            }
        ],
        "routes": [
          {
            "dst": "0.0.0.0/0"
          }
        ], // 以上是ipam的返回内容,保持不变
        "interfaces": [ //以下是cni添加的内容
            {
                "name": "cni0", // 网桥名
                "mac": "00:11:22:33:44:55"  // 网桥的mac
            },
            {
                "name": "veth3243", // veth-pair 接在网桥的一段
                "mac": "55:44:33:22:11:11" // 其mac
            },
            {
                "name": "eth0",  // 容器ns 内部的eth0 网卡,veth-pair的另一端
                "mac": "99:88:77:66:55:44", // mac
                "sandbox": "/var/run/netns/blue" // ns 路径
            }
        ],
        "dns": {
          "nameservers": [ "10.1.0.1" ]
        }
    }
    
    
    
    

    这个结果就是 bridge cni操作后的 prevResult。

    第一个插件,一般是cni 二进制,它的prevResult是空的,但是它的(下一个|上层)插件会基于第一个插件的result作为prevResult。

    1. 基于 tuning (二进制)插件, 基于CNI_COMMAND=ADD, 调用 tuning的 add函数,基于 prevResult, 结合mac的capability (特性),stdin 收到的配置请求体如下
    
    {
      "cniVersion": "1.0.0",
      "name": "dbnet",
      "type": "tuning", // 二进制插件
      "sysctl": {
        "net.core.somaxconn": "500"
      },
      "runtimeConfig": { // 特性, 将eth0的mac调整为该值
        "mac": "00:11:22:33:44:66"
      },
      "prevResult": { // 下层的返回
        "ips": [
            {
              "address": "10.1.0.5/16",
              "gateway": "10.1.0.1",
              "interface": 2
            }
        ],
        "routes": [
          {
            "dst": "0.0.0.0/0"
          }
        ],
        "interfaces": [
            {
                "name": "cni0",
                "mac": "00:11:22:33:44:55"
            },
            {
                "name": "veth3243",
                "mac": "55:44:33:22:11:11"
            },
            {
                "name": "eth0",
                "mac": "99:88:77:66:55:44",
                "sandbox": "/var/run/netns/blue"
            }
        ],
        "dns": {
          "nameservers": [ "10.1.0.1" ]
        }
      }
    }
    
    
    

    tuning 成功执行后的返回结果,可以看到eth0 mac已发生变化

    
    
    {
        "ips": [
            {
              "address": "10.1.0.5/16",
              "gateway": "10.1.0.1",
              "interface": 2
            }
        ],
        "routes": [
          {
            "dst": "0.0.0.0/0"
          }
        ],
        "interfaces": [
            {
                "name": "cni0",
                "mac": "00:11:22:33:44:55"
            },
            {
                "name": "veth3243",
                "mac": "55:44:33:22:11:11"
            },
            {
                "name": "eth0",
                "mac": "00:11:22:33:44:66", // 已变化
                "sandbox": "/var/run/netns/blue"
            }
        ],
        "dns": {
          "nameservers": [ "10.1.0.1" ]
        }
    }
    
    

    tuning返回的prevResult

    1. 最后 调用 portmap 插件,CNI_COMMAND=ADD 对应 portmap add函数,使用的 prevResult如上,
      而 tuning 执行完毕后,如下
    
    {
      "cniVersion": "1.0.0",
      "name": "dbnet",
      "type": "portmap", // 增加了该部分
      "runtimeConfig": {
        "portMappings" : [
          { "hostPort": 8080, "containerPort": 80, "protocol": "tcp" }
        ]
      },
      "prevResult": {// 下层的返回
        "ips": [
            {
              "address": "10.1.0.5/16",
              "gateway": "10.1.0.1",
              "interface": 2
            }
        ],
        "routes": [
          {
            "dst": "0.0.0.0/0"
          }
        ],
        "interfaces": [
            {
                "name": "cni0",
                "mac": "00:11:22:33:44:55"
            },
            {
                "name": "veth3243",
                "mac": "55:44:33:22:11:11"
            },
            {
                "name": "eth0",
                "mac": "00:11:22:33:44:66",
                "sandbox": "/var/run/netns/blue"
            }
        ],
        "dns": {
          "nameservers": [ "10.1.0.1" ]
        }
      }
    }
    
    

    DELETE正好是add 的逆操作,所以最上层的插件先执行,执行的参数就是上面ADD最后返回的结果。

    相关文章

      网友评论

          本文标题:cni设计简析

          本文链接:https://www.haomeiwen.com/subject/iyruurtx.html