Kubernetes13-Kubernetes集群管理

1、管理Node

1.1、隔离和恢复Node

  • 在硬件升级、硬件维护等情况下,需要将某些Node隔离,使其脱离Kubernetes集群的调度范围。Kubernetes提供了一种机制,既可以将Node纳入调度范围,也可以将Node脱离调度范围。
    • 隔离Node:新创建的pod不会调度到该node上。
    • 恢复Node:恢复后,新创建的pod会被调度到该节点。
  • 注意,将某个Node脱离调度范围时,在其上运行的Pod并不会自动停止,管理员需要手动停止在该Node上运行的Pod。

1、使用cordon、uncordon子命令

//隔离某个Node
kubectl cordon <node_name>
//对某个Node恢复调度
kubectl uncordon <node_name>

示例:

]# kubectl get nodes
NAME          STATUS   ROLES                  AGE   VERSION
k8s-master1   Ready    control-plane,master   35h   v1.20.14
k8s-node1     Ready    <none>                 35h   v1.20.14
k8s-node2     Ready    <none>                 35h   v1.20.14

//隔离k8s-node1节点
]# kubectl cordon k8s-node1
node/k8s-node1 cordoned

]# kubectl get nodes
NAME          STATUS                     ROLES                  AGE   VERSION
k8s-master1   Ready                      control-plane,master   35h   v1.20.14
k8s-node1     Ready,SchedulingDisabled   <none>                 35h   v1.20.14
k8s-node2     Ready                      <none>                 35h   v1.20.14

2、使用patch子命令

//将spec.unschedulable设置为true,表示隔离该Node
kubectl patch node k8s-node1 -p '{"spec":{"unschedulable":true}}'
//将spec.unschedulable设置为false,表示恢复该Node
kubectl patch node k8s-node1 -p '{"spec":{"unschedulable":false}}'

1.2、Node的扩容

  • 将一个新Node的加入到Kubernetes集群中非常简单。
    • (1)在新的Node上安装Docker、kubelet和kube-proxy服务,然后配置kubelet和kubeproxy的启动参数,将Master URL指定为当前Kubernetes集群Master的地址,最后启动这些服务。
    • (2)通过kubelet默认的自动注册机制,新的Node将会自动加入现有的Kubernetes集群中。
  • Kubernetes Master在接受了新Node的注册之后,会自动将其纳入当前集群的调度范围,之后创建容器时,就可以对新的Node进行调度了。
  • 重新生成用于加入到集群的token(要添加master,可以在token后加--control-plane参数)
]# kubeadm token create --print-join-command
kubeadm join 10.1.1.11:6443 --token swosas.n9p71fcgsx7vcll0     --discovery-token-ca-cert-hash sha256:c8f0875af59e971434e352867943ed9f3d8453a4de086cecb8b031fa43d5bd7c

2、标签和标签选择器

2.1、标签(Label)

  • 标签就是"键值对"类型的数据,可以在创建资源时指定,也可以随时按需添加,然后用标签选择器匹配标签从而完成资源挑选。
    • (1)一个对象可拥有多个标签,同一个标签也可被添加到多个对象之上
    • (2)可以为资源添加多个不同的标签,以实现灵活的资源分组管理。例如版本标签、环境标签、分层架构标签等。
  • 标签由组成。
    • 键通常由键前缀键名组成(键前缀是可选的),其格式如:"KEY PREFIX/KEY_NAME"。
      • a、键前缀必须为DNS子域名格式,且不能超过253个字符。省略键前缀时,键将被视为用户的私有数据,不过由Kubernetes系统组件或第三方组件自动为用户资源添加的键必须使用键前缀,而"kubernetes.io/"前缀预留给kubernetes的核心组件使用。
      • b、键名至多能使用63个字符,可使用字母、数字、连接号(-)、下划线(-)、点号(.)等字符,且只能以字母或数字开头。
    • 标签的值必须不能多于63个字符,它要么为空,要么是以字母或数字开头及结尾,且中间仅使用了字母、数字、连接号(一)、下划线(-)或点号(.)等字符的数据。

2.2、标签选择器(Label Selector)

  • 标签选择器使用查询条件或选择标准筛选对象, Kubernetes API目前支持两个选择器:
    • 基于等值关系(equality-based)
      • 操作符有=、==和!=三种,其中前两个意义相同,都表示“等值"关系,最后一个表示"不等"关系。
    • 基于集合关系(set-based)
      • KEY in (VALUE1,VALUE2....)
      • KEY not in (VALUE1,VALUE2....)
      • KEY:所有存在此键名标签的资源
      • !KEY:所有不存在此键名标签的资源。
  • 使用标签选择器时还将遵循以下逻辑:
    • 同时指定多个选择器,它们之间的逻辑关系是"与"。
    • 使用空值,意味着每个资源对象都将被选中。
    • 空的标签选择器无法选出任何资源对象。
  • 定义标签选择器的方式
    • kubernetes的诸多资源对象必须以标签选择器的方式关联到Pod资源对象,例如Service,Deployment和ReplicaSet类型的资源等,它们在spec字段中嵌套使用嵌套的"selector"字段,通过"matchLabels"来指定标签选择器,有的甚至还支持使用"matchExpressions"构造复杂的标签选择机制
      • matchLabels:通过指定的键值选择资源对象。
      • matchExpressions:基于表达式选择资源对象,如"{key: KEY_NAME, operator: OPERATOR, values:[VALUE 1, VALUE2, ...])"。使用In或NotIn操作符时,其values非必须为非空的字符串列表,而使用Exists或DostNotExist时,其values必须为空。

2.3、常用命令

  • node标签操作
//查看node的标签
]# kubectl get nodes --show-labels

//为k8s-node1节点打上一个node-name=node1标签
]# kubectl label nodes k8s-node1 node-name=node1
  • pod标签操作
//查看pod的标签
]# kubectl get pods --show-labels

//添加标签
]# kubectl label pods pod-test1 hh=hengha -n default
//修改标签(使用--overwrite)
]# kubectl label pods pod-test1 --overwrite hh=hello -n default
//删除标签(标签后加个减号)
]# kubectl label pods pod-test1 hh- -n default
  • 标签选择器的使用
]# kubectl get pods -l rel=stable
]# kubectl get pods -l rel=stable
]# kubectl get pods -l rel=stable -L app --show-labels
]# kubectl get pods -l "app in (flannel,pod-test4)" --show-labels
]# kubectl get pods -l "app notin (flannel,pod-test4)" --show-labels

3、Namespace:集群环境共享与隔离

  • 在一个组织内部,不同的工作组可以在同一个Kubernetes集群中工作,Kubernetes通过命名空间对不同的工作组进行区分,使得它们既可以共享同一个Kubernetes集群的服务,也能够互不干扰,如图10.2所示。

  • 假设在我们的组织中有两个工作组:开发组和生产运维组。
    • 开发组在Kubernetes集群中需要不断创建、修改、删除各种Pod、RC、Service等资源对象,以便实现敏捷开发。
    • 生产运维组则需要使用严格的权限设置来确保生产系统中的Pod、RC、Service处于正常运行状态且不会被误操作。

1、使用create子命令

kubectl create namespace NAME [--dry-run=server|client|none] [options]

2、使用yaml文件

//创建名称空间的yaml文件
apiVersion: v1
kind: Namespace
metadata:
  name: my-namespace

//使用yaml文件创建名称空间
kubectl apply -f namespace.yaml

3、查看名称空间

kubectl get ns

