乐趣区

关于后端:Kubernetes-网络模型

概述

集群网络系统是 Kubernetes 的外围局部,然而想要精确理解它的工作原理可是个不小的挑战。上面列出的是网络系统的的四个次要问题:

  1. 高度耦合的容器间通信:这个曾经被 Pod 和 localhost 通信解决了。
  2. Pod 间通信:这是本文档讲述的重点。
  3. Pod 与 Service 间通信:涵盖在 Service 中。
  4. 内部与 Service 间通信:也涵盖在 Service 中。

Kubernetes 的主旨就是在利用之间共享机器。通常来说,共享机器须要两个利用之间不能应用雷同的端口,然而在多个利用开发者之间去大规模地协调端口是件很艰难的事件,尤其是还要让用户裸露在他们管制范畴之外的集群级别的问题上。

为每个利用都须要设置一个端口的参数,而 API 服务器还须要晓得如何将动静端口数值插入到配置模块中,服务也须要晓得如何找到对方等等。这无疑会为零碎带来很多复杂度,Kubernetes 抉择了减少形象层的形式去合成零碎的复杂度。

Kubernetes 网络模型

集群中每一个 Pod 都会取得一个举世无双的 IP 地址,这就意味着你不须要显式地在 Pod 之间创立链接,不须要解决容器端口到主机端口之间的映射。kubernetes 的网络模型里,Pod 能够被视作虚拟机或者物理主机。

Kubernetes 强制要求所有网络设施都满足以下根本要求(从而排除了无意隔离网络的策略):

  1. Pod 可能与所有其余节点上的 Pod 通信,且不须要网络地址转译(NAT)
  2. 节点上的守护过程 (kubelet) 能够和节点上的所有 Pod 通信

如果你的工作开始是在虚拟机中运行的,你的虚拟机有一个 IP,能够和我的项目中其余虚拟机通信。这里的模型是基本相同的。这与 kubernetes 的网络模型基本相同,它能够帮忙你实现从虚拟机向容器平滑迁徙。

Kubernetes 的 IP 地址存在于 Pod 范畴内,容器共享它们的网络命名空间,包含它们的 IP 地址和 MAC 地址。这就意味着 Pod 内的容器都能够通过 localhost 达到对方端口。这和虚拟机中的过程仿佛没有什么不同,这也被称为“一个 Pod 一个 IP”模型。

实现以上需要,Kubernetes 网络还须要解决四方面的问题:

  1. 一个 Pod 中的容器之间通过本地回路(loopback)通信。
  2. 集群网络在不同 Pod 之间提供通信。
  3. 向外裸露 Pod 中运行的利用,以反对来自于集群内部的拜访。
  4. 提供专门用于裸露 HTTP 应用程序、网站和 API 的额定性能

第一个问题,同一个 Pod 的容器共享同一个网络命名空间,它们之间的拜访能够用 localhost 地址 + 容器端口就能够拜访。

第二个问题,提供一种容器网络机制,将 Pod 的 IP 和所在的 Node 的 IP 关联起来,通过这个关联让 Pod 能够相互拜访。

第三个问题,kubernetes 提供了 Service 资源容许你 向外裸露 Pod 中运行的利用,以反对来自于集群内部的拜访,Service 是一组 Pod 的服务形象,相当于一组 Pod 的 LB,负责将申请分发给对应的 Pod,Service 会为这个 LB 提供一个 IP,个别称为 ClusterIP。

第四个问题,kubernetes 提供了 Ingress 资源于裸露 HTTP 应用程序、网站和 API 等。

