1 缘起

使用任何软件都绕不过去的问题:硬件资源分配(CPU和内存),
只有可以控制资源的分配,才能保证资源高效利用。
K8S做到了,
使用K8S部署服务,可以为Pod指定CPU和内存的使用,
保证集群节点资源占用是可控的,
一方面,为不同的服务配置不同的资源,
另一方面,高效(充分)利用节点资源,
想要正确使用资源配置,需要从理论学起,
分享如下,帮助读者学习如何配置K8S的资源分配。
官网文档:https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/

2 Pod和容器的资源管理

我们可以设定某个Pod中容器的资源使用,比如CPU和内存资源,等等。
为Pod中的容器设定资源时,kube-scheduler根据需要的资源信息来决定将Pod部署在哪个节点(Node)。当为某个容器设定了资源上限时,kubelet会严格控制容器的资源使用,让其占用设定的资源范围内运行。kubelet也会为容器保留所需要的最少系统资源,即容器运行同样需要资源,不止是容器中服务需要硬件资源。

2.1 资源下限和资源上限

如果Pod运行的节点(Node)有“足够”可用的资源,容器使用的资源可以比设定的资源下限多,但是,不能比设定的上限资源多。
例如,容器设定的内存资源下限(memory)为256MiB,而节点(Node)有8GiB的内存,并且没有其他Pod使用,此时,该容器可以申请更多的内存使用。
如果容器设定的资源上限(memory)为4GiB,kubelet(和容器运行时)会强制执行该上限逻辑,运行时会阻止容器使用比设定的资源上限多的资源。如果容器中的进程尝试使用更多的内存,系统内核会终止资源分配的进程,并抛出内存溢出(OOM)错误。
资源限制有两种实现方式:
(1)响应式:系统发现违规行为直接干预;
(2)强制干预:系统阻止容器超过资源上限。
同样的限制,不同的运行时有不同的实现方式。
注意:如果为资源指定了上限值,但是没有指定下限值,并且没有为资源配置默认的准入时间机制,Kubernetes则会将上限值作为下限值使用。

2.2 资源类型

Kubernetes中可资源的类型有CPU和内存,资源的配置按照各自单位进行,CPU单位为Kubernetes可用的CPU个数,表示计算处理,内存以字节单位指定。Linux工作负载中,可以配置大页资源,大页是Linux的特色之一,节点内核分配的内存块可以大于默认的页尺寸。
比如,系统的默认页尺寸为4KiB,可以指定上限为80Mi,即hugepages-2Mi: 80Mi。如果容器分配超过40个2MiB大页(总量80MiB),会导致分配失败。
*注意:不能过度使用hupages-资源,这是区别于cpu和内存资源的地方。
CPU和内存统称为计算资源或者资源,计算资源的数量是可观测的,这些资源是可以申请、分配和消耗的,与API资源不同的是,计算资源是固定的物理资源(确定的运行机器中),不可实时变更,而像Pod或者Service这些对象资源是可以读取并通过Kubernetes API服务实时修改的。

2.3 Pod和容器的资源上限和下限

对于每个容器,可以指定资源的上限和下限,包括:

  • spec.containers[].resources.limits.cpu
  • spec.containers[].resources.limits.memory
  • spec.containers[].resources.limits.hugepages-
  • spec.containers[].resources.requests.cpu
  • spec.containers[].resources.requests.memory
  • spec.containers[].resources.requests.hugepges-

虽然可以为每个容器配置资源的上限和下限,但是,仍要全面了解配置Pod资源上限和下限的意义。对于特殊的资源,Pod的资源上限/下限是Pod中容器的资源上限/下限的总和。

2.4 Kubernetes的资源单位

2.4.1 CPU资源单位

CPU单元中CPU资源的上限和下限是可观测的,Kubernetes中,1个CPU单元对应物理机的一个CPU核或者1个虚拟核,这取决于节点(Node)在是物理主机还是者物理机中运行的虚拟主机。