4、Kubernetes资源管理

  • Pod的两个重要参数:CPU Request与Memory Request
  • 在大多数情况下,我们在定义Pod时并没有定义这两个参数,此时Kubernetes会认为该Pod所需的资源很少,并可以将其调度到任何可用的Node上。这样一来,当集群中的计算资源不很充足时,如果集群中的Pod负载突然加大,就会使某个Node的资源严重不足。
  • 为了避免系统挂掉,该Node会选择“清理”一些Pod来释放资源,此时每个Pod都可能被清理。但有些Pod担负着更重要的职责,比其他Pod更重要,比如与数据存储相关的、与登录相关的、与查询余额相关的,即使系统资源严重不足,也需要保障这些Pod的存活,Kubernetes中该保障机制的核心如下。
    • 通过资源限额来确保不同的Pod只能占用指定的资源。
    • 允许集群的资源被超额分配,以提高集群的资源利用率。
    • 为Pod划分等级,确保不同等级的Pod有不同的服务质量(QoS),资源不足时,低等级的Pod会被清理,以确保高等级的Pod稳定运行。
  • Kubernetes集群里的节点提供的资源主要是计算资源,计算资源是可计量的能被申请、分配和使用的基础资源,这使之区别于API资源(API Resources,例如Pod和Services等)。当前Kubernetes集群中的计算资源主要包括CPUGPUMemory,绝大多数常规应用是用不到GPU的,因此这里重点介绍CPU与Memory的资源管理问题。
  • CPU与Memory是被Pod使用的,因此在配置Pod时可以通过参数CPU Request及Memory Request为其中的每个容器指定所需使用的CPU与Memory量,Kubernetes会根据Request的值去查找有足够资源的Node来调度此Pod,如果没有,则调度失败。
  • 一个程序所使用的CPU与Memory是一个动态的量,确切地说,是一个范围,跟它的负载密切相关:负载增加时,CPU和Memory的使用量也会增加。因此最准确的说法是,某个进程的CPU使用量为0.1个CPU~1个CPU,内存占用则为500MB~1GB。对应到Kubernetes的Pod容器上,就是下面这4个参数:
    • spec.container[].resources.requests.cpu
    • spec.container[].resources.limits.cpu
    • spec.container[].resources.requests.memory
    • spec.container[].resources.limits.memory
  • limits对应资源量的上限,即最多允许使用这个上限的资源量。
    • CPU资源是可压缩的,进程无论如何也不可能突破上限,因此设置起来比较容易。
    • Memory资源是不可压缩的,因此它的Limit设置就是一个问题,如果设置得小了,当进程在业务繁忙期试图请求超过Limit限制的Memory时,此进程就会被Kubernetes杀掉。因此,Memory的Request与Limit的值需要结合进程的实际需求谨慎设置。
  • 如果不设置CPU或Memory的Limit值,会怎样呢?考虑下面的例子:
    • Pod A的Memory Request被设置为1GB,Node A当时空闲的Memory为1.2GB,符合Pod A的需求,因此Pod A被调度到Node A上。运行3天后,Pod A的访问请求大增,内存需要增加到1.5GB,此时Node A的剩余内存只有200MB,由于Pod A新增的内存已经超出系统资源,所以在这种情况下,Pod A就会被Kubernetes杀掉。
  • 如果我们有成百上千个不同的Pod,那么先手动设置每个Pod的这4个参数,再检查并确保这些参数的设置,都是合理的。比如不能出现内存超过2GB或者CPU占据2个核心的Pod。最后还得手工检查不同租户(Namespace)下的Pod的资源使用量是否超过限额。为此,Kubernetes提供了另外两个相关对象:LimitRangeResourceQuota,前者解决request与limit参数的默认值和合法取值范围等问题,后者则解决约束租户的资源配额问题。
  • 本章从计算资源管理(Compute Resources)、服务质量管理(QoS)、资源配额管理(LimitRange、ResourceQuota)等方面,对Kubernetes集群内的资源管理进行详细说明,并结合实践操作、常见问题分析和一个完整的示例,力求对Kubernetes集群资源管理相关的运维工作提供指导。

4.1、计算资源管理

  • 计算资源管理(Compute Resources):限制pod对内存和CPU的使用量。
  • 计算资源管理的yaml文件:
apiVersion: v1
kind: Pod
metadata:
  name: busybox-pod
spec:
  containers:
  - name: busybox-container
    image: busybox:1.28
    imagePullPolicy: IfNotPresent
    command: ["/bin/sh", "-c", "sleep 3600"]
    resources:
      requests:
        memory: "32Mi"
        cpu: "250m"
      limits:
        memory: "64Mi"
        cpu: "500m"

4.1.1、详解Requests和Limits参数

  • 以CPU为例,图10.3显示了未设置Limits和设置了Requests、Limits的CPU使用率的区别。

  • Requests和Limits只能被设置到容器上。对于CPU和内存而言,Pod的Requests或Limits是指该Pod中所有容器的Requests或Limits的总和(对于Pod中没有设置Requests或Limits的容器,该项的值被当作0或者按照集群配置的默认值来计算)。
  • 对CPU和内存这两种计算资源的说明:
    • CPU的Requests和Limits是通过CPU数(cpus)来度量的。CPU的资源值是绝对值,而不是相对值,比如0.1CPU在单核或多核机器上是一样的,都严格等于0.1 CPU core。
    • Memory的Requests和Limits计量单位是字节数。使用整数或者定点整数加上国际单位制(International System of Units)来表示内存值。国际单位制包括十进制的E、P、T、G、M、K、m,或二进制的Ei、Pi、Ti、Gi、Mi、Ki。KiB与MiB是以二进制表示的字节单位,常见的KB与MB则是以十进制表示的字节单位,比如:
      • 1 KB(KiloByte)= 1000 Bytes = 8000 Bits;
      • 1 KiB(KibiByte)= 2^10 Bytes = 1024 Bytes = 8192 Bits。
  • Kubernetes的计算资源单位是大小写敏感的,因为m可以表示千分之一单位(milli unit),而M可以表示十进制的1000,二者的含义不同;同理,小写的k不是一个合法的资源单位。

示例:

  • 该Pod包含两个容器,每个容器配置的Requests都是0.25CPU和64MiB内存,而Limits都是0.5CPU和128MiB内存。因此,这个Pod的Requests和Limits等于Pod中所有容器对应配置的总和,所以Pod的Requests是0.5CPU和128MiB内存,Limits是1CPU和256MiB内存。
apiVersion: v1
kind: Pod
metadata:
  name: busybox-pod
spec:
  containers:
  - name: busybox-container
    image: busybox:1.28
    imagePullPolicy: IfNotPresent
    command: ["/bin/sh", "-c", "sleep 3600"]
    resources:
      requests:
        memory: "32Mi"
        cpu: "250m"
      limits:
        memory: "64Mi"
        cpu: "500m"
  - name: busybox-container2
    image: busybox:1.28
    imagePullPolicy: IfNotPresent
    command: ["/bin/sh", "-c", "sleep 3600"]
    resources:
      requests:
        memory: "32Mi"
        cpu: "250m"
      limits:
        memory: "64Mi"
        cpu: "500m"

4.1.2、基于Requests和Limits的Pod调度机制

  • 当一个Pod创建时,Kubernetes调度器(Scheduler)会为该Pod选择一个节点。对于每种计算资源(CPU和Memory)而言,每个节点都有一个能用于运行Pod的最大容量值。调度器在调度时,首先要确保调度后该节点上所有Pod的CPU和内存的Requests总和不超过该节点能提供给Pod使用的CPU和Memory的最大容量值。
    • 例如,某个节点上的CPU资源充足,而内存为4GB,其中3GB可以运行Pod,而某Pod的Memory Requests为1GB、Limits为2GB,那么在这个节点上最多可以运行3个这样的Pod。
  • 注意,可能某节点上的资源实际使用量非常低,但是已运行Pod配置的Requests值的总和非常高,再加上需要调度的Pod的Requests值,会超过该节点提供给Pod的资源容量上限,这时Kubernetes就不会将Pod调度到该节点上。如果Kubernetes将Pod调度到该节点上,之后该节点上运行的Pod又面临服务峰值等情况,就可能导致Pod资源短缺。
    • 接着上面的例子,假设该节点已经启动3个Pod实例,而这3个Pod的实际内存使用都不足500MB,那么理论上该节点的可用内存应该大于1.5GB。但是由于该节点的Pod Requests总和已经达到节点的可用内存上限,因此Kubernetes不会再将任何Pod实例调度到该节点上。

4.1.3、Requests和Limits的背后机制

  • kubelet在启动Pod的某个容器时,会将容器的Requests和Limits值转化为相应的容器启动参数传递给容器执行器(Docker或者rkt)。
  • 如果容器的执行环境是Docker,那么容器的如下4个参数是这样传递给Docker的。

1、spec.container[].resources.requests.cpu

  • 这个参数会转化为core数(比如配置的100m会转化为0.1),然后乘以1024,再将这个结果作为--cpu-shares参数的值传递给docker run命令。在docker run命令中,--cpu-share参数是一个相对权重值(RelativeWeight),这个相对权重值会决定Docker在资源竞争时分配给容器的资源比例。
    • 举例说明--cpu-shares参数在Docker中的含义:比如将两个容器的CPU Requests分别设置为1和2,那么容器在docker run启动时对应的--cpu-shares参数值分别为1024和2048,在主机CPU资源产生竞争时,Docker会尝试按照1∶2的配比将CPU资源分配给这两个容器使用。
  • 这里需要区分清楚的是:这个参数对于Kubernetes而言是绝对值,主要用于Kubernetes调度和管理;同时Kubernetes会将这个参数的值传递给docker run的--cpu-shares参数。--cpu-shares参数对于Docker而言是相对值,主要用于资源分配比例。

2、spec.container[].resources.limits.cpu

  • 这个参数会转化为millicore数(比如配置的1被转化为1000,而配置的100m被转化为100),将此值乘以100000,再除以1000,然后将结果值作为--cpu-quota参数的值传递给docker run命令。docker run命令中另外一个参数--cpu-period默认被设置为100000,表示Docker重新计量和分配CPU的使用时间间隔为100000μs(100ms)。
  • Docker的--cpu-quota参数和--cpu-period参数一起配合完成对容器CPU的使用限制:比如Kubernetes中配置容器的CPU Limits为0.1,那么计算后--cpu-quota为10000,而--cpu-period为100000,这意味着Docker在100ms内最多给该容器分配10ms×core的计算资源用量,10/100=0.1 core的结果与Kubernetes配置的意义是一致的。
  • 注意:如果kubelet的启动参数--cpu-cfs-quota被设置为true,那么kubelet会强制要求所有Pod都必须配置CPU Limits(如果Pod没有配置,则集群提供了默认配置也可以)。从Kubernetes 1.2版本开始,这个--cpu-cfs-quota启动参数的默认值就是true。

3、spec.container[].resources.requests.memory

  • 这个参数值只提供给Kubernetes调度器作为调度和管理的依据,不会作为任何参数传递给Docker。

