kubernetes网络概念

Kubernetes本身并不提供网络功能,只是把网络接口开放出来,通过插件的形式实现。

一. 网络要解决的问题

既然Kubernetes中将容器的联网通过插件的方式来实现,那么该如何解决容器的联网问题呢?

如果您在本地单台机器上运行docker容器的话会注意到所有容器都会处在docker0网桥自动分配的一个网络IP段内(172.17.0.1/16)。该值可以通过docker启动参数–bip来设置。这样所有本地的所有的容器都拥有了一个IP地址,而且还是在一个网段内彼此就可以互相通信了。

但是Kubernetes管理的是集群,Kubernetes中的网络要解决的核心问题就是每台主机的IP地址网段划分,以及单个容器的IP地址分配。概括为:

  • 保证每个Pod拥有一个集群内唯一的IP地址
  • 保证不同节点的IP地址划分不会重复
  • 保证跨节点的Pod可以互相通信
  • 保证不同节点的Pod可以与跨节点的主机互相通信

为了解决该问题,出现了一系列开源的Kubernetes中的网络插件与方案,如:

  • Flannel:最为普遍的实现,提供多种网络backend实现,覆盖多种场景
  • Calico:采用BGP提供网络直连,功能丰富,对底层网络有要求
  • Romana:采用BGP或者OSPF提供网络直连能力的方案
  • WeaveNet:采用UDP封装实现L2 Overlay,支持用户态(慢,可加密)和内核态(快,不能加密)两种实现
  • Kube-router:同样采用BGP提供网络直连,集成基于LVS的负载均衡能力
  • Cilium:基于eBPF和XDP的高性能Overlay网络方案
  • Canal:(flannel for network + Calico for firewalling),嫁接型创新项目

只要实现Kubernetes官方的设计的 CNI - Container Network Interface(容器网络接口)就可以自己写一个网络插件。

容器的网络复杂性就在于它其实是寄生在 Host 网络之上的。从这个角度讲,可以把容器网络方案大体分为 Underlay/Overlay 两大派别:

  • Underlay 的标准是它与 Host 网络是同层的,从外在可见的一个特征就是它是不是使用了 Host 网络同样的网段、输入输出基础设备、容器的 IP 地址是不是需要与 Host 网络取得协同(来自同一个中心分配或统一划分)。
  • Overlay 不一样的地方就在于它并不需要从 Host 网络的 IPM 的管理的组件去申请IP,一般来说,它只需要跟 Host 网络不冲突,这个 IP 可以自由分配的。

现在社区推崇 perPodperIP 模型。

二. Flannel 方案

Flannel 是目前使用最为普遍的方案,通过将backend机制独立,目前已经支持多种数据路径,也可以适用于overlay/underlay等多种场景,封装可以选用用户态UDP(纯用户态实现)、内核Vxlan(性能好),如集群规模不大,处于同一二层域,还可以选择host-gw方式。

  • 用户态的UDP,最早期的实现;
  • 内核的Vxlan,这两种都是overlay的方案。Vxlan的性能会比较好些,但是对内核的版本是有要求的,需要内核支持Vxlan的特性功能;
  • host-gw方式,如果集群规模不大,又处于同一个二层域,可以选择这种方式,这种方式的backend基本上是由一段广播路由规则来启动的,性能比较高。

2.1 工作过程

首先,flannel利用Kubernetes API或者etcd用于存储整个集群的网络配置,其中最主要的内容为设置集群的网络地址空间。例如,设定整个集群内所有容器的IP都取自网段“10.244.0.0/16”。

接着,flannel在每个主机中运行flanneld作为agent,它会为所在主机从集群的网络地址空间中,获取一个小的网段subnet,本主机内所有容器的IP地址都将从中分配。

然后,flanneld再将本主机获取的subnet以及用于主机间通信的Public IP,同样通过kubernetes API或者etcd存储起来。

最后,flannel利用各种backend ,例如udp,vxlan,host-gw等等,跨主机转发容器间的网络流量,完成容器间的跨主机通信。

2.2 实现原理

Flannel为每个主机提供独立的子网,整个集群的网络信息存储在etcd上。对于跨主机的转发,目标容器的IP地址,需要从etcd获取。

kubernetes是通过一个叫CNI接口维护一个单独网桥来代替docker0,这个CNI网桥,默认叫做cni0。