资源下限可以配置为小数,为容器配置的CPU下限spec.containers[].resources.requests.cpu为0.5时,使用半个CPU核(与1个核相比)。于CPU资源的单位而言,配置0.1与100m等价,即使用100毫个CPU,或者100毫个核。
配置的CPU资源是固定且确定的,比如500m的CPU资源无论在单核、双核或48核的机器都是相同的。
注意:Kubernetes可配的CPU资源最小粒度为1m,这对使用毫CPU单位的CPU资源且小于1.0或者1000m的情况很有用,比如,使用5m而不是0.005。

2.4.2 内存单位

内存资源的上限和下限的单位是字节(bytes),配置内存资源可以直接使用整数或者带后缀的数据,如E、P、T、G、M、k。也可以使用两个字符的后缀,与前面的含义相同,如Ei、Pi、Ti、Gi、Mi、Ki。
样例,相同值,不同的格式:

128974848, 129e6, 129M,  128974848000m, 129Mi

后缀格式说明,400m即0.4bytes,400mebibytes(400Mi)等价400megabytes(400M)。

2.5 容器资源配置样例

Pod中的两个容器资源配置如下。两个容器的CPU和内存资源上限分别为0.25个CPU、64MiB的内存,资源上限为0.5个CPU和128MiB的内存。

---
apiVersion: v1
kind: Pod
metadata:
  name: frontend
spec:
  containers:
  - name: app
    image: images.my-company.example/app:v4
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"
  - name: log-aggregator
    image: images.my-company.example/log-aggregator:v6
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

2.6 Pod资源上限如何调度?

创建Pod时,Kubernetes的调度器为Pod的运行选择一个节点(Node),在物理上,每个节点为Pod提供CPU和内存资源提供的资源都是有上限的。调度器为确保资源正确分配,为容器调配的CPU和内存总和不大于节点可提供的资源。即使某个Pod的容器运行需要的物理机CPU和内存很少,调度器仍然可以在检查物理机资源是强制失败,即不分配资源,放弃启动容器服务。这样可有效保护运行的服务后续使用资源增加时正常运行,比如,请求高峰期,保证服务正常。

2.7 Kubernetes如何使资源上限和下限生效?

kubelet启动Pod中的容器时,kubelet将容器的CPU和内存上限和下限传递给容器运行时。
Linux系统中,容器运行时通常配置内核cgroups来应用并强制执行定义的上限。

  • CPU上限定义了容器可以使用的CPU资源极限。在每个调度的时间分片上,Linux内核会检测是否达到上限,如果达到资源上限,内核等待cgroup恢复执行。
  • CPU下限通常定义一个权重。如果cgroups不同的容器运行在资源有限的系统中,工作负载有更大CPU下限值的容器分配到的CPU时间比工作负载少CPU的多。
  • 内存的下限值主要用于Pod调度。在运行cgroups v2节点(Node)上,容器运行时或许将内存下限值作为配置memroy.min和memory.low的提示。
  • 内存上限为cgroup定义了可用内存的上限值。如果容器尝试分配大于上限值的内存,Linux内核内存溢出子系统激活,并且,通常通过停止容器中的进程阻止内存分配。如果进程是容器的PID为1线程,容器标记为可重启,Kubernetes重启容器。
  • Pod或容器的内存上限值可以作用在内存支持卷中,如emptyDir。kubelet将tmpfs空目录卷作为容器内存使用,而不是作为本地临时存储

如果容器运行在配置的内存下限中,但是节点(Node)内存日趋紧张(不够用),容器的Pod会被强制剔除。
容器可能允许,也可能不允许长时间超过CPU上限值运行,然而容器运行时不会因为过多使用CPU终止Pod或者容器。

2.7.1 监控计算资源和内存资源的使用

kubelet汇报Pod资源使用情况,并传给Pod的status。
如果集群中有监控工具,Pod资源使用数据可以通过测量API直接获取,也可以通过监控工具获取。