4、spec.container[].resources.limits.memory

  • 这个参数值会转化为单位为Bytes的整数,数值会作为--memory参数传递给docker run命令。
  • 如果一个容器在运行过程中使用了超出了其内存Limits配置的内存限制值,那么它可能会被杀掉,如果这个容器是一个可重启的容器,那么之后它会被kubelet重新启动。因此对容器的Limits配置需要进行准确测试和评估。
  • 与内存Limits不同的是,CPU在容器技术中属于可压缩资源,因此对CPU的Limits配置一般不会因为偶然超标使用而导致容器被系统杀掉。

4.1.4、计算资源使用情况监控

  • Pod的资源用量会作为Pod的状态信息一同上报给Master。如果在集群中配置了Heapster来监控集群的性能数据,那么还可以从Heapster中查看Pod的资源用量信息。

4.1.5、计算资源相关常见问题分析

1、Pod状态为Pending,错误信息为FailedScheduling

  • 如果Kubernetes调度器在集群中找不到合适的节点来运行Pod,那么这个Pod会一直处于未调度状态,直到调度器找到合适的节点为止。每次调度器尝试调度失败时,Kubernetes都会产生一个事件,我们可以通过下面这种方式来查看事件的信息:
]# kubectl describe pods busybox-pod | grep -A 10 'Events'
Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  8s    default-scheduler  0/3 nodes are available: 1 Insufficient cpue.kubernetes.io/unreachable: }, that the pod didn't tolerate.
  Warning  FailedScheduling  8s    default-scheduler  0/3 nodes are available: 1 Insufficient cpue.kubernetes.io/unreachable: }, that the pod didn't tolerate.
  • 在上面这个例子中,名为frontend的Pod由于节点的CPU资源不足而调度失败(Insufficient cpu),同样,如果内存不足,则也可能导致调度失败(Insufficient Memory)。
  • 如果一个或者多个Pod调度失败且有这类错误,那么可以尝试以下几种解决方法。
    • 添加更多的节点到集群中。
    • 停止一些不必要的运行中的Pod,释放资源。
    • 检查Pod的配置,错误的配置可能导致该Pod永远无法被调度执行。比如整个集群中所有节点都只有1 CPU,而Pod配置的CPU Requests为2,该Pod就不会被调度执行。
  • 查看集群中节点的计算资源容量和已使用量:
]# kubectl describe nodes
...
Name:               k8s-node1
Capacity:
  cpu:                2
  ephemeral-storage:  17394Mi
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             1863100Ki
  pods:               110
Allocatable:
  cpu:                2
  ephemeral-storage:  16415037823
  hugepages-1Gi:      0
  hugepages-2Mi:      0
  memory:             1760700Ki
  pods:               110
Non-terminated Pods:          (4 in total)
  Namespace                   Name                                        CPU Requests  CPU Limits  Memory Requests  Memory Limits  AGE
  ---------                   ----                                        ------------  ----------  ---------------  -------------  ---
  default                     test-busybox-deployment-5dd4f8dc7f-q6gkz    0 (0%)        0 (0%)      0 (0%)           0 (0%)         2d7h
  kube-flannel                kube-flannel-ds-xq667                       100m (5%)     100m (5%)   50Mi (2%)        50Mi (2%)      2d7h
  kube-system                 coredns-7f89b7bc75-f9w6h                    100m (5%)     0 (0%)      70Mi (4%)        170Mi (9%)     2d7h
  kube-system                 kube-proxy-56njb                            0 (0%)        0 (0%)      0 (0%)           0 (0%)         2d7h
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  Resource           Requests    Limits
  --------           --------    ------
  cpu                200m (10%)  100m (5%)
  memory             120Mi (6%)  220Mi (12%)
  ephemeral-storage  0 (0%)      0 (0%)
  hugepages-1Gi      0 (0%)      0 (0%)
  hugepages-2Mi      0 (0%)      0 (0%)
...
  • 超过可用资源容量上限(Capacity)和已分配资源量(Allocatable)差额的Pod无法运行在该Node上。
  • 还可以配置针对一组Pod的Requests和Limits总量的限制,这种限制可以作用于命名空间,通过这种方式可以防止一个命名空间下的用户将所有资源据为己有。

2、容器被强行终止(Terminated)

  • 如果容器使用的资源超过了它配置的Limits,那么该容器可能会被强制终止。我们可以通过kubectl describe pod命令来确认容器是否因为这个原因被终止:
]# kubectl describe pod pod_name
...
    Restart Count:  5    #容器被强制终止并重启了5次
...
  • 可以用kubectl get pod命令时添加-o yaml格式参数来读取pod被强制终止的信息。
]# kubectl get pod pod_name -o yaml
...
lastState: {...reason:OOM Killed...}
...

4.1.6、对大内存页(Huge Page)资源的支持

  • 为了使节点能够上报巨页容量,Kubernetes节点必须预先分配巨页。每个节点只能预先分配一种特定规格的巨页。
  • 在计算机发展的早期阶段,程序员是直接对内存物理地址编程的,并且需要自己管理内存,很容易由于内存地址错误导致操作系统崩溃,而且出现了一些恶意程序对操作系统进行破坏。后来人们将硬件和软件(操作系统)相结合,推出了虚拟地址的概念,同时推出了内存页的概念,以及CPU的逻辑内存地址与物理内存(条)地址的映射关系。
  • 在现代操作系统中,内存是以Page(页,有时也可以称之为Block)为单位进行管理的,而不以字节为单位,包括内存的分配和回收都基于Page。典型的Page大小为4KB,因此用户进程申请1MB内存就需要操作系统分配256个Page,而1GB内存对应26万多个Page!
  • 为了实现快速内存寻址,CPU内部以硬件方式实现了一个高性能的内存地址映射的缓存表——TLB(Translation Lookaside Buffer),用来保存逻辑内存地址与物理内存的对应关系。若目标地址的内存页物理地址不在TLB的缓存中或者TLB中的缓存记录失效,CPU就需要切换到低速的、以软件方式实现的内存地址映射表进行内存寻址,这将大大降低CPU的运算速度。针对缓存条目有限的TLB缓存表,提高TLB效率的最佳办法就是将内存页增大,这样一来,一个进程所需的内存页数量会相应减少很多。如果把内存页从默认的4KB改为2MB,那么1GB内存就只对应512个内存页了,TLB的缓存命中率会大大增加。这是不是意味着我们可以任意指定内存页的大小,比如1314MB的内存页?答案是否定的,因为这是由CPU来决定的,比如常见的Intel X86处理器可以支持的大内存页通常是2MB,个别型号的高端处理器则支持1GB的大内存页。
  • 在Linux平台下,对于那些需要大量内存(1GB以上内存)的程序来说,大内存页的优势是很明显的,因为Huge Page大大提升了TLB的缓存命中率,又因为Linux对Huge Page提供了更为简单、便捷的操作接口,所以可以把它当作文件来进行读写操作Linux使用Huge Page文件系统hugetlbfs支持巨页,这种方式更为灵活,我们可以设置Huge Page的大小,比如1GB、2GB甚至2.5GB,然后设置有多少物理内存用于分配Huge Page,这样就设置了一些预先分配好的Huge Page。可以将hugetlbfs文件系统挂载在/mnt/huge目录下,通过执行下面的指令完成设置:
]# mkdir /mnt/huge
]# mount -t hugetlbfs nodev /mnt/huge
  • 在设置完成后,用户进程就可以使用mmap映射Huge Page目标文件来使用大内存页了,Intel DPDK便采用了这种做法,测试表明应用使用大内存页比使用4KB的内存页性能提高了10%~15%。
  • Kubernetes 1.14版本对Linux Huge Page的支持正式更新为GA稳定版。我们可以将Huge Page理解为一种特殊的计算资源:拥有大内存页的资源。而拥有Huge Page资源的Node也与拥有GPU资源的Node一样,属于一种新的可调度资源节点(Schedulable Resource Node)。
  • Huge Page也支持ResourceQuota来实现配额限制,类似CPU或者Memory,但不同于CPU或者内存,Huge Page资源属于不可超限使用的资源,拥有Huge Page能力的Node会将自身支持的Huge Page的能力信息自动上报给Kubernetes Master。
  • 为此,Kubernetes引入了一个新的资源类型hugepages-<size>,来表示大内存页这种特殊的资源,比如hugepages-2Mi表示2MiB规格的大内存页资源。一个能提供2MiB规格Huge Page的Node,会上报自己拥有Hugepages-2Mi的大内存页资源属性,供需要这种规格的大内存页资源的Pod使用,而需要Huge Page资源的Pod只要给出相关的Huge Page的声明,就可以被正确调度到匹配的目标Node上了。相关例子如下:
apiVersion: v1
kind: Pod
metadata:
  name: busybox-pod
spec:
  containers:
  - name: busybox-container1
    image: busybox:1.28
    imagePullPolicy: IfNotPresent
    command: ["/bin/sh", "-c", "sleep 3600"]
    resources:
      requests:
        memory: "100Mi"         #!
      limits:
        memory: "100Mi"         #!
        hugepages-2Mi: 100Mi    #!
    volumeMounts:               #!
    - name: hugepage
      mountPath: /hugepages
  volumes:                      #!
  - name: hugepage
    emptyDir:
      medium: HugePages
  • 在上面的定义中有以下几个关键点:
    • Huge Page需要被映射到Pod的文件系统中;
    • Huge Page申请的request与limit必须相同,即申请固定大小的Huge Page,不能是可变的;
    • 在目前的版本中,Huge Page属于Pod级别的资源,未来计划成为Container级别的资源,即实现更细粒度的资源管理;
    • 存储卷emptyDir(挂载到容器内的/hugepages目录)的后台是由Huge Page支持的,因此应用不能使用超过request声明的内存大小。
  • 在Kubernetes未来的版本中,计划继续实现下面的一些高级特性:
    • 支持容器级别的Huge Page的隔离能力;
    • 支持NUMA亲和能力以提升服务的质量;
    • 支持LimitRange配置Huge Page资源限额。

