Kubernetes Network

学习参考:网络策略

环境准备

root@master30:~# kubectl create ns network
root@master30:~# kubectl config set-context --current --namespace network

单主机网络通信

Docker 单机网络

Docker中的网络接口默认都是虚拟的接口。虚拟接口的最大优势就是转发效率极高。这是因为Linux在内核中进行数据复制来实现虚拟接口之间的数据转发,即发送接口的发送缓存中的数据包将被直接复制到接收接口的接收缓存中,而无需通过外部物理网络设备进行交换。

Docker 服务默认会创建一个名称为docker0的Linux网桥(其上有一个docker0内部接口),利用了Linux虚拟网络技术,在本地主机和容器内分别创建一个虚拟接口,并让它们彼此连通(这样的一对接口叫做vethpair)。Docker默认指定了docker0接口的IP地址和子网掩码,让主机和容器之间可以通过网桥相互通信。

在这里插入图片描述

说明:brctl工具由bridge-utils提供。

外部主机要想访问容器,需要通过端口映射实现。

Containerd 单机网络

Containerd 中的网络 与Docker类似,所有网络接口默认都是虚拟接口。

创建容器时,Containerd 会在本地主机和容器内分别创建一个虚拟接口,并让它们彼此连通。

跨主机网络通信

跨主机通信架构

在这里插入图片描述

以上图片来源于《基于 kubernetes 的容器云平台实战 》。

跨主机通信方法比较多,可以使用:

  1. docker 原生的方案:overlay 和 macvlan。
  2. 第三方提供的方案:flannel 、calico、weave等。

重点关注两点:配置难易程度是否支持网络策略

以下数据来源于《kubernetes 权威指南》。

方案 特性 Flannel Calico macvlan OpenVswitch 直接路由
方案特性 通过虚拟设备flannel0实现对docker0的管理 基于BGP协议的纯三层的网络方案 基于Linux Kernel 的macvlan技术 基于隧道的虚拟路由器技术 基于Linux Kernel 的 vRouter 技术
对底层网络的要求 三层互通 三层互通 二层互通 三层互通 二层互通
配置难易程度 简单-基于etcd 简单-基于etcd 简单-直接使用宿主机网络,需要仔细规划IP地址 复杂-需手工配置个节点的bridge 简单-使用宿主机vRoute功能,需要仔细规划每个Node的IP地址
网络性能 host-gw > VxLAN BGP 模式性能随时小、IPIP 模式较小 性能随时可忽略 性能随时较小 性能随时较小
网络连通性限制 在不支持BGP的网络环境下无法使用 基于macvlan的容器无法与宿主机网络通信 在无法实现大二层互通的网络环境下无法使用

跨主机通信方案

我们将从如下几个方面比较,大家可以根据不同场景选择最合适的方案。

  • 网络模型,采用何种网络模型支持 multi-host 网络?
  • Distributed Store,是否需要 etcd 或 consul 这类分布式 key-value 数据库存储网络信息?
  • IPMA,如何管理容器网络的 IP?
  • 连通与隔离,提供怎样的网络连通性?支持容器间哪个级别和哪个类型的隔离?
  • 性能,性能比较。
网络模型

跨主机网络意味着将不同主机上的容器用同一个虚拟网络连接起来。这个虚拟网络的拓扑结构和实现技术就是网络模型。

  • overlay建立主机间 VxLAN 隧道,原始数据包在发送端被封装成 VxLAN 数据包,到达目的后在接收端解包。
  • Macvlan ,网络在二层上通过 VLAN 连接容器,在三层上依赖外部网关连接不同 macvlan。数据包直接发送,不需要封装,属于underlay 网络。
  • Flannel 支持 backend:vxlan 和 host-gw。vxlan 与 Docker overlay 类似,属于 overlay 网络。host-gw 将主机作为网关,依赖三层 IP 转发,不需要像 vxlan 那样对包进行封装,属于 underlay 网络。
  • Weave, 是 VxLAN 实现,属于 overlay 网络。
  • Calico ,与Flannel使用host-gw类似,将主机作为网关,依赖三层 IP 转发,不需要像 vxlan 那样对包进行封装,属于 underlay 网络。
Distributed Store

**Docker Overlay、Flannel 和 Calico 都需要 etcd 或 consul。**Macvlan 是简单的 local 网络,不需要保存和共享网络信息。Weave 自己负责在主机间交换网络配置信息,也不需要 Distributed Store。

IPAM

Docker Overlay 网络中所有主机共享同一个 subnet,容器启动时会顺序分配 IP,可以通过 --subnet 定制此 IP 空间。

Macvlan 需要用户自己管理 subnet,为容器分配 IP,不同 subnet 通信依赖外部网关。

Flannel 为每个主机自动分配独立的 subnet,用户只需要指定一个大的 IP 池。不同 subnet 之间的路由信息也由 Flannel 自动生成和配置。

Weave 的默认配置下所有容器使用 10.32.0.0/12 subnet,如果此地址空间与现有 IP 冲突,可以通过 --ipalloc-range 分配特定的 subnet。

Calico 从 IP Pool(可定制)中为每个主机分配自己的 subnet。

连通与隔离

同一 Docker Overlay 网络中的容器可以通信,但不同网络之间无法通信,要实现跨网络访问,只有将容器加入多个网络。与外网通信可以通过 docker_gwbridge 网络。

Macvlan 网络的连通或隔离完全取决于二层 VLAN 和三层路由。

不同 Flannel 网络中的容器直接就可以通信,没有提供隔离。与外网通信可以通过 bridge 网络。

Weave 网络默认配置下所有容器在一个大的 subnet 中,可以自由通信,如果要实现隔离,需要为容器指定不同的 subnet 或 IP。与外网通信的方案是将主机加入到 weave 网络,并把主机当作网关。

Calico 默认配置下只允许位于同一网络中的容器之间通信,但通过其强大的 Policy 能够实现几乎任意场景的访问控制。

性能

性能测试是一个非常严谨和复杂的工程,这里我们只尝试从技术方案的原理上比较各方案的性能。

最朴素的判断是:Underlay 网络性能优于 Overlay 网络

**Overlay 网络利用隧道技术,将数据包封装到 UDP 中进行传输。**因为涉及数据包的封装和解封,存在额外的 CPU 和网络开销。虽然几乎所有 Overlay 网络方案底层都采用 Linux kernel 的 vxlan 模块,这样可以尽量减少开销,但这个开销与 Underlay 网络相比还是存在的。所以 Macvlan、Flannel host-gw、Calico 的性能会优于 Docker overlay、Flannel vxlan 和 Weave。

Overlay 较 Underlay 可以支持更多的二层网段,能更好地利用已有网络,以及有避免物理交换机 MAC 表耗尽等优势,所以在方案选型的时候需要综合考虑。

跨主机网络模型

容器网络的配置是一个复杂的过程,为了应对各式各样的需求:

  • 容器网络的解决方案也多种多样,例如有flannel,calico,kube-ovn,weave等。
  • 容器平台/运行时也是多样的,例如有Kubernetes,Openshift,rkt等。

想要解决这个问题,我们需要一个抽象的接口层,将容器网络配置方案与容器平台方案解耦。

CNM

CNM( Container Network Model,容器网络模型),由 Docker 公司提出,在 docker 项目下的 libnetwork 项目中被采用。按照该模型开发出的 driver 就能与 docker daemon 协同工作,实现容器网络。

  • docker 原生的 driver 包括 none、bridge、overlay 和 macvlan。
  • 第三方 driver 包括 flannel、weave、calico 等。

在这里插入图片描述

容器网络模型对容器网络进行了抽象,由以下三类组件组成:

  • Sandbox 是容器的网络栈,包含容器的 interface、路由表和 DNS 设置。 Linux Network Namespace 是 Sandbox 的标准实现。Sandbox 可以包含来自不同 Network 的 Endpoint。
  • Endpoint 的作用是将 Sandbox 接入 Network。Endpoint 的典型实现是 veth pair,后面我们会举例。一个 Endpoint 只能属于一个网络,也只能属于一个 Sandbox。
  • Network 包含一组 Endpoint,同一 Network 的 Endpoint 可以直接通信。Network 的实现可以是 Linux Bridge、VLAN 等。

如图所示两个容器,一个容器一个 Sandbox,每个 Sandbox 都有一个 Endpoint 连接到 Network 1,第二个 Sandbox 还有一个 Endpoint 将其接入 Network 2.

下面我们以 docker bridge driver 为例讨论 libnetwork CNM 是如何被实现的。

  1. 两个 Network:默认网络 “bridge” 和自定义网络 “my_net2”。实现方式是 Linux Bridge:“docker0” 和 “br-5d863e9f78b6”。
  2. 三个 Enpoint,由 veth pair 实现,一端(vethxxx)挂在 Linux Bridge 上,另一端(eth0)挂在容器内。
  3. 三个 Sandbox,由 Network Namespace 实现,每个容器有自己的 Sanbox。

CNI

CNI,全称 Container Network Interface,是 Google 和 **CoreOS **联合定制的网络标准,CNI 规范定义了容器和网络插件之间通信规范,实现多容器通信。各个网络厂商通过该接口实现互相通信。

在这里插入图片描述

一个容器可以被加入到被不同插件所驱动的多个网络之中。一个网络有自己对应的插件和唯一的名称。

CNI 模型包含2个概念:

  • 容器: 独立的 linux 网络命名空间。

  • 网络: 用于互联实体, 这些实体拥有各自独立且唯一的 IP 地址, 可以是容器, 物理机, 或者其他网络设备。

    通过插件设置网络, 包括 CNI Plugin 和 IPAM ( IP address management) Plugin 两类插件:

    • CNI Plugin 负责配置容器网络。
    • IPAM plugin 负责分配容器的IP 地址。

    IPAM Plugin 作为 CNI plugin 的一部分, 与CNI plugin 一起工作。

CNM和CNI比较

特点 CNM CNI
标准规范 Libnetwork cni
最小单元 容器 POD
对守护进程的依赖 依赖 dockerd 不依赖任何守护进程
扩主机通信 依赖外部 KV 数据库 用本身的 KV 数据库
灵活程度 被 docker 绑定 插件可随意替换

网络策略

网络策略介绍

默认情况,集群网络连通性如下:

  • 集群外部主机可以访问集群内部应用
  • 集群内部应用也可以访问集群外部主机
  • 各个namespace之间没有做任何的隔离策略

如果希望在 IP 地址或端口层面控制网络流量, 考虑使用 Kubernetes 网络策略(NetworkPolicy)。

  • NetworkPolicy 是一种以应用为中心的结构,允许你设置如何允许 Pod 与网络上的各类网络“实体” 通信。
  • NetworkPolicy 适用于一端或两端与 Pod 的连接,与其他连接无关。

**提示:**网络策略通过网络插件来实现。 要使用网络策略,你必须使用支持 NetworkPolicy 的网络解决方案,例如 calico。

网络策略规约

