成功的应用部署、管理与共享云环境中的共存基础在于识别和声明应用程序的资源需求和运行时依赖。这个“可预测的需求”模式指明了如何声明应用程序的要求,无论是硬性运行时依赖还是资源需求。声明需求对于 Kubernetes 寻找集群中适当的位置来运行应用程序至关重要。
问题
Kubernetes 可以管理用不同编程语言编写的应用程序,只要这些应用程序能够在容器中运行。然而,不同语言对资源的需求不同。通常,编译型语言运行速度更快,相较于即时运行时或解释型语言,通常需要更少的内存。尽管许多现代编程语言在相同类别中的资源需求类似,但从资源消耗的角度来看,更重要的是应用程序的领域、业务逻辑和实际实现细节。
除了资源需求,应用程序的运行时还依赖于平台管理的功能,例如数据存储或应用配置。
解决方案
了解容器的运行时需求主要有两个原因。首先,当所有运行时依赖和资源需求都被定义后,Kubernetes 可以做出智能决策,决定将容器放置在集群中的哪个位置,以实现最有效的硬件利用。在一个拥有大量进程和不同优先级的共享资源环境中,确保成功共存的唯一方法是提前了解每个进程的需求。然而,智能放置只是其中一方面。
容器资源配置文件对于容量规划也至关重要。根据特定服务的需求和总服务数量,我们可以对不同环境进行容量规划,并制定出满足整个集群需求的最具成本效益的主机配置文件。服务资源配置文件和容量规划相辅相成,确保长期成功的集群管理。
在深入讨论资源配置文件之前,让我们先看看如何声明运行时依赖。
运行时依赖
最常见的运行时依赖之一是用于保存应用程序状态的文件存储。容器文件系统是短暂的,容器关闭时会丢失其内容。Kubernetes 提供了 Volume 作为 Pod 级别的存储工具,它可以在容器重启时存活。
最简单的 Volume 类型是 emptyDir,它与 Pod 一起存在。当 Pod 被删除时,其内容也会丢失。为了在 Pod 重启时保持数据,Volume 需要由另一种存储机制支持。如果应用程序需要读取或写入文件到这种长期存储中,你必须在容器定义中显式声明这一依赖,如示例 2-1 所示。
示例 2-1. 对 PersistentVolume 的依赖
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
volumeMounts:
- mountPath: "/logs"
name: log-volume
volumes:
- name: log-volume
persistentVolumeClaim:
claimName: random-generator-log
PersistentVolumeClaim (PVC) 的依赖需要存在并绑定。
调度器评估 Pod 需要的 Volume 类型,这影响 Pod 的放置位置。如果 Pod 需要的 Volume 在集群中的任何节点上都没有提供,Pod 将不会被调度。Volumes 是运行时依赖的一个例子,它影响 Pod 能运行的基础设施类型以及 Pod 是否能被调度。
类似的依赖发生在你要求 Kubernetes 通过 hostPort 在主机系统上暴露容器端口时。使用 hostPort 创建了对节点的另一个运行时依赖,并限制了 Pod 的调度位置。hostPort 在集群中的每个节点上保留端口,并且每个节点上最多只能调度一个 Pod。由于端口冲突,你的 Pod 的数量只能扩展到 Kubernetes 集群中的节点数量。
配置是另一种类型的依赖。几乎每个应用程序都需要一些配置信息,而 Kubernetes 推荐的解决方案是通过 ConfigMaps 提供。你的服务需要有一个消费设置的策略——无论是通过环境变量还是文件系统。在这两种情况下,这都引入了容器对命名 ConfigMaps 的运行时依赖。如果没有创建所有预期的 ConfigMaps,容器会被调度到节点上,但不会启动。
与 ConfigMaps 类似,Secrets 提供了一种稍微更安全的方式来将环境特定的配置分发到容器。使用 Secret 的方式与 ConfigMaps 相同,使用 Secret 会引入相同类型的容器到命名空间的依赖。ConfigMaps 和 Secrets 的详细信息将在第 20 章“配置资源”中解释,示例 2-2 显示了如何将这些资源用作运行时依赖。
示例 2-2. 对 ConfigMap 的依赖
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
env:
- name: PATTERN
valueFrom:
configMapKeyRef:
name: random-generator-config
key: pattern
对 ConfigMap random-generator-config 的强制依赖。
虽然 ConfigMap 和 Secret 对象的创建是我们必须执行的简单部署任务,但集群节点提供存储和端口号。其中一些依赖限制了 Pod 的调度位置(如果有的话),其他依赖可能会阻止 Pod 启动。在设计具有这些依赖的容器化应用程序时,请始终考虑它们将来会带来的运行时约束。
资源配置文件
指定容器依赖如 ConfigMap、Secret 和 volumes 是直接的。确定容器的资源需求则需要更多的思考和实验。在 Kubernetes 的上下文中,计算资源被定义为可以被请求、分配和消耗的容器资源。这些资源被分类为可压缩的(即可以被限制,如 CPU 或网络带宽)和不可压缩的(即不能被限制,如内存)。
区分可压缩和不可压缩资源很重要。如果容器消耗了过多的可压缩资源,如 CPU,它们会被限制,但如果它们使用了过多的不可压缩资源(如内存),它们会被终止(因为没有其他方法要求应用程序释放已分配的内存)。
根据应用程序的性质和实现细节,你必须指定所需的最小资源量(称为请求)和它可以增长到的最大量(限制)。每个容器定义可以以请求和限制的形式指定所需的 CPU 和内存。在高层次上,请求/限制的概念类似于软/硬限制。例如,我们可以通过 -Xms 和 -Xmx 命令行选项为 Java 应用程序定义堆大小。
请求的量(但不是限制)被调度器在将 Pods 放置到节点时使用。对于给定的 Pod,调度器仅考虑那些仍有足够容量来容纳 Pod 及其所有容器的节点,通过汇总请求的资源量来计算。在这方面,每个容器的请求字段影响 Pod 是否可以被调度。示例 2-3 显示了如何为 Pod 指定这些限制。
示例 2-3. 资源限制
apiVersion: v1
kind: Pod
metadata:
name: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
memory: 200Mi
CPU 和内存的初始资源请求。
直到我们希望应用程序增长到最大值的上限。我们故意不指定 CPU 限制。
以下类型的资源可以用作请求和限制规格中的键:
- memory:这种类型用于指定应用程序的堆内存需求,包括 emptyDir 类型的 volumes,配置 medium: Memory。内存资源不可压缩,因此超出配置内存限制的容器将触发 Pod 被驱逐,即被删除并可能在不同节点上重新创建。
- cpu:cpu 类型用于指定应用程序所需的 CPU 周期范围。然而,它是一种可压缩资源,这意味着在节点超额分配的情况下,所有运行容器的 CPU 插槽将根据其指定的请求被限制。因此,强烈建议你设置 CPU 资源的请求而不设置限制,以便它们可以受益于所有额外的 CPU 资源,这些资源本来会被浪费。
- ephemeral-storage:每个节点都有一些文件系统空间用于临时存储,保存日志和可写容器层。未存储在内存文件系统中的 emptyDir volumes 也使用临时存储。使用此请求和限制类型,你可以指定应用程序的最小和最大需求。ephemeral-storage 资源不可压缩,如果 Pod 使用的存储超过其限制,Pod 将被驱逐。
hugepage-<size>:Huge pages 是可以作为 volumes 挂载的大型连续预分配内存页。根据你的 Kubernetes 节点配置,提供几种大小的 huge pages,如 2 MB 和 1 GB 页。你可以指定请求和限制来表示你希望消耗的某种类型的 huge pages 数量(例如,hugepages-1Gi: 2Gi 请求两个 1 GB 的 huge pages)。Huge pages 不能被超额分配,因此请求和限制必须相同。
根据你指定请求、限制或两者,平台提供三种质量服务(QoS)类型:
- Best-Effort:未为其容器设置任何请求和限制的 Pods 具有 Best-Effort QoS。这种 Best-Effort Pod 被认为是最低优先级的,当 Pod 所在节点的不可压缩资源耗尽时,最有可能最先被终止。
- Burstable:定义了不等量的请求和限制值(并且限制大于请求的 Pod 被标记为 Burstable)。这种 Pod 有最小的资源保证,但也愿意在有空闲资源时消耗更多的资源,直到其限制。当节点处于不可压缩资源压力下时,这些 Pods 可能会被终止,特别是当没有 Best-Effort Pods 时。
- Guaranteed:具有相等请求和限制资源的 Pod 属于 Guaranteed QoS 类别。这些是优先级最高的 Pods,保证在 Best-Effort 和 Burstable Pods 之前不会被终止。这种 QoS 模式是应用程序内存资源的最佳选择,因为它意味着最小的意外情况,并避免了内存不足引发的驱逐。
因此,你为容器定义或省略的资源特性直接影响其 QoS,并定义了在资源紧张时 Pod 的相对重要性。定义 Pod 的资源需求时,务必考虑这一后果。
关于 CPU 和内存资源的建议
虽然你有许多选项来声明应用程序的内存和 CPU 需求,但我们和其他人建议遵循以下规则:
- 对于内存,始终将请求设置为等于限制。
- 对于 CPU,设置请求而不设置限制。
有关为何不应为 CPU 使用限制的更深入解释,请参阅博客文章《For the Love of God, Stop Using CPU Limits on Kubernetes》,有关推荐内存设置的详细信息,请参阅博客文章《What Everyone Should Know About Kubernetes Memory Limits》。
Pod 优先级
我们之前解释了容器资源声明如何定义 Pod 的 QoS,并在资源紧缺时影响 Kubelet 杀死 Pod 中容器的顺序。还有两个相关概念是 Pod 优先级和抢占。Pod 优先级允许您指示一个 Pod 相对于其他 Pod 的重要性,从而影响 Pod 调度的顺序。让我们在示例 2-4 中看看这个概念的实际应用。
示例 2-4. Pod 优先级
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: high-priority
value: 1000
globalDefault: false
description: This is a very high-priority Pod class
---
apiVersion: v1
kind: Pod
metadata:
name: random-generator
labels:
env: random-generator
spec:
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
priorityClassName: high-priority
PriorityClass 对象的名称。
对象的优先级值。
globalDefault 设置为 true 时用于那些没有指定 priorityClassName 的 Pod。只有一个 PriorityClass 可以将 globalDefault 设置为 true。
用于此 Pod 的优先级类,定义在 PriorityClass 资源中。
我们创建了一个 PriorityClass,这是一个用于定义基于整数优先级的非命名空间对象。我们的 PriorityClass 名为 high-priority,优先级为 1000。现在我们可以通过其名称 priorityClassName: high-priority 将此优先级分配给 Pod。PriorityClass 是一种指示 Pod 之间相对重要性的机制,数值越高表示 Pod 越重要。
Pod 优先级影响调度器将 Pod 放置到节点上的顺序。首先,优先级准入控制器使用 priorityClassName 字段为新 Pod 填充优先级值。当多个 Pod 等待放置时,调度器首先按最高优先级对等待队列中的 Pod 进行排序。任何待定的 Pod 都会在调度队列中其他优先级较低的 Pod 之前被选择,并且如果没有任何限制条件阻止其调度,则该 Pod 会被调度。
这里是关键部分。如果没有节点有足够的容量来放置一个 Pod,调度器可以从节点上抢占(移除)较低优先级的 Pod 以释放资源,并放置优先级更高的 Pod。因此,如果满足所有其他调度要求,优先级较高的 Pod 可能会比优先级较低的 Pod 更早被调度。该算法有效地允许集群管理员控制哪些 Pod 是更关键的工作负载,并通过允许调度器驱逐较低优先级的 Pod 来为优先级更高的 Pod 腾出空间。如果一个 Pod 无法被调度,调度器将继续放置其他较低优先级的 Pod。
假设您希望您的 Pod 以特定优先级进行调度,但不希望驱逐任何现有的 Pod。在这种情况下,您可以将 PriorityClass 标记为 preemptionPolicy: Never。分配给此优先级类的 Pod 将不会触发任何运行中 Pod 的驱逐,但仍将根据其优先级值进行调度。
Pod QoS(前面讨论过)和 Pod 优先级是两个正交特性,它们之间没有直接联系,只有少量的重叠。QoS 主要由 Kubelet 用来在可用计算资源不足时保持节点稳定性。Kubelet 在驱逐 Pod 之前首先考虑 QoS,然后才考虑 Pod 的 PriorityClass。另一方面,当调度器选择抢占目标时,调度器驱逐逻辑完全忽略 Pod 的 QoS。调度器尝试选择一组优先级最低的 Pod 来满足等待放置的优先级较高的 Pod 的需求。
当 Pod 指定了优先级时,可能会对其他被驱逐的 Pod 产生不良影响。例如,虽然会遵守 Pod 的优雅终止策略,但第十章“Singleton Service”中讨论的 PodDisruptionBudget 并不能得到保证,这可能会破坏依赖于 Pod 法定人数的较低优先级集群应用程序。
另一个需要注意的问题是恶意或不知情的用户创建优先级最高的 Pod,并驱逐所有其他 Pod。为防止这种情况发生,ResourceQuota 已扩展以支持 PriorityClass,并为不应通常被抢占或驱逐的关键系统 Pod 保留了较高的优先级号。
总之,Pod 优先级应该谨慎使用,因为用户指定的数值优先级会引导调度器和 Kubelet 决定要放置或杀死哪些 Pod,且可能被用户利用。任何改变都可能影响许多 Pod,并可能阻碍平台提供可预测的服务级别协议。
项目资源
Kubernetes 是一个自助服务平台,允许开发人员在指定的隔离环境中按需运行应用程序。然而,在共享的多租户平台中工作也需要存在特定的边界和控制单元,以防止某些用户消耗平台的所有资源。ResourceQuota 就是其中一个工具,它提供了限制在命名空间内聚合资源消耗的约束。通过 ResourceQuotas,集群管理员可以限制消耗的计算资源(CPU、内存)和存储的总和。它还可以限制在命名空间中创建的对象(例如 ConfigMaps、Secrets、Pods 或 Services)的总数。示例 2-5 展示了限制某些资源使用的实例。有关可以通过 ResourceQuotas 限制使用的受支持资源的完整列表,请参见 Kubernetes 官方文档。
示例 2-5. 资源约束的定义
apiVersion: v1
kind: ResourceQuota
metadata:
name: object-counts
namespace: default
spec:
hard:
pods: 4
limits.memory: 5Gi
应用资源约束的命名空间。
在此命名空间中允许四个活动的 Pod。
此命名空间中所有 Pod 的内存限制总和不得超过 5 GB。
另一个有用的工具是 LimitRange,它允许您为每种资源类型设置资源使用限制。除了为不同资源类型指定允许的最小值和最大值以及这些资源的默认值外,它还允许您控制请求和限制之间的比率,也称为超量承诺级别。示例 2-6 展示了 LimitRange 及其可能的配置选项。
示例 2-6. 允许和默认资源使用限制的定义
apiVersion: v1
kind: LimitRange
metadata:
name: limits
namespace: default
spec:
limits:
- min:
memory: 250Mi
cpu: 500m
max:
memory: 2Gi
cpu: 2
default:
memory: 500Mi
cpu: 500m
defaultRequest:
memory: 250Mi
cpu: 250m
maxLimitRequestRatio:
memory: 2
cpu: 4
type: Container
请求和限制的最小值。
请求和限制的最大值。
当未指定限制时的默认限制值。
当未指定请求时的默认请求值。
限制/请求的最大比率,用于指定允许的超量承诺级别。在这里,内存限制不得大于内存请求的两倍,CPU 限制可以达到 CPU 请求的四倍。
type 可以是 Container、Pod(适用于所有容器组合)或 PersistentVolumeClaim(用于指定请求持久卷的范围)。
LimitRanges 有助于控制容器资源配置文件,以确保没有容器需要的资源超出集群节点所能提供的资源。LimitRanges 还可以防止集群用户创建消耗大量资源的容器,从而使节点无法为其他容器分配资源。考虑到请求(而非限制)是调度器用于放置 Pod 的主要容器特性,LimitRequestRatio 允许您控制容器请求和限制之间的差异。当请求和限制之间的差异很大时,增加了节点上超量承诺的可能性,并可能在许多容器同时需要超过初始请求的资源时,导致应用程序性能下降。
请记住,在达到任何资源限制之前,其他共享的节点级资源如进程 ID (PIDs) 可能会被耗尽。Kubernetes 允许您为系统保留一定数量的节点 PID,以确保它们不会被用户工作负载耗尽。同样,Pod PID 限制允许集群管理员限制在 Pod 中运行的进程数量。我们在此不详细讨论这些内容,因为它们由集群管理员作为 Kubelet 配置选项设置,并不被应用程序开发人员使用。
容量规划
考虑到容器在不同环境中可能具有不同的资源配置文件,并且实例数量各异,显然为多用途环境进行容量规划并非易事。例如,为了实现最佳硬件利用率,在非生产集群中,您可能主要使用 Best-Effort 和 Burstable 类型的容器。在这样的动态环境中,许多容器同时启动和关闭,即使一个容器在资源紧缺时被平台杀死,也不是致命的。在生产集群中,我们希望环境更加稳定和可预测,容器可能主要是 Guaranteed 类型,并且有些可能是 Burstable 类型。如果一个容器被杀死,这很可能是集群容量应该增加的信号。
表 2-1 展示了一些具有 CPU 和内存需求的服务。
表 2-1. 容量规划示例
| Pod | CPU 请求 | 内存请求 | 内存限制 | 实例数 |
|---|---|---|---|---|
| A | 500 m | 500 Mi | 500 Mi | 4 |
| B | 250 m | 250 Mi | 1000 Mi | 2 |
| C | 500 m | 1000 Mi | 2000 Mi | 2 |
| D | 500 m | 500 Mi | 500 Mi | 1 |
| 总计 | 4000 m | 5000 Mi | 8500 Mi | 9 |
当然,在实际场景中,使用 Kubernetes 平台的更可能原因是有更多的服务需要管理,其中一些即将退役,而另一些仍在设计和开发阶段。即使这是一个不断变化的目标,基于前面描述的类似方法,我们可以计算出每个环境中所有服务所需的总资源量。
请记住,在不同的环境中,容器的数量也不同,您甚至可能需要为自动扩展、构建作业、基础设施容器等留出一些空间。基于这些信息以及基础设施提供商,您可以选择提供所需资源的最具成本效益的计算实例。
讨论
容器不仅仅有助于进程隔离和作为打包格式。在识别了资源配置文件之后,它们也是成功进行容量规划的构建块。进行一些早期测试以发现每个容器的资源需求,并使用这些信息作为未来容量规划和预测的基础。
Kubernetes 可以通过 Vertical Pod Autoscaler (VPA) 来帮助您实现这一目标,VPA 会随着时间的推移监控您的 Pod 的资源消耗,并提供请求和限制的建议。在“垂直 Pod 自动扩展”章节中对此有详细描述。
然而,更重要的是,资源配置文件是应用程序与 Kubernetes 进行沟通,以辅助调度和管理决策的方式。如果您的应用程序没有提供任何请求或限制,Kubernetes 所能做的就是将您的容器视为不透明的盒子,当集群满了时就丢弃它们。因此,对于每个应用程序来说,考虑并提供这些资源声明几乎是必不可少的。
现在您已经知道如何为我们的应用程序进行容量规划,在第 3 章“声明性部署”中,您将学习多种策略来在 Kubernetes 上安装和更新我们的应用程序。
更多信息
- 可预测的需求示例
- 配置 Pod 使用 ConfigMap
- Kubernetes 最佳实践:资源请求和限制
- Pod 和容器的资源管理
- 管理 HugePages
- 配置命名空间的默认内存请求和限制
- 节点压力驱逐
- Pod 优先级和抢占
- 配置 Pod 的服务质量
- Kubernetes 中的资源服务质量
- 资源配额
- 限制范围
- 进程 ID 限制和预留
- 为了上帝的爱,停止在 Kubernetes 上使用 CPU 限制
- 每个人都应该了解的 Kubernetes 内存限制