4.2、资源配置范围管理(LimitRange)

  • 如果集群管理员希望对整个集群中容器或者Pod配置的Requests和Limits做限制,那么可以通过配置Kubernetes命名空间中的LimitRange来达到该目的。在Kubernetes集群中,如果Pod没有显式定义Limits和Requests,那么Kubernetes系统会将该Pod所在的命名空间中定义的LimitRange的default和defaultRequests配置到该Pod上
  • LimitRange就是对下面四个参数的设置做出限制或设置默认值
    • spec.container[].resources.requests.cpu
    • spec.container[].resources.limits.cpu
    • spec.container[].resources.requests.memory
    • spec.container[].resources.limits.memory
  • 在默认情况下,Kubernetes不会对Pod加上CPU和内存限制,这意味着Kubernetes系统中任何Pod都可以使用其所在节点的所有可用的CPU和内存。通过配置Pod的计算资源Requests和Limits,我们可以限制Pod的资源使用,但对于Kubernetes集群管理员而言,配置每一个Pod的Requests和Limits是烦琐的,而且很受限制。
  • Kubernetes集群管理员可以对集群内Requests和Limits的配置做一个全局限制。常见的配置场景如下。
    • 集群中的每个节点都有2GB内存,集群管理员不希望任何Pod申请超过2GB的内存:因为在整个集群中都没有任何节点能满足超过2GB内存的请求。如果某个Pod的内存配置超过2GB,那么该Pod将永远都无法被调度到任何节点上执行。为了防止这种情况的发生,集群管理员希望能在系统管理功能中设置禁止Pod申请超过2GB内存。
    • 集群由同一个组织中的两个团队共享,分别运行生产环境和开发环境。生产环境最多可以使用8GB内存,而开发环境最多可以使用512MB内存。集群管理员希望通过为这两个环境创建不同的命名空间,并为每个命名空间设置不同的限制来满足这个需求
    • 用户创建Pod时使用的资源可能会刚好比整个机器资源的上限稍小,而恰好剩下的资源大小非常尴尬:不足以运行其他任务但整个集群加起来又非常浪费。因此,集群管理员希望设置每个Pod都必须至少使用集群平均资源值(CPU和内存)的20%,这样集群能够提供更好的资源一致性的调度,从而减少了资源浪费。
  • 针对这些需求,Kubernetes提供了LimitRange机制对Pod和容器的Requests和Limits配置进一步做出限制。在下面的示例中,将说明如何将LimitsRange应用到一个Kubernetes的命名空间中,然后说明LimitRange的几种限制方式,比如最大及最小范围、Requests和Limits的默认值、Limits与Requests的最大比例上限等。
  • LimitRange的yaml文件:
apiVersion: v1
kind: LimitRange
metadata:
  name: mylimits
  namespace: test
spec:
  limits:
  - type: Pod
    max:
      cpu: "4"
      memory: 2Gi
    min:
      cpu: 200m
      memory: 6Mi
    maxLimitRequestRatio:
      cpu: 3
      memory: 2
  - type: Container
    default:
      cpu: 300m
      memory: 200Mi
    defaultRequest:
      cpu: 200m
      memory: 100Mi
    max:
      cpu: "2"
      memory: 1Gi
    min:
      cpu: 100m
      memory: 3Mi
    maxLimitRequestRatio:
      cpu: 5
      memory: 4
  • 下面解释LimitRange中各项配置的意义和特点:
    • (1)不论是CPU还是内存,在LimitRange中,Pod和Container都可以设置Min、Max和Max Limit/Requests Ratio参数。Container还可以设置Default Request和Default Limit参数,而Pod不能设置Default Request和Default Limit参数。
    • (2)对Pod和Container的参数解释如下。
      • Pod部分
        • Pod的Min(上面的200m和6Mi)是Pod中所有容器的Requests值的总和下限;
        • Pod的Max(上面的4和2Gi)是Pod中所有容器的Limits值的总和上限。
        • 当容器未指定Requests值或者Limits值时,将使用Container的Default Request值或者Default Limit值。
      • Container部分
        • Container的Min(上面的100m和3Mi)是容器的Requests值下限;
        • Container的Max(上面的2和1Gi)是容器的Limits值上限;
        • Container的Default Request(上面的200m和100Mi)是未指定Requests值的容器的默认Requests值;
        • Container的Default Limit(上面的300m和200Mi)是未指定Limits值的容器的默认Limits值。
        • 对于同一资源类型,这4个参数必须满足以下关系:Min ≤ Default Request ≤ Default Limit ≤ Max。
      • Container的Max Limit/Requests Ratio(上面的5和4)限制了容器的Limits值与Requests值的比例上限;而Pod的MaxLimit/Requests Ratio(上面的3和2)限制了Pod中所有容器的Limits值总和与Requests值总和的比例上限。
    • 如果设置了Container的Max,那么对于该类资源而言,整个集群中的所有容器都必须设置Limits,否则无法成功创建。Pod内的容器未配置Limits时,将使用Default Limit的值(本例中的300m CPU和200MiB内存),如果也未配置Default,则无法成功创建。
    • 如果设置了Container的Min,那么对于该类资源而言,整个集群中的所有容器都必须设置Requests。如果创建Pod的容器时未配置该类资源的Requests,那么在创建过程中会报验证错误。Pod里容器的Requests在未配置时,可以使用默认值defaultRequest(本例中的200mCPU和100MiB内存);如果未配置而又没有使用默认值defaultRequest,那么会默认等于该容器的Limits;如果此时Limits也未定义,就会报错。
    • 对于任意一个Pod而言,该Pod中所有容器的Requests总和必须大于或等于6MiB,而且所有容器的Limits总和必须小于或等于1GiB;同样,所有容器的CPU Requests总和必须大于或等于200m,而且所有容器的CPU Limits总和必须小于或等于2。
    • Pod里任何容器的Limits与Requests的比例都不能超过Container的Max Limit/Requests Ratio;Pod里所有容器的Limits总和与Requests的总和的比例不能超过Pod的Max Limit/Requests Ratio。
  • 创建Pod时触发LimitRange限制
    • 命名空间中LimitRange只会在Pod创建或者更新时执行检查。如果手动修改LimitRange为一个新的值,那么这个新的值不会去检查或限制之前已经在该命名空间中创建好的Pod。
    • 如果在创建Pod时配置的资源值(CPU或者内存)超过了LimitRange的限制,那么该创建过程会报错,在错误信息中会说明详细的错误原因。
  • 注意,CPU Limits强制配置这个选项在Kubernetes集群中默认是开启的;除非集群管理员在部署kubelet时,通过设置参数--cpucfs-quota=false来关闭该限制。

4.3、资源服务质量管理(Resource QoS)

  • 本节对Kubernetes如何根据Pod的Requests和Limits配置来实现针对Pod的不同级别的资源服务质量控制(QoS)进行说明。
  • 在Kubernetes的资源QoS体系中,需要保证高可靠性的Pod可以申请可靠资源,而一些不需要高可靠性的Pod可以申请可靠性较低或者不可靠的资源。在4.1节中讲到了容器的资源配置分为Requests和Limits,其中Requests是Kubernetes调度时能为容器提供的完全可保障的资源量(最低保障),而Limits是系统允许容器运行时可能使用的资源量的上限(最高上限)。Pod级别的资源配置是通过计算Pod内所有容器的资源配置的总和得出来的。
  • Kubernetes中Pod的Requests和Limits资源配置有如下特点:
    • (1)如果Pod配置的Requests值等于Limits值,那么该Pod可以获得的资源是完全可靠的。
    • (2)如果Pod的Requests值小于Limits值,那么该Pod获得的资源可分成两部分:
      • 完全可靠的资源,资源量的大小等于Requests值;
      • 不可靠的资源,资源量最大等于Limits与Requests的差额,这份不可靠的资源能够申请到多少,取决于当时主机上容器可用资源的余量。
  • 通过这种机制,Kubernetes可以实现节点资源的超售(OverSubscription),比如在CPU完全充足的情况下,某机器共有32GiB内存可提供给容器使用,容器配置为Requests值1GiB,Limits值为2GiB,那么在该机器上最多可以同时运行32个容器,每个容器最多可以使用2GiB内存,如果这些容器的内存使用峰值能错开,那么所有容器都可以正常运行。
  • 超售机制能有效提高资源的利用率,同时不会影响容器申请的完全可靠资源的可靠性。

4.3.1、Requests和Limits对不同计算资源类型的限制机制

  • 根据前面的内容可知,容器的资源配置满足以下两个条件:
    • Requests<=节点可用资源;
    • Requests<=Limits。
  • Kubernetes根据Pod配置的Requests值来调度Pod,Pod在成功调度之后会得到Requests值定义的资源来运行;而如果Pod所在机器上的资源有空余,则Pod可以申请更多的资源,最多不能超过Limits的值。
  • 下面看一下Requests和Limits针对不同计算资源类型的限制机制的差异。这种差异主要取决于计算资源类型是可压缩资源还是不可压缩资源。