Pod 有两种隔离: 出口的隔离入口的隔离

  • 默认情况下,**一个 Pod 的出口是非隔离的,即所有外向连接都是被允许的。**如果有任何的 NetworkPolicy 选择该 Pod 并在其 policyTypes 中包含 “Egress”,则该 Pod 是出口隔离的, 我们称这样的策略适用于该 Pod 的出口。当一个 Pod 的出口被隔离时, 唯一允许的来自 Pod 的连接是适用于出口的 Pod 的某个 NetworkPolicy 的 egress 列表所允许的连接。 这些 egress 列表的效果是相加的。

  • 默认情况下,**一个 Pod 对入口是非隔离的,即所有入站连接都是被允许的。**如果有任何的 NetworkPolicy 选择该 Pod 并在其 policyTypes 中包含 “Ingress”,则该 Pod 被隔离入口, 我们称这种策略适用于该 Pod 的入口。当一个 Pod 的入口被隔离时,唯一允许进入该 Pod 的连接是来自该 Pod 节点的连接和适用于入口的 Pod 的某个 NetworkPolicy 的 ingress 列表所允许的连接。这些 ingress 列表的效果是相加的。

**网络策略是相加的,所以不会产生冲突。**如果策略适用于 Pod 某一特定方向的流量, Pod 在对应方向所允许的连接是适用的网络策略所允许的集合。 因此,评估的顺序不影响策略的结果。

**要允许从源 Pod 到目的 Pod 的连接,则源 Pod 的出口策略和目的 Pod 的入口策略都需要允许连接。**如果任何一方不允许连接,建立连接将会失败。

示例:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: test-network-policy
  namespace: default
spec:
  # 使用标签过滤限制哪些pod
  podSelector:
    matchLabels:
      role: db
  policyTypes:
  # Ingress控制外部访问内部
  - Ingress
  # Egress控制pod访问外部
  - Egress
  ingress:
  # from限制允许哪些主机可以访问pod
  - from:
    # 通过ip限制
    - ipBlock:
        cidr: 172.17.0.0/16
        except:
        - 172.17.1.0/24
    # 限制namespace中pod
    - namespaceSelector:
        matchLabels:
          project: myproject
    # 限制同一namespace中pod
    - podSelector:
        matchLabels:
          role: frontend
    # 限制pod上哪些端口可以访问
    ports:
    - protocol: TCP
      port: 6379
  egress:
  - to:
    - ipBlock:
        cidr: 10.0.0.0/24
    ports:
    - protocol: TCP
      port: 5978
  • 必需字段:与所有其他的 Kubernetes 配置一样,NetworkPolicy 需要 apiVersionkindmetadata 字段。

  • spec:NetworkPolicy 规约 中包含了在一个命名空间中定义特定网络策略所需的所有信息。

  • spec.podSelector:每个 NetworkPolicy 都包括一个 podSelector, 它对该策略所适用的一组 Pod 进行选择。示例中的策略选择带有 “role=db” 标签的 Pod。 空的 podSelector 选择命名空间下的所有 Pod。

  • spec.policyTypes:每个 NetworkPolicy 都包含一个 policyTypes 列表,其中包含 IngressEgress 或两者兼具。policyTypes 字段表示给定的策略是应用于进入所选 Pod 的入站流量还是来自所选 Pod 的出站流量,或两者兼有。 如果 NetworkPolicy 未指定 policyTypes 则默认情况下始终设置 Ingress; 如果 NetworkPolicy 有任何出口规则的话则设置 Egress

  • spec.ingress:用于配置入站规则,每个 NetworkPolicy 可包含一个 ingress 规则的白名单列表。 每个规则都允许同时匹配 fromports 部分的流量。示例策略中包含一条简单的规则: 它匹配某个特定端口,来自三个来源中的一个:

    • 第一个通过 ipBlock 指定
    • 第二个通过 namespaceSelector 指定
    • 第三个通过 podSelector 指定。
  • spec.egress:用于配置出站规则,每个 NetworkPolicy 可包含一个 egress 规则的白名单列表。 每个规则都允许匹配 toport 部分的流量。该示例策略包含一条规则, 该规则将指定端口上的流量匹配到 10.0.0.0/24 中的任何目的地。

to 和 from 选择器

可以在 ingressfrom 部分或 egressto 部分中指定四种选择器:

  • podSelector:此选择器将在与 NetworkPolicy 相同的命名空间中选择特定的 Pod,应将其允许作为入站流量来源或出站流量目的地。

  • namespaceSelector:此选择器将选择特定的命名空间,应将所有 Pod 用作其入站流量来源或出站流量目的地。

  • namespaceSelector 和 podSelector:指定 namespaceSelectorpodSelectorto/from 条目选择特定命名空间中的特定 Pod。

    示例:

      ...
      ingress:
      - from:
        - namespaceSelector:
            matchLabels:
              user: alice
        - podSelector:
            matchLabels:
              role: client
      ...
    

    from 数组中包含两个元素,允许来自本地命名空间中标有 role=client 的 Pod 的连接,来自任何命名空间中标有 user=alice 的任何 Pod 的连接。

  • ipBlock:此选择器将选择特定的 IP CIDR 范围以用作入站流量来源或出站流量目的地。 这些应该是集群外部 IP,因为 Pod IP 存在时间短暂的且随机产生。

    集群的入站和出站机制通常需要重写数据包的源 IP 或目标 IP。 在发生这种情况时,不确定在 NetworkPolicy 处理之前还是之后发生, 并且对于网络插件、云提供商、Service 实现等的不同组合,其行为可能会有所不同。

    • 对入站流量而言,这意味着在某些情况下,你可以根据实际的原始源 IP 过滤传入的数据包, 而在其他情况下,NetworkPolicy 所作用的 源IP 则可能是 LoadBalancer 或 Pod 的节点等。

    • 对于出站流量而言,这意味着从 Pod 到被重写为集群外部 IP 的 Service IP 的连接可能会或可能不会受到基于 ipBlock 的策略的约束。

NetworkPolicy

要精细化管控 Pod 之间入 / 出站流量,必须创建 NetworkPolicy

一、不创建 netpol 时的默认网络状态

集群网络插件(Calico)默认无隔离:

  1. 同 namespace 所有 Pod 自由互通;

  2. 跨 namespace 所有 Pod 自由互通;

  3. Pod ↔ 节点、Pod ↔ Service、Pod ↔ 集群外部都能连通。

    没有任何访问限制,流量完全放开。

二、创建 netpol 才会触发流量隔离逻辑

NetworkPolicy 分两种管控方向:Ingress(入站,别人访问我)、Egress(出站,我访问别人)

  1. 只写 Ingress被 podSelector选中的 Pod:默认拒绝所有外部入站流量,仅放行 from里配置的来源。

    出站流量不受限制,Pod 随便往外访问。

  2. 只写 Egress被选中 Pod:默认禁止所有出站流量,仅放行 to里配置的目标。

    入站流量不受限制。

  3. 同时写 Ingress + Egress,入、出站全部默认拦截,两条都要手动配置放行规则。

三、两个关键限制
  1. 网络插件依赖

    Flannel 原生不支持 NetworkPolicy;必须使用 Calico、Cilium 这类带策略能力的 CNI,netpol 才会生效。

  2. netpol 只作用于 Pod 之间四层流量

    • DNS 解析(CoreDNS)不受 netpol 管控,所以会出现 “能解析 IP,但连不上端口”;
    • 不控制集群节点、LoadBalancer 外部流量、NodePort 节点端口层面的访问。

实施网络策略

准备实验环境

环境说明:

  • namespace-web 中有3个 pod:web1、web2、test
  • namespace-hgq中有1个pod:test
  • 如没有特别说明,默认namespace是web
root@master30~ 10:21:54# kubectl create ns web
root@master30~ 10:25:21# kubens web

# 创建 web1
root@master30~ 10:25:27# kubectl run web1 --image=docker.io/library/nginx --image-pull-policy=IfNotPresent
root@master30~ 10:26:24# kubectl exec -it web1 -- bash -c "echo hello web1 > /usr/share/nginx/html/index.html"

# 创建 web2
root@master30~ 10:26:57# kubectl run web2 --image=docker.io/library/nginx --image-pull-policy=IfNotPresent
root@master30~ 10:27:31# kubectl exec web2 -- bash -c " echo hello web2 > /usr/share/nginx/html/index.html"

# 创建service web1和web2
root@master30~ 10:28:03# kubectl expose pods web1 --port=80 --target-port=80 --type=NodePort
root@master30~ 10:28:43# kubectl expose pods web2 --port=80 --target-port=80 --type=NodePort
root@master30~ 10:29:11# kubectl get svc
NAME   TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
web1   NodePort   10.102.169.176   <none>        80:32617/TCP   31s
web2   NodePort   10.102.237.251   <none>        80:31219/TCP   3s

# 同一namespace中web1和web2互相访问
# -- sleep 3600:容器启动后执行 sleep,保持 Pod 运行 1 小时不退出
root@master30~ 10:29:14# kubectl run test --image=docker.io/library/busybox --image-pull-policy=IfNotPresent sleep 3600
root@master30~ 10:30:03# kubectl exec -it test -- sh
/ # wget web2.web -O web2-index.html
Connecting to web2.web (10.102.237.251:80)
saving to 'web2-index.html'
web2-index.html      100% |*****************************************|    11  0:00:00 ETA
'web2-index.html' saved
/ # cat web2-index.html
hello web2
/ # wget web1.web -O web1-index.html
Connecting to web1.web (10.102.169.176:80)
saving to 'web1-index.html'
web1-index.html      100% |*****************************************|    11  0:00:00 ETA
'web1-index.html' saved
/ # cat web1-index.html
hello web1
/ # exit

# 在不同namespace-hgq中创建测试pod-test,访问web1和web2
root@master30~ 10:31:54# kubectl create ns hgq
root@master30~ 10:31:56# kubectl run test -n hgq --image=docker.io/library/busybox --image-pull-policy=IfNotPresent sleep 3600
root@master30~ 10:32:43# kubectl exec -n hgq test -- wget web1.web -O /tmp/web1.html
root@master30~ 10:33:22# kubectl exec -n hgq test -- wget web2.web -O /tmp/web2.html

# 集群内访问service web1和web2
root@master30~ 11:01:25# curl 10.102.169.176
hello web1
root@master30~ 11:01:31# curl 10.102.237.251
hello web2

# 集群外节点访问web1和web2
root@master30~ 10:34:38# curl 10.1.8.30:32617
hello web1
root@master30~ 10:38:25# curl 10.1.8.30:31219
hello web2

kubectl run web2 创建 Pod 时,会自动打上 1 个标签run: web2

  • key:run
  • value:你 run 后面跟的 Pod 名称 web2

web1.web

  • web1:Service 的名称(kubectl get svc 里的 NAME 字段)

  • web:当前 Pod 所在的 Namespace 名字

  • 等价完整域名:web1.web.svc.cluster.local

  • K8s 集群内置 CoreDNS,会自动为每个 Service 生成 DNS A 记录:

    • 域分区:svc.cluster.local 专门存放 Service 解析

    • 层级:服务名.命名空间.svc.cluster.local

    • Pod 内部 DNS 搜索域默认包含 web.svc.cluster.localsvc.cluster.local,所以输入 web1.web 不用补全后缀也能解析成功

  • 跨 ns 访问必须用这种格式,显式指定命名空间,可读性更好

  • 三种写法示例:

    • 最简写法web1
    • 服务名.命名空间 写法 web1.web
    • 完整标准:web1.web.svc.cluster.local
根据 pod 标签限定⭐️