2.8 本地临时存储

节点有本地临时存储,由本地可写设备或RAM支持。“临时”意味着不保证持久化。
Pod使用临时本地存储来缓存、存储日志。kubelet可以为Pod提供暂存空间,即使用的本地临时存储挂载emptyDir卷到容器中。
kubelet同样可以使用这种存储空间保留节点级别的容器日志,容器镜像和运行中镜像的可写图层。
注意:如果节点失败,临时存储空间的数据会丢失。

2.8.1 配置本地临时存储

节点中Kubernetes支持两种本地临时存储配置。

  • 单文件系统
    该配置中,将所有的临时本地数据(emptyDir卷、可写图层、容器镜像、日志)放在一个文件系统。配置kubelet最有效的方式为:将文件系统给Kubernetes数据专用。
    kubelet将节点级别的容器日志写入文件系统,如同写入临时存储
    kubelet将日志写入配置的日志目录(默认/var/log),同时有一个基础目录存储其他本地数据(默认为/var/lib/kubelet)。
    通常,/var/lib/kubelet和/var/log是系统根文件系统,kubelet的设计考虑到了这种布局。

  • 双文件系统
    节点的文件系统,用于存储来自运行Pod的日志和emptyDir文件。
    kubelet同样会将节点级别的容器日志写入到第一文件系统,如同写入到临时存储。
    可以使用不同的逻辑存储设备来支持隔离文件系统,这些配置中,告知kubelet放置容器镜像图层和可写图层的目录位于第二个文件系统上。
    第一文件系统不会存储任何镜像图层或可写图层。

kubelet可以检测本地存储的使用量,通过如下提供:

  • LocalStorageCapacityIsolation特征开关(默认开启:on);
  • 使用本地临时存储支持的配置设置节点;

注意:kubelet跟踪的tmpfs emptyDir卷作为容器内存使用,而不是本地临时存储

2.8.2 为本地临时存储配置上限和下限

通过配置ephemeral-storage管理本地临时存储
Pod的每个容器可以指定上限和下限,如下:

  • spec.containers[].resources.limits.ephemeral-storage
  • spec.containers[].resources.requests.emphemeral-storage
    ephemeral-storage的上限和下限通过字节单位观测,配置时可以使用整数或者固定的后缀,单字母后缀:E、P、T、G、M、k,对应的等价双字母后缀:Ei、Pi、Ti、Gi、Mi、Ki,不同单位的同等数值:
  • 128974848
  • 129e6
  • 129M
  • 129Mi

需要注意后缀的使用,400m等价0.4bytes,400mebibytes(400Mi)等价400megabytes(400M)。

下面的样例中,Pod有连个容器,每个容器配置了2GiB的本地临时存储下限,4GiB的本地临时存储上限,因此,该Pod的本地存储下限为4GiB,上限为8GiB。

apiVersion: v1
kind: Pod
metadata:
  name: frontend
spec:
  containers:
  - name: app
    image: images.my-company.example/app:v4
    resources:
      requests:
        ephemeral-storage: "2Gi"
      limits:
        ephemeral-storage: "4Gi"
    volumeMounts:
    - name: ephemeral
      mountPath: "/tmp"
  - name: log-aggregator
    image: images.my-company.example/log-aggregator:v6
    resources:
      requests:
        ephemeral-storage: "2Gi"
      limits:
        ephemeral-storage: "4Gi"
    volumeMounts:
    - name: ephemeral
      mountPath: "/tmp"
  volumes:
    - name: ephemeral
      emptyDir: {}

2.8.3 Pod临时存储的上限是如何调度的?

创建Pod时,Kubernetes调度器为Pod的运行指定节点,每个节点为Pod提供的本地临时存储是有限的。
调度器会保证容器调度中分配的资源总和在节点可提供的上限范围内。

2.8.4 临时存储消耗管理