1、可压缩资源

  • Kubernetes目前支持的可压缩资源是CPU
  • Pod可以得到Pod的Requests配置的CPU使用量,而能否使用超过Requests值的部分取决于系统的负载和调度。不过由于目前Kubernetes和Docker的CPU隔离机制都是在容器级别隔离的,所以Pod级别的资源配置并不能完全得到保障;Pod级别的cgroups正在紧锣密鼓地开发中,如果将来引入,就可以确保Pod级别的资源配置准确运行。
  • 空闲CPU资源按照容器Requests值的比例分配。举例说明:容器A的CPU配置为Requests 1 Limits 10,容器B的CPU配置为Request 2 Limits 8,A和B同时运行在一个节点上,初始状态下容器的可用CPU为3 cores,那么A和B恰好得到在它们的Requests中定义的CPU用量,即1 CPU和 2CPU。如果A和B都需要更多的CPU资源,而恰好此时系统的其他任务释放出1.5 CPU,那么这1.5CPU将按照A和B的Requests值的比例1∶2分配给A和B,即最终A可使用1.5CPU,B可使用3CPU。
  • 如果Pod使用了超过在Limits 10中配置的CPU用量,那么cgroups会对Pod中的容器的CPU使用进行限流(Throttled);如果Pod没有配置Limits 10,那么Pod会尝试抢占所有空闲的CPU资源(Kubernetes从1.2版本开始默认开启--cpu-cfs-quota,因此在默认情况下必须配置Limits)。

2、不可压缩资源

  • Kubernetes目前支持的不可压缩资源是内存
  • Pod可以得到在Requests中配置的内存。如果Pod使用的内存量小于它的Requests的配置,那么这个Pod可以正常运行(除非出现操作系统级别的内存不足等严重问题);如果Pod使用的内存量超过了它的Requests的配置,那么这个Pod有可能被Kubernetes杀掉:
    • 比如Pod A使用了超过Requests而不到Limits的内存量,此时同一机器上另外一个Pod B之前只使用了远少于自己的Requests值的内存,此时程序压力增大,Pod B向系统申请的总量不超过自己的Requests值的内存,那么Kubernetes可能会直接杀掉Pod A;
    • 另外一种情况是Pod A使用了超过Requests而不到Limits的内存量,此时Kubernetes将一个新的Pod调度到这台机器上,新的Pod需要使用内存,而只有Pod A使用了超过了自己的Requests值的内存,那么Kubernetes也可能会杀掉Pod A来释放内存资源。
  • 如果Pod使用的内存量超过了它的Limits设置,那么操作系统内核会杀掉Pod所有容器的所有进程中使用内存最多的一个,直到内存不超过Limits为止。

4.3.2、对调度策略的影响

  • Kubernetes的kubelet通过计算Pod中所有容器的Requests的总和来决定对Pod的调度。
  • 不管是CPU还是内存,Kubernetes调度器和kubelet都会确保节点上所有Pod的Requests的总和不会超过在该节点上可分配给容器使用的资源容量上限。(Requests的总和不会超过节点的物理容量的大小

4.3.3、服务质量等级(QoS Classes)

  • 在一个超用(Over Committed,容器Limits总和大于系统容量上限)系统中,由于容器负载的波动可能导致操作系统的资源不足,最终可能导致部分容器被杀掉。在这种情况下,我们当然会希望优先杀掉那些不太重要的容器,那么如何衡量重要程度呢?
  • Kubernetes将容器划分成3个QoS等级:Guaranteed(完全可靠的)、Burstable(弹性波动、较可靠的)和BestEffort(尽力而为、不太可靠的),这三种优先级依次递减,如图10.4所示。

  • 从理论上来说,QoS级别应该作为一个单独的参数来提供API,并由用户对Pod进行配置,这种配置应该与Requests和Limits无关。但在当前版本的Kubernetes的设计中,为了简化模式及避免引入太多的复杂性,QoS级别直接由Requests和Limits来定义。在Kubernetes中容器的QoS级别等于容器所在Pod的QoS级别,而Kubernetes的资源配置定义了Pod的三种QoS级别。

1、Guaranteed

  • 如果Pod中的所有容器对所有资源类型都定义了Limits和Requests,并且所有容器的Limits值都和Requests值全部相等且都不为0),那么该Pod的QoS级别就是Guaranteed。
  • 注意:在这种情况下,容器可以不定义Requests,因为Requests值在未定义时默认等于Limits。
//不定义Requests值时默认等于Limits
  containers:
  - name: busybox-container1
    image: busybox:1.28
    resources:
      limits:
        memory: "64Mi"
        cpu: "500m"
//显示定义Requests与Limits相等
  containers:
  - name: busybox-container2
    image: busybox:1.28
    resources:
      limits:
        memory: "64Mi"
        cpu: "500m"
      requests:
        memory: "64Mi"
        cpu: "500m"

2、BestEffort

  • 如果Pod中所有容器都未定义资源配置(Requests和Limits都未定义),那么该Pod的QoS级别就是BestEffort。

3、Burstable

  • 当一个Pod既不为Guaranteed级别,也不为BestEffort级别时,该Pod的QoS级别就是Burstable。
  • Burstable级别的Pod包括两种情况。
    • 第1种情况:Pod中的一部分容器在一种或多种资源类型的资源配置中定义了Requests值和Limits值(都不为0),且Requests值小于Limits值;
    • 第2种情况:Pod中的一部分容器未定义资源配置(Requests和Limits都未定义)。
  • 注意:在容器未定义Limits时,Limits值默认等于节点资源容量的上限。

4、Kubernetes QoS的工作特点

  • Pod的CPU Requests无法得到满足(比如节点的系统级任务占用过多的CPU导致无法分配足够的CPU给容器使用)时,容器得到的CPU会被压缩限流。
  • 由于内存是不可压缩的资源,所以针对内存资源紧缺的情况,会按照以下逻辑进行处理:
    • (1)BestEffort Pod的优先级最低,在这类Pod中运行的进程会在系统内存紧缺时被第一优先杀掉。当然,从另外一个角度来看,BestEffortPod由于没有设置资源Limits,所以在资源充足时,它们可以充分使用所有的闲置资源。
    • (2)Burstable Pod的优先级居中,这类Pod初始时会分配较少的可靠资源,但可以按需申请更多的资源。当然,如果整个系统内存紧缺,又没有BestEffort容器可以被杀掉以释放资源,那么这类Pod中的进程可能会被杀掉。
    • (3)Guaranteed Pod的优先级最高,而且一般情况下这类Pod只要不超过其资源Limits的限制就不会被杀掉。当然,如果整个系统内存紧缺,又没有其他更低优先级的容器可以被杀掉以释放资源,那么这类Pod中的进程也可能会被杀掉。

5、OOM计分系统

  • OOM(Out Of Memory)计分规则包括如下内容:
    • OOM计分的计算方法为:计算进程使用内存在系统中占的百分比,取其中不含百分号的数值,再乘以10的结果,这个结果是进程OOM的基础分;将进程OOM基础分的分值再上这个进程的OOM分数调整值OOM_SCORE_ADJ的值,作为进程OOM的最终分值(除root启动的进程外)。在系统发生OOM时,OOM Killer会优先杀掉OOM计分更高的进程。
    • 进程的OOM计分的基本分数值范围是0~1000,如果A进程的调整值OOM_SCORE_ADJ减去B进程的调整值的结果大于1000,那么A进程的OOM计分最终值必然大于B进程,会优先杀掉A进程。
    • 不论调整OOM_SCORE_ADJ值为多少,任何进程的最终分值范围也是0~1000。
  • 在Kubernetes,不同QoS的OOM计分调整值规则如表10.1所示。

    • BestEffort Pod设置OOM_SCORE_ADJ调整值为1000,因此BestEffort Pod中容器里所有进程的OOM最终分肯定是大于1000。
    • Guaranteed Pod设置OOM_SCORE_ADJ调整值为-998,因此Guaranteed Pod中容器里所有进程的OOM最终分一般是0或者1(因为基础分不可能是1000)。
    • Burstable Pod规则分情况说明:如果Burstable Pod的内存Requests超过了系统可用内存的99.8%,那么这个Pod的OOM_SCORE_ADJ调整值固定为2;否则,设置OOM_SCORE_ADJ调整值为1000-10×(% of memory requested);如果内存Requests为0,那么OOM_SCORE_ADJ调整值固定为999。这样的规则能确保OOM_SCORE_ADJ调整值的范围为2~999,而Burstable Pod中所有进程的OOM最终分数范围为2~1000。Burstable Pod进程的OOM最终分数始终大于Guaranteed Pod的进程得分,因此它们会被优先杀掉。如果一个Burstable Pod使用的内存比它的内存Requests少,那么可以肯定的是它的所有进程的OOM最终分数会小于1000,此时能确保它的优先级高于BestEffort Pod。如果在一个Burstable Pod的某个容器中某个进程使用的内存比容器的Requests值高,那么这个进程的OOM最终分数会是1000,否则它的OOM最终分会小于1000。假设在下面的容器中有一个占用内存非常大的进程,那么当一个使用内存超过其Requests的Burstable Pod与另外一个使用内存少于其Requests的Burstable Pod发生内存竞争冲突时,前者的进程会被系统杀掉。如果在一个Burstable Pod内部有多个进程的多个容器发生内存竞争冲突,那么此时OOM评分只能作为参考,不能保证完全按照资源配置的定义来执行OOM Kill。
  • OOM还有一些特殊的计分规则,如下所述:
    • kubelet进程和Docker进程的调整值OOM_SCORE_ADJ为-998。
    • 如果配置进程调整值OOM_SCORE_ADJ为-999,那么这类进程不会被OOM Killer杀掉。