你也能够应用 Service 来公布仅供集群外部应用的服务。集群网络解释了如何为集群设置网络,还概述了所波及的技术。
在 Kubernetes 网络中存在两种 IP(Pod IP 和 Service Cluster IP),Pod IP 地址是理论存在于某个网卡 (能够是虚构设施) 上的,Service Cluster IP 它是一个虚构 IP,是由 kube-proxy 应用 Iptables 规定从新定向到其本地端口,再平衡到后端 Pod 的。上面讲讲 Kubernetes Pod 网络设计模型:

  1. 根本准则:每个 Pod 都领有一个独立的 IP 地址(IP per Pod),而且假设所有的 pod 都在一个能够间接连通的、扁平的网络空间中。
  2. 设计起因:用户不须要额定思考如何建设 Pod 之间的连贯,也不须要思考将容器端口映射到主机端口等问题。
  3. 网络要求:所有的容器都能够在不必 NAT 的形式下同别的容器通信;所有节点都可在不必 NAT 的形式下同所有容器通信;容器的地址和他人看到的地址是同一个地址。

Kubernetes Service

service 对于 Cluster IP 的定义有点相似 linux 服务器上罕用的 VIP。kubernetes 在每个节点上都会启动一个名为 Kube-proxy 的守护过程,它会保护一张 Cluster IP 与 Pod IP 映射表,用户申请到 Cluster IP 后,这条申请依据 Cluster IP 与 Pod IP 映射表,被转发到对应的 Pod 中。

除了通过 IP 拜访 Service,kubernetes 还提供了一种机制容许用户能够通过 service name 与 Pod 进行通信。kubernetes 个别会部署 一个 名为 coredns 的 DNS 服务,它用来为 service 调配子域名,在集群中能够通过名称拜访 service;通常 coredns 会为 service 赋予一个名为“service-name.namespace.svc.cluster.local”的 A 记录,用来解析 service 的 Cluster IP。

Kubernetes 网络插件

如何实现 Pod IP,如何实现 Pod IP 之间的通信,Kubernetes 并没有给出具体的实现计划。然而社会依据下面提过的设计思路,提供了一套名为容器网络接口 (CNI)的,Kubernetes 网络插件协定。

CNI(Container Network Interface)是 CNCF 旗下的一个我的项目,由一组用于配置 Linux 容器的网络接口的标准和库组成,同时还蕴含了一些插件。CNI 仅关怀容器创立时的网络调配,和当容器被删除时开释网络资源。通过此链接浏览该我的项目:https://github.com/containern…。

CNI 工作原理

kubernetes 管制立体实现了 deployment、ststateful、pod 集群负载的调度后,kubelet 开始 pod 的创立工作,当创立 pod 时 kubelet 会通过预约义的 cni 回调调用网络插件(比方 flannel、calico、ovs)实现网络管理(例如:创立、回收 IP 等工作)。在早起的 kubernetes 版本 (应用运行时 docker 及 docker-shim) 中 kubelet 通过命令行参数 cni-bin-dir 和 cni-conf-dir 获知 cni 插件的二进制文件及配置文件的门路,启用 cni 插件。cni-bin-dir 默认门路为 /opt/cni/bin,是各个计算节点的放着这些插件的门路,cni-conf-dir 默认指向 /etc/cni/net.d,是 cni 配置文件寄存在的目录。

$ ls -l /etc/cni/net.d/
total 4
-rw-r--r-- 1 root root 311 Nov 18 17:13 10-flannel.conflist
$ cat /etc/cni/net.d/10-flannel.conflist
{
  "name":"cbr0",
  "cniVersion":"1.0.0",
  "plugins":[
    {
      "type":"flannel",
      "delegate":{
        "hairpinMode":true,
        "forceAddress":true,
        "isDefaultGateway":true
      }
    },
    {
      "type":"portmap",
      "capabilities":{"portMappings":true}
    }
  ]
}

改用 containerd 作为容器运行时后,搜寻和加载 cni 网络插件的工作由 containerd 接管。其工作流程如下。

这里 containerd 中所形容的 pod 与 kubernetes 中的 pod 是雷同概念,而 Sandbox Contaienr 也个别咱们也成为 Infrastructure Container,在 kubernetes 中,它还有一个大家很相熟的名字 pause 容器。