封装步骤

  • IP数据报被封装并通过容器的eth0发送。
  • Container1的eth0通过veth对与cni0交互并将数据包发送到cni0。然后cni0转发包。
  • cni0确定目的Container的IP地址,通过查询本地路由表到外部容器,并将数据包发送到虚拟NIC Flannel.1。
  • Flannel.1收到的数据包被转发到Flanneld进程。
  • Flanneld进程封装了数据包通过查询etcd维护的路由表并发送数据包通过主机的eth0。
  • 数据包确定网络中的目标主机主机。
  • 目的主机的Flanneld进程监听端口,负责解封包。
  • 解封装的数据包将转发到虚拟NIC Flannel.1。
  • Flannel.1查询路由表,解封包,并将数据包发送到cni0。
  • cni0确定目标容器并发送包到目标容器。

如果使用vxlan协议封装,封装协议如下所示:

还可以使用host-gw模式,这种不需要封装,hostgw是最简单的backend,它的原理非常简单,直接添加路由,将目的主机当做网关,直接路由原始封包。

不过host-gw 要求主机网络二层直接互联。所以每个节点上有n-1个路由,而n个节点一共有n(n-1)/2个路由以保证flannel的flat网络能力。

2.3 K8S如何使用Flannel网络

Kubernetes集群内部存在三类IP,分别是:

  • Node IP:宿主机的IP地址
  • Pod IP:使用网络插件创建的IP(如flannel),使跨主机的Pod可以互通
  • Cluster IP:虚拟IP,通过iptables规则访问服务

Flannel打通Overlay网络

当使用kubeadm来部署k8s,网络组件(如flannel)是通过Add-ons的方式来部署的。我们使用这个yml文件来部署的flannel网络。

---
apiVersion: policy/v1beta1
kind: PodSecurityPolicy         ##Pod安全策略
metadata:
  name: psp.flannel.unprivileged
  annotations:
    seccomp.security.alpha.kubernetes.io/allowedProfileNames: docker/default
    seccomp.security.alpha.kubernetes.io/defaultProfileName: docker/default
    apparmor.security.beta.kubernetes.io/allowedProfileNames: runtime/default
    apparmor.security.beta.kubernetes.io/defaultProfileName: runtime/default
spec:
  privileged: false
  volumes:
    - configMap
    - secret
    - emptyDir
    - hostPath
  allowedHostPaths:
    - pathPrefix: "/etc/cni/net.d"
    - pathPrefix: "/etc/kube-flannel"
    - pathPrefix: "/run/flannel"
  readOnlyRootFilesystem: false
  # Users and groups
  runAsUser:
    rule: RunAsAny
  supplementalGroups:
    rule: RunAsAny
  fsGroup:
    rule: RunAsAny
  # Privilege Escalation
  allowPrivilegeEscalation: false
  defaultAllowPrivilegeEscalation: false
  # Capabilities
  allowedCapabilities: ['NET_ADMIN']
  defaultAddCapabilities: []
  requiredDropCapabilities: []
  # Host namespaces
  hostPID: false
  hostIPC: false
  hostNetwork: true
  hostPorts:
  - min: 0
    max: 65535
  # SELinux
  seLinux:
    # SELinux is unused in CaaSP
    rule: 'RunAsAny'
---
kind: ClusterRole       ##创建一个ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: flannel
rules:
  - apiGroups: ['extensions']
    resources: ['podsecuritypolicies']
    verbs: ['use']
    resourceNames: ['psp.flannel.unprivileged']
  - apiGroups:
      - ""
    resources:
      - pods
    verbs:
      - get
  - apiGroups:
      - ""
    resources:
      - nodes
    verbs:
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - nodes/status
    verbs:
      - patch
---
kind: ClusterRoleBinding     ##创建 ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: flannel
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: flannel
subjects:
- kind: ServiceAccount
  name: flannel 
  namespace: kube-system
---
apiVersion: v1
kind: ServiceAccount   ##创建ServiceAccount 名称为flannel
metadata:
  name: flannel
  namespace: kube-system
---
kind: ConfigMap            ##创建ConfigMap,内容是配置信息
apiVersion: v1
metadata:
  name: kube-flannel-cfg
  namespace: kube-system
  labels:
    tier: node
    app: flannel
data:                        
  cni-conf.json: |
    {
      "name": "cbr0",
      "cniVersion": "0.3.1",
      "plugins": [
        {
          "type": "flannel",
          "delegate": {
            "hairpinMode": true,
            "isDefaultGateway": true
          }
        },
        {
          "type": "portmap",
          "capabilities": {
            "portMappings": true
          }
        }
      ]
    }
  net-conf.json: |
    {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "vxlan"
      }
    }
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: kube-flannel-ds-amd64
  namespace: kube-system
  labels:
    tier: node
    app: flannel