6、QoS的演进

  • 目前Kubernetes基于QoS的超用机制日趋完善,但还有一些问题需要解决。
    • (1)内存Swap的支持。当前的QoS策略都是假定主机不启用内存Swap。如果主机启用了Swap,那么上面的QoS策略可能会失效。举例说明:两个Guaranteed Pod都刚好达到了内存Limits,那么由于内存Swap机制,它们还可以继续申请使用更多的内存。如果Swap空间不足,最终这两个Pod中的进程就可能会被杀掉。由于Kubernetes和Docker尚不支持内存Swap空间的隔离机制,所以这一功能暂时还未实现。
    • (2)更丰富的QoS策略。当前的QoS策略都是基于Pod的资源配置(Requests和Limits)来定义的,而资源配置本身又承担着对Pod资源管理和限制的功能。两种不同维度的功能使用同一个参数来配置,可能会导致某些复杂需求无法满足,比如当前Kubernetes无法支持弹性的、高优先级的Pod。自定义QoS优先级能提供更大的灵活性,完美地实现各类需求,但同时会引入更高的复杂性,而且过于灵活的设置会给予用户过高的权限,对系统管理也提出了更大的挑战。

4.4、资源配额管理(Resource Quotas)

  • 如果一个Kubernetes集群被多个用户或者多个团队共享,就需要考虑资源公平使用的问题,因为某个用户可能会使用超过基于公平原则分配给其的资源量。
  • Resource Quotas就是解决这个问题的工具。通过ResourceQuota对象,我们可以定义资源配额,这个资源配额可以为每个命名空间都提供一个总体的资源使用的限制:它可以限制命名空间中某种类型的对象的总数目上限也可以设置命名空间中Pod可以使用的计算资源的总上限
  • 典型的资源配额使用方式如下:
    • 不同的团队工作在不同的命名空间下,目前这是非约束性的,在未来的版本中可能会通过ACL(Access Control List,访问控制列表)来实现强制性约束。
    • 集群管理员为集群中的每个命名空间都创建一个或者多个资源配额项。
    • 当用户在命名空间中使用资源(创建Pod或者Service等)时,Kubernetes的配额系统会统计、监控和检查资源用量,以确保使用的资源用量没有超过资源配额的配置。
    • 如果在创建或者更新应用时资源使用超过了某项资源配额的限制,那么创建或者更新的请求会报错(HTTP 403 Forbidden),并给出详细的出错原因说明。
    • 如果命名空间中的计算资源(CPU和内存)的资源配额启用,那么用户必须为相应的资源类型设置Requests或Limits;否则配额系统可能会直接拒绝Pod的创建。这里可以使用LimitRange机制来为没有配置资源的Pod提供默认资源配置。
  • 下面是非常适合使用资源配额来做资源控制管理的场景:
    • 集群共有32GB内存和16 CPU,两个小组。A小组使用20GB内存和10 CPU,B小组使用10GB内存和2 CPU,剩下的2GB内存和2 CPU作为预留。
    • 在名为testing的命名空间中,限制使用1 CPU和1GB内存;在名为production的命名空间中,资源使用不受限制。
  • 在使用资源配额时,需要注意以下两点:
    • 如果集群中总的可用资源小于各命名空间中资源配额的总和,那么可能会导致资源竞争。资源竞争时,Kubernetes系统会遵循先到先得的原则。
    • 不管是资源竞争还是配额的修改,都不会影响已经创建的资源使用对象。
  • 资源配额管理的yaml文件:
//与LimitRange相似,ResourceQuota也被设置在Namespace中
apiVersion: v1
kind: ResourceQuota
metadata:
  name: Quota-NAME
  namespace: test
spec:
  hard:
    #计算资源配额
    requests.cpu: "1"
    requests.memory: 1Gi
    limits.cpu: "2"
    limits.memory: 2Gi
    #对象数量配额
    services.loadbalancers: "3"     #限制负载均衡器的数量
    services.nodeports: "1"         #限制NodePort端口的数量
    pods: "10"                      #限制pod的数量
    services: "3"                   #限制service的数量
    #存储资源配额
    persistentvolumeclaims: "10"    #限制持久存储卷的数量
    requests.storage: 110Gi
  scopes:                         #配额的作用域,可用值有:BestEffort、Terminating、NotTerminating和NotBestEffort
  - BestEffort

4.4.1、在Master中开启资源配额

  • 资源配额可以通过在kube-apiserver的--admission-control参数值中添加ResourceQuota参数进行开启。
  • 如果在某个命名空间的定义中存在ResourceQuota,那么对于该命名空间而言,资源配额就是开启的。
  • 一个命名空间可以有多个ResourceQuota配置项

1、计算资源配额(Compute Resource Quota)

  • 资源配额可以限制一个命名空间中所有Pod的计算资源的总和。目前支持的计算资源类型如表10.2所示。

2、存储资源配额(Volume Count Quota)

  • 可以在给定的命名空间中限制所使用的存储资源(StorageResources)的总量,目前支持的存储资源名称如表10.3所示。

3、对象数量配额(Object Count Quota)

  • 指定类型的对象数量可以被限制。表10.4列出了ResourceQuota支持限制的对象类型。

  • 例如,我们可以通过资源配额来限制在命名空间中能创建的Pod的最大数量。这种设置可以防止某些用户大量创建Pod而迅速耗尽整个集群的Pod IP和计算资源。

4.4.2、配额的作用域(Quota Scopes)

  • 每项资源配额都可以单独配置一组作用域,配置了作用域的资源配额只会对符合其作用域的资源使用情况进行计量和限制,作用域范围内超过了资源配额的请求都会报验证错误。表10.5列出了ResourceQuota的4种作用域。

  • 其中,BestEffort作用域可以限定资源配额来追踪pods资源的使用,Terminating、NotTerminating和NotBestEffort这三种作用域可以限定资源配额来追踪以下资源的使用。
    • cpu
    • limits.cpu
    • limits.memory
    • memory
    • pods
    • requests.cpu
    • requests.memory

4.4.3、在资源配额(ResourceQuota)中设置Requests和Limits

  • 资源配额也可以设置Requests和Limits
  • 如果在资源配额中指定了requests.cpu或requests.memory,那么它会强制要求每个容器都配置自己的CPU Requests或CPU Limits(可使用LimitRange提供的默认值)。
  • 同理,如果在资源配额中指定了limits.cpu或limits.memory,那么它也会强制要求每个容器都配置自己的内存Requests或内存Limits(可使用LimitRange提供的默认值)。

4.4.4、资源配额与集群资源总量的关系

  • 资源配额与集群资源总量是完全独立的。资源配额是通过绝对的单位来配置的,这也就意味着如果在集群中新添加了节点,那么资源配额不会自动更新,而该资源配额所对应的命名空间中的对象也不能自动增加资源上限。
  • 在某些情况下,我们可能希望资源配额支持更复杂的策略,如下所述。
    • 对于不同的租户,按照比例划分整个集群的资源。
    • 允许每个租户都能按照需要来提高资源用量,但是有一个较宽容的限制,以防止意外的资源耗尽情况发生。
    • 探测某个命名空间的需求,添加物理节点并扩大资源配额值。这些策略可以通过将资源配额作为一个控制模块、手动编写一个控制器来监控资源使用情况,并调整命名空间上的资源配额来实现。资源配额将整个集群中的资源总量做了一个静态划分,但它并没有对集群中的节点做任何限制:不同命名空间中的Pod仍然可以运行在同一个节点上。

4.5、ResourceQuota和LimitRange实践

  • 集群管理员根据集群用户的数量来调整集群配置,以达到这个目的:能控制特定命名空间中的资源使用量,最终实现集群的公平使用和成本控制。
  • 需要实现的功能如下:
    • 限制运行状态的Pod的计算资源用量。
    • 限制持久存储卷的数量以控制对存储的访问。
    • 限制负载均衡器的数量以控制成本。
    • 防止滥用网络端口这类稀缺资源。
    • 提供默认的计算资源Requests以便于系统做出更优化的调度。

1、创建名称空间

]# kubectl create namespace test

2、设置名称空间的资源配额

apiVersion: v1
kind: ResourceQuota
metadata:
  name: quota-name
  namespace: test
spec:
  hard:
    #设置限定对象数目的资源配额
    persistentvolumeclaims: "10"    #限制持久存储卷的数量
    services.loadbalancers: "3"     #限制负载均衡器的数量
    services.nodeports: "1"         #限制NodePort端口的数量
    pods: "10"                      #限制pod的数量
    count/services: "3"             #限制service的数量
    #设置限定计算资源的资源配额
    requests.cpu: "1"
    requests.memory: 1Gi
    limits.cpu: "2"
    limits.memory: 2Gi
  • 创建,并查看资源配额
//创建资源配额
]# kubectl apply -f quota_name.yaml