根据pod标签限定:

  • 限定同一ns中pod之间访问
  • 所有其他ns中pod或者集群外主机都无法访问ns中pod

示例1允许同一ns中具有标签run: test的pod,访问具有标签run: web1的pod 80端口。

root@master30~ 11:01:37# vim netpol.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: my-network-policy
spec:
  # 1. 策略作用对象:匹配标签 run=web1 的 Pod
  podSelector:
    matchLabels:
      run: web1
  # 策略类型:控制入站流量
  policyTypes:
  - Ingress
  ingress:
  # 允许的流量来源
  - from:
    # 来源条件1:同一个namespace内、标签 run=test 的Pod
    - podSelector:
        matchLabels:
          run: test
    # 允许访问的端口:TCP 80
    ports:
    - protocol: TCP
      port: 80
# 创建network policy
root@master30~ 11:02:42# kubectl apply -f netpol.yaml
root@master30~/network 11:03:22# kubectl get netpol
NAME                POD-SELECTOR   AGE
my-network-policy   run=web1       7s

# 同一namespace中test可以访问web1,web2不可以访问web1
root@master30~ 10:29:14# kubectl run test --image=docker.io/library/busybox --image-pull-policy=IfNotPresent sleep 3600

root@master30~/network 11:04:01# kubectl exec test -- wget web1
Connecting to web1 (10.102.169.176:80)
saving to 'index.html'
index.html           100% |********************************|    11  0:00:00 ETA


# 修改现有标签
root@master30~/network 11:07:43# kubectl label pod test run=web2 --overwrite
root@master30~/network 11:07:54#  kubectl get pods --show-labels
NAME   READY   STATUS    RESTARTS   AGE   LABELS
test   1/1     Running   0          37m   run=web2
web1   1/1     Running   0          41m   run=web1
web2   1/1     Running   0          40m   run=web2

# 只能解析ip,但是流量无法到达目标pod
root@master30~/network 11:07:59# kubectl exec test -- wget web1.web
Connecting to web1.web (10.102.169.176:80)

# 不同 namespace 中pod,即使标签满足也不可以访问
root@master30~/network 11:09:21# kubectl exec test -n hgq -- wget web1.web
Connecting to web1.web (10.102.169.176:80)

分析

  • 场景 1:test 标签 run=test,能正常 wget web1
    Pod test 标签 run=test,完全匹配 NetPol 的 podSelector 放行条件,流量放行,请求成功。
  • 场景 2:修改 test 标签为 run=web2,wget 卡住超时
    执行 kubectl label pod test run=web2 --overwrite 后:
    • DNS 解析依然正常:CoreDNS 解析 Service 不受 NetworkPolicy 管控,所以能拿到 web1 的 ClusterIP;
    • TCP 连接被拦截:网络层拒绝流量,wget 卡在连接阶段,无法建立连接。
    • 原因:此时 test 的标签不再是 run=test,不符合放行规则,入站流量被拒绝。
  • 场景 3:其他 Pod(web2)本身标签 run=web2,同样无法访问 web1
    逻辑同上,标签不匹配,直接拦截。
  • 场景 4:跨命名空间 Pod(-n hgq 的 test)就算标签 run=test 也访问不通
    • 关键点:podSelector 只匹配当前 NetworkPolicy 所在 namespace 的 Pod,跨命名空间 Pod 就算标签一致,也不满from.podSelector 条件。
    • 如果想要放开跨命名空间访问,必须额外增加 namespaceSelector 规则

示例2允许同一namespace中所有pod访问具有标签run: web1的pod 80端口。

matchLabels中不使用任何标签,则允许同一ns中所有pod。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: my-network-policy
spec:
  podSelector:
    matchLabels:
      run: web1
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector: {}
    # 或者将以上一行改为两行
    # - podSelector:
    #     matchLabels:
    ports:
    - protocol: TCP
      port: 80
# 应用策略
root@master30~/network 11:45:04# kubectl apply -f netpol.yaml

# 同一namespace中所有pod都可以访问web1
root@master30~/network 11:46:45# kubectl exec test -- wget web1
Connecting to web1 (10.99.207.36:80)
saving to 'index.html'
index.html           100% |********************************|    11  0:00:00 ETA
'index.html' saved

root@master30~/network 11:46:47# kubectl exec web2 -- curl web1
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    11  100    11    0     0    472      0 --:--:-- --:--:-- --:--:--   478
hello web1

# 不同namespace中pod不可以访问web1
root@master30~/network 11:47:44# kubectl exec test -n hgq -- wget web1.web
Connecting to web1.web (10.99.207.36:80)
根据 pod 所属 ns 限定⭐️

用于限定,来源于其他namespace中pod。

示例1允许具有标签project: myproject的namespace中所有pod,访问当前ns中具有标签run: web1的pod 80端口。

root@master30~/network 11:48:53# vim netpol.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: my-network-policy
  namespace: web
spec:
  podSelector:
    matchLabels:
      run: web1
  policyTypes:
  - Ingress
  ingress:
  - from:
  	#匹配整个 ns 所有 Pod
    - namespaceSelector:
        matchLabels:
          project: myproject
    ports:
    - protocol: TCP
      port: 80
# 应用策略
root@master30~/network 11:49:26# kubectl apply -f netpol.yaml

# 此时namespace-web中pod和namespace-hgq中pod都无法访问web1
root@master30~/network 11:49:46# kubectl exec test -- wget web1
root@master30~/network 11:50:31# kubectl exec -n hgq test -- wget web1.web

# 给namespace-hgq添加标签project=myproject,此时namespace-hgq中pod可以访问web1.web
# 添加的是namespace标签
root@master30~/network 11:50:46# kubectl label namespaces hgq project=myproject
root@master30~/network 11:52:24# kubectl exec -n hgq test -- wget web1.web
Connecting to web1.web (10.99.207.36:80)
saving to 'index.html'
index.html           100% |********************************|    11  0:00:00 ETA
'index.html' saved

示例2允许所有namespace中所有pod,访问当前ns中具有标签run: web1的pod 80端口。

root@master30:~# vim netpol.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: my-network-policy
  namespace: web
spec:
  podSelector:
    matchLabels:
      run: web1
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector: {}
    ports:
    - protocol: TCP
      port: 80
# 创建network policy
root@master30~/network 12:23:00# kubectl apply -f netpol.yaml

# 所有namespace中所有pod都可以访问web1
root@master30~/network 12:23:46# kubectl exec test -- wget web1
Connecting to web1 (10.99.207.36:80)
saving to 'index.html'
index.html           100% |********************************|    11  0:00:00 ETA
'index.html' saved

root@master30~/network 12:24:42# kubectl exec -n hgq test -- wget web1.web
Connecting to web1.web (10.99.207.36:80)
saving to 'index.html'
index.html           100% |********************************|    11  0:00:00 ETA
'index.html' saved
根据 pod IP 限定

示例1允许网段172.17.0.0/16但不包括子网172.17.1.0/24中主机,访问具有标签run: web1的pod 80端口。

root@master30~/network 12:25:15# vim netpol.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: my-network-policy
  namespace: web
spec:
  podSelector:
    matchLabels:
      run: web1
  policyTypes:
  - Ingress
  ingress:
  - from:
    - ipBlock:
        # 放行 网段10.1.8.0/24
        cidr: 10.1.8.0/24
        # 不放行网段
        except:
        - 10.1.8.128/26
    ports:
    - protocol: TCP
      port: 80
# 应用策略
root@master30~/network 12:25:29# kubectl apply -f netpol.yaml

# 访问失败
root@master30~/network 12:26:11# kubectl exec test -- wget web1
root@master30~/network 12:26:32# kubectl exec -n hgq test -- wget web1.web
root@master30~/network 12:27:00# curl 10.1.8.30:30181
root@master30~/network 12:27:50# curl 10.1.8.32:30181

# 访问成功
root@master30~/network 12:27:19# curl 10.1.8.31:30181
hello web1

# 理论上:10.1.8.0/24网段中主机都可以通过集群任意节点访问web1
# 实际测试:只可以通过pod所在主机的IP访问web1 (10.1.8.31:30181),不可以访问10.1.8.30:30181或10.1.8.32:30181

从实验测试结果来看,根据网段控制来源还不完善。

如果想放行所有主机,阻止部分主机,规则如下:

    - ipBlock:
        cidr: 0.0.0.0/0
        except:
        - 10.1.8.0/26
不限定端口
root@master30:~# vim netpol.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: my-network-policy
  namespace: web
spec:
  podSelector:
    matchLabels:
      run: web1
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          run: test
# 应用策略
root@master30~/network 12:30:29# kubectl apply -f netpol.yaml

root@master30~/network 12:30:40# kubectl get pods -o wide
NAME   READY   STATUS    RESTARTS      AGE   IP              NODE                 NOMINATED NODE   READINESS GATES
test   1/1     Running   1 (15m ago)   75m   10.224.96.231   worker32.hgq.cloud   <none>           <none>
web1   1/1     Running   0             76m   10.224.128.57   worker31.hgq.cloud   <none>           <none>
web2   1/1     Running   0             76m   10.224.96.230   worker32.hgq.cloud   <none>           <none>

# 此时满足条件的pod可以ping通pod地址
root@master30~/network 12:33:18# kubectl run --rm -it ubuntu -l run=test --image ubuntu -- bash
If you don't see a command prompt, try pressing enter.
root@ubuntu:/# apt update
root@ubuntu:/# apt install -y iputils-ping
root@ubuntu:/# ping -c1 10.224.128.57
PING 10.224.128.57 (10.224.128.57) 56(84) bytes of data.
64 bytes from 10.224.128.57: icmp_seq=1 ttl=63 time=0.129 ms

--- 10.224.128.57 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.129/0.129/0.129/0.000 ms

root@ubuntu:/# ping web1 -c1
PING web1.web.svc.cluster.local (10.99.207.36) 56(84) bytes of data.

--- web1.web.svc.cluster.local ping statistics ---
1 packets transmitted, 0 received, 100% packet loss, time 0ms

root@ubuntu:/# exit

# 仍然 ping不通 service,因为service只开放80端口
限定端口范围
root@master30~/network 12:36:05# vim netpol.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: my-network-policy
  namespace: web
spec:
  podSelector:
    matchLabels:
      run: web1
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          run: test
    ports:
    - protocol: TCP
      port: 32000
      endPort: 32768
不限定项目中特定pod

设置podSelector: {},则针对namespace中所有pod。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: my-network-policy
  namespace: web
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
  - from:
......
多条件规则

只要有一个满足条件就可以访问pod。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: my-network-policy
  namespace: web
spec:
  podSelector:
    matchLabels:
      run: web1
  policyTypes:
  - Ingress
  ingress:
  - from:
    - ipBlock:
        cidr: 10.1.1.0/24
    - namespaceSelector:
        matchLabels:
          project: myproject
    - podSelector:
        matchLabels:
          run: test
    ports:
    - protocol: TCP
      port: 80
# 创建network policy
root@master30:~# kubectl apply -f netpol.yaml

# namespace-web中pod-test可以访问web1
root@master30:~# kubectl exec test -- wget web1
Hello web1

默认策略

默认允许所有入站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all-ingress
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
  - {}
默认拒绝所有入站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
spec:
  podSelector: {}
  policyTypes:
  - Ingress

