美文网首页云计算
kube-ovn源码分析

kube-ovn源码分析

作者: 梅_梅 | 来源:发表于2024-12-15 11:07 被阅读0次

总体架构

功能介绍

总体来看,Kube-OVN 作为 Kubernetes 和 OVN 之间的一个桥梁,将成熟的 SDN 和云原生相结合。 这意味着 Kube-OVN 不仅通过 OVN 实现了 Kubernetes 下的网络规范,例如 CNI,Service 和 Networkpolicy,还将大量的 SDN 领域能力带入云原生,例如逻辑交换机,逻辑路由器,VPC,网关,QoS,ACL 和流量镜像。

同时 Kube-OVN 还保持了良好的开放性可以和诸多技术方案集成,例如 Cilium,Submariner,Prometheus,KubeVirt 等等。

image.png

网络拓扑

ovn-kubernetes 是一个将 OVN 网络方案引入到 k8s 体系兼容 CNI 标准的一套代码,将 pod、service 网络都用 OVN 网络来实现。

ovn-kubernetes 组件是以 k8s 对象的形式在 k8s 集群内部署, 网络架构如下

image.png

以上 logical router / logical switch 都是逻辑/虚拟的,每添加一台新的 node,ovn-kubernetes 会新建一台用 nodename 命名的 logical switch 连接到全局唯一的 OvnClusterRouter 上,每当有新的 pod 调度到 node,pod 就连接到对应 node 的 logical switch 上,用来形成 pod 网络(overlay / 东西向流量)。

添加新 node 的同时,ovn-kubernetes 还会建立一个绑定到每一台 node 的 gateway router,连接到 join(join 是与 OvnClusterRouter 相连的 logical switch),用来连接 pod 网络(overlay)和 node 网络(underlay)(南北向流量)

组件介绍

以kubesphere部署的kubernetes为例(使用ovn网络组件),其中ovn相关服务如下

root@node1:~# kubectl -n=kube-system get deployment
NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
kube-ovn-controller           1/1     1            1           3h43m
kube-ovn-monitor              1/1     1            1           3h43m
ovn-central                   1/1     1            1           3h43m
root@node1:~# kubectl -n=kube-system get daemonset
NAME              DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   
kube-ovn-cni      1         1         1       1            1           
kube-ovn-pinger   1         1         1       1            1          
ovs-ovn           1         1         1       1            1           

核心组件

ovn-central

ovn-central Deployment 运行 OVN 的管理平面组件,包括 ovn-nb, ovn-sb, 和 ovn-northd

  • ovn-nb: 保存虚拟网络配置,并提供 API 进行虚拟网络管理。kube-ovn-controller 将会主要和 ovn-nb 进行交互配置虚拟网络。

  • ovn-sb: 保存从 ovn-nb 的逻辑网络生成的逻辑流表,以及各个节点的实际物理网络状态。

  • ovn-northd:将 ovn-nb 的虚拟网络翻译成 ovn-sb 中的逻辑流表。

多个 ovn-central 实例会通过 Raft 协议同步数据保证高可用。

ovs-ovn

ovs-ovn 以 DaemonSet 形式运行在每个节点,在 Pod 内运行了 openvswitch, ovsdb, 和 ovn-controller。这些组件作为 ovn-central 的 Agent 将逻辑流表翻译成真实的网络配置。

kube-ovn-controller

该组件为一个 Deployment 执行所有 Kubernetes 内资源到 OVN 资源的翻译工作,其作用相当于整个 Kube-OVN 系统的控制平面。 kube-ovn-controller 监听了所有和网络功能相关资源的事件,并根据资源变化情况更新 OVN 内的逻辑网络。主要监听的资源包括: Pod,Service,Endpoint,Node,NetworkPolicy,VPC,Subnet,Vlan,ProviderNetwork。

以 Pod 事件为例, kube-ovn-controller 监听到 Pod 创建事件后,通过内置的内存 IPAM 功能分配地址,并调用 ovn-central 创建 逻辑端口,静态路由和可能的 ACL 规则。接下来 kube-ovn-controller 将分配到的地址,和子网信息例如 CIDR,网关,路由等信息写会到 Pod 的 annotation 中。该 annotation 后续会被 kube-ovn-cni 读取用来配置本地网络。

kube-ovn-cni

该组件为一个 DaemonSet 运行在每个节点上,实现 CNI 接口,并操作本地的 OVS 配置单机网络。

该 DaemonSet 会复制 kube-ovn 二进制文件到每台机器,作为 kubeletkube-ovn-cni 之间的交互工具,将相应 CNI 请求 发送给 kube-ovn-cni 执行。该二进制文件默认会被复制到 /opt/cni/bin 目录下。