kubelet将本地临时存储作为资源进行管理,kubelet在如下方面观测存储用量:

  • emptyDir卷,除了tmpfs emptDir卷
  • 存储节点级日志的目录
  • 可写容器图层

如果Pod使用的临时存储空间高于设定的上限值,kubelet会触发驱逐Pod。
对于容器级别的隔离,如果容器可写的图层和日志使用的空间达到设定的上限,kubelet标记Pod为驱逐状态。
对于pod级别的隔离,kubelet通过汇总Pod中所有容器的存储值来比对Pod的存储上限,作为进一步动作的依据。如果所有容器使用的临时存储超过设定的上限值,并且Pod的emptyDir卷超过全局的Pod存储上限,kubelet同样将Pod标记为驱逐状态。

注意:如果kubelet没有观测本地临时存储,Pod使用的临时存储超过本地存储上限时不会被标识为驱逐状态。然而,如果文件系统不足以存储可写容器图层、节点级别日志或者emptyDir卷,节点会由于本地存储不足,为自身添加“污点”,转为污点证人,该污点会触发驱逐,因为Pod不容忍污点。

kubelet提供两种观测Pod存储空间使用的方法:

  • 周期扫描
    kubelet周期执行,定期检查扫描的emptyDir卷、容器日志目录和可写的容器图层结果。
    注意:这种方式,kubelet不会追踪已删除的文件的打开文件描述。如果在emptyDir卷中创建文件,并打开这个文件,在其打开时删除该文件,索引节点会在关闭文件前保留已删除的文件节点,但是,kubelet不会将该文件的占用的空间计入使用已使用空间。

  • 文件系统项目配额
    项目配额是操作系统级别的特性,为了管理文件系统存储空间使用。对于Kubernetes,可以开启项目配额来检测存储空间的使用。首先,要保证节点中文件系统支持的emptyDir卷可以提供项目配额功能,比如XFS和ext4fs均提供项目配额。
    注意:项目配额允许监控存储空间使用,不会强制执行限制(没理解啥意思)。
    Kubernetes使用的项目ID从1048576开始。使用的ID注册在/etc/projects和/etc/projid中注册。如果该范围内(0~1048576)的项目ID用于系统的其他功能,项目ID必须注册在/etc/projects和/etc/projid保证Kubernetes不会使用他们。
    配额比目录扫描更快、更准确。为项目分配一个目录后,所有文件会在该项目的目录中创建,内核只需要追踪文件有多少个块被使用。某个文件被新建然后删除,但是有打开文件描述,仍然会计算文件的占用空间。配额追踪的记录精确到被删除的文件。
    使用项目配额的方式:

  • 通过LocalStorageCapacityIsolationFSQuotaMonitoring=ture启用kubelet配置的featureGates属性或者使用命令行:–feature-gates

  • 确保根文件系统(或者运行时文件系统)开启项目配额。所有XFS文件系统支持项目配额,而ext4文件系统需要开启项目配额追踪(当文件系统没有挂载时)

# For ext4, with /dev/block-device not mounted
sudo tune2fs -O project -Q prjquota /dev/block-device
  • 保证根文件系统(或运行时文件系统)挂载项目配额开启,XFS和ext4fs挂载命名为prjquota

2.9 扩展资源

扩展资源是kubernets.io域名之外的全限定资源名。允许集群运营商发布,并允许用户使用非Kubernetes内建资源。
使用外部资源有两步:
(1)集群运营商必须发布扩展资源;
(2)用户必须在Pod中请求扩展资源;

2.9.1 管理扩展资源

  • 节点级别扩展资源
    节点级别的扩展资源与节点是绑定的
  • 设备插件管理资源
    待输出
  • 其他资源
    为发布新的节点级别扩展资源,集群运营商可以提交PATCH类型的HTTP请求到API服务,通过status.capacity属性为集群节点指定可用量。该操作之后,节点的status.capacity将包含新资源。status.allocatable属性会由kubelet异步自动更新。
    因为调度器评估Pod健康度时,会使用节点的status.allocatable值,所以,调度器只关心异步更新后的新值。在使用新资源补充节点容量和第一个Pod在节点中请求可调度资源之间或许会有时延。
    如,使用curl发送HTTP请求发布5个example.com/foo资源到k8s-node-1中(主节点为k8s-master)。