此策略可以确保即使容器没有选择其他任何 NetworkPolicy,也仍然可以被隔离。 此策略不会更改默认的出口隔离行为。

默认允许所有出站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all-egress
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - {}
默认拒绝所有出站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
spec:
  podSelector: {}
  policyTypes:
  - Egress

此策略可以确保即使没有被其他任何 NetworkPolicy 选择的 Pod 也不会被允许流出流量。 此策略不会更改默认的入站流量隔离行为。

默认拒绝所有入口和所有出站流量
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

此策略可以确保即使没有被其他任何 NetworkPolicy 选择的 Pod 也不会被 允许入站或出站流量。

无法完成的工作

  • 特定于节点的策略

  • 基于名字的策略。

  • 实现适用于所有命名空间或 Pods 的默认策略。

  • 显式地拒绝策略的能力。NetworkPolicy 的模型默认采用拒绝操作, 其唯一的能力是添加允许策略。

  • **禁止本地回路或指向宿主的网络流量。**Pod 目前无法阻塞 localhost 访问, 它们也无法禁止来自所在节点的访问请求)。

Kubernetes Pod Scheduler

学习参考:调度、抢占和驱逐

环境准备

root@master30~/scheduler 12:38:32# kubectl create ns scheduler
root@master30~/scheduler 13:42:35# kubens scheduler

调度介绍

Kubernetes 调度是指将 Pod 放置到合适的节点上的过程。

kube-scheduler 是 Kubernetes 集群的默认调度器,通过监测(Watch)机制发现集群中未被调度到节点上的 Pod,并将这些未调度的 Pod 调度到一个合适的节点上来运行。

调度过程

Kube-scheduler 选择一个最佳节点来运行新创建的或尚未调度(unscheduled)的 Pod。 由于 Pod 中的容器和 Pod 本身可能有不同的要求,调度程序会过滤掉任何不满足 Pod 特定调度需求的节点。

调度术语

  • 满足 Pod 调度请求的所有节点称之为 可调度节点

  • 调度器将 pod 调度到特定节点的这个过程叫做 绑定

Kubernetes 调度过程:

  1. 过滤,将满足 Pod 调度需求的所有节点选出来。 例如,PodFitsResources 过滤函数会检查候选节点的可用资源能否满足 Pod 的资源请求。 在过滤之后,得出一个节点列表,里面包含了所有可调度节点;通常情况下, 这个节点列表包含不止一个节点。如果这个列表是空的,代表这个 Pod 不可调度。
  2. 打分,根据当前启用的打分规则,调度器会给每一个可调度节点进行打分,调度器将 Pod 调度到得分最高的节点上。 如果存在多个得分最高的节点,kube-scheduler 会从中随机选取一个。

调度器步骤: 找到匹配 labelSelector 的目标 Pod; 获取目标 Pod 所在节点的 topologyKey 对应标签值; 只在相同标签值的节点组内执行亲和 / 反亲和判断。

在做调度决定时需要考虑的因素包括:

  • 单独和整体的资源请求
  • 硬件/软件/策略限制
  • 亲和以及反亲和要求
  • 数据局部性
  • 负载间的干扰
  • 等等。

kubernetes 使用以下两种方式配置调度器的过滤和打分行为:

  1. 调度策略,配置过滤所用的 断言(Predicates) 和打分所用的 优先级(Priorities)
  2. 调度配置,允许配置实现不同调度阶段的插件, 包括:QueueSortFilterScoreBindReservePermit 等等。

过滤

以下**断言(Predicates)**用于主机过滤:

  • PodFitsHostPorts:检查节点是否有空闲端口(网络协议类型)用于 Pod 请求的 Pod 端口。
  • PodFitsHost:检查 Pod 是否通过其主机名指定特定节点。
  • PodFitsResources: 检查 Node 是否有空闲资源(例如 CPU 和内存)来满足 Pod 的要求。
  • MatchNodeSelector: 检查 Pod 的 Node Selector匹配节点的 标签.
  • NoVolumeZoneConflict: 评估是否 考虑到该存储的故障区域限制,Pod 请求在节点上可用。
  • NoDiskConflict:评估 Pod 是否可以根据它请求的卷以及已经挂载的卷安装在节点上。
  • MaxCSIVolumeCount: 决定多少 CSI 应附加卷,以及是否超过配置的限制。
  • PodToleratesNodeTaints: 检查Pod 的 toleration 可以容忍节点的 taints
  • CheckVolumeBinding:评估 Pod 是否因它请求的卷而适合。这适用于绑定和未绑定 PVCs.

计分

以下 **优先级 **Priorities 用于主机评分:

  • SelectorSpreadPriority: 跨主机传播 Pod,考虑属于相同的 Pod 服务, 状态集 或者 副本集
  • InterPodAffinityPriority:实现首选的 pod 间亲和性和反亲和性
  • LeastRequestedPriority:支持请求资源较少的节点。换句话说,节点上放置的 Pod 越多,这些 Pod 使用的资源越多,该策略给出的排名就越低。
  • MostRequestedPriority:支持请求资源最多的节点。此策略将使计划的 Pod 适合运行整个工作负载集所需的最少数量的节点。
  • RequestedToCapacityRatioPriority:使用默认资源评分函数形状创建基于 requestsToCapacity 的 ResourceAllocationPriority。
  • BalancedResourceAllocation:支持资源使用均衡的节点。
  • NodePreferAvoidPodsPriority:根据节点注释对节点进行优先级排序 scheduler.alpha.kubernetes.io/preferAvoidPods。您可以使用它来暗示两个不同的 Pod 不应在同一个节点上运行。
  • NodeAffinityPriority:根据PreferredDuringSchedulingIgnoredDuringExecution 中指示的节点关联性调度首选项对节点进行优先级排序。您可以在将 Pod 分配给节点中阅读更多相关信息。
  • TaintTolerationPriority:根据节点上不可容忍污点的数量,为所有节点准备优先级列表。此策略会在考虑该列表的情况下调整节点的等级。
  • ImageLocalityPriority: 优先选择已经拥有 容器镜像 对于本地缓存的 Pod。
  • ServiceSpreadingPriority:对于给定的 Service,此策略旨在确保 Service 的 Pod 运行在不同的节点上。它倾向于调度到没有已分配服务的 Pod 的节点上。总体结果是服务对单个节点故障变得更有弹性。
  • EqualPriority:给所有节点一个相等的权重。
  • EvenPodsSpreadPriority:实现首选 pod 拓扑扩展约束

控制 pod 运行位置

参考学习:将 Pod 指派给节点

你可以约束一个 Pod 只能在特定的 节点 上运行。 通常这样的约束不是必须的,因为调度器将自动进行合理的放置(比如,将 Pod 分散到节点上, 而不是将 Pod 放置在可用资源不足的节点上等等)。不过有些情况我们希望将Pod部署到指定的Node, 比如将有大量磁盘I/O的Pod部署到配置了SSD的Node; 或者Pod需要GPU, 需要运行在配置了GPU的节点上。

你可以使用下列方法中的任何一种来选择 Kubernetes 对特定 Pod 的调度:

nodeName

nodeName 是 PodSpec 的一个字段,其值是节点名称,调度器将Pod调度到给定节点上运行 。

nodeName 是节点选择约束的最简单方法,也是优先级最高的方法,通常不使用。

使用 nodeName 选择节点的一些限制:

  • 如果指定的节点不存在,Pod 将不会运行,甚至会被自动删除。

  • 如果指定的节点没有资源来容纳 Pod,Pod 将会调度失败,并且显示实际原因。

    例如:OutOfmemory 或 OutOfcpu。

  • 云环境中的节点名称并非总是可预测或稳定的。

示例:

示例:

root@master30~/scheduler 13:55:25# kubectl create deployment webapp --image nginx --replicas 5 -o yaml --dry-run=client > deploy-webapp.yaml
root@master30~/scheduler 13:57:29# vim deploy-webapp.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: webapp
  name: webapp
spec:
  replicas: 5
  selector:
    matchLabels:
      app: webapp
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: webapp
    spec:
      # 添加nodeName配置
      nodeName: worker32.hgq.cloud
      containers:
      - image: nginx
        name: nginx
        resources: {}
status: {}
root@master30~/scheduler 14:23:57# kubectl apply -f deploy-webapp.yaml
root@master30~/scheduler 14:24:12# kubectl get pods -o wide | awk '{print$1,$7}'
NAME NODE
webapp-55c6b896bc-8q7fc worker32.hgq.cloud
webapp-55c6b896bc-j76rl worker32.hgq.cloud
webapp-55c6b896bc-q95fz worker32.hgq.cloud
webapp-55c6b896bc-rm5n8 worker32.hgq.cloud
webapp-55c6b896bc-wxwzq worker32.hgq.cloud

nodeSelector

nodeSelector 是 Pod.Spec 的一个字段。 它包含键值对的映射。为了使 pod 可以在某个节点上运行,该节点的标签中 必须包含这里的每个键值对(它也可以具有其他标签)。

**最常见的用法的是使用label。**label是key-value对, 各种资源都可以设置label, 灵活添加各种自定义属性。

提示:nodeSelector 是节点选择约束的推荐形式。

# 查看node标签
root@master30~/scheduler 13:59:05# kubectl get node --show-labels
NAME                 STATUS   ROLES           AGE     VERSION   LABELS
master30.hgq.cloud   Ready    control-plane   6d20h   v1.30.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=master30.hgq.cloud,kubernetes.io/os=linux,node-role.kubernetes.io/control-plane=,node.kubernetes.io/exclude-from-external-load-balancers=
worker31.hgq.cloud   Ready    <none>          6d20h   v1.30.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=worker31.hgq.cloud,kubernetes.io/os=linux
worker32.hgq.cloud   Ready    <none>          6d20h   v1.30.2   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=worker32.hgq.cloud,kubernetes.io/os=linux

# 标注worker31 disktype是ssd
root@master30:~# kubectl label node worker31.hgq.cloud disktype=ssd

root@master30~/scheduler 14:00:13# kubectl get node -L disktype
NAME                 STATUS   ROLES           AGE     VERSION   DISKTYPE
master30.hgq.cloud   Ready    control-plane   6d20h   v1.30.2
worker31.hgq.cloud   Ready    <none>          6d20h   v1.30.2   ssd
worker32.hgq.cloud   Ready    <none>          6d20h   v1.30.2
# -L(--label-columns):把指定标签 disktype 单独作为一列展示输出

在Pod模板的spec里通过nodeSelector指定将此Pod部署到具有label为 disktype=ssd 的Node上。

root@master30~/scheduler 14:00:28# vim deploy-webapp.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: webapp
  name: webapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      # 添加 nodeSelector
      nodeSelector:
        disktype: ssd
      containers:
      - image: nginx
        name: nginx
root@master30~/scheduler 14:01:24# kubectl apply -f deploy-webapp.yaml
root@master30~/scheduler 14:01:40# kubectl get pods -o wide | awk '{print$1,$7}'
NAME NODE
webapp-7d675f9d9c-7mx6s worker31.hgq.cloud
webapp-7d675f9d9c-d7clv worker31.hgq.cloud
webapp-7d675f9d9c-hpm2w worker31.hgq.cloud
webapp-7d675f9d9c-mxfsd worker31.hgq.cloud
webapp-7d675f9d9c-p8j2t worker31.hgq.cloud