上面以 containerd 以及 cni 提供的 example 为例来阐明 cni 网络插件的工作机制。

package main

import (
    "context"
    "fmt"
    "log"

    gocni "github.com/containerd/go-cni"
)

func main() {
    id := "example"
    netns := "/var/run/netns/example-ns-1"

    // CNI allows multiple CNI configurations and the network interface
    // will be named by eth0, eth1, ..., ethN.
    ifPrefixName := "eth"
    defaultIfName := "eth0"

    // cni 初始化
    l, err := gocni.New(gocni.WithMinNetworkCount(2),
        gocni.WithPluginConfDir("/etc/cni/net.d"),
        gocni.WithPluginDir([]string{"/opt/cni/bin"}),
        gocni.WithInterfacePrefix(ifPrefixName))
    if err != nil {log.Fatalf("failed to initialize cni library: %v", err)
    }

    // 加载配置
    if err := l.Load(gocni.WithLoNetwork, gocni.WithDefaultConf); err != nil {log.Fatalf("failed to load cni configuration: %v", err)
    }

    labels := map[string]string{
        "K8S_POD_NAMESPACE":          "namespace1",
        "K8S_POD_NAME":               "pod1",
        "K8S_POD_INFRA_CONTAINER_ID": id,
        "IgnoreUnknown": "1",
    }

    ctx := context.Background()

    // 敞开网络
    defer func() {if err := l.Remove(ctx, id, netns, gocni.WithLabels(labels)); err != nil {log.Fatalf("failed to teardown network: %v", err)
        }
    }()

    // 设置网络
    result, err := l.Setup(ctx, id, netns, gocni.WithLabels(labels))
    if err != nil {log.Fatalf("failed to setup network for namespace: %v", err)
    }

    IP := result.Interfaces[defaultIfName].IPConfigs[0].IP.String()
    fmt.Printf("IP of the default interface %s:%s", defaultIfName, IP)
}

首先看一下 containerd 启动是如何加载 cni 插件。

// 代码门路:pkg/cri/server/service.go
func NewCRIService(config criconfig.Config, client *containerd.Client) (CRIService, error) {
    var err error
    labels := label.NewStore()
    c := &criService{
        config:                      config,
        client:                      client,
        os:                          osinterface.RealOS{},
        sandboxStore:                sandboxstore.NewStore(labels),
        containerStore:              containerstore.NewStore(labels),
        imageStore:                  imagestore.NewStore(client),
        snapshotStore:               snapshotstore.NewStore(),
        sandboxNameIndex:            registrar.NewRegistrar(),
        containerNameIndex:          registrar.NewRegistrar(),
        initialized:                 atomic.NewBool(false),
        netPlugin:                   make(map[string]cni.CNI),
        unpackDuplicationSuppressor: kmutex.New(),}

    if client.SnapshotService(c.config.ContainerdConfig.Snapshotter) == nil {return nil, fmt.Errorf("failed to find snapshotter %q", c.config.ContainerdConfig.Snapshotter)
    }

    c.imageFSPath = imageFSPath(config.ContainerdRootDir, config.ContainerdConfig.Snapshotter)
    logrus.Infof("Get image filesystem path %q", c.imageFSPath)

    if err := c.initPlatform(); err != nil {return nil, fmt.Errorf("initialize platform: %w", err)
    }

    c.streamServer, err = newStreamServer(c, config.StreamServerAddress, config.StreamServerPort, config.StreamIdleTimeout)
    if err != nil {return nil, fmt.Errorf("failed to create stream server: %w", err)
    }

    c.eventMonitor = newEventMonitor(c)

    // cni 网络插件初始化
    c.cniNetConfMonitor = make(map[string]*cniNetConfSyncer)
    for name, i := range c.netPlugin {
        path := c.config.NetworkPluginConfDir
        if name != defaultNetworkPlugin {if rc, ok := c.config.Runtimes[name]; ok {path = rc.NetworkPluginConfDir}
        }
        if path != "" {m, err := newCNINetConfSyncer(path, i, c.cniLoadOptions())
            if err != nil {return nil, fmt.Errorf("failed to create cni conf monitor for %s: %w", name, err)
            }
            c.cniNetConfMonitor[name] = m
        }
    }

    c.baseOCISpecs, err = loadBaseOCISpecs(&config)
    if err != nil {return nil, err}

    return c, nil
}