curl --header "Content-Type: application/json-patch+json" \
--request PATCH \
--data '[{"op": "add", "path": "/status/capacity/example.com~1foo", "value": "5"}]' \
http://k8s-master:8080/api/v1/nodes/k8s-node-1/status

注意:上面的请求中,~1是字符/的编码结果,JSON-Patch操作路径值解析为JSON-Pointer,详见:IETF RFC 6901,第三部分

2.9.2 集群级别扩展资源

集群级别扩展资源不是绑定在节点的,通常由扩展调度器管理,调度器处理资源消耗和资源配额。
可以为扩展调度器指定其管理的扩展资源。
样例如下,从调度器配置的策略中可知,集群级别的扩展资源example.com/foo由扩展调度器处理。

  • 只有Pod请求example.com/foo时,调度器才会将Pod发送给扩展调度器。
  • ignoreByScheduler属性说明调度器无需在PodFitsResources检查example.com/foo资源。
{
  "kind": "Policy",
  "apiVersion": "v1",
  "extenders": [
    {
      "urlPrefix":"<extender-endpoint>",
      "bindVerb": "bind",
      "managedResources": [
        {
          "name": "example.com/foo",
          "ignoredByScheduler": true
        }
      ]
    }
  ]
}

2.9.3 使用扩展资源

用户在Pod中可以使用扩展资源(如同使用CPU和内存一样)。调度器负责资源核算,保证同时分配给Pod的资源不会超过可用量。
API服务将扩展资源的数量限制为整数。有效的数值:3,3000m及3Ki,无效的数据如0.5和1500m。
如果使用Pod中的扩展资源,需要将资源名称作为键映射到容器的属性:spec.containers[].resources.limits。
注意:扩展资源不可过度使用,因此上限和下限需要遵循当前容器的限制。

Pod的正常调度需要同时满足所有资源需求,如CPU、内存和扩展资源。如果资源不满足使用,Pod会一直处于PENDING状态。
如下样例中,Pod需要CPU的两个核,1个扩展资源(example.com/foo)。

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: my-container
    image: myimage
    resources:
      requests:
        cpu: 2
        example.com/foo: 1
      limits:
        example.com/foo: 1

2.10 问题排查

2.10.1 Pod一直Pending(提示信息FailedScheduling)

如果调度器发现没有适合Pod的节点(Node),Pod会一直Pending,直到发现可用的节点。当调度器无法为Pod分配合适的节点时会触发告警事件,可以使用kubectl查看Pod事件:

kubectl describe pod frontend | grep -A 9999999999 Events

结果:

Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  23s   default-scheduler  0/42 nodes available: insufficient cpu

上面的样例中个,Pod的名称为frontend,异常信息表明节点中没有足够的CPU使用,同类的资源不足有内存资源不足。对于资源不足,通常,有如下方案:

  • 集群中增加节点。
  • 终止不需要的Pod,为Pending的Pod留出资源。
  • 检查Pod需求的资源是否比集群中所有节点都多。如,所有节点的CPU为1核,而Pod需要的CPU为1.1核,则无法正常调度。
  • 检查节点是否被污染。如果大多数节点被污染(资源不足),新的Pod无法容忍污点,无法运行。

通过如下命令检查节点的容量和可分配的资源:

kubectl describe nodes e2e-test-node-pool-4lw4

结果如下:

Name:            e2e-test-node-pool-4lw4
[ ... lines removed for clarity ...]
Capacity:
 cpu:                               2
 memory:                            7679792Ki
 pods:                              110
Allocatable:
 cpu:                               1800m
 memory:                            7474992Ki
 pods:                              110