# 删除worker31标签,并不会在新的node上部署pod,即现有已经运行在 worker31 上的 Pod 不会自动漂移、不会自动调度到其他节点,减号‘-’代表删除
root@master30~/scheduler 14:02:25# kubectl label node worker31.hgq.cloud disktype-

root@master30~/scheduler 14:02:57# kubectl get node -L disktype
NAME                 STATUS   ROLES           AGE     VERSION   DISKTYPE
master30.hgq.cloud   Ready    control-plane   6d20h   v1.30.2
worker31.hgq.cloud   Ready    <none>          6d20h   v1.30.2
worker32.hgq.cloud   Ready    <none>          6d20h   v1.30.2 

# 删除模版中nodeSelector属性,会自动触发重新部署。

Affinity and AntiAffinity

亲和性功能包含两种类型的亲和性,即“节点亲和性”和“Pod 间亲和性/反亲和性”。

spec:
  affinity:
    podAffinity:        # Pod亲和
    podAntiAffinity:    # Pod反亲和(和podAffinity同级)
    nodeAffinity:       # 节点亲和

三个平级子字段:

  1. podAffinity:希望和某类 Pod 部署在同一拓扑域
  2. podAntiAffinity:希望避开某类 Pod,不要放同一拓扑域
  3. nodeAffinity:根据节点标签选机器
nodeAffinity

概念上节点亲和性类似于 nodeSelector,它使你可以根据节点上的标签来约束 Pod 可以调度到哪些节点。

节点亲和性有两种:

  • requiredDuringSchedulingIgnoredDuringExecution ,指定 必须 将 Pod 调度到满足规则的节点上,就像 nodeSelector
  • preferredDuringSchedulingIgnoredDuringExecution,指定 优先 将 Pod 调度到满足规则的节点上,但不会强制执行调度。

注意:如果节点的标签在运行时发生变更,那么 Pod 将仍然继续在该节点上运行。

使用 Pod 规约中的 .spec.affinity.nodeAffinity 字段来设置节点亲和性。

示例:

root@master30~/scheduler 14:08:06# vim deploy-nodeaffinity.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: web
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      affinity:
        nodeAffinity:
          #硬约束,硬性强制规则,不满足则 Pod 无法调度,永远处于 Pending
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: CPU
                operator: In
                values:
                - L1
                - L2
          #软约束,软性偏好,优先调度满足条件节点;无满足节点时,可调度其他节点
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 1
            preference:
              matchExpressions:
              - key: MEM
              	#operator是匹配运算符,In是节点对应标签的值必须在 values 列表里
                operator: In
                values:
                - L1
                - L2
      containers:
      - image: docker.io/library/nginx
        name: nginx

第一层硬性门槛:节点必须有 CPU=L1/L2,否则直接不调度

第二层择优:在符合 CPU 条件的节点中,优先选择带 MEM=L1/L2 的节点

若所有符合 CPU 的节点都没有 MEM 标签,Pod 依然可以正常调度

示例说明:

  • 硬亲和不满足 = 不能调度;软亲和不满足 = 能调度,只是不优先。

  • 此节点亲和性规则表示:Pod 只能放置在具有标签键 CPU 且标签值为 L1L2 的节点上。 另外,在满足这些标准的节点中,具有标签键为 MEM 且标签值为 L1L2 的节点应该优先使用。

  • preferredDuringSchedulingIgnoredDuringExecution 中的 weight 字段值的范围是 1-100。 对于每个符合所有调度要求(资源请求、RequiredDuringScheduling 亲和性表达式等) 的节点,调度器将遍历该字段的元素来计算总和,并且如果节点匹配对应的 MatchExpressions,则添加“权重”到总和。 然后将这个评分与该节点的其他优先级函数的评分进行组合。 总分最高的节点是最优选的。

  • 你可以使用 operator 字段来为 Kubernetes 设置在解释规则时要使用的逻辑操作符。operator(操作符)支持: InNotInExistsDoesNotExistGtLt

  • 你可以使用 NotInDoesNotExist 来实现节点反亲和性行为,或者使用 节点污点 将 Pod 从特定节点中驱逐。

  • 如果你同时指定了 nodeSelectornodeAffinity两者必须都要满足, 才能将 Pod 调度到候选节点上。

  • 如果你指定了多个与 nodeAffinity 类型关联的 nodeSelectorTerms,则 如果其中一个 nodeSelectorTerms 满足的话,pod将可以调度到节点上。

  • 如果你指定了多个与 nodeSelectorTerms 关联的 matchExpressions,则 只有当所有 matchExpressions 满足的话,Pod 才会可以调度到节点上。

如果你修改或删除了 pod 所调度到的节点的标签,Pod 不会被删除。 换句话说,亲和性选择只在 Pod 调度期间有效。

验证1:节点未打标签

root@master30~/scheduler 14:08:46# kubectl apply -f deploy-nodeaffinity.yaml

root@master30~/scheduler 14:08:54# kubectl get pods
NAME                  READY   STATUS    RESTARTS   AGE
web-c74fd5fbd-4xt52   0/1     Pending   0          5s
web-c74fd5fbd-p49rr   0/1     Pending   0          5s

# 删除deploy
root@master30~/scheduler 14:08:59# kubectl delete deployments.apps web

验证2:节点打标签CPU

root@master30~/scheduler 14:09:24# kubectl label nodes worker31.hgq.cloud CPU=L1
root@master30~/scheduler 14:10:00# kubectl label node worker32.hgq.cloud CPU=L2
root@master30~/scheduler 14:10:13# kubectl apply -f deploy-nodeaffinity.yaml

# 节点平均分布
root@master30~/scheduler 14:10:24# kubectl get pods -o wide --no-headers | awk '{print$1,$3,$7}'
web-c74fd5fbd-687n2 Running worker31.hgq.cloud
web-c74fd5fbd-shxpr Running worker32.hgq.cloud

# 删除deploy
root@master30~/scheduler 14:12:09# kubectl delete deployments.apps web

验证3:worker32节点打标签MEM

root@master30~/scheduler 14:12:15# kubectl label node worker32.hgq.cloud MEM=L2
root@master30~/scheduler 14:12:38# kubectl apply -f deploy-nodeaffinity.yaml

# 只在worker32上运行
root@master30~/scheduler 14:13:23# kubectl get pods -o wide --no-headers | awk '{print $1,$3,$7}'
web-c74fd5fbd-br9nd Running worker32.hgq.cloud
web-c74fd5fbd-nx649 Running worker32.hgq.cloud

清理环境

root@master30~/scheduler 14:13:32# kubectl delete deployments.apps web
root@master30~/scheduler 14:14:10# kubectl label node worker31.hgq.cloud CPU-
root@master30~/scheduler 14:14:23# kubectl label node worker32.hgq.cloud CPU-
root@master30~/scheduler 14:14:28# kubectl label node worker32.hgq.cloud MEM-

节点亲和性权重

用户可以为 preferredDuringSchedulingIgnoredDuringExecution 亲和性类型的每个实例设置 weight 字段,其取值范围是 1 到 100。 当调度器找到能够满足 Pod 的其他调度请求的节点时,调度器会遍历节点满足的所有的偏好性规则, 并将对应表达式的 weight 值加和。

最终的加和值会添加到该节点的其他优先级函数的评分之上。 在调度器为 Pod 作出调度决定时,总分最高的节点的优先级也最高。

例如,考虑下面的 Pod 规约:

apiVersion: v1
kind: Pod
metadata:
  name: with-affinity-anti-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: kubernetes.io/os
            operator: In
            values:
            - linux
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: label-1
            operator: In
            values:
            - key-1
      - weight: 50
        preference:
          matchExpressions:
          - key: label-2
            operator: In
            values:
            - key-2
  containers:
  - name: with-node-affinity
    image: docker.io/library/nginx

如果存在两个候选节点,都满足 preferredDuringSchedulingIgnoredDuringExecution 规则, 其中一个节点具有标签 label-1: key-1,另一个节点具有标签 label-2: key-2, 调度器会考察各个节点的 weight 取值,并将该权重值添加到节点的其他得分值之上。

Inter-pod affinity and anti-affinity(Pod间亲和性与反亲和性)

Pod 间 亲和性反亲和性 提供基于已经在节点上运行的 Pod 的标签来约束 Pod 可以调度到的节点,而不是基于节点上的标签。

Pod 间亲和性与反亲和性规则的格式为,如果 X 节点上已经运行了一个或多个满足规则 Y 的 Pod:

  • 在亲和性的情况下,Pod 应该运行在 X 节点。
  • 在反亲和性的情况下,Pod 不应该运行在 X 节点。

说明:

  • 这里的 X 可以是节点、机架、云提供商可用区或地理区域或类似的拓扑域。你可以使用 topologyKey 来表示它,topologyKey 是节点标签的键以便系统 用来表示这样的拓扑域。
  • Y 则是 Kubernetes 尝试满足的规则,通过标签选择算符 的形式来表达规则(Y)。

注意:

  • Pod 间亲和性与反亲和性会消耗大量计算资源,可能会显著减慢大规模集群中的调度。 我们不建议在超过数百个节点的集群中使用它们。
  • Pod 反亲和性需要对节点进行一致的标记,即集群中的每个节点必须具有适当的标签匹配 topologyKey。如果某些或所有节点缺少指定的 topologyKey 标签,可能会导致意外行为。

Pod 间亲和性与反亲和性的类型

  • requiredDuringSchedulingIgnoredDuringExecution,例如将两个通信非常频繁的 Pod 调度到同一个可用区内。
  • preferredDuringSchedulingIgnoredDuringExecution,例如将同一服务的多个 Pod 调度到不同可用区中。

使用 Pod 规约中的 .affinity.podAffinity 字段设置Pod 间亲和性。

使用 Pod 规约中的 .affinity.podAntiAffinity 字段设置Pod 间反亲和性。

语法说明

本示例定义了一条 Pod 亲和性规则和一条 Pod 反亲和性规则。

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-podaffinity
spec:
  affinity:
  	#Pod亲和性规则
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: security
            operator: In
            values:
            - S1
        topologyKey: topology.kubernetes.io/zone
    #Pod反亲和性规则
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchExpressions:
            - key: security
              operator: In
              values:
              - S2
          topologyKey: topology.kubernetes.io/zone
  containers:
  - name: pod-with-podaffinity
    image: docker.io/library/nginx
  • 亲和性规则规定,只有节点属于特定的 区域 且该区域中的其他 Pod 已打上 security=S1 标签时,调度器才可以将示例 Pod 调度到此节点上。 例如,如果我们有一个具有指定区域(称之为 “Zone V”)的集群,此区域由带有 topology.kubernetes.io/zone=V 标签的节点组成,那么只要 Zone V 内已经至少有一个 Pod 打了 security=S1 标签, **调度器就可以将此 Pod 调度到 Zone V 内的任何节点。**相反,如果 Zone V 中没有带有 security=S1 标签的 Pod, 则调度器不会将示例 Pod 调度给该区域中的任何节点。
  • 反亲和性规则规定,如果节点属于特定的 区域 且该区域中的其他 Pod 已打上 security=S2 标签,则调度器应尝试避免将 Pod 调度到此节点上。 例如,如果我们有一个具有指定区域(我们称之为 “Zone R”)的集群,此区域由带有 topology.kubernetes.io/zone=R 标签的节点组成,只要 Zone R 内已经至少有一个 Pod 打了 security=S2 标签, 调度器应避免将 Pod 分配给 Zone R 内的任何节点。相反,如果 Zone R 中没有带有 security=S2 标签的 Pod, 则反亲和性规则不会影响将 Pod 调度到 Zone R。