在 containerd 中接管到从 kubelet 发送过去的容器运行指令后,containerd 将会创立并运行一个 sanbox 容器,cni 的创立容器网络的工作被放在了 sanbox 运行逻辑中,代码如下所示。

// pkg/cri/sbserver/sandbox_run.go
func (c *criService) RunPodSandbox(ctx context.Context, r *runtime.RunPodSandboxRequest) (_ *runtime.RunPodSandboxResponse, retErr error) {
    // ...

    if podNetwork {netStart := time.Now()
        // Pod 须要不在 host 网络空间下时,才会由 cni 为 Pod 调配 IP
        var netnsMountDir = "/var/run/netns"
        if c.config.NetNSMountsUnderStateDir {netnsMountDir = filepath.Join(c.config.StateDir, "netns")
        }
        sandbox.NetNS, err = netns.NewNetNS(netnsMountDir)
        if err != nil {return nil, fmt.Errorf("failed to create network namespace for sandbox %q: %w", id, err)
        }
        sandbox.NetNSPath = sandbox.NetNS.GetPath()
        defer func() {
            if retErr != nil {deferCtx, deferCancel := ctrdutil.DeferContext()
                defer deferCancel()
                // Teardown network if an error is returned.
                if err := c.teardownPodNetwork(deferCtx, sandbox); err != nil {log.G(ctx).WithError(err).Errorf("Failed to destroy network for sandbox %q", id)
                }

                if err := sandbox.NetNS.Remove(); err != nil {log.G(ctx).WithError(err).Errorf("Failed to remove network namespace %s for sandbox %q", sandbox.NetNSPath, id)
                }
                sandbox.NetNSPath = ""
            }
        }()

        // 设置容器网络
        if err := c.setupPodNetwork(ctx, &sandbox); err != nil {return nil, fmt.Errorf("failed to setup network for sandbox %q: %w", id, err)
        }
        sandboxCreateNetworkTimer.UpdateSince(netStart)
    }

    // ...
}

// 设置容器网络
func (c *criService) setupPodNetwork(ctx context.Context, sandbox *sandboxstore.Sandbox) error {
    var (
        id        = sandbox.ID
        config    = sandbox.Config
        path      = sandbox.NetNSPath
        netPlugin = c.getNetworkPlugin(sandbox.RuntimeHandler)
    )
    if netPlugin == nil {return errors.New("cni config not initialized")
    }

    opts, err := cniNamespaceOpts(id, config)
    if err != nil {return fmt.Errorf("get cni namespace options: %w", err)
    }
    log.G(ctx).WithField("podsandboxid", id).Debugf("begin cni setup")

    // 调用插件
    result, err := netPlugin.Setup(ctx, id, path, opts...)
    if err != nil {return err}
    logDebugCNIResult(ctx, id, result)
    // Check if the default interface has IP config
    if configs, ok := result.Interfaces[defaultIfName]; ok && len(configs.IPConfigs) > 0 {sandbox.IP, sandbox.AdditionalIPs = selectPodIPs(ctx, configs.IPConfigs, c.config.IPPreference)
        sandbox.CNIResult = result
        return nil
    }
    return fmt.Errorf("failed to find network info for sandbox %q", id)
}

更多技术分享浏览我的博客:

https://thierryzhou.github.io

本文由 mdnice 多平台公布

退出移动版