spec:
  selector:
    matchLabels:
      app: flannel
  template:
    metadata:
      labels:
        tier: node
        app: flannel
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: beta.kubernetes.io/os
                    operator: In
                    values:
                      - linux
                  - key: beta.kubernetes.io/arch
                    operator: In
                    values:
                      - amd64
      hostNetwork: true
      tolerations:
      - operator: Exists
        effect: NoSchedule
      serviceAccountName: flannel
      initContainers:
      - name: install-cni
        image: quay.io/coreos/flannel:v0.11.0-amd64
        command:
        - cp
        args:
        - -f
        - /etc/kube-flannel/cni-conf.json
        - /etc/cni/net.d/10-flannel.conflist
        volumeMounts:
        - name: cni
          mountPath: /etc/cni/net.d
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      containers:
      - name: kube-flannel
        image: quay.io/coreos/flannel:v0.11.0-amd64
        command:
        - /opt/bin/flanneld
        args:
        - --ip-masq
        - --kube-subnet-mgr
        - --iface=ens32
        resources:
          requests:
            cpu: "100m"
            memory: "50Mi"
          limits:
            cpu: "100m"
            memory: "50Mi"
        securityContext:
          privileged: false
          capabilities:
            add: ["NET_ADMIN"]
        env:
        - name: POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        volumeMounts:
        - name: run
          mountPath: /run/flannel
        - name: flannel-cfg
          mountPath: /etc/kube-flannel/
      volumes:
        - name: run
          hostPath:
            path: /run/flannel
        - name: cni
          hostPath:
            path: /etc/cni/net.d
        - name: flannel-cfg
          configMap:
            name: kube-flannel-cfg

通过分析该yaml文件可以知道:该pod内有两个container,分别为install-cni和 kube-flannel。

install-cni

主要负责将config-map中适用于该flannel的CNI netconf配置拷贝到主机的CNI netconf 路径下,从而使得K8S在创建pod的时候可以遵照标准的CNI流程挂载网卡。

kube-flannel

主要启动flanneld,该command中为flanneld的启动指定了两个参数: –ip-masq, –kube-subnet-mgr。第一个参数–ip-masq代表需要为其配置SNAT;第二个参数–kube-subnet-mgr代表其使用kube类型的subnet-manager。该类型有别于使用etcd的local-subnet-mgr类型,使用kube类型后,flannel上各Node的IP子网分配均基于K8S Node的spec.podCIDR属性

[root@k8s-master k8s-yaml]# kubectl get nodes k8s-master -o yaml
apiVersion: v1
kind: Node
metadata:
  annotations:
    flannel.alpha.coreos.com/backend-data: '{"VtepMAC":"f2:49:98:92:15:f1"}'
    flannel.alpha.coreos.com/backend-type: vxlan
    flannel.alpha.coreos.com/kube-subnet-manager: "true"
    flannel.alpha.coreos.com/public-ip: 192.168.154.200
    kubeadm.alpha.kubernetes.io/cri-socket: /var/run/dockershim.sock
    node.alpha.kubernetes.io/ttl: "0"
    volumes.kubernetes.io/controller-managed-attach-detach: "true"
  creationTimestamp: "2020-03-13T09:43:50Z"
  labels:
    beta.kubernetes.io/arch: amd64
    beta.kubernetes.io/os: linux
    kubernetes.io/arch: amd64
    kubernetes.io/hostname: k8s-master
    kubernetes.io/os: linux
    node-role.kubernetes.io/master: ""
  name: k8s-master
  resourceVersion: "1010850"
  selfLink: /api/v1/nodes/k8s-master
  uid: 10d6d32b-571d-40e6-8ecb-a48319317b47
spec:
  podCIDR: 10.244.0.0/24
  podCIDRs:
  - 10.244.0.0/24
  taints:
  - effect: NoSchedule
    key: node-role.kubernetes.io/master

.....略

另外,flannel的subnet-manager通过监测K8S的node变化来维护了一张路由表,这张表里面描述了要到达某个Pod子网需要先到达哪个EndPoint。

CNI挂载容器到隧道端点

如果说flannel为Pod打通了一张跨node的大网,那么CNI就是将各个终端Pod挂载到这张大网上的安装工人。

通过传统方式来部署flannel都需要通过脚本来修改dockerd的参数,从而使得通过docker创建的容器能够挂载到指定的网桥上。但是flannel的CNI实现并没有采用这种方式。

flannel CNI的流程

  • 读取netconf配置文件,并加载/run/flannel/subnet.env环境变量信息。基于加载的信息,生成适用于其delegate的ipam和CNI bridge的netconf文件;其中指定ipam使用host-local,CNI bridge type为bridge。
  • 调用deletgate(CNI bridge type)对应的二进制文件来挂载容器到网桥上。