补充说明:

  • Pod 亲和性与反亲和性的合法操作符( operator)有 InNotInExistsDoesNotExist
  • 原则上,topologyKey 可以是任何合法的标签键。 出于性能和安全考虑,topologyKey 受到一些限制:
    1. 对于 Pod 亲和性而言,在 requiredDuringSchedulingIgnoredDuringExecutionpreferredDuringSchedulingIgnoredDuringExecution 中,topologyKey 不允许为空。
    2. 对于 Pod 反亲和性而言,requiredDuringSchedulingIgnoredDuringExecutionpreferredDuringSchedulingIgnoredDuringExecution 中,topologyKey 不允许为空。
    3. 除上述情况外,topologyKey 可以是任何合法的标签键。
示例 1:根据节点拓扑–亲和性调度
# node节点打标签
root@master30~/scheduler 15:08:48# kubectl label node worker31.hgq.cloud topology.kubernetes.io/zone=v
root@master30~/scheduler 15:09:34# kubectl label node worker32.hgq.cloud topology.kubernetes.io/zone=v

root@master30~/scheduler 15:13:36# kubectl get nodes -L topology.kubernetes.io/zone
NAME                 STATUS   ROLES           AGE     VERSION   ZONE
master30.hgq.cloud   Ready    control-plane   6d22h   v1.30.2
worker31.hgq.cloud   Ready    <none>          6d22h   v1.30.2   v
worker32.hgq.cloud   Ready    <none>          6d22h   v1.30.2   v

# 创建两个pod
root@master30~/scheduler 15:14:16# vim pod-with-podaffinity.yaml
---
apiVersion: v1
kind: Pod
metadata:
  name: web
  labels:
    app: web
spec:
  containers:
  - name: nginx
    image: docker.io/library/nginx
    imagePullPolicy: IfNotPresent
  nodeName: worker31.hgq.cloud
---
apiVersion: v1
kind: Pod
metadata:
  name: pod-with-podaffinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      #匹配的是集群里其他已存在的 Pod 的标签
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - web
        #拓扑域:可用区
        topologyKey: topology.kubernetes.io/zone
  containers:
  - name: web
    image: docker.io/library/nginx
root@master30~/scheduler 15:52:34# kubectl apply -f pod-with-podaffinity.yaml
root@master30~/scheduler 16:13:07# kubectl describe pod pod-with-podaffinity | grep Node:
Node:             worker32.hgq.cloud/10.1.8.32

执行流程:

  1. 调度器先遍历集群所有已存在 Pod,筛选出带 app=web 的 Pod;
  2. 获取这些 Pod 所在节点的 topology.kubernetes.io/zone 标签值,拿到对应的可用区拓扑域
  3. 只会在同一个 zone 内的所有节点里挑选节点部署当前 Pod;
  4. 硬约束 required:该 zone 无空闲节点 → Pod 卡在 Pending,绝不跨 zone 调度。

**实验结果:**pod-with-podaffinity 调度到 worker32节点。

如果此时将work32节点的 topology.kubernetes.io/zone 标签删除,重新创建pod pod-with-podaffinity,会调度到哪个节点呢?

root@master30~/scheduler 16:13:10# kubectl delete pods pod-with-podaffinity --force
root@master30~/scheduler 16:14:20# kubectl label node worker32.hgq.cloud topology.kubernetes.io/zone-
root@master30~/scheduler 16:15:03# kubectl apply -f pod-with-podaffinity.yaml

root@master30~/scheduler 16:15:40# kubectl describe pod pod-with-podaffinity | grep Node:
Node:             worker31.hgq.cloud/10.1.8.31

# 清理环境
# 根据文件内容删除集群内对应资源,本地文件不动
root@master30~/scheduler 16:15:55# kubectl delete -f pod-with-podaffinity.yaml
root@master30~/scheduler 16:16:28# kubectl label  node worker31.hgq.cloud topology.kubernetes.io/zone-

**结论:**pod 亲和性调度必须满足两个条件。

  1. node 必须具有相应标签。这些node根据标签定义,既可以属于同一个机架,也可以属于同一个房间等物理区域。
  2. 在具有相应标签的 node上至少运行一个具有相应标签的pod。

Pod 间亲和性与反亲和性在与更高级别的集合(例如 ReplicaSets、StatefulSets、 Deployments 等)一起使用时,更加有用。 可以轻松配置一组应位于相同定义拓扑(例如,节点)中的工作负载。

示例 2:根据节点主机名–反亲和性调度

下面是一个简单 redis Deployment 的 YAML 代码段,它有三个副本和选择器标签 app=store。 Deployment 配置了 PodAntiAffinity,用来确保调度器不会将多个副本调度到单个节点上。

# 准备deployment
root@master30~/scheduler 19:33:29# vim deploy-with-podAntiAffinity.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: store
spec:
  selector:
    matchLabels:
      app: store
  replicas: 3
  template:
    metadata:
      labels:
        app: store
    spec:
      affinity:
      	#反亲和性
        podAntiAffinity:
          #硬约束,即不能和标签为app:store相同的Pod处于用一个节点中
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            #拓扑域:节点主机名
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: web-server
        image: docker.io/library/nginx
# 查看pod调度情况
root@master30~/scheduler 19:52:29# kubectl get pods -o wide --no-headers | awk '{print$1,$3,$7}'
store-5596cf4c84-h6szb Running worker32.hgq.cloud
store-5596cf4c84-pvnbt Running worker31.hgq.cloud
store-5596cf4c84-xw46g Pending <none>

调度器步骤:

  1. 找到匹配 labelSelector 的目标 Pod;
  2. 获取目标 Pod 所在节点的 topologyKey 对应标签值;
  3. 只在相同标签值的节点组内执行反亲和判断。

结果:

topologyKey: "kubernetes.io/hostname作用下,**每个节点只能运行一个pod。**因为每个节点的hostname都是自己的主机名,是唯一的。最后一个pod,找不到可用节点。

示例 3:多个应用亲和性和反亲和性调度

下面 webserver Deployment 的 YAML 代码段中配置了 podAntiAffinitypodAffinity,确保每个 web 服务器副本不会调度到单个节点上,同时所有副本与具有 app=store 选择器标签的 Pod 放置在一起。

# 缩容 deployment
root@master30~/scheduler 19:57:45# kubectl scale deployment store --replicas=1
root@master30~/scheduler 19:58:06# kubectl get pods -o wide --no-headers |awk '{print $1,$3,$7}'
store-5596cf4c84-h6szb Running worker32.hgq.cloud

# 准备新deployment
root@master30~/scheduler 19:58:30# vim deploy-with-multi-Affinity.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-store
spec:
  selector:
    matchLabels:
      app: web-store
  replicas: 1
  template:
    metadata:
      labels:
        app: web-store
    spec:
      affinity:
        podAntiAffinity:
          #硬约束,即不能和标签为app:web-store相同的Pod处于同一个节点中
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - web-store
            topologyKey: "kubernetes.io/hostname"
        podAffinity:
          #硬约束,必须调度到存在标签 app:store 的 Pod 的同一节点上
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - store
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: web-app
        image: docker.io/library/nginx
root@master30~/scheduler 20:00:55# kubectl apply -f deploy-with-multi-Affinity.yamld
root@master30~/scheduler 20:02:09# kubectl get pods -o wide --no-headers |awk '{print $1,$3,$7}'
store-5596cf4c84-h6szb Running worker32.hgq.cloud
web-store-7b4779c958-fw87t Running worker32.hgq.cloud

# 扩容:验证新副本位置
root@master30~/scheduler 20:03:42# kubectl scale deployment web-store --replicas=2
root@master30~/scheduler 20:04:01# kubectl get pods -o wide --no-headers | awk '{print$1,$3,$7}'
store-5596cf4c84-h6szb Running worker32.hgq.cloud
web-store-7b4779c958-9fdzn Pending <none>
web-store-7b4779c958-fw87t Running worker32.hgq.cloud
# worker31上没有匹配的pod,所以没有选择出来。
# worker32上在反亲和性作用下,不允许运行。

Taint 和 Toleration

学习参考:污点和容忍度

  • node 使用 节点污点(Taint),允许特定Pod在本机运行,未匹配的Pod则不能在该节点运行,打在 Node 上,用来排斥 Pod

  • pod 使用 Pod 容忍度(Toleration),允许被调度到带有与之匹配的污点的节点上,写在 Pod 上,用来接纳带污点的节点

污点和容忍度相互配合,可以用来避免 Pod 被分配到不合适的节点上。 每个节点上都可以应用一个或多个污点,这表示对于那些不能容忍这些污点的 Pod,是不会被该节点接受的。

思考:nodeAffinity 和 Taint 区别?

  • nodeAffinity,Pod 选 Node 的必要条件。
  • Taint,Node 选 Pod 的必要条件。

设置 Node Taints

节点污点标准格式:

key=value:effect

effect 类型只能三选一:

NoSchedule/PreferNoSchedule/NoExecute

使用命令 kubectl taint 给节点增加一个污点。

示例:给节点 worker31.hgq.cloud 增加一个污点,键名 CPU,键值 L1,效果 NoSchedule

root@master30~ 16:27:25# kubectl taint nodes master30.hgq.cloud CPU=L1:NoSchedule

CPU=L1:NoSchedule:

  • key=value:effect,即CPU是污点键,L1是污点值,而NoSchedule是污点策略

表示拥有和这个污点相匹配的tolerations的 Pod 才能够被分配到 worker31.hgq.cloud 这个节点。

若要移除worker污点,执行以下命令:

root@master30~ 16:51:10# kubectl taint nodes master30.hgq.cloud CPU:NoSchedule-

设置 Pod tolerations⭐️

在 Pod.Spec 中定义 Pod 的 tolerations。

示例

tolerations:
- key: "CPU"
  operator: "Equal"
  value: "L1"
  effect: "NoSchedule"

operator 可选值

  • Equal,精准匹配,默认值,容忍度和污点的键值对相同,则“匹配”。

  • Exists,存在匹配,此时容忍度不能指定 value,只要污点中存在 key ,则容忍度和污点相“匹配”,即污点只要 key=CPU,不管值是 L1/L2/L3,全部匹配。

      tolerations:
      - key: "CPU"
        operator: "Exists"
        effect: "NoSchedule"
    