[ ... lines removed for clarity ...]
Non-terminated Pods:        (5 in total)
  Namespace    Name                                  CPU Requests  CPU Limits  Memory Requests  Memory Limits
  ---------    ----                                  ------------  ----------  ---------------  -------------
  kube-system  fluentd-gcp-v1.38-28bv1               100m (5%)     0 (0%)      200Mi (2%)       200Mi (2%)
  kube-system  kube-dns-3297075139-61lj3             260m (13%)    0 (0%)      100Mi (1%)       170Mi (2%)
  kube-system  kube-proxy-e2e-test-...               100m (5%)     0 (0%)      0 (0%)           0 (0%)
  kube-system  monitoring-influxdb-grafana-v4-z1m12  200m (10%)    200m (10%)  600Mi (8%)       600Mi (8%)
  kube-system  node-problem-detector-v0.1-fj7m3      20m (1%)      200m (10%)  20Mi (0%)        100Mi (1%)
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.)
  CPU Requests    CPU Limits    Memory Requests    Memory Limits
  ------------    ----------    ---------------    -------------
  680m (34%)      400m (20%)    920Mi (11%)        1070Mi (13%)

由结果可知,

CPU(核)内存(Mi)
总资源27679792Ki=7499Mi
可用资源1.87474992Ki=7299Mi
已用资源0.68920Mi
剩余资源1800m-680m=1120m=1.1127299Mi-920Mi=6379Mi≈6.23Gi

如果Pod需要的CPU资源大于1.12核,内存高于6.23Gi,则该节点无法运行Pod。

2.10.2 容器终止运行

容器或许因为资源有限而终止运行。因此需要检查容器是否因为资源问题而被终止,命令如下:

kubectl describe pod simmemleak-hra99

结果如下:

Name:                           simmemleak-hra99
Namespace:                      default
Image(s):                       saadali/simmemleak
Node:                           kubernetes-node-tf0f/10.240.216.66
Labels:                         name=simmemleak
Status:                         Running
Reason:
Message:
IP:                             10.244.2.75
Containers:
  simmemleak:
    Image:  saadali/simmemleak:latest
    Limits:
      cpu:          100m
      memory:       50Mi
    State:          Running
      Started:      Tue, 07 Jul 2019 12:54:41 -0700
    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137
      Started:      Fri, 07 Jul 2019 12:54:30 -0700
      Finished:     Fri, 07 Jul 2019 12:54:33 -0700
    Ready:          False
    Restart Count:  5
Conditions:
  Type      Status
  Ready     False
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  42s   default-scheduler  Successfully assigned simmemleak-hra99 to kubernetes-node-tf0f
  Normal  Pulled     41s   kubelet            Container image "saadali/simmemleak:latest" already present on machine
  Normal  Created    41s   kubelet            Created container simmemleak
  Normal  Started    40s   kubelet            Started container simmemleak
  Normal  Killing    32s   kubelet            Killing container with id ead3fb35-5cf5-44ed-9ae1-488115be66c6: Need to kill Pod

由结果可知,重启了5次(Restart Count 5)说明Pod的容器终止和重新启动了5次,原因OOMKilled表明容器使用的内存超过配置的上限值。
接下来可以检查代码是否有内存泄露,如果容器需要的资源确实比配置的高,则需要提高容器的内存上限配置。

3 小结

核心:
(1)资源配置类型:CPU和内存;
(2)CPU单位:最少1m,换算:1核=1000m,可以配置hugepages;内存单位:bytes,同时可以使用E、P、T、G、M、k;
(3)Pod申请资源时,可以指定下限和上限;
(4)Pod中容器可以配置本地临时存储,同样有上限和下限,并且可以挂载到本机卷;
(5)可以为Pod配置扩展资源;
(6)如果Pod需要的资源大于配置的资源上限,会一直处于PENDING状态,如果节点资源不足,会将节点标记为污点节点,无法部署新的Pod。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