这里的环境变量文件/run/flannel/subnet.env是由flanneld生成的,里面包含了该主机所能够使用的IP子网网段,具体内容如下:

[root@k8s-master ~]# cat /run/flannel/subnet.env 
FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.0.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true

2.4 CNI:Container Network Interface(容器网络接口)

CNI(Container Network Interface)是CNCF旗下的一个项目,由一组用于配置Linux容器的网络接口的规范和库组成,同时还包含了一些插件。CNI仅关心容器创建时的网络分配,和当容器被删除时释放网络资源。通过此链接浏览该项目:https://github.com/containernetworking/cni。

Kubernetes源码的vendor/github.com/containernetworking/cni/libcni目录中已经包含了CNI的代码,也就是说kubernetes中已经内置了CNI。

接口定义

CNI的接口中包括以下几个方法:

type CNI interface {
    AddNetworkList(net *NetworkConfigList, rt *RuntimeConf) (types.Result, error)
    DelNetworkList(net *NetworkConfigList, rt *RuntimeConf) error

    AddNetwork(net *NetworkConfig, rt *RuntimeConf) (types.Result, error)
    DelNetwork(net *NetworkConfig, rt *RuntimeConf) error
}

该接口只有四个方法,添加网络、删除网络、添加网络列表、删除网络列表。

设计考量

CNI设计的时候考虑了以下问题:

  • 容器运行时必须在调用任何插件之前为容器创建一个新的网络命名空间。然后,运行时必须确定这个容器应属于哪个网络,并为每个网络确定哪些插件必须被执行。
  • 网络配置采用JSON格式,可以很容易地存储在文件中。网络配置包括必填字段,如nametype以及插件(类型)。网络配置允许字段在调用之间改变值。为此,有一个可选的字段args,必须包含不同的信息。
  • 容器运行时必须按顺序为每个网络执行相应的插件,将容器添加到每个网络中。
  • 在完成容器生命周期后,运行时必须以相反的顺序执行插件(相对于执行添加容器的顺序)以将容器与网络断开连接。
  • 容器运行时不能为同一容器调用并行操作,但可以为不同的容器调用并行操作。
  • 容器运行时必须为容器订阅ADD和DEL操作,这样ADD后面总是跟着相应的DEL。 DEL可能跟着额外的DEL,但是,插件应该允许处理多个DEL(即插件DEL应该是幂等的)。
  • 容器必须由ContainerID唯一标识。存储状态的插件应该使用(网络名称,容器ID)的主键来完成。
  • 运行时不能调用同一个网络名称或容器ID执行两次ADD(没有相应的DEL)。换句话说,给定的容器ID必须只能添加到特定的网络一次。

CNI插件

CNI插件必须实现一个可执行文件,这个文件可以被容器管理系统(例如rkt或Kubernetes)调用。

CNI插件负责将网络接口插入容器网络命名空间(例如,veth对的一端),并在主机上进行任何必要的改变(例如将veth的另一端连接到网桥)。然后将IP分配给接口,并通过调用适当的IPAM插件来设置与“IP地址管理”部分一致的路由。

参数

CNI插件必须支持以下操作:

  • 将容器添加到网络
  • 从网络中删除容器

IP分配

作为容器网络管理的一部分,CNI插件需要为接口分配(并维护)IP地址,并安装与该接口相关的所有必要路由。这给了CNI插件很大的灵活性,但也给它带来了很大的负担。众多的CNI插件需要编写相同的代码来支持用户需要的多种IP管理方案(例如dhcp、host-local)。

为了减轻负担,使IP管理策略与CNI插件类型解耦,定义了IP地址管理插件(IPAM插件)。CNI插件的职责是在执行时恰当地调用IPAM插件。 IPAM插件必须确定接口IP/subnet,网关和路由,并将此信息返回到“主”插件来应用配置。 IPAM插件可以通过协议(例如dhcp)、存储在本地文件系统上的数据、网络配置文件的“ipam”部分或上述的组合来获得信息。

2.5 flannel配置

配置

如果 --kube-subnet-mgr 参数是true, flannel 将从 /etc/kube-flannel/net-conf.json 文件读取配置;如果其值为 false,则从etcd读取配置,默认的位置是/coreos.com/network/config (可以使用--etcd-prefix参数来覆盖)。