//查看名称中的有哪些资源配额
]# kubectl get quota -n test
NAME         AGE    REQUEST                                                                                                                                                          LIMIT
quota-name   106s   count/services: 0/3, persistentvolumeclaims: 0/10, pods: 0/10, requests.cpu: 0/1, requests.memory: 0/1Gi, services.loadbalancers: 0/3, services.nodeports: 0/1   limits.cpu: 0/2, limits.memory: 0/2Gi

//查看资源配额的信息
]# kubectl describe quota quota-name -n test
Name:                   quota-name
Namespace:              test
Resource                Used  Hard
--------                ----  ----
count/services          0     3
limits.cpu              0     2
limits.memory           0     2Gi
persistentvolumeclaims  0     10
pods                    0     10
requests.cpu            0     1
requests.memory         0     1Gi
services.loadbalancers  0     3
services.nodeports      0     1

3、配置默认Requests和Limits

apiVersion: v1
kind: LimitRange
metadata:
  name: limitrange-name
  namespace: test
spec:
  limits:
  - type: Container
    default:
      cpu: 300m
      memory: 200Mi
    defaultRequest:
      cpu: 200m
      memory: 100Mi
  • 创建,并查看
//创建
]# kubectl apply -f limitrange-name.yaml

//查看
]# kubectl get limits -n test
NAME              CREATED AT
limitrange-name   2022-10-05T16:47:43Z

//查看详细信息
]# kubectl describe limits limitrange-name -n test
Name:       limitrange-name
Namespace:  test
Type        Resource  Min  Max  Default Request  Default Limit  Max Limit/Request Ratio
----        --------  ---  ---  ---------------  -------------  -----------------------
Container   cpu       -    -    200m             300m           -
Container   memory    -    -    100Mi            200Mi          -
  • 每个新建的未指定资源限制的Pod都等价于使用下面的资源限制:
kubectl run nginx \
  --image=nginx \
  --requests=cpu=200m,memory=100Mi \
  --limits=cpu=300m,memory=200Mi \
  --namespace=test

4、指定资源配额的作用域

apiVersion: v1
kind: ResourceQuota
metadata:
  name: quota-name2
  namespace: test
spec:
  hard:
    pods: "10"   
  scopes:
  - BestEffort
  • 创建,并查看
//创建
]# kubectl apply -f quota-name2.yaml

//查看
]# kubectl get quota -n test
NAME          AGE   REQUEST                                                                                                                                                                 LIMIT
quota-name    18m   count/services: 0/3, persistentvolumeclaims: 0/10, pods: 2/10, requests.cpu: 200m/1, requests.memory: 512Mi/1Gi, services.loadbalancers: 0/3, services.nodeports: 0/1   limits.cpu: 400m/2, limits.memory: 1Gi/2Gi
quota-name2   29s   pods: 0/10                                                                                                                                                              

//查看详细信息
]# kubectl describe quota quota-name2 -n test
Name:       quota-name2
Namespace:  test
Scopes:     BestEffort
 * Matches all pods that do not have resource requirements set. These pods have a best effort quality of service.
Resource  Used  Hard
--------  ----  ----
pods      0     10

5、资源紧缺时的Pod驱逐机制

  • 如何在系统硬件资源紧缺的情况下保证Node的稳定性,是kubelet需要解决的一个重要问题。尤其对于内存和磁盘这种不可压缩的资源,紧缺就意味着不稳定。下面对驱逐的策略、信号、阈值、监控频率和驱逐操作进行详细说明。

5.1、驱逐策略

  • kubelet持续监控主机的资源使用情况,并尽量防止计算资源被耗尽。一旦出现资源紧缺的迹象,kubelet就会主动终止一个或多个Pod的运行,以回收紧缺的资源。当一个Pod被终止时,其中的容器会全部停止,Pod的状态会被设置为Failed。

5.2、驱逐信号

  • 在表10.6中提到了一些信号,kubelet能够利用这些信号作为决策依据来触发驱逐行为。其中,描述列中的内容来自kubelet Summary API;每个信号都支持整数值或者百分比的表示方法,百分比的分母部分就是各个信号相关资源的总量。

  • memory.available的值取自cgroupfs,而不是free -m命令,这是因为free -m不支持在容器内工作。如果用户使用了node allocatable功能,则除了节点自身的内存需要判断,还需要利用cgroup根据用户Pod部分的情况进行判断。
  • kubelet假设inactive_file(不活跃LRU列表中的file-backed内存,以字节为单位)在紧缺情况下可以回收,因此对其进行了排除。kubelet支持以下两种文件系统。
    • (1)nodefs:保存kubelet的卷和守护进程日志等。
    • (2)imagefs:在容器运行时保存镜像及可写入层。
  • kubelet使用cAdvisor自动监控这些文件系统。kubelet不关注其他文件系统,不支持所有其他类型的配置,例如保存在独立文件系统中的卷和日志。
  • 磁盘压力相关的资源回收机制正在逐渐被驱逐策略接管,未来会停止对现有垃圾收集方式的支持。

5.3、驱逐阈值

  • kubelet可以定义驱逐阈值,一旦超出阈值,就会触发kubelet进行资源回收操作。
  • 阈值的定义方式为:
    • eviction-signal是在表10.6中列出了驱逐信号的名称。
    • operator是运算符,当前仅支持一个:< (小于)。
    • quantity需要符合Kubernetes的数量表达方式,也可以用以%结尾的百分比表示。
<eviction-signal> <operator> <quantity>
  • 例如,如果一个节点有10GiB内存,我们希望在可用内存不足1GiB时进行驱逐,就可以用下面任一方式来定义驱逐阈值。
    • memory.available < 10%。
    • memory.available < 1GiB。
  • 驱逐阈值有两种方式进行设置:软阈值和硬阈值。

1、驱逐软阈值

  • 驱逐软阈值由一个驱逐阈值和一个管理员设定的宽限期共同定义。
    • 当系统资源消耗达到软阈值时,在这一状况的持续时间达到宽限期之前,kubelet不会触发驱逐动作。
    • 如果没有定义宽限期,则kubelet会拒绝启动。
  • 另外,可以定义终止Pod的最大宽限期。如果定义了这一宽限期,那么kubelet会使用pod.Spec.TerminationGracePeriodSeconds和最大宽限期这两个值中较小的数值进行宽限,如果没有指定,则kubelet会立即杀掉Pod。
  • 软阈值的定义包括以下几个(kubelet)参数:
    • --eviction-soft:描述驱逐阈值(例如memory.available<1.5GiB),如果满足这一条件的持续时间超过宽限期,就会触发对Pod的驱逐动作。
    • --eviction-soft-grace-period:驱逐宽限期(例如memory.available=1m30s),用于定义达到软阈值之后持续时间超过多久才进行驱逐。
    • --eviction-max-pod-grace-period:在达到软阈值后,终止Pod的最大宽限时间(单位为s)。

2、驱逐硬阈值

  • 硬阈值没有宽限期,如果达到了硬阈值,则kubelet会立即杀掉Pod并进行资源回收。
  • 硬阈值的定义包括参数:
    • --eviction-hard:驱逐硬阈值,一旦达到阈值,就会触发对Pod的驱逐操作。
  • kubelet的默认硬阈值定义如下:
--eviction-hard=memory.available<100Mi

5.4、驱逐监控频率

  • kubelet的--housekeeping-interval参数定义了一个时间间隔,kubelet每隔一个这样的时间间隔就会对驱逐阈值进行评估。

5.5、节点的状况

  • kubelet会将一个或多个驱逐信号与节点的状况对应起来。
  • 无论触发了硬阈值还是软阈值,kubelet都会认为当前节点的压力太大,如表10.7所示为节点状况与驱逐信号的对应关系。

  • kubelet会持续向Master报告节点状态的更新过程,这一频率由参数--node-statusupdate- frequency指定,默认为10s。

5.6、节点状况的抖动

  • 如果一个节点的状况在软阈值的上下抖动,但是又没有超过宽限期,则会导致该节点的相应状态在True和False之间不断变换,可能会对调度的决策过程产生负面影响。
  • 要防止这种状况,可以使用参数--eviction-pressure-transitionperiod(在脱离压力状态前需要等待的时间,默认值为5m0s),为kubelet设置在脱离压力状态之前需要等待的时间。
  • 这样一来,kubelet在把压力状态设置为False之前,会确认在检测周期之内该节点没有达到驱逐阈值。

5.7、回收Node级别的资源

  • 如果达到了驱逐阈值,并且也过了宽限期,kubelet就会回收超出限量的资源,直到驱逐信号量回到阈值以内。
  • kubelet在驱逐用户Pod之前,会尝试回收Node级别的资源。
  • 在观测到磁盘压力的情况下,基于服务器是否为容器运行时定义了独立的imagefs,会导致不同的资源回收过程。
    • 有Imagefs的情况
      • (1)如果nodefs文件系统达到了驱逐阈值,则kubelet会删掉死掉的Pod、容器来清理空间。
      • (2)如果imagefs文件系统达到了驱逐阈值,则kubelet会删掉所有无用的镜像来清理空间。
    • 没有Imagefs的情况
      • 如果nodefs文件系统达到了驱逐阈值,则kubelet会按照下面的顺序来清理空间。
        • (1)删除死掉的Pod、容器。
        • (2)删除所有无用的镜像。