effect 可选值

  • NoSchedule,除非具有匹配的容忍度规约,否则新的 Pod 不会被调度到带有污点的节点上。 当前正在节点上运行的 Pod 不会被驱逐。

    • 只拦截调度,不碰存量
    • 节点打了NoSchedule 污点,新建 Deployment/StatefulSet 等 Pod:无对应容忍,即effect的值不是NoSchedule,则调度会被拦截
    • 节点上早已运行的 Pod:哪怕没有容忍,照常运行,不会被赶走
  • PreferNoSchedule

    • 同样不驱逐存量 Pod,只影响新建调度
    • 优先级权重机制,不是强制拦截
      • 集群资源充足:调度器优先把 Pod 分配到无污点节点
      • 集群无其他空闲节点、资源耗尽:哪怕 Pod 无容忍,也会调度到该污点节点救急
  • NoExecute 影响已在节点上运行的 Pod,具体影响如下:

    • 强约束,管控新调度 + 驱逐存量运行 Pod,唯一带驱逐能力
    • 如果 Pod 不能容忍这类污点,会马上被驱逐。
    • 如果 Pod 能够容忍这类污点,但是在容忍度定义中没有指定 tolerationSeconds, 则 Pod 还会一直在这个节点上运行。
    • 如果 Pod 能够容忍这类污点,而且指定了 tolerationSeconds, 则 Pod 还能在这个节点上继续运行这个指定的时间长度。 这段时间过去后,节点生命周期控制器从节点驱除这些 Pod。
    • 如果 Pod不能容忍且还未在节点上运行,系统不会将 Pod 分配到该节点。
  • ,则可以与所有键名 CPU 的效果相匹配。

**注意:**如果容忍度的 key 为空且 operatorExists, 表示这个容忍度与任意的 key 、value 和 effect 都匹配,即这个容忍度能容忍任意 taint。

污点匹配逻辑

污点匹配三要素同时校验:

  1. key 相等
  2. value 匹配(Equal/Exists)
  3. effect 匹配
    • Pod容忍无 effect:自动兼容污点任意 effect;
    • Pod容忍有 effect:必须和污点 effect 一模一样才算匹配。

一个容忍度和一个污点 匹配⭐️

示例:

设置 node 污点:

root@master30~ 18:28:05# kubectl taint nodes worker31.hgq.cloud CPU=L1:NoSchedule
root@master30~ 17:21:45# kubectl taint nodes worker32.hgq.cloud CPU=L2:NoSchedule

创建 pod :

root@master30~ 17:22:06# vim deploy-with-tolerations.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: web
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      tolerations:
      - key: "CPU"
        operator: "Equal"
        value: "L3"
        effect: "NoSchedule"
      containers:
      - name: nginx
        image: docker.io/library/nginx
        imagePullPolicy: IfNotPresent
# 创建上述pod示例
root@master30~ 17:22:31# kubectl apply -f deploy-with-tolerations.yaml
root@master30~ 18:28:33# kubectl get pods
NAME                   READY   STATUS    RESTARTS   AGE
web-7d9d5f896c-n4qw9   0/1     Pending   0          4s
web-7d9d5f896c-pk74g   0/1     Pending   0          4s
# pod状态为Pending

# 事件表明3个节点上的污点与pod不匹配,所以无法创建pod
root@master30~ 18:29:03# kubectl describe pods web-7d9d5f896c-n4qw9 | grep ^Events -A10
Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  51s   default-scheduler  0/3 nodes are available: 1 node(s) had untolerated taint {CPU: L1}, 1 node(s) had untolerated taint {CPU: L2}, 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }. preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling.



# 删除 deployments,将容忍度改为CPU=L1
root@master30~ 18:29:35# kubectl delete deployments.apps web
root@master30~ 18:30:22# vim deploy-with-tolerations.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: web
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      tolerations:
      - key: "CPU"
        operator: "Equal"
        value: "L1"
        effect: "NoSchedule"
      containers:
      - name: nginx
        image: docker.io/library/nginx
        imagePullPolicy: IfNotPresent
root@master30~ 18:30:45# kubectl apply -f deploy-with-tolerations.yaml
root@master30~ 18:31:12# kubectl get pods -o wide
NAME                  READY   STATUS    RESTARTS   AGE   IP              NODE                 NOMINATED NODE   READINESS GATES
web-78945b5cc-5dmn8   1/1     Running   0          29s   10.224.128.40   worker31.hgq.cloud   <none>           <none>
web-78945b5cc-k56f5   1/1     Running   0          29s   10.224.128.34   worker31.hgq.cloud   <none>           <none>
# Pod调度成功,而且分配到了worker31

# 清理环境
root@master30~ 18:31:37# kubectl taint nodes worker31.hgq.cloud CPU:NoSchedule-
root@master30~ 18:32:14# kubectl taint nodes worker32.hgq.cloud CPU:NoSchedule-
root@master30~ 18:32:33# kubectl delete deployments.apps web

多个容忍度和多个污点 匹配

Kubernetes 可以给一个节点添加多个污点,也可以给一个 Pod 添加多个容忍度设置。

Kubernetes 处理多个污点和容忍度的过程就像一个过滤器:从一个节点的所有污点开始遍历, 过滤掉那些 Pod 中存在与之相匹配的容忍度的污点。余下未被过滤的污点的 effect 值决定了 Pod 是否会被分配到该节点,特别是以下情况:

  • 如果未被过滤的污点中存在至少一个 effect 值为 NoSchedule 的污点, 则 Kubernetes 不会将 Pod 分配到该节点。
  • 如果未被过滤的污点中不存在 effect 值为 NoSchedule 的污点, 但存在 effect 值为 PreferNoSchedule 的污点, 则 Kubernetes 尝试 不将 Pod 分配到该节点。
  • 如果未被过滤的污点中存在至少一个 effect 值为 NoExecute 的污点, 则 Kubernetes 不会将 Pod 分配到该节点(如果 Pod 还未在节点上运行), 或者将 Pod 从该节点驱逐(如果 Pod 已经在节点上运行)。

例如,某个节点添加了如下污点:

root@master30~ 19:15:27# kubectl taint nodes worker31.hgq.cloud CPU=L1:NoSchedule
root@master30~ 19:15:40# kubectl taint nodes worker31.hgq.cloud CPU=L1:NoExecute
root@master30~ 19:15:44# kubectl taint nodes worker31.hgq.cloud MEM=L2:NoSchedule

假定某个 Pod 有两个容忍度:

tolerations:
- key: "CPU"
  operator: "Equal"
  value: "L1"
  effect: "NoSchedule"
- key: "CPU"
  operator: "Equal"
  value: "L1"
  effect: "NoExecute"

在这种情况下,上述 Pod 不会被调度到上述节点,因为其没有容忍度和第三个污点相匹配。 但是如果在给节点添加上述污点之前,该 Pod 已经在上述节点运行, 那么它还可以继续运行在该节点上,因为第三个污点是三个污点中唯一不能被这个 Pod 容忍的。

示例:

设置 node 多个污点

root@master30~ 19:15:27# kubectl taint nodes worker31.hgq.cloud CPU=L1:NoSchedule
root@master30~ 19:15:40# kubectl taint nodes worker31.hgq.cloud CPU=L1:NoExecute
root@master30~ 19:15:44# kubectl taint nodes worker31.hgq.cloud MEM=L2:NoSchedule
root@master30~ 19:17:52# kubectl taint nodes worker32.hgq.cloud CPU=L2:NoSchedule

创建 Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: web
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      tolerations:
      - key: "CPU"
        operator: "Equal"
        value: "L1"
        effect: "NoSchedule"
      - key: "CPU"
        operator: "Equal"
        value: "L1"
        effect: "NoExecute"
      containers:
      - name: nginx
        image: docker.io/library/nginx
        imagePullPolicy: IfNotPresent
# 创建上述pod示例
root@master30~ 19:21:25# kubectl apply -f deploy-with-tolerations.yaml
root@master30~ 19:21:37# kubectl get pods
NAME                   READY   STATUS    RESTARTS   AGE
web-745f6fc44d-fdpgg   0/1     Pending   0          4s
web-745f6fc44d-hwl56   0/1     Pending   0          4s
#worker31 不能调度:虽然能容忍 CPU=L1 相关污点,但节点还有 MEM=L2:NoSchedule 污点,Pod 未容忍该污点,被拦截;
#worker32 不能调度:污点是 CPU=L2,Pod 容忍只匹配 CPU=L1,无法匹配,被拦截;


# 事件表明节点上的3个污点与pod不匹配,所以无法创建pod
root@master30~ 19:26:01# kubectl describe pods web-745f6fc44d-fdpgg | grep ^Events -A10
Events:
  Type     Reason            Age    From               Message
  ----     ------            ----   ----               -------
  Warning  FailedScheduling  4m42s  default-scheduler  0/3 nodes are available: 1 node(s) had untolerated taint {CPU: L2}, 1 node(s) had untolerated taint {MEM: L2}, 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: }. preemption: 0/3 nodes are available: 3 Preemption is not helpful for scheduling.


# 删除deployments,添加容忍度MEM=L2
root@master30~ 19:26:19# kubectl delete deployments.apps web
root@master30~ 19:26:35# vim deploy-with-tolerations.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: web
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      tolerations:
      - key: "CPU"
        operator: "Equal"
        value: "L1"
        effect: "NoSchedule"
      - key: "CPU"
        operator: "Equal"
        value: "L1"
        effect: "NoExecute"
      - key: "MEM"
        operator: "Equal"
        value: "L2"
        effect: "NoSchedule"
      containers:
      - name: nginx
        image: docker.io/library/nginx
        imagePullPolicy: IfNotPresent
root@master30~ 19:27:44# kubectl apply -f deploy-with-tolerations.yaml
root@master30~ 19:28:10# kubectl get pod -o wide
NAME                  READY   STATUS    RESTARTS   AGE   IP              NODE                 NOMINATED NODE   READINESS GATES
web-65bfcb4fc-jrt9k   1/1     Running   0          4s    10.224.128.37   worker31.hgq.cloud   <none>           <none>
web-65bfcb4fc-svvfd   1/1     Running   0          4s    10.224.128.36   worker31.hgq.cloud   <none>           <none>
# Pod 调度成功,而且分配到了worker31

# 清理环境
root@master30~ 19:28:14# kubectl taint nodes worker31.hgq.cloud CPU=L1:NoSchedule-
root@master30~ 19:28:42#  kubectl taint nodes worker31.hgq.cloud CPU=L1:NoExecute-
root@master30~ 19:28:46# kubectl taint nodes worker31.hgq.cloud MEM=L2:NoSchedule-
root@master30~ 19:28:48# kubectl taint nodes worker32.hgq.cloud CPU=L2:NoSchedule-
root@master30~ 19:28:52# kubectl delete deployments.apps web

Taint 和 Toleration 用例

通过污点和容忍度,可以灵活地让 Pod 避开 某些节点或者将 Pod 从某些节点驱逐。

下面是几个使用例子:

  • 用户专用节点:如果想将某些节点专门分配给特定的一组用户使用,可以给这些节点添加一个污点,例如 dedicated=groupName:NoSchedule 。然后给这组用户的 Pod 添加一个相对应的 toleration。 拥有上述容忍度的 Pod 就能够被分配到上述专用节点,同时也能够被分配到集群中的其它节点。

    如果希望这些 Pod 只能被分配到上述专用节点,那么还需要给这些专用节点添加一个和上述 污点类似的 label ,例如 dedicated=groupName ,同时还要给 Pod 增加节点亲和性,要求上述 Pod 只能被分配到添加了 dedicated=groupName 标签的节点上。

  • 配备了特殊硬件的节点:在部分节点配备了特殊硬件(比如 GPU)的集群中, 我们希望不需要这类硬件的 Pod 不要被分配到这些特殊节点,以便为后继需要这类硬件的 Pod 保留资源。 要达到这个目的,可以先给配备了特殊硬件的节点添加 taint ,例如 special=true:NoSchedule, 然后给使用这类特殊硬件的 Pod 添加一个相匹配的 toleration。