配置采用json格式:

  • Network (string): 采用CIDR格式的IPV4网络,这是惟一的被强制的项;
  • SubnetLen (integer): 分配给每个主机的子网大小。默认值为24(即/24),除非Network配置小于/24,在这种情况下,它比Network小1位。
  • SubnetMin (string): 分配开始的子网,默认为Network第一个子网。
  • SubnetMax (string): 分配结束的子网,默认为Network的最后一个子网.
  • Backend (dictionary): 后端类型以及后端的指定配置项,默认是 udp 后端。

子网租约持续时间为24小时,提前1小时续订,可以使用--subnet-lease-renew-margin选项进行更改。

例如:

{
	"Network": "10.0.0.0/8",
	"SubnetLen": 20,
	"SubnetMin": "10.10.0.0",
	"SubnetMax": "10.99.0.0",
	"Backend": {
		"Type": "udp",
		"Port": 7890
	}
}

命令行配置选项

  • –public-ip="": 其他节点可访问的用于主机间通信的IP。默认为用于通信的接口的IP。
  • –etcd-endpoints=http://127.0.0.1:4001: etcd的endpoints;
  • –etcd-prefix=/coreos.com/network: etcd 前缀;
  • –etcd-keyfile="": 用于安全的etcd通信的SSL秘钥文件;
  • –etcd-certfile="": 用于安全的etcd通信的SSL证书文件;
  • –etcd-cafile="": 用于安全的etcd通信的SSL Certificate Authority file(即CA证书);
  • –kube-subnet-mgr: 使用 Kubernetes API 为子网分配,来替代etcd;
  • –iface="": 主机间通信的接口,默认是具有默认路由的那个接口,
  • –iface-regex="": 正则表达式以匹配要用于主机间通信的第一个接口(IP或名称)。如果未指定,将默认为计算机上默认路由的接口。可以多次指定此项以按顺序检查每个正则表达式。返回找到的第一个匹配项。此选项将被iface选项取代,并且仅当没有匹配iface选项中指定的任何选项时才使用。
  • –iptables-resync=5: iptables规则的重新同步周期(秒)。默认为5秒;
  • –subnet-file=/run/flannel/subnet.env: 包含env环境变量的文件名(subnet和MTU值)
  • –net-config-path=/etc/kube-flannel/net-conf.json: 要使用的网络配置文件的路径;
  • –subnet-lease-renew-margin=60: 子网续约时间;
  • –ip-masq=false: 为flannel网络外部的流量设置IP伪装。flannel假设NAT表中的 POSTROUTING链的默认策略是ACCEPT;
  • -v=0: 日志级别,设置为1可查看与数据路径相关的消息;
  • –healthz-ip=“0.0.0.0”: healthz server侦听的地址(default “0.0.0.0”)
  • –healthz-port=0: healthz server 侦听的端口(0 to disable)
  • –version: 打印版本

注意:MTU由flannel自动计算和设置。然后在subnet.env中报告该值。无法更改。

环境变量

上面概述的命令行选项也可以通过环境变量指定。例如--etcd-endpoints=http://10.0.0.2:2379相当于FLANNELD_etcd_endpoints=http://10.0.0.2:2379环境变量。

任何命令行选项都可以转换为环境变量,方法是在其前面加上FLANNELD_前缀,转换为大写,并将所有其他虚线替换为下划线。

健康检查(Health Check)

Flannel 提供了一个健康检查的endporint healthz。当flannel是运行的时候,当前这个endpoint是盲目的返回OK(HTTP状态码200)。这个特性默认是关闭的,设置healthz-port为一个非0的值,将开启flannel的healthz server。

后端(Backends)

flannel可以搭配几个不同的后端。一旦设置好,就不应该在运行时更改后端。

  • 推荐选择VXLAN
  • host-gw推荐给有经验的用户,有更好的性能
  • UDP不建议生产环境使用

配置的类型及选型:

VXLAN
  • Type (string): vxlan
  • VNI (number): 被使用的 VXLAN Identifier (VNI) 。 对于Linux,默认为1. 在Windows上应该是大于或等于4096;
  • Port (number): 用于发送封装报文的UDP端口。对于Linux默认是kernel默认值, 当前为8472,但在Windows上必须是4789;
  • GBP (Boolean): 开启VXLAN Group Based Policy。默认是不开启的。 GBP 不支持Windows;
  • DirectRouting (Boolean): 开启 direct routes (类似于 host-gw) 当host存在于相同的子网中时,VXLAN 将仅用于在不同的子网中通信的hosts之间的流量进行封装,默认是不开启的,DirectRouting 不支持Windows;
  • MacPrefix (String): 仅支持Windows,设置MAC 地址前缀。默认为0E-2A。
host-gw
  • Type (string): host-gw
UDP
  • Type (string): udp
  • Port (number): UDP发送封装包的端口,默认为8285。