kube-ovn-cni 会配置具体的网络来执行相应流量操作,主要工作包括: 1. 配置 ovn-controllervswitchd。 2. 处理 CNI add/del 请求: 1. 创建删除 veth 并和 OVS 端口绑定。 2. 配置 OVS 端口信息。 3. 更新宿主机的 iptables/ipset/route 等规则。 3. 动态更新容器 QoS. 4. 创建并配置 ovn0 网卡联通容器网络和主机网络。 5. 配置主机网卡来实现 Vlan/Underlay/EIP 等功能。 6. 动态配置集群互联网关。

监控与扩展组件

该部分组件主要提供监控,诊断,运维操作以及和外部进行对接,对 Kube-OVN 的核心网络能力进行扩展,并简化日常运维操作。

kube-ovn-speaker

该组件为一个 DaemonSet 运行在特定标签的节点上,对外发布容器网络的路由,使得外部可以直接通过 Pod IP 访问容器。

更多相关使用方式请参考 BGP 支持

kube-ovn-pinger

该组件为一个 DaemonSet 运行在每个节点上收集 OVS 运行信息,节点网络质量,网络延迟等信息,收集的监控指标可参考 Kube-OVN 监控指标

kube-ovn-monitor

该组件为一个 Deployment 收集 OVN 的运行信息,收集的监控指标可参考 Kube-OVN 监控指标

kubectl-ko

该组件为 kubectl 插件,可以快速运行常见运维操作,更多使用请参考 kubectl 插件使用

内核中,每个宿主机一个

|

源码分析

东西向流量连通

主要分析容器创建时ovn为该容器设置标签,设置网卡信息并配置网卡接入主机的ovn网络的流程。

在该节点下接入ovs网络的容器 东西向网络即可联通。

容器用例

下面是在ovn作为网络插件时的业务pod。该集群使用开源项目kubesphere 部署,网络插件选择ovn。

ovn为该容器中被设置了如ovn.kubernetes.io/xxx 的注解,标识容器接入ovn网络的配置。

ovn最终根据注解中网卡的分配信息,为该容器具体绑定网卡并接入ovn网络。

apiVersion: v1
kind: Pod
metadata:
  annotations:
    ovn.kubernetes.io/allocated: "true"
    ovn.kubernetes.io/cidr: 10.233.64.0/18
    ovn.kubernetes.io/gateway: 10.233.64.1
    ovn.kubernetes.io/ip_address: 10.233.64.4
    ovn.kubernetes.io/logical_router: ovn-cluster
    ovn.kubernetes.io/logical_switch: ovn-default
    ovn.kubernetes.io/mac_address: "00:00:00:38:41:33"
    ovn.kubernetes.io/pod_nic_type: veth-pair
    ovn.kubernetes.io/routed: "true"
  creationTimestamp: "2024-12-04T02:49:03Z"
  generateName: ks-installer-5594ffc86d-
  labels:
    app: ks-installer
    pod-template-hash: 5594ffc86d
  name: ks-installer-5594ffc86d-tg7d8
  namespace: kubesphere-system
spec:
  containers:
  - image: registry.cn-beijing.aliyuncs.com/kubesphereio/ks-installer:v3.4.1
    imagePullPolicy: IfNotPresent
    name: installer
    ...

容器网卡设置

当容器创建时,kube-ovn-controller将根据pod信息查询与该pod关联的subnet配置,使用该配置参考 动态生成ovn网卡配置信息,该配置将设置在pod的注解中。

下面是用例容器与空间以及subnet配置的关联关系

root@node1:~# kubectl get ns kubesphere-system -o yaml
apiVersion: v1
kind: Namespace
metadata:
  annotations:
    ovn.kubernetes.io/cidr: 10.233.64.0/18
    ovn.kubernetes.io/exclude_ips: 10.233.64.1
    ovn.kubernetes.io/logical_switch: ovn-default
spec:
  finalizers:
  - kubernetes
status:
  phase: Active

空间注解指向的subnet

root@node1:~# kubectl get subnets.kubeovn.io ovn-default
NAME          PROVIDER   VPC           PROTOCOL   CIDR             PRIVATE   NAT    DEFAULT   GATEWAYTYPE   V4USED   V4AVAILABLE   V6USED   V6AVAILABLE   EXCLUDEIPS
ovn-default   ovn        ovn-cluster   IPv4       10.233.64.0/18             true   true      distributed   14       16367         0        0             ["10.233.64.1"]

