基于 Kubernetes 的云原生 DevOps 第 5 章 资源管理
🏷️ Kubernetes 《基于 Kubernetes 的云原生 DevOps》
Nothing is enough to the man for whom enough is too little.
-- Epicurus
5.1 了解资源
为了有效地调度 Pod,调度器必须知道每个 Pod 需求的最小和最大资源量。这两个值在 Kubernetes 中用 资源请求值(Request) 和 约束值(Limit) 表示。
Kubernetes 知道如何管理两种资源: CPU 和 内存 。
由于大多数 Pod 不需要整个 CPU,因此通常请求和约束以 毫 CPU(millicpus) 或 毫核(millicores) 表示。
内存以字节为单位,更方便的做法是以 兆字节(MiB) 为单位。
如果没有任何节点拥有足够的空闲容量,则 Pod 将一直处于 pending 状态,直到有空闲资源出现。
资源约束可以指定允许 Pod 使用的最大资源量。
- 如果 Pod 使用的 CPU 量超出分配的上限,那么就会被限流,从而导致性能下降。
- 如果 Pod 使用的内存量超过允许的最大内存上限,则 Pod 会被终止。如果可以的话,被终止的 Pod 会被重新调度。重新调度的实际效果可能就是在同一节点上重新启动 Pod。
资源请求和约束的示例:
spec:
containers:
- name: demo
image: cloudnatived/demo:hello
ports:
- containerPort: 8888
resources:
requests:
memory: "10Mi"
cpu: "100m"
limits:
memory: "20Mi"
cpu: "250m"
Kubernetes 允许资源被过度使用,也就是说,一个节点上所有容器的资源约束值总和可以超过该节点实际的总资源量。这是一种赌博,调度器赌的是在大多数情况下,大多数容器需要的资源量不会达到各自的资源约束。
如果赌博失败,而且资源使用总量接近节点的最大容量,则 Kubernetes 会更加积极地终止容器。在面临资源压力的时候,即使那些超出了请求值但未超过约束值的容器也可能会被终止。
在所有条件都相同的情况下,如果 Kubernetes 需要终止 Pod,则它会从超出请求值最多的 Pod 开始下手。除非在极少数情况下(例如 Kubernetes 无法运行 kubelet 等系统组件),否则在请求值内的 Pod 不会被终止。
最佳实践
请务必为容器指定资源请求和约束。这有助于 Kubernetes 正确调度并管理你的 Pod。
我们应该尽可能地减小容器地大小:
- 容器越小,构建速度越快。
- 镜像占用地存储空间越少。
- 拉取镜像地速度越快。
- 受到攻击的可能性越小。
5.2 管理容器的生命周期
存活探针
Kubernetes 允许在容器的规范中指定存活探针。这是一种健康检查的手段,拥有确定容器是否处于活动状态(即正在运行)。
对于 HTTP 服务器容器,存活探针的规范通常如下:
livenessProbe:
httpGet:
path: /healthz
port: 8888
initialDelaySeconds: 3
periodSeconds: 3
如果应用程序响应的 HTTP 状态码为 2xx 或 3xx,则 Kubernetes 认为该容器处于活动状态。
如果返回其它代码,或者关闭不响应,则 Kubernetes 认为该容器已死,然后它会重新启动该容器。
initialDelaySeconds 字段可以告诉 Kubernetes 在首次尝试存货探针之前需要等待多长时间。
periodSeconds 字段可以指定检查存活探针的频率。
其它类型的探针:
- tcpSocket
如果能够与指定端口成功建立 TCP 连接,则表明该容器处于活动状态。yamllivenessProbe: tcpSocket: port: 8888
- exec
如果能够成功在容器内运行特定的命令(即以零状态退出),则探针成功。
通常 exec 更适合用作就绪探针。yamllivenessProbe: exec: command: - cat - /tmp/healthy
- gRPC 探针
可以使用 grpc-health-probe 工具。
将该工具添加到容器中,就可以使用 exec 来检查了。
就绪探针
就绪探针与存活探针相关,但语义不同。有时,应用程序需要向 Kubernetes 发出信号,表明它暂时无法处理请求。可能是因为它正在执行冗长的初始化进程,或者是等待某些子程序完成。就绪探针可以提供这种功能。
readinessProbe:
httpGet:
path: /healthz
port: 8888
initialDelaySeconds: 3
periodSeconds: 3
如果就绪探针失败,则容器会从与 Pod 匹配的所有服务中删除。这有点像将发生故障的节点从负载均衡池中移除,这样就不会有任何流量发送到该 Pod,直到就绪探针成功为止。
通常,在 Pod 启动后,一旦容器处于运行状态,Kubernetes 就会开始向其发送流量。都是,如果容器包含就绪探针,则 Kubernetes 会等到探针成功后再向其发送请求。对于零停机时间升级来说,这一点至关重要。
尚未准备就绪的容器也会显示成 Running,但 READY 列将显示 Pod 中有一个或多个未就绪的容器。
就绪探针应仅返回 HTTP 200 OK 的状态码。尽管 Kubernetes 认为 2xx 和 3xx 状态码都代表准备就绪,但云负载均衡器并不一定这样认为。
例如,如果你结合使用 Ingress 资源和云负载均衡器,而且就绪探针返回 301 重定向,则负载均衡器可能会将所有 Pod 标记为不健康。
请确保就绪探针仅返回状态码 200。
基于文件的就绪探针
还可以让应用程序在容器的文件系统上创建一个名为 /tmp/healthy 的文件,并使用 exec 就绪探针来检查该文件是否存在。
这种探针非常实用,因为如果你为了调试,想暂时停止使用该容器,则可以进入容器并删除 /tmp/healthy 文件。这样下一次就绪探针就会失败,然后 Kubernetes 就会从匹配的服务中删除该容器。(不过,更好的方法是调整容器的标签,使其不再与服务匹配。)
最佳实践
使用就绪调整与存活探针可以方便 Kubernetes 知道应用程序何时准备就绪可以处理请求,或者何时出现问题并需要重新启动。
在某些情况下,可能需要让容器运行一会,以确保其稳定。此时可以在容器上设置 minReadySeconds 字段(默认值为 0)。只有等到容器或 Pod 的就绪探针成功 minReadySeconds 后,才会被认为就绪。
Pod 中断预算
有时 Kubernetes 可能会驱逐你的 Pod,即使 Pod 处于活动状态,且已准备就绪。然而,只要保证足够的副本运行,就不会导致应用程序停机。
这种情况下可是使用 Pod 中断预算(PodDisruptionBudget)资源指定应用程序在任何给定时间内可以承受损失多少 Pod。
例如,可以指定一次中断的数量不应超过应用程序 Pod 的 10% 。或者指定 Kubernetes 可以驱逐任意数量的 Pod,但需要保证至少有三个副本在运行。
- miniAvailable : 指定需要保持运行的最小 Pod 数量。yaml
spec: miniAvailable: 3 selector: matchLabels: app: demo
- maxUnavailable : 允许 Kubernetes 驱逐的 Pod 总数或百分比:yaml
spec: maxUnavailable: 10% selector: matchLabels: app: demo
以上这些仅适用于 “自愿驱逐” ,即由 Kubernetes 发起的驱逐。但如果诸如某个节点发送硬件故障或被删除的情况,则该节点上的 Pod 会被“非自愿”地驱逐,即便这会违反中断预算。
在所有条件都相同的情况下,Kubernetes 倾向于让 Pod 均匀地分布在各个节点上,因此考虑集群需要多少个节点时需要牢记这一点。
最佳实践
对于重要地业务应用程序,设置 Pod 中断预算可确保即使有 Pod 被驱逐,也有足够的副本来维持服务。
5.3 命名空间
一个命名空间中的名称在其他命名空间中是不可见的。
查看集群上已有的命名空间:
PS C:\projects\github\cloudnativedevops\demo\hello-helm3> kubectl get namespaces
NAME STATUS AGE
default Active 4d2h
kube-node-lease Active 4d2h
kube-public Active 4d2h
kube-system Active 4d2h
kubernetes-dashboard Active 4d2h
命名空间有的像计算机硬盘上的文件夹。
使用 --namespace
(简写 -n
)标志指定命名空间,不指定时会在默认的命名空间(default)中执行。
PS C:\projects\github\cloudnativedevops\demo\hello-namespace\k8s> kubectl get pods --namespace demo
No resources found in demo namespace.
如何将集群划分成命名空间,完全有你决定。 一种直观的做法是每个应用程序或每个团队拥有一个命名空间。
Namespace 资源文件:
apiVersion: v1
kind: Namespace
metadata:
name: demo
应用命名空间资源清单:
PS C:\projects\github\cloudnativedevops\demo\hello-namespace\k8s> kubectl apply -f .\namespace.yaml
namespace/demo created
请勿删除命名空间,除非确实是临时的命名空间,而且你确定其中不包含任何生产资源。
最佳实践
为基础设置中的每个应用程序或每个逻辑组件创建单独的命名空间。
不要使用默认的命名空间,因为很容易出错。
尽管命名空间彼此隔离,但它们仍然可以与其它命名空间中的服务通信。
如何跨越不同的命名空间通信呢?服务的 DNS 名称始终遵循以下格式:服务.命名空间.svc.cluster.local
。
其中 .svc.cluster.local
部分是可选的,命名空间也是可选的。
假如你想与 prod 命名空间中的 demo 服务进行对话,则可以使用:demo.prod
。
限制命名空间的资源使用量(资源配额 ResourceQuota):
apiVersion: v1
kind: ResourceQuota
metadata:
name: demo-resourcequota
spec:
hard:
pods: "100"
将这个清单应用到 demo 命名空间:
PS C:\projects\github\cloudnativedevops\demo\hello-namespace\k8s> kubectl apply --namespace demo -f .\resourcequota.yaml
resourcequota/demo-resourcequota created
虽然你可以限制命名空间中 Pod 使用 CPU 和内存的总量,但我们不建议这样做。
Pod 限制有助于防止因配置错误或输入错误而生成无限 Pod。
最佳实践
在每个命名空间中使用资源配额来限制该命名空间中可以运行的 Pod 数量。
查看指定命名空间中的资源配置状态:
PS C:\projects\github\cloudnativedevops\demo\hello-namespace\k8s> kubectl get resourcequotas -n demo
NAME AGE REQUEST LIMIT
demo-resourcequota 3m53s pods: 0/100
默认资源请求和约束(LimitRange):
apiVersion: v1
kind: LimitRange
metadata:
name: demo-limitrange
spec:
limits:
- default:
cpu: "500m"
memory: "256Mi"
defaultRequest:
cpu: "200m"
memory: "128Mi"
type: Container
使用 LimitRange 为命名空间中所有的容器设置默认请求和约束值。
命名空间中任何未指定资源约束或请求的容器都会继承 LimitRange 的默认值。
最佳实践
在每个命名空间中使用 LimitRanges 来设置容器默认的资源请求和约束,但不要依赖它们。把它们当成最后一道防线。 请务必在容器规范中指定明确的请求和约束。
5.4 优化集群的成本
我们应该理智的使用副本。集群只能运行有效数量的 Pod。我们应该将这些有限的 Pod 用到真正需要最大化可用性和性能的应用程序。
在很多情况下,即使大幅缩减部署也不会引发用户能注意到的服务降级。
最佳实践
在满足性能和可用性要求的前提下,最小化部署使用的 Pod 数量。逐渐减少副本数量,直到刚好达到服务水平目标。
应该定期审查各种工作负载的资源请求和约束,并与实际使用的资源进行比较。
大多数托管的 Kubernetes 服务都提供某种形式的仪表板,显示一段时间内容器使用的 CPU 和 内存的情况。
也可以使用 Prometheus 和 Grafana 构建自己的仪表板和统计信息。
通常,容器的资源约束应该略高于正常操作中使用的最大资源量。
请求的设置没有约束那么重要,但仍然不能设置的太高(这回导致 Pod 永远无法被调度)或太低(因为超过请求的 Pod 会成为首先被驱逐的对象)。
Pod 垂直自动伸缩器
这是一个 Kubernetes 插件,可以帮助找出资源请求的理想值。
它会监视指定的部署,并根据实际使用情况自动调整 Pod 的资源请求。
它提供演戏模式,而且在演戏模式下只提供建议,而不会真的修改正在运行的 Pod,因此可能会有帮助。
优化节点
Kubernetes 支持各种大小的节点,但其中某些节点的性能会更好一些。你需要在真实的需求条件下,运行真实的工作负载,并观察节点的实际性能。这种方式可以帮助你确定性价比最高的实例类型。
每个节点上都必须有一个操作系统,而这个操作系统会占用磁盘、内存和 CPU 资源。Kubernetes 系统组件和容器运行时也是如此。 节点越小,这部分开销所占的总资源比例就越大。
小型节点更有可能出现被搁浅资源的现象。被搁浅资源指的是,虽然存在未使用的内存空间和 CPU 时间,但对任何现有的 Pod 来说太小了,导致无法分配。
一条很好的经验法则是,节点应该能够运行至少 5 个典型的 Pod,并将被搁浅资源的比例保持在 10% 以下。如果节点可以运行 10 个或更多 Pod,则被搁浅资源应低于 5% 。
Kubernetes 中默认的约束是每个节点 110 个 Pod。 尽管可以通过 kubelet 的 --max-pods 设置来提高这个约束,但在某些托管服务中无法这样做,而且最好还是保留 Kubernetes 的默认值,除非你有充分的更改理由。
最佳实践
节点越大性价比越高,因为系统开销消耗的资源越少。你可以通过查看集群的实际利用率来确定节点的大小,最好保持每个节点 10 ~ 100 个 Pod 。
优化存储
通常,云或 Kubernetes 提供商的控制台能够显示节点上实际使用了多少 IOPS,而且你可以通过这些数字来决定消减何处的成本。
最佳实践
不要使用存储空间超过实际使用的实例类型。根据实际使用的吞吐量和空间,尽可能使用最小、IOPS 最低的磁盘卷。
清理未使用的资源
你应该定期检查各种资源的使用情况,找出并删除未使用的实例。
当节点上的磁盘空间不足时,Kubernetes 会自动清理未使用的镜像。
利用所有者的元数据
减少未使用资源的一种有效方法是在整个组织范围内指定策略,给每个资源都打上标签来表明其所有者。可以使用 Kubernetes 注释来实现这一点。
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
example.com/owner: "Customer Apps Team"
所有者的元数据应指定为某个人或团队,在遇到有关该资源的问题时可以与之联系。最好在自定义注释的开头加上公司的域名,如 example.com,以防止与其它同名的注释发生冲突。
最佳实践
为所有资源设置所有者注释,让大家知道当该资源出现问题,或者资源几乎已被弃用且可以被终止时,应当与何人联系。
寻找未充分利用的资源
每个 Pod 都应该将接收到的请求数量作为指标公开。我们可以使用指标来查找流量较低或为零的 Pod,并列出可以被终止的候选资源。
还可以通过 Web 控制台,检查每个 Pod 使用 CPU 以及内存的情况,并找到集群中利用率最低的 Pod。
如果 Pod 拥有所有者元数据,则请联系其所有者,以查明这些 Pod 是否确实有必要。
你可以使用另一个自定义的 Kubernetes 注释(如 example.com/lowtraffic)来标记那些没有接受请求但处于某种原因仍然有必要存在的 Pod。
最佳实践
定期检查集群,找出未充分利用或废弃的资源并消灭它们。所有者注释可以提供帮助。
清理已完成的作业
Kubernetes 作业也是 Pod,但只运行一次,并且不会重新启动。但 Job 对象仍然会留在 Kubernetes 的数据库中,如果有大量已完成的 Job,就有可能影响 API 性能。
kube-job-cleaner 是一款非常便捷的清理已完成作业的工具。
检查备用容量
无论何时,集群都应该有足够的备用容量来应对某个工作节点发生故障。
可以通过把最大的节点排干来检查备用容量是否足够。
使用预留实例
有些云提供商会根据计算机的生命周期提供不同的实例类。预留实例可在价格和灵活性之间取得平衡。
如果你很清楚可预见未来的需求,则预留实例和承诺使用折扣是一个不错的选择。但是,没有使用的预留也不予退款,你必须提前付清整个预留期的费用。因此,只有在需求不太可能发生显著变化的期间内,才应该选择预留实例。
然而,如果你能够提前做出一年或两年的计划,那么使用预留实例可以省一大笔钱。
最佳实践
当你的需求在一两年内不太可能改变时,请使用预留实例,但无比三思而后行,因为一旦完成付款后就无法更改或退款。
抢占式(Spot)实例
AWS 称这种实例为 Spot 实例,Google 称之为抢占式虚拟机。它们不提供可用性保证,而且生命周期通常都是有限的。因此,它们是价格与可用性之间的折中。
Spot 实例很便宜,但随时可能被暂停或继续,并且可能被完全终止。
Google 云的抢占式虚拟机是按固定费率计费的,但抢占率会随时变化。
使用抢占式节点是减少 Kubernetes 集群成本的一种非常有效的方法。尽管你可能需要多运行几个节点,以确保工作负载能够在抢占发生时存活下来,但事实证明,抢占式节点可以将每个节点的成本降到一半。
使用抢占式节点可以在集群中加入一些混乱工程,前提是你的应用程序已经做好准备接受混乱测试。
你应该确保足够的不可抢占节点来处理集群最低限度的工作负载。风险不能超过你的承受范围。如果你有很多抢占式节点,那么最好使用集群自动伸缩来确保尽快替换所有被抢占的节点。
从理论上讲,有可能所有可抢占节点会同时消失。因此,尽管能够节省成本,但最好还是讲抢占式节点限制在集群的三分之二以内。
最佳实践
让某些节点使用抢占式节点或 Spot 实例可以降低成本,但是不要让损失超过你能够承受的范围。务必保留一些非抢占式节点。
使用节点亲和性控制调度
你可以使用 Kubernetes 的节点亲和性来保证无法承受失败的 Pod 不会被调度到抢占式节点上(例如,GKE 的可抢占节点带有标签 cloud.google.com/gke-preemptible )。
告诉 Kubernetes 不要将 Pod 调度到抢占式节点上:
apiVersion: v1
kind: Pod
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: cloud.google.com/gke-preemptible
operator: DoesNotExist
节点亲和性 requiredDuringSchedulingIgnoredDuringExecution 是必须的,凡是拥有该亲和性的 Pod 不会被调度到与选择器表达式不匹配的节点上。这叫做硬亲和性。
也可以告诉 Kubernetes 将一些不太重要的 Pod 优先调度到抢占式节点上。
这种情况下,可使用软亲和性:
apiVersion: v1
kind: Pod
spec:
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- preference:
- matchExpressions:
- key: cloud.google.com/gke-preemptible
operator: Exists
weight: 100
如果可以,请将该 Pod 调度到抢占式节点上;如果不可以也没有关系。
最佳实践
如果你正在运行抢占式节点,请使用 Kubernetes 节点亲和性来确保重要的工作负载不会被抢占。
保持工作负载均衡
调度器的目标是将工作负载均匀地分散到节点上,但这个目标有时会与维持某个服务的高可用性冲突。
解决这个问题的方法之一是使用一种名为解调度器的工具。你可以频繁运行这个工具,就像 Kubernetes 作业一样,它会尽力重新平衡集群,方法是找到需要移动的 Pod 并干掉它们。
解调度器具有可供配置的各种策略和方针。
- 例如,有一种策略会寻找未充分利用的节点,并杀死其它节点上的 Pod,强制将它们重新调度到这个空闲的节点上。
- 还有一种策略是寻找重复的 Pod,即寻找在同一个节点上运行的同一个 Pod 的两个或多个副本,并驱逐它们。
5.5 小结
- Kubernetes 根据请求和约束为容器分配 CPU 及内存资源。
- 容器的请求值是运行容器所需的最小资源量。容器的约束值指定的是允许使用的最大资源量。
- 最低限度的容器镜像可以更快地构建、推送、部署和启动。容器越小,潜在的安全漏洞越少。
- 存活探针告诉 Kubernetes 容器是否正常工作。如果容器的存活探针失败,那么容器会被干掉并重新启动。
- 就绪探针告诉 Kubernetes 容器已准备就绪,可以处理需求。如果就绪探针失败,则容器会从所有引用它的服务中删除,从而导致容器与用户流量断开连接。
- Pod 中断预算能够限制在驱逐期间内可以一次性停止的 Pod 数量,目的是为了保证应用程序的高可用性。
- 命名空间是对集群进行逻辑分区的一种方法。你可以为每个应用程序或一组相关的应用程序创建一个命名空间。
- 如果想在另一个命名空间中引用服务,则可以使用如下格式的 DNS 地址:服务。命名空间 。
- 资源配额允许你为命名空间设置资源的使用总额约束。
- LimitRanges 指定命名空间中容器默认的资源请求值和约束值。
- 设置资源约束,可以确保应用程序在正常使用中不会超过这个限制。
- 不要分配超过需求的云存储,也不要置备高带宽存储,除非这些存储对应用程序的性能至关重要。
- 在所有资源上设置所有者注释,并定期审查集群,找到没有所有者的资源。
- 找出并清理未使用的资源(但不要忘记与所有者联系)。
- 如果你有长期的使用计划,则预留实例可以为你省很多钱。
- 抢占式实例可在短期内省钱,但要做好它们随时可能会消失的准备。使用节点亲和性可以避免不允许发生故障的 Pod 被分配到抢占节点上。