基于污点的驱逐

前文提到过污点 effect 值 NoExecute 会影响已经在节点上运行的 Pod:

  • 如果 Pod 不能忍受 effect 值为 NoExecute 的污点,那么 Pod 将马上被驱逐。
  • 如果 Pod 能够忍受 effect 值为 NoExecute 的污点,但是在容忍度定义中没有指定 tolerationSeconds,则 Pod 还会一直在这个节点上运行。
  • 如果 Pod 能够忍受 effect 值为 NoExecute 的污点,而且指定了 tolerationSeconds, 则 Pod 还能在这个节点上继续运行这个指定的时间长度。

当某种条件为真时,节点控制器会自动给节点添加一个污点。当前内置的污点包括:

  • node.kubernetes.io/not-ready:节点未准备好。这相当于节点状态 Ready 的值为 “False”。
  • node.kubernetes.io/unreachable:节点控制器访问不到节点. 这相当于节点状态 Ready 的值为 “Unknown”。
  • node.kubernetes.io/memory-pressure:节点存在内存压力。
  • node.kubernetes.io/disk-pressure:节点存在磁盘压力。
  • node.kubernetes.io/pid-pressure: 节点的 PID 压力。
  • node.kubernetes.io/network-unavailable:节点网络不可用。
  • node.kubernetes.io/unschedulable: 节点不可调度。
  • node.cloudprovider.kubernetes.io/uninitialized:如果 kubelet 启动时指定了一个 “外部” 云平台驱动, 它将给当前节点添加一个污点将其标志为不可用。在 cloud-controller-manager 的一个控制器初始化这个节点后,kubelet 将删除这个污点。

节点被驱逐时,节点控制器或者 kubelet 会添加带有 NoExecute 效应的相关污点。 如果异常状态恢复正常,kubelet 或节点控制器能够移除相关的污点。

说明: 为了保证由于节点问题引起的 Pod 驱逐 速率限制 行为正常, 系统实际上会以限定速率的方式添加污点。在像主控节点与工作节点间通信中断等场景下, 这样做可以避免 Pod 被大量驱逐。

使用这个功能特性,结合 tolerationSeconds,Pod 就可以指定当节点出现一个 或全部上述问题时还将在这个节点上运行多长的时间。

比如,一个使用了很多本地状态的应用程序在网络断开时,仍然希望停留在当前节点上运行一段较长的时间, 愿意等待网络恢复以避免被驱逐。在这种情况下,Pod 的容忍度可能是下面这样的:

tolerations:
- key: "node.kubernetes.io/unreachable"
  operator: "Exists"
  effect: "NoExecute"
  tolerationSeconds: 6000

说明:

Kubernetes 会自动给 Pod 添加一个 key 为 node.kubernetes.io/not-ready 的容忍度,并配置 tolerationSeconds=300,除非用户提供的 Pod 配置中已经已存在了 key 为 node.kubernetes.io/not-ready 的容忍度。

同样,Kubernetes 会给 Pod 添加一个 key 为 node.kubernetes.io/unreachable 的容忍度并配置 tolerationSeconds=300,除非用户提供的 Pod 配置中已经已存在了 key 为 node.kubernetes.io/unreachable 的容忍度。

这种自动添加的容忍度意味着在其中一种问题被检测到时 Pod 默认能够继续停留在当前节点运行 5 分钟。

DaemonSet 中的 Pod 被创建时, 针对以下污点自动添加的 NoExecute 的容忍度将不会指定 tolerationSeconds

  • node.kubernetes.io/unreachable
  • node.kubernetes.io/not-ready

这保证了出现上述问题时 DaemonSet 中的 Pod 永远不会被驱逐。

cordon 和 uncordon

kubectl cordon 命令,标记node为不可调度,将无法在node上创建新的pod。

root@master30~ 19:31:53# kubectl cordon -h
Mark node as unschedulable.
......
Usage:
  kubectl cordon NODE [options]

root@master30~ 19:32:34# kubectl get nodes
NAME                 STATUS   ROLES           AGE   VERSION
master30.hgq.cloud   Ready    control-plane   8d    v1.30.2
worker31.hgq.cloud   Ready    <none>          8d    v1.30.2
worker32.hgq.cloud   Ready    <none>          8d    v1.30.2


# 标记worker31.hgq.cloud为SchedulingDisabled
root@master30~ 19:32:38# kubectl cordon worker31.hgq.cloud
root@master30~ 19:33:36# kubectl get nodes
NAME                 STATUS                     ROLES           AGE   VERSION
master30.hgq.cloud   Ready                      control-plane   8d    v1.30.2
worker31.hgq.cloud   Ready,SchedulingDisabled   <none>          8d    v1.30.2
worker32.hgq.cloud   Ready                      <none>          8d    v1.30.2


# 创建一个deployment
root@master30~ 19:33:51# kubectl create deployment web --image=docker.io/library/nginx --replicas 2
root@master30~ 19:34:48# kubectl get pods -o wide
NAME                   READY   STATUS    RESTARTS   AGE   IP              NODE                 NOMINATED NODE   READINESS GATES
web-68b95c775c-65j7b   1/1     Running   0          26s   10.224.96.206   worker32.hgq.cloud   <none>           <none>
web-68b95c775c-7vcsg   1/1     Running   0          26s   10.224.96.203   worker32.hgq.cloud   <none>           <none>

# 取消worker31.hgq.cloud标记SchedulingDisabled
root@master30~ 19:35:41# kubectl uncordon worker31.hgq.cloud
root@master30~ 19:36:26# kubectl get nodes
NAME                 STATUS   ROLES           AGE   VERSION
master30.hgq.cloud   Ready    control-plane   8d    v1.30.2
worker31.hgq.cloud   Ready    <none>          8d    v1.30.2
worker32.hgq.cloud   Ready    <none>          8d    v1.30.2

root@master30~ 19:36:36# kubectl scale deployment web --replicas 4
root@master30~ 19:36:55# kubectl get pods -o wide
NAME                   READY   STATUS    RESTARTS   AGE     IP              NODE                 NOMINATED NODE   READINESS GATES
web-68b95c775c-65j7b   1/1     Running   0          2m31s   10.224.96.206   worker32.hgq.cloud   <none>           <none>
web-68b95c775c-7vcsg   1/1     Running   0          2m31s   10.224.96.203   worker32.hgq.cloud   <none>           <none>
web-68b95c775c-ffk27   1/1     Running   0          13s     10.224.128.39   worker31.hgq.cloud   <none>           <none>
web-68b95c775c-p2d4p   1/1     Running   0          13s     10.224.128.41   worker31.hgq.cloud   <none>           <none>

drain

学习参考:安全地清空一个节点

在对节点执行维护(例如内核升级、硬件维护等)之前, 可以使用 kubectl drain 从节点安全地逐出所有 Pod。 安全的驱逐过程允许 Pod 的容器体面地终止, 并确保满足指定的 PodDisruptionBudgets

准备开始

此任务假定你已经满足了以下先决条件:

  1. 在节点清空期间,确保应用具有高可用性。
  2. 你已经了解了 PodDisruptionBudget 的概念, 并为需要它的应用配置了 PodDisruptionBudget

为了确保负载在维护期间仍然可用,可以配置一个 PodDisruptionBudget。 如果可用性对于正在清空的该节点上运行或可能在该节点上运行的任何应用程序很重要, 首先 配置一个 PodDisruptionBudgets 并继续遵循本指南。

建议为你的 PodDisruptionBudgets 设置 AlwaysAllow 不健康 Pod 驱逐策略, 以在节点清空期间支持驱逐异常的应用程序。 默认行为是等待应用程序的 Pod 变为 健康后, 才能进行清空操作。

kubectl drain 实践

kubectl drain 命令,驱逐 node 上所有 pod,同时标记 node 为不可调度。默认情况下,该命令忽略节点上不能驱逐的 Pod。有关更多细节,请参阅 kubectl drain 文档。

root@master30~ 19:52:52# kubectl drain -h
Drain node in preparation for maintenance.

 The given node will be marked unschedulable to prevent new pods from arriving.
'drain' evicts the pods if the APIServer supports.

Usage:
  kubectl drain NODE [options]
  
# 默认不驱逐 DaemonSet 控制的pod
root@master30~ 19:53:01# kubectl drain worker31.hgq.cloud
node/worker31.hgq.cloud cordoned
error: unable to drain node "worker31.hgq.cloud" due to error:cannot delete DaemonSet-managed Pods (use --ignore-daemonsets to ignore): kube-system/calico-node-csnj7, kube-system/kube-proxy-qvg5m, metallb-system/speaker-jl9q7, continuing command...
There are pending nodes to be drained:
 worker31.hgq.cloud
cannot delete DaemonSet-managed Pods (use --ignore-daemonsets to ignore): kube-system/calico-node-csnj7, kube-system/kube-proxy-qvg5m, metallb-system/speaker-jl9q7


# 使用--ignore-daemonsets驱逐DaemonSet控制的pod
root@master30~ 19:54:27# kubectl drain worker31.hgq.cloud --ignore-daemonsets
node/worker31.hgq.cloud already cordoned
Warning: ignoring DaemonSet-managed Pods: kube-system/calico-node-csnj7, kube-system/kube-proxy-qvg5m, metallb-system/speaker-jl9q7
evicting pod quota/web-68b95c775c-p2d4p
evicting pod quota/web-68b95c775c-ffk27
pod/web-68b95c775c-ffk27 evicted
pod/web-68b95c775c-p2d4p evicted
node/worker31.hgq.cloud drained

# 还可以使用以下选项驱逐特定pod:
  --pod-selector='': Label selector to filter pods on the node
  -l, --selector='': Selector (label query) to filter on,supports '=', '==', and '!='.(e.g. -l CPU=L1,MEM=L2). Matching objects must satisfy all of the specified label constraints.

root@master30~ 19:55:50# kubectl get nodes
NAME                 STATUS                     ROLES           AGE   VERSION
master30.hgq.cloud   Ready                      control-plane   8d    v1.30.2
worker31.hgq.cloud   Ready,SchedulingDisabled   <none>          8d    v1.30.2
worker32.hgq.cloud   Ready                      <none>          8d    v1.30.2


# 删除worker31上SchedulingDisabled标记
root@master30~ 19:56:08# kubectl uncordon worker31.hgq.cloud

并行清空多个节点

kubectl drain 命令一次只能发送给一个节点,可以在不同的终端或后台为不同的节点并行地运行多个 kubectl drain 命令。 同时运行的多个 drain 命令仍然遵循你指定的 PodDisruptionBudget

例如,一个三副本的 Deployments, 设置了一个PodDisruptionBudget,指定 minAvailable: 2。 如果所有的三个 Pod 处于健康(healthy)状态, 并且你并行地发出多个 drain 命令,那么 kubectl drain 只会从 Deployments中逐出一个 Pod, 因为 Kubernetes 会遵守 PodDisruptionBudget 并确保在任何时候只有一个 Pod 不可用 (最多不可用 Pod 个数的计算方法:replicas - minAvailable)。 任何会导致处于健康(healthy) 状态的副本数量低于指定预算的清空操作都将被阻止。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