Pod创建时将调用handleAddOrUpdatePod 回调事件函数

func (c *Controller) handleAddOrUpdatePod(key string) (err error) {
    // ...
    // 查询该容器关联的ovn 关联的subnet 配置, 用于参考为pod动态分配ovn网络
    // 先从pod上寻找 ovn.kubernetes.io/logical_switch注解对应值
    // 再尝试从空间下寻找 ovn.kubernetes.io/logical_switch 注解
    // 此外还会使用 v1.multus-cni.io/default-network 注解 查询pod额外关联的多
    // 网卡配置,此处不重点分析
    podNets, err := c.getPodKubeovnNets(pod)
    if err != nil {
        klog.Errorf("failed to get pod nets %v", err)
        return err
    }
    // ...
    needAllocatePodNets := needAllocateSubnets(pod, podNets)
    if len(needAllocatePodNets) != 0 {
        // 使用获取到的kube-ovn subnet 配置信息,为该pod生成ovn网卡配置信息
        if cachedPod, err = c.reconcileAllocateSubnets(cachedPod, pod, needAllocatePodNets); err != nil {
            klog.Error(err)
            return err
        }
       //...
    }
    // ...

}

// 根据subnet具体为pod分配网卡配置信息
func (c *Controller) reconcileAllocateSubnets(cachedPod, pod *v1.Pod, needAllocatePodNets []*kubeovnNet) (*v1.Pod, error) {
    namespace := pod.Namespace
    name := pod.Name
    //...
    // Avoid create lsp for already running pod in ovn-nb when controller restart
    for _, podNet := range needAllocatePodNets {
        // 根据subnet中的网段,分配地址
        v4IP, v6IP, mac, subnet, err := c.acquireAddress(pod, podNet)
        if err != nil {
            c.recorder.Eventf(pod, v1.EventTypeWarning, "AcquireAddressFailed", err.Error())
            klog.Error(err)
            return nil, err
        }
        ipStr := util.GetStringIP(v4IP, v6IP)
        // 将生成的动态生成的地址,以及关联的sunbet中的基本信息,通过注解的方式设置到pod中。
        pod.Annotations[fmt.Sprintf(util.IPAddressAnnotationTemplate, podNet.ProviderName)] = ipStr
        if mac == "" {
            delete(pod.Annotations, fmt.Sprintf(util.MacAddressAnnotationTemplate, podNet.ProviderName))
        } else {
            pod.Annotations[fmt.Sprintf(util.MacAddressAnnotationTemplate, podNet.ProviderName)] = mac
        }
        pod.Annotations[fmt.Sprintf(util.CidrAnnotationTemplate, podNet.ProviderName)] = subnet.Spec.CIDRBlock
        pod.Annotations[fmt.Sprintf(util.GatewayAnnotationTemplate, podNet.ProviderName)] = subnet.Spec.Gateway
        if isOvnSubnet(podNet.Subnet) {
            pod.Annotations[fmt.Sprintf(util.LogicalSwitchAnnotationTemplate, podNet.ProviderName)] = subnet.Name
            if pod.Annotations[fmt.Sprintf(util.PodNicAnnotationTemplate, podNet.ProviderName)] == "" {
                pod.Annotations[fmt.Sprintf(util.PodNicAnnotationTemplate, podNet.ProviderName)] = c.config.PodNicType
            }
        } else {
            delete(pod.Annotations, fmt.Sprintf(util.LogicalSwitchAnnotationTemplate, podNet.ProviderName))
            delete(pod.Annotations, fmt.Sprintf(util.PodNicAnnotationTemplate, podNet.ProviderName))
        }

        //...
}

设置的部分关键注解对应标签

PodNicAnnotationTemplate          = "%s.kubernetes.io/pod_nic_type"
RoutesAnnotationTemplate        = "%s.kubernetes.io/routes"
MacAddressAnnotationTemplate    = "%s.kubernetes.io/mac_address"
IPAddressAnnotationTemplate     = "%s.kubernetes.io/ip_address"
CidrAnnotationTemplate          = "%s.kubernetes.io/cidr"
GatewayAnnotationTemplate       = "%s.kubernetes.io/gateway"
IPPoolAnnotationTemplate        = "%s.kubernetes.io/ip_pool"
LogicalSwitchAnnotationTemplate = "%s.kubernetes.io/logical_switch"
LogicalRouterAnnotationTemplate = "%s.kubernetes.io/logical_router"
VlanIDAnnotationTemplate        = "%s.kubernetes.io/vlan_id"

容器网卡接入

在创建pod创建时kubelet通过读取/etc/cni/net.d下 各个cni的配置信息。直接调用cni插件对应的二进制程序。发起各cni组件的网络创建的请求。

image.png

Kube-ovn-cni 作为daemonset 运行在各个节点上,当服务启动时,将kube-ovn 的cni 配置文件拷贝至/etc/cni/net.d/目录下。

func CmdMain() {
// 拷贝配置文件至 /etc/cni/net.d/01-kube-ovn.conflist(默认配置)
if err := mvCNIConf(config.CniConfDir, config.CniConfFile, config.CniConfName); err != nil {
        util.LogFatalAndExit(err, "failed to mv cni config file")
    }
    //...
}

cni配置内容

//etc/cni/net.d/01-kube-ovn.conflist
{
    "name":"kube-ovn",
    "cniVersion":"0.3.1",
    "plugins":[
        {
            "type":"kube-ovn",
            "server_socket":"/run/openvswitch/kube-ovn-daemon.sock"
        },
        {
            "type":"portmap",
            "capabilities":{
                "portMappings":true
            }
        }
    ]
}

对应cni执行文件

root@node1:~# ls /opt/cni/bin/kube-ovn  -la
-rwxr-xr-x 1 root root 56786944 12月  4 11:33 /opt/cni/bin/kube-ovn
root@node1:~# 

kubelet创建容器,在创建网络时将执行bin文件并调用下面的函数

func cmdAdd(args *skel.CmdArgs) error {
    // 加载配置 /etc/cni/net.d/01-kube-ovn.conflist
    netConf, cniVersion, err := loadNetConf(args.StdinData)
    if err != nil {
        return err
    }

    // 向 cni 服务发起为pod创建ovn网络的请求
    client := request.NewCniServerClient(netConf.ServerSocket)
    response, err := client.Add(request.CniRequest{
        CniType:                    netConf.Type,
        PodName:                    podName,
        PodNamespace:               podNamespace,
        ContainerID:                args.ContainerID,
        NetNs:                      args.Netns,
        IfName:                     args.IfName,
        Provider:                   netConf.Provider,
        Routes:                     netConf.Routes,
        DNS:                        netConf.DNS,
        DeviceID:                   netConf.DeviceID,
        VfDriver:                   netConf.VfDriver,
        VhostUserSocketVolumeName:  netConf.VhostUserSocketVolumeName,
        VhostUserSocketName:        netConf.VhostUserSocketName,
        VhostUserSocketConsumption: netConf.VhostUserSocketConsumption,
    })
    //...
}

最终请求会调用至cni常驻服务接口

pkg/daemon/server.go

在daemon中可见注册了容器创建时调用的CNI接口

func createHandler(csh *cniServerHandler) http.Handler {
    //...
    // 容器创建时调用
    ws.Route(
        ws.POST("/add").
            To(csh.handleAdd).
            Reads(request.CniRequest{}))
    //...

    return wsContainer
}

// 添加接口的具体逻辑
func (csh cniServerHandler) handleAdd(req *restful.Request, resp *restful.Response) {
    podRequest := request.CniRequest{}
    if err := req.ReadEntity(&podRequest); err != nil {
       //...
    }
    klog.V(5).Infof("request body is %v", podRequest)
    podSubnet, exist := csh.providerExists(podRequest.Provider)
    if !exist {
       //...
    }

    //... 
    for i := 0; i < 20; i++ {
        // ifName是容器内部tap的接口名,可见默认为eth0
        if ifName = podRequest.IfName; ifName == "" {
            ifName = "eth0"
        }

        switch {
        case podRequest.DeviceID != "":
            nicType = util.OffloadType
        case podRequest.VhostUserSocketVolumeName != "":
            nicType = util.DpdkType
            if err = createShortSharedDir(pod, podRequest.VhostUserSocketVolumeName, podRequest.VhostUserSocketConsumption, csh.Config.KubeletDir); err != nil {
                klog.Error(err.Error())
                if err = resp.WriteHeaderAndEntity(http.StatusInternalServerError, request.CniResponse{Err: err.Error()}); err != nil {
                    klog.Errorf("failed to write response: %v", err)
                }
                return
            }
        default:
 // nictype 是来自本用例pod注解的 ovn.kubernetes.io/pod_nic_type: veth-pair
 nicType = pod.Annotations[fmt.Sprintf(util.PodNicAnnotationTemplate, podRequest.Provider)]
        }

        break
    }
    // ...

    var mtu int
    routes = append(podRequest.Routes, routes...)
    if strings.HasSuffix(podRequest.Provider, util.OvnProvider) && subnet != "" {
        //...
        podNicName = ifName
        switch nicType {
        case util.InternalType:
            podNicName, routes, err = csh.configureNicWithInternalPort(podRequest.PodName, podRequest.PodNamespace, podRequest.Provider, podRequest.NetNs, podRequest.ContainerID, ifName, macAddr, mtu, ipAddr, gw, isDefaultRoute, detectIPConflict, routes, podRequest.DNS.Nameservers, podRequest.DNS.Search, ingress, egress, podRequest.DeviceID, nicType, latency, limit, loss, jitter, gatewayCheckMode, u2oInterconnectionIP)
        case util.DpdkType:
            err = csh.configureDpdkNic(podRequest.PodName, podRequest.PodNamespace, podRequest.Provider, podRequest.NetNs, podRequest.ContainerID, ifName, macAddr, mtu, ipAddr, gw, ingress, egress, getShortSharedDir(pod.UID, podRequest.VhostUserSocketVolumeName), podRequest.VhostUserSocketName, podRequest.VhostUserSocketConsumption)
            routes = nil
        default:
            // 使用获取到的 veth-pair 模式
            routes, err = csh.configureNic(podRequest.PodName, podRequest.PodNamespace, podRequest.Provider, podRequest.NetNs, podRequest.ContainerID, podRequest.VfDriver, ifName, macAddr, mtu, ipAddr, gw, isDefaultRoute, detectIPConflict, routes, podRequest.DNS.Nameservers, podRequest.DNS.Search, ingress, egress, podRequest.DeviceID, nicType, latency, limit, loss, jitter, gatewayCheckMode, u2oInterconnectionIP, oldPodName)
        }

    } 
    //...
}

pkg/daemon/ovs_linux.go

func (csh cniServerHandler) configureNic(podName, podNamespace, provider, netns, containerID, vfDriver, ifName, mac string, mtu int, ip, gateway string, isDefaultRoute, detectIPConflict bool, routes []request.Route, _, _ []string, ingress, egress, deviceID, nicType, latency, limit, loss, jitter string, gwCheckMode int, u2oInterconnectionIP, oldPodName string) ([]request.Route, error) {
    var err error
    var hostNicName, containerNicName string
    if deviceID == "" {
        // 使用linux接口创建tap网卡对
        hostNicName, containerNicName, err = setupVethPair(containerID, ifName, mtu)
        ///...

    } else {
      ...
    }

    ipStr := util.GetIPWithoutMask(ip)
    ifaceID := ovs.PodNameToPortName(podName, podNamespace, provider)
    ovs.CleanDuplicatePort(ifaceID, hostNicName)
    // Add veth pair host end to ovs port
    // hostNicName 的网卡绑定ovs的端口, 而 containerNicName 为容器使用
    output, err := ovs.Exec(ovs.MayExist, "add-port", "br-int", hostNicName, "--",
        "set", "interface", hostNicName, fmt.Sprintf("external_ids:iface-id=%s", ifaceID),
        fmt.Sprintf("external_ids:vendor=%s", util.CniTypeName),
        fmt.Sprintf("external_ids:pod_name=%s", podName),
        fmt.Sprintf("external_ids:pod_namespace=%s", podNamespace),
        fmt.Sprintf("external_ids:ip=%s", ipStr),
        fmt.Sprintf("external_ids:pod_netns=%s", netns))
    if err != nil {
        return nil, fmt.Errorf("add nic to ovs failed %v: %q", err, output)
    }
    // ...
    // 获取容器对应的空间
    podNS, err := ns.GetNS(netns)
    if err != nil {
        err = fmt.Errorf("failed to open netns %q: %v", netns, err)
        klog.Error(err)
        return nil, err
    }
    // 打开容器空间,设置 containerNicName 的网卡到该空间使用, 默认将别名网卡名为ifname
    finalRoutes, err := configureContainerNic(containerNicName, ifName, ip, gateway, isDefaultRoute, detectIPConflict, routes, macAddr, podNS, mtu, nicType, gwCheckMode, u2oInterconnectionIP)
    if err != nil {
        klog.Error(err)
        return nil, err
    }
    return finalRoutes, nil
}

按照该流程,调度到节点上的容器都将创建并关联该节点的ovs网络端口,从而节点内的容器可以相互通信。

下面分析节点间网络的连接配置,从而完成节点间容器的通信。

南北向流量连通

节点用例

下面是在ovn作为网络插件时的集群节点。该集群使用开源项目kubesphere 部署,网络插件选择ovn。

root@node1:~# kubectl get nodes node1 -o yaml
apiVersion: v1
kind: Node
metadata:
  annotations:
    ovn.kubernetes.io/chassis: ebdfd1ea-142c-4dad-8e4c-99ba4269a65b
    ovn.kubernetes.io/cidr: 100.64.0.0/16
    ovn.kubernetes.io/gateway: 100.64.0.1
    ovn.kubernetes.io/ip_address: 100.64.0.2
    ovn.kubernetes.io/logical_switch: join
    ovn.kubernetes.io/mac_address: 00:00:00:FE:82:93
    ovn.kubernetes.io/port_name: node-node1
    volumes.kubernetes.io/controller-managed-attach-detach: "true"
  labels:
    beta.kubernetes.io/arch: amd64
    beta.kubernetes.io/os: linux
    kube-ovn/role: master
    kubernetes.io/arch: amd64
    kubernetes.io/hostname: node1
    kubernetes.io/os: linux
    node-role.kubernetes.io/control-plane: "true"
    node-role.kubernetes.io/master: "true"
    node-role.kubernetes.io/worker: ""
  name: node1
spec:
  podCIDR: 10.233.64.0/24
  podCIDRs:
  - 10.233.64.0/24
status:
  addresses:
  - address: 192.168.41.131
    type: InternalIP
  - address: node1
    type: Hostname

subnet资源

root@node1:~# kubectl get subnet
NAME          PROVIDER   VPC           PROTOCOL   CIDR             PRIVATE   NAT    DEFAULT   GATEWAYTYPE   V4USED   V4AVAILABLE   V6USED   V6AVAILABLE   EXCLUDEIPS
join          ovn        ovn-cluster   IPv4       100.64.0.0/16                               distributed   1        65532         0        0             ["100.64.0.1"]
ovn-default   ovn        ovn-cluster   IPv4       10.233.64.0/18             true   true      distributed   14       16367         0        0             ["10.233.64.1"]

vpc资源

root@node1:~# kubectl get vpc
NAME          STANDBY   SUBNETS                  NAMESPACES
ovn-cluster   true      ["ovn-default","join"]   

节点添加处理

pkg/controller/node.go

节点添加事件将进入下列函数进行处理

func (c *Controller) handleAddNode(key string) error {
    //...

    // 从节点的node.Status.Addresses 中查找internalIP, 即主机地址
    nodeIPv4, nodeIPv6 := util.GetNodeInternalIP(*node)
    //...

    if err = c.handleNodeAnnotationsForProviderNetworks(node); err != nil {
        klog.Errorf("failed to handle annotations of node %s for provider networks: %v", node.Name, err)
        return err
    }

    // 获取节点使用的subnet,kubesphere 安装的ovn 集群中,该subnet 为 join(100.64.0.0/16)
    subnet, err := c.subnetsLister.Get(c.config.NodeSwitch)
    if err != nil {
        klog.Errorf("failed to get node subnet: %v", err)
        return err
    }

    var v4IP, v6IP, mac string
    // 拼接出节点的portname, 规则为 node-节点名
    portName := util.NodeLspName(key)
    if node.Annotations[util.AllocatedAnnotation] == "true" && node.Annotations[util.IPAddressAnnotation] != "" && node.Annotations[util.MacAddressAnnotation] != "" {
        macStr := node.Annotations[util.MacAddressAnnotation]
        //为节点申请静态地址
        v4IP, v6IP, mac, err = c.ipam.GetStaticAddress(portName, portName, node.Annotations[util.IPAddressAnnotation],
            &macStr, node.Annotations[util.LogicalSwitchAnnotation], true)
        if err != nil {
            klog.Errorf("failed to alloc static ip addrs for node %v: %v", node.Name, err)
            return err
        }
    } else {
        // 为节点分配动态地址
        v4IP, v6IP, mac, err = c.ipam.GetRandomAddress(portName, portName, nil, c.config.NodeSwitch, "", nil, true)
        if err != nil {
            klog.Errorf("failed to alloc random ip addrs for node %v: %v", node.Name, err)
            return err
        }
    }
    // 将ipv4 ipv6 的所有地址使用逗号分割连接成字符串
    ipStr := util.GetStringIP(v4IP, v6IP)
    // 在连接节点的逻辑交换机上为该节点创建 端口, 端口名为 node-节点名
    if err := c.OVNNbClient.CreateBareLogicalSwitchPort(c.config.NodeSwitch, portName, ipStr, mac); err != nil {
        klog.Errorf("failed to create logical switch port %s: %v", portName, err)
        return err
    }

    for _, ip := range strings.Split(ipStr, ",") {
        if ip == "" {
            continue
        }

        nodeIP, af := nodeIPv4, 4
        protocol := util.CheckProtocol(ip)
        if protocol == kubeovnv1.ProtocolIPv6 {
            nodeIP, af = nodeIPv6, 6
        }
        // 将目标为internalIP 的包,即主机地址的包
        // 下一跳转向该节点的ovn网卡地址
        if nodeIP != "" {
            var (
                match       = fmt.Sprintf("ip%d.dst == %s", af, nodeIP)
                action      = kubeovnv1.PolicyRouteActionReroute
                externalIDs = map[string]string{
                    "vendor":         util.CniTypeName,
                    "node":           node.Name,
                    "address-family": strconv.Itoa(af),
                }
            )
            klog.Infof("add policy route for router: %s, match %s, action %s, nexthop %s, externalID %v", c.config.ClusterRouter, match, action, ip, externalIDs)
            // match internalIP 的包 路由的 nexthop 为 为节点申请的ip地址
            if err = c.addPolicyRouteToVpc(
                c.config.ClusterRouter,
                &kubeovnv1.PolicyRoute{
                    Priority:  util.NodeRouterPolicyPriority,
                    Match:     match,
                    Action:    action,
                    NextHopIP: ip,
                },
                externalIDs,
            ); err != nil {
                klog.Errorf("failed to add logical router policy for node %s: %v", node.Name, err)
                return err
            }

            // ...
        }
    }

    // 路由器配置 端口 ovn-default to join 的路由
    if err := c.addNodeGatewayStaticRoute(); err != nil {
        klog.Errorf("failed to add static route for node gw: %v", err)
        return err
    }

    // 为节点更新 将配置信息同步更新到节注解上
    annotations := map[string]any{
        util.IPAddressAnnotation:     ipStr,
        util.MacAddressAnnotation:    mac,
        util.CidrAnnotation:          subnet.Spec.CIDRBlock,
        util.GatewayAnnotation:       subnet.Spec.Gateway,
        util.LogicalSwitchAnnotation: c.config.NodeSwitch,
        util.AllocatedAnnotation:     "true",
        util.PortNameAnnotation:      portName,
    }
    if err = util.UpdateNodeAnnotations(c.config.KubeClient.CoreV1().Nodes(), node.Name, annotations); err != nil {
        klog.Errorf("failed to update annotations of node %s: %v", node.Name, err)
        return err
    }
    //...
}

配置结果

进入ovn-ovs 组件对应容器,可使用命令 ovn-nbctl show 查询到ovn的整体配置拓扑信息

  1. 东西向流量: router(ovn-cluster) 连接了swtich(ovn-cluster), 该switch 可能有多个,每个switch对应一个节点,该交换机为所属节点上的每个pod提供一个网络端口,供pod连接入ovn网络。

  2. 南北向流量: router(ovn-cluster) 连接了swtich(json), 该switch将为每个加入ovn集群的节点提供一个端口。

  3. 东西 南北 向流量连通:router承载跨网段访问的连接,将两个方向的流量进行连接。

# ovn-nbctl show
switch d8549759-d2f4-4d10-82db-40bc6033826e (join)
    port join-ovn-cluster
        type: router
        router-port: ovn-cluster-join
    port node-node1
        addresses: ["00:00:00:FE:82:93 100.64.0.2"]
switch 6cbc5f72-9ac1-4473-a15d-5de7348ba768 (ovn-default)
    port kube-ovn-pinger-dghxg.kube-system
        addresses: ["00:00:00:F4:E2:64 10.233.64.5"]
    port ovn-default-ovn-cluster
        type: router
        router-port: ovn-cluster-ovn-default
    port default-http-backend-5bf68ff9b8-8d8r8.kubesphere-controls-system
        addresses: ["00:00:00:F1:EC:D4 10.233.64.9"]
    port prometheus-operator-8955bbd98-g4fv4.kubesphere-monitoring-system
        addresses: ["00:00:00:CD:D0:EE 10.233.64.11"]
    port alertmanager-main-0.kubesphere-monitoring-system
        addresses: ["00:00:00:78:F3:21 10.233.64.15"]
    port ks-apiserver-7fd66f7885-cqr55.kubesphere-system
        addresses: ["00:00:00:DD:03:C6 10.233.64.10"]
router a2313806-4ad7-4165-b1d9-38b608911fb7 (ovn-cluster)
    port ovn-cluster-ovn-default
        mac: "00:00:00:81:42:01"
        networks: ["10.233.64.1/18"]
    port ovn-cluster-join
        mac: "00:00:00:4A:F8:D5"
        networks: ["100.64.0.1/16"]

Router ovn-cluster的路由信息

# ovn-nbctl lr-route-list ovn-cluster
IPv4 Routes
Route Table <main>:
                0.0.0.0/0                100.64.0.1 dst-ip

节点网关连接交换机

进入节点添加时调用的函数 CreateBareLogicalSwitchPort

// CreateBareLogicalSwitchPort create logical switch port with basic configuration
func (c *OVNNbClient) CreateBareLogicalSwitchPort(lsName, lspName, ip, mac string) error {
    // lsname 即交换机名称默认为 josin
    // lspName 为node-节点名
    //...
    /* create logical switch port */
    // 为节点xxx 在join 交换机上创建了 名为 node-xxx 的端口,该端口需最终与节点实体连同则需要
    // 关联节点上的端口
    lsp := &ovnnb.LogicalSwitchPort{
        UUID:      ovsclient.NamedUUID(),
        Name:      lspName,
        Addresses: []string{strings.TrimSpace(strings.Join(addresses, " "))}, // addresses is the first element of addresses
    }

    ops, err := c.CreateLogicalSwitchPortOp(lsp, lsName)
    if err != nil {
        klog.Error(err)
        return err
    }

    if err = c.Transact("lsp-add", ops); err != nil {
        klog.Error(err)
        return fmt.Errorf("create logical switch port %s: %w", lspName, err)
    }

    return nil
}

通过节点上创建的ovn0 端口作为与 全局节点逻辑交换机join的连接端口, 关注网关初始函数

// InitNodeGateway init ovn0
func InitNodeGateway(config *Configuration) error {
    var portName, ip, cidr, macAddr, gw, ipAddr string
    for {
        nodeName := config.NodeName
        node, err := config.KubeClient.CoreV1().Nodes().Get(context.Background(), nodeName, metav1.GetOptions{})
        // ...
        macAddr = node.Annotations[util.MacAddressAnnotation]
        ip = node.Annotations[util.IPAddressAnnotation]
        cidr = node.Annotations[util.CidrAnnotation]
        portName = node.Annotations[util.PortNameAnnotation]
        gw = node.Annotations[util.GatewayAnnotation]
        break
    }
    mac, err := net.ParseMAC(macAddr)
    if err != nil {
        return fmt.Errorf("failed to parse mac %s %w", mac, err)
    }

    ipAddr, err = util.GetIPAddrWithMask(ip, cidr)
    if err != nil {
        klog.Errorf("failed to get ip %s with mask %s, %v", ip, cidr, err)
        return err
    }
    // portName  即为在join交换机上为该节点分配的端口, 该函数负责在节点创建端口ovn0并连接上
    // 交换机上为该节点分配的 node-xxx 端口
    // br-int(on node)--->ovn0--->node-xxx--->switch(join)
    return configureNodeNic(portName, ipAddr, gw, cidr, mac, config.MTU)
}
func configureNodeNic(portName, ip, gw, joinCIDR string, macAddr net.HardwareAddr, mtu int) error {
    ipStr := util.GetIPWithoutMask(ip)
    // util.NodeNic 为节点上的ovn0
    // external_ids:iface-id=portName 中的portName 即为 join交换机上为该节点开的端口node-xxx 
    raw, err := ovs.Exec(ovs.MayExist, "add-port", "br-int", util.NodeNic, "--",
        "set", "interface", util.NodeNic, "type=internal", "--",
        "set", "interface", util.NodeNic, fmt.Sprintf("external_ids:iface-id=%s", portName),
        fmt.Sprintf("external_ids:ip=%s", ipStr))
    if err != nil {
        klog.Errorf("failed to configure node nic %s: %v, %q", portName, err, raw)
        return errors.New(raw)
    }

    if err = configureNic(util.NodeNic, ip, macAddr, mtu, false, false, true); err != nil {
        klog.Error(err)
        return err
    }

    //...
}

参考文章

https://juejin.cn/post/7164211658766680100

https://www.kancloud.cn/digest/openvswitch/117251

https://kubesphere.io/zh/blogs/use-kubekey-to-install-and-deploy-kubernetes-and-kubeovn/

https://addozhang.medium.com/source-code-analysis-understanding-cnis-usage-from-kubelet-container-runtime-24d72f29466b

https://atbug.com/how-kubelete-container-runtime-work-with-cni/

相关文章

网友评论

    本文标题:kube-ovn源码分析

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