5.8、驱逐用户的Pod

  • 如果kubelet无法通过节点级别的资源回收获取足够的资源,就会驱逐用户的Pod。
  • kubelet会按照下面的标准对Pod的驱逐行为进行判断。
    • Pod要求的服务质量。
    • 根据Pod调度请求的被耗尽资源的消耗量。
  • 接下来,kubelet按照下面的顺序驱逐Pod。
    • BestEffort:紧缺资源消耗最多的Pod最先被驱逐。
    • Burstable:根据相对请求来判断,紧缺资源消耗最多的Pod最先被驱逐,如果没有Pod超出它们的请求,则策略会瞄准紧缺资源消耗量最大的Pod。
    • Guaranteed:根据相对请求来判断,紧缺资源消耗最多的Pod最先被驱逐,如果没有Pod超出它们的请求,策略会瞄准紧缺资源消耗量最大的Pod。
  • Guaranteed Pod永远不会因为其他Pod的资源消费被驱逐。如果系统进程(例如kubelet、docker、journald等)消耗了超出system-reserved或者kube-reserved的资源,而在这一节点上只运行了Guaranteed Pod,那么为了保证节点的稳定性并降低异常消耗对其他Guaranteed Pod的影响,必须选择一个Guaranteed Pod进行驱逐。
  • 本地磁盘是一种BestEffort资源。如有必要,kubelet会在DiskPressure的情况下,对Pod进行驱逐以回收磁盘资源。kubelet会按照QoS进行评估。
    • 如果kubelet判定缺乏inode资源,就会通过驱逐最低QoS的Pod的方式来回收inodes。
    • 如果kubelet判定缺乏磁盘空间,就会在相同QoS的Pod中,选择消耗最多磁盘空间的Pod进行驱逐。
  • 下面针对有Imagefs和没有Imagefs的两种情况,说明kubelet在驱逐Pod时选择Pod的排序算法,然后按顺序对Pod进行驱逐。
    • 有Imagefs的情况
      • 如果nodefs触发了驱逐,则kubelet会根据nodefs的使用情况(以Pod中所有容器的本地卷和日志所占的空间进行计算)对Pod进行排序。
      • 如果imagefs触发了驱逐,则kubelet会根据Pod中所有容器消耗的可写入层的使用空间进行排序。
    • 没有Imagefs的情况
      • 如果nodefs触发了驱逐,则kubelet会对各个Pod中所有容器的总体磁盘消耗(以本地卷+日志+所有容器的写入层所占的空间进行计算)进行排序。

5.9、资源最少回收量

  • 在某些场景下,驱逐Pod可能只回收了很少的资源,这就导致了kubelet反复触发驱逐阈值。另外,回收磁盘这样的资源,是需要消耗时间的。
  • 要缓和这种状况,kubelet可以对每种资源都定义minimum-reclaim。kubelet一旦监测到了资源压力,就会试着回收不少于minimum-reclaim的资源数量,使得资源消耗量回到期望的范围。
  • 例如,可以配置--eviction-minimum-reclaim如下:
--eviction-hard=memory.available<500Mi,nodefs.available<1Gi,imagefs.available<100Gi
--eviction-minimum-reclaim="memory.available=0Mi,nodefs.available=500Mi,imagefs.available=2Gi"
  • 这样配置的效果如下:
    • 当memory.available超过阈值触发了驱逐操作时,kubelet会启动资源回收,并保证memory.available至少有500MiB。
    • 如果是nodefs.available超过阈值并触发了驱逐操作,则kubelet会恢复nodefs.available到至少1.5GiB。
    • 对于imagefs.available超过阈值并触发了驱逐操作的情况,kubelet会保证imagefs.available恢复到最少102GiB。
  • 在默认情况下,所有资源的eviction-minimum-reclaim都为0。

5.10、节点资源紧缺情况下的系统行为

1、调度器的行为

  • 在节点资源紧缺的情况下,节点会向Master报告这一状况。在Master上运行的调度器(Scheduler)以此为信号,不再继续向该节点调度新的Pod。如表10.8所示为节点状况与调度行为的对应关系。

2、Node的OOM行为

  • 如果节点在kubelet能够回收内存之前遭遇了系统的OOM(内存不足),节点则依赖oom_killer的设置进行响应(OOM评分系统详见10.4节的描述)。
  • kubelet根据Pod的QoS为每个容器都设置了一个oom_score_adj值,如表10.9所示。

  • 如果kubelet无法在系统OOM之前回收足够的内存,则oom_killer会根据内存使用比率来计算oom_score,将得出的结果和oom_score_adj相加,得分最高的Pod首先被驱逐。
  • 这个策略的思路是,QoS最低且相对于调度的Request来说消耗最多内存的Pod会首先被驱逐,来保障内存的回收。
  • 与Pod驱逐不同,如果一个Pod的容器被OOM杀掉,则是可能被kubelet根据RestartPolicy重启的。

3、对DaemonSet类型的Pod驱逐的考虑

  • 通过DaemonSet创建的Pod具有在节点上自动重启的特性,因此我们不希望kubelet驱逐这种Pod;然而kubelet目前并没有能力分辨DaemonSet的Pod,所以无法单独为其制定驱逐策略,所以强烈建议不要在DaemonSet中创建BestEffort类型的Pod,避免产生驱逐方面的问题。

5.11、可调度的资源和驱逐策略实践

  • 假设一个集群的资源管理需求如下。
    • 节点内存容量:10GiB。
    • 保留1.5G的内存给系统守护进程(内核、kubelet等)。
    • 在内存使用率达到95%时驱逐Pod,以此降低系统压力并防止系统OOM。
--eviction-hard=memory.available<500Mi
--system-reserved=memory=1.5Gi
  • 在这个配置方式中隐式包含这样一个设置:系统预留内存也包括资源驱逐阈值。
  • 如果内存占用超出这一设置,则要么是Pod占用了超过其Request的内存,要么就是系统使用了超过500MiB内存。
  • 在这种设置下,节点一旦开始接近内存压力,调度器就不会向该节点部署Pod,并且假定这些Pod使用的资源数量少于其请求的资源数量。

5.12、现阶段的问题

1、kubelet无法及时观测到内存压力

  • kubelet目前从cAdvisor定时获取内存使用状况的统计情况。如果内存使用在这个时间段内发生了快速增长,且kubelet无法观察到MemoryPressure,则可能会触发OOMKiller。Kubernetes正在尝试将这一过程集成到memcg通知API中来减少这一延迟,而不是让内核首先发现这一情况。
  • 对用户来说,一个较为可靠的处理方式就是设置驱逐阈值为大约75%,这样就降低了发生OOM的几率,提高了驱逐的标准,有助于集群状态的平衡。

2、kubelet可能会错误地驱逐更多的Pod

  • 这也是状态搜集存在时间差导致的。未来可能会通过按需获取根容器的统计信息来减少计算偏差(https://github.com/google/cadvisor/issues/1247)。

6、保障一定数量的Pod

  • 在Kubernetes集群运行的过程中,许多管理操作都可能对Pod进行主动驱逐,“主动”一词意味着这一操作可以安全地延迟一段时间,目前主要针对以下两种场景。
    • 节点的维护或升级时(kubectl drain)。
    • 对应用的自动缩容操作(autoscaling down)。
  • 由于节点不可用(Not Ready)导致的Pod驱逐就不能被称为主动了。
  • 对于主动驱逐的场景来说,应用如果能够保持存活的Pod的数量,则会非常有用。通过使用PodDisruptionBudget,应用可以保证那些会主动移除Pod的集群操作永远不会在同一时间停掉太多Pod,从而导致服务中断或者服务降级等。例如,在对某些Node进行维护时,系统应该保证应用以不低于一定数量的Pod保障服务的正常运行。kubectl drain操作将遵循PodDisruptionBudget的设定,如果在该节点上运行了属于同一服务的多个Pod,则为了保证最少存活数量,系统将确保每终止一个Pod后,一定会在另一台健康的Node上启动新的Pod,再继续终止下一个Pod。
  • PodDisruptionBudget资源对象在Kubernetes 1.5版本时升级为Beta版本,用于指定一个Pod集合在一段时间内存活的最小实例数量或者百分比。一个PodDisruptionBudget作用于一组被同一个控制器管理的Pod,例如ReplicaSet或RC。与通常的Pod删除不同,驱逐Pod的控制器将使用/eviction接口对Pod进行驱逐,如果这一主动驱逐行为违反了PodDisruptionBudget的约定,就会被API Server拒绝。
  • PodDisruptionBudget本身无法真正保障指定数量或百分比的Pod的存活。例如,在一个节点中包含了目标Pod中的一个,如果节点故障,就会导致Pod数量少于minAvailable。PodDisruptionBudget对象的保护作用仅仅针对主动驱逐的场景,而非所有场景。
  • 对PodDisruptionBudget的定义包括如下两部分:
    • Label Selector:用于筛选被管理的Pod。
    • minAvailable:指定驱逐过程需要保障的最少Pod数量。minAvailable可以是一个数字,也可以是一个百分比,例如100%就表示不允许进行主动驱逐。
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: busybox-poddisruptionbudget
spec:
  minAvailable: 2
  selector:
    matchLabels:
      name: busybox-pod

1

#                                                                                                                      #
posted @ 2022-10-04 21:37  麦恒  阅读(175)  评论(0编辑  收藏  举报