滚动更新、应用保障、集群管理
参考:Kubernetes 入门实战课 | 极客时间 (opens new window) 第 27-29 讲
# 1. 滚动更新:如何做到平滑的应用升级降级?
Deployment、DaemonSet 和 StatefulSet 让我们能够部署任意形式的应用了,但为了让应用稳定运行,还需要有持续的运维工作。之前学习的 kubectl scale
来调整 Deployment 或 StatefulSet 下属的 pod 数量就是一种常见的运维操作,但除了应用伸缩,我们还需要应用更新、版本回退等运维操作。
这一章以 Deployment 为例,讲解 Kubernetes 在应用管理方面的高级操作:滚动更新,使用 kubectl rollout
实现用户无感知的应用升级和降级。
# 1.1 Kubernetes 如何定义应用版本
对一个线上运行的系统进行更新是一件棘手的事情,“给空中的飞机换引擎”,而 Kubernetes 把这个过程给抽象出来,让计算机去完成那些复杂繁琐的人工操作。
K8s 中版本更新主要是两个命令:kubectl apply 和 kubectl rollout。
首先我们需要知道所谓的“版本”是什么。Kubernetes 中应用都是以 Pod 来运行,而 Pod 又被 Deployment 管理,所以应用的“版本更新”实际上更新的是整个 Pod。而 Pod 其实又是 Deployment 对象中的 template
字段,所以 Kubernetes 中应用的版本变化就是 template 字段中 Pod 的变化。因此,哪怕 template
里只变动了一个字段,那也会形成一个新的版本,也算是版本变化。
Kubernetes 使用摘要算法计算 template
里 Hash 值作为版本号,这虽不太方便识别,但很实用。
我们以之前运行的 nginx 作为例子:
名字中的随机数"6796..."就是 Pod 模板的 Hash 值,也就是 Pod 的“版本号”。如果你变动了 Deployment 下 template 中 Pod 的 YAML 任一描述,都会生成一个新的应用版本,kubectl apply 后就会重新创建 Pod:
可以看到,Pod 名字中的 hash 值发生了变动,这表示 Pod 的版本更新了。
# 1.2 Kubernetes 如何实现应用更新
我们可以尝试进行应用更新,由于 Kubernetes 的动作太快了,为了能够观察到应用的更新过程,我们可以加一个字段 minReadySeconds
,让 Kubernetes 在更新过程中等待一点时间,确认 Pod 没问题之后才继续其余 Pod 的创建工作:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-dep
spec:
minReadySeconds: 15 # 确认Pod就绪的等待时间
replicas: 4
... ...
containers:
- image: nginx:1.22-alpine
... ...
2
3
4
5
6
7
8
9
10
11
12
注意,
minReadySeconds
这个字段不属于 Pod 模板,所以它不会影响 Pod 的版本。
当我们对 Deployment 对象中 template 字段下的 Pod 描述进行更改后,可以执行 kubectl apply
来更新应用,这个过程会触发“版本更新”,可以使用 kubectl rollout status 来查看应用更新的状态:
kubectl apply -f ngx-v2.yml
kubectl rollout status deployment ngx-dep
2
更新完成之后,再去执行 kubectl get pod
就可以看到,Pod 已经全部替换成了新的版本号。
仔细观察 kubectl rollout status
的输出信息可以发现,Kubernetes 并不是把旧的 Pod 全部销毁然后再一次性创建出来新的 Pod,而是逐个创建新 Pod,同时也在销毁旧 Pod,保证系统里始终有足够数量的 Pod 在运行,不会有“空窗期”中断服务。这就是滚动更新。
使用命令 kubectl describe
可以更清楚地看到 Pod 的变化情况:
kubectl describe deploy ngx-dep
- 一开始的时候V1 Pod(即ngx-dep-54b865d75)的数量是4;
- 当“滚动更新”开始的时候,Kubernetes创建1个 V2 Pod(即ngx-dep-d575d5776),并且把V1 Pod数量减少到3;
- 接着再增加V2 Pod的数量到2,同时V1 Pod的数量变成了1;
- 最后V2 Pod的数量达到预期值4,V1 Pod的数量变成了0,整个更新过程就结束了。
其实可以看出来,滚动更新的过程就是由 Deployment 控制的两个同步进行的“应用伸缩”操作,老版本缩容到 0,同时新版本扩容到指定值,是一个”此消彼长“的过程。
这个滚动更新的过程可以参考下图:
# 1.3 Kubernetes 如何管理应用更新
当 Kubernetes 的滚动更新过程中发生了错误或者更新后发现有 Bug 该怎么办?
要解决这两个问题,还是要用 kubectl rollout
命令:
- 在应用的更新过程中,你可以随时用 kubectl rollout pause 来暂停更新、检查和修改 Pod,或者测试验证;
- 如果确认没问题,再用 kubectl rollout resume 来继续更新。
要注意这两条命令只支持Deployment,不能用在DaemonSet、StatefulSet上(最新的1.24支持了StatefulSet的滚动更新)。
对于更新后出现的问题,Kubernetes为我们提供了“后悔药”,也就是更新历史,你可以查看之前的每次更新记录,并且回退到任何位置,和我们开发常用的Git等版本控制软件非常类似。
查看更新历史使用的命令是 kubectl rollout history:
kubectl rollout history deploy ngx-dep
但 kubectl rollout history
的列表输出的有用信息太少,你可以在命令后加上参数 --revision
来查看每个版本的详细信息,包括标签、镜像名、环境变量、存储卷等等,通过这些就可以大致了解每次都变动了哪些关键字段:
假设我们认为刚刚更新的 nginx:1.22-alpine
不好,想要回退到上一个版本,就可以使用命令 kubectl rollout undo
,也可以加上参数 --to-revision
回退到任意一个历史版本:
kubectl rollout undo
的操作过程其实和 kubectl apply
是一样的,执行的仍然是“滚动更新”,只不过使用的是旧版本Pod模板,把新版本Pod数量收缩到0,同时把老版本Pod扩展到指定值。
这个V2到V1的“版本降级”的过程如下图,它和“版本升级”过程是完全一样的,不同的只是版本号的变化方向:
# 1.4 Kubernetes 如何添加更新描述
每次版本更新时,能不能加一个说明信息呢?
可以,只需要在 Deployment 的 metadata 里加上一个新的字段 annotations。
annotations
字段的含义是“注解”“注释”,形式上和 labels
一样,都是Key-Value,也都是给API对象附加一些额外的信息,但是用途上区别很大:
annotations
添加的信息一般是给Kubernetes内部的各种对象使用的,有点像是“扩展属性”;labels
主要面对的是Kubernetes外部的用户,用来筛选、过滤对象的。
如果用一个简单的比喻来说呢,annotations
就是包装盒里的产品说明书,而 labels
是包装盒外的标签贴纸。借助 annotations
,Kubernetes既不破坏对象的结构,也不用新增字段,就能够给API对象添加任意的附加信息,这就是面向对象设计中典型的OCP“开闭原则”,让对象更具扩展性和灵活性。
annotations
里的值可以任意写,Kubernetes会自动忽略不理解的Key-Value,但要编写更新说明就需要使用特定的字段 kubernetes.io/change-cause。
如下是 3 个版本的 nginx 应用,同时里面含有更新说明:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-dep
annotations:
kubernetes.io/change-cause: v1, ngx=1.21
...
2
3
4
5
6
7
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-dep
annotations:
kubernetes.io/change-cause: update to v2, ngx=1.22
...
2
3
4
5
6
7
apiVersion: apps/v1
kind: Deployment
metadata:
name: ngx-dep
annotations:
kubernetes.io/change-cause: update to v3, change name
...
2
3
4
5
6
7
注意以上三个版本的 metadata 部分,使用 kubernetes.io/change-cause
描述了版本更新的情况,这样更新版本之后,我们在使用 kubectl rollout history
命令就可以看到更新历史:
这次显示的列表信息就好看多了,每个版本的主要变动情况列得非常清楚,和Git版本管理的感觉很像。
# 1.5 小结
这一章主要讲的滚动更新,小结一下要点:
- Kubernetes 使用 template 的 hash 值作为版本号。
- Kubernetes 的更新过程是滚动更新,保证了在更新过程中服务的始终可用。
- 管理应用更新使用的命令是
kubectl rollout
,子命令有status
、history
、undo
等。 - Kubernetes会记录应用的更新历史,可以使用
history --revision
查看每个版本的详细信息,也可以在每次更新时添加注解kubernetes.io/change-cause
。
另外,在Deployment里还有其他一些字段可以对滚动更新的过程做更细致的控制,它们都在 spec.strategy.rollingUpdate
里,比如 maxSurge
、 maxUnavailable
等字段,分别控制最多新增Pod数和最多不可用Pod数,一般用默认值就足够了,你如果感兴趣也可以查看Kubernetes文档进一步研究。
课外小贴士:
- Deployment 在版本更新的时候实际控制的是 ReplicaSet 对象,创建不同版本的 ReplicaSet,再由 ReplicaSet 来伸缩 Pod 数量。
- 除了使用 kubectl apply 来触发应用更新,你也可以使用其他任何能够修改 API 对象的方式,比如 kubectl edit、kubectl patch、kubectl set image 等命令。
- Kubernetes 不会记录所有的更新历史,默认只会保留最近的 10 次操作,但这个值可以用字段 revisionHistoryLimit 调整。
# 2. 应用保障:如何让 Pod 运行得更健康?
作为Kubernetes里的核心概念和原子调度单位,Pod的主要职责是管理容器,以逻辑主机、容器集合、进程组的形式来代表应用,它的重要性是不言而喻的。
这一章回过头来看看 Pod 里面的两种配置:资源配额Resources、检查探针Probe。它们能够给Pod添加各种运行保障,让应用运行得更健康。
# 2.1 容器资源配额
容器有三大隔离技术:namespace、cgroup、chroot。其中的 namespace 实现了独立的进程空间,chroot 实现了独立的文件系统,而 cgroup 的作用是管控CPU、内存,保证容器不会无节制地占用基础资源,进而影响到系统里的其他应用。
但容器总是要使用CPU和内存的,该怎么处理好需求与限制这两者之间的关系呢?
Kubernetes 的做法是,容器需要先提出一个“书面申请”,Kubernetes再依据这个“申请”决定资源是否分配和如何分配。由于 CPU、内存等是直接内置在节点系统中的,不需要像存储卷那样“外挂”,所以申请和管理的过程就简单了许多。
具体的申请方法很简单,只要在Pod容器的描述部分添加一个新字段 resources
就可以了,它就相当于申请资源的 Claim。如下是一个 YAML 示例:
apiVersion: v1
kind: Pod
metadata:
name: ngx-pod-resources
spec:
containers:
- image: nginx:alpine
name: ngx
resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: 20m
memory: 200Mi
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这个YAML文件定义了一个Nginx Pod,我们需要重点学习的是 containers.resources
,它下面有两个字段:
- requests:意思是容器要申请的资源,也就是说要求Kubernetes在创建Pod的时候必须分配这里列出的资源,否则容器就无法运行。
- limits:意思是容器使用资源的上限,不能超过设定值,否则就有可能被强制停止运行。
在请求 cpu
和 memory
这两种资源的时候,你需要特别注意它们的表示方式。
内存的写法和磁盘容量一样,使用 Ki
、 Mi
、 Gi
来表示 KB
、 MB
、 GB
,比如 512Ki
、 100Mi
、 0.5Gi
等。
而CPU因为在计算机中数量有限,非常宝贵,所以Kubernetes允许容器精细分割CPU,即可以1个、2个地完整使用CPU,也可以用小数0.1、0.2的方式来部分使用CPU。这其实是效仿了UNIX“时间片”的用法,意思是进程最多可以占用多少CPU时间。
不过CPU时间也不能无限分割,Kubernetes里CPU的最小使用单位是0.001,为了方便表示用了一个特别的单位 m
,也就是“milli”“毫”的意思,比如说500m就相当于0.5。
现在我们再来看这个YAML,你就应该明白了,它向系统申请的是1%的CPU时间和100MB的内存,运行时的资源上限是2%CPU时间和200MB内存。有了这个申请,Kubernetes就会在集群中查找最符合这个资源要求的节点去运行Pod。
如果 Pod 不写 resources
字段,那就意味着 Pod 对运行的资源要求“既没有下限,也没有上限“,Kubernetes 不用管CPU和内存是否足够,可以把 Pod 调度到任意的节点上,而且后续 Pod 运行时也可以无限制地使用 CPU 和内存。在生产环境下,为避免 Pod 因资源不足而运行缓慢或占用太多资源,应当合理评估 Pod 的资源使用情况,并尽量为 Pod 加上限制。
那如果预估错误导致 Pod 申请的资源过多,而系统无法满足会怎么样?我们尝试让 Pod 申请 10 个 CPU,可以发现,API 对象可以创建成功,但当我们使用 kubectl get pod
去查看时,会发现它一直处于 "Pending" 状态,实际上没有被真正调度:
如果我们使用命令 kubectl describe
来查看具体原因,会发现有这么一句提示:
这就很明确地告诉我们Kubernetes调度失败,当前集群里的所有节点都无法运行这个Pod,因为它要求的CPU实在是太多了。
# 2.2 容器状态探针
当一个程序正常启动后,也有可能在运行时发生死锁或死循环等故障,这时在外部看进程一切正常,但在内部已经是一团糟了。所以,我们还希望Kubernetes这个“保姆”能够更细致地监控Pod的状态,除了保证崩溃重启,还必须要能够探查到Pod的内部运行状态,定时给应用做“体检”,让应用时刻保持“健康”,能够满负荷稳定工作。
那应该用什么手段来检查应用的健康状态呢?
由于各种程序对外界来说就是一个黑盒子,只能看到启动、执行、停止这三个基本状态,此外就没有什么好的办法来知道它内部是否正常了。所以我们必须把应用变成灰盒子,让部分内部信息对外可见,这样Kubernetes才能够探查到内部的状态。
这里的检查过程就有点像核酸检测,Kubernetes 使用探针(Probe)在应用的检查口去提取信息,然后根据这些信息来判断应用是否健康。
Kubernetes 为检查应用状态定义了三种探针,它们分别对应容器不同的状态:
- Startup,启动探针,用来检查应用是否已经启动成功,适合那些有大量初始化工作要做,启动很慢的应用。
- Liveness,存活探针,用来检查应用是否正常运行,是否存在死锁、死循环。
- Readiness,就绪探针,用来检查应用是否可以接收流量,是否能够对外提供服务。
你需要注意这三种探针是递进的关系:应用程序先启动,加载完配置文件等基本的初始化数据就进入了Startup状态,之后如果没有什么异常就是Liveness存活状态,但可能有一些准备工作没有完成,还不一定能对外提供服务,只有到最后的Readiness状态才是一个容器最健康可用的状态。
这三个状态与探针的关系如下图:
那 Kubernetes 具体是如何使用状态和探针来管理容器的呢?
如果一个 Pod 里的容器配置了探针,Kubernetes 在启动容器后就会不断地调用探针来检查容器的状态:
- 如果Startup探针失败,Kubernetes会认为容器没有正常启动,就会尝试反复重启,当然其后面的Liveness探针和Readiness探针也不会启动。
- 如果Liveness探针失败,Kubernetes就会认为容器发生了异常,也会重启容器。
- 如果Readiness探针失败,Kubernetes会认为容器虽然在运行,但内部有错误,不能正常提供服务,就会把容器从Service对象的负载均衡集合中排除,不会给它分配流量。
知道了Kubernetes对这三种状态的处理方式,我们就可以在开发应用的时候编写适当的检查机制,让Kubernetes用“探针”定时为应用做“体检”了。
下图在刚才图的基础上,又补充上了 k8s 的处理动作,从这张图就可以很好地理解容器探针的工作流程了:
# 2.3 如何使用容器状态探针
我们看看如何在 Pod 的 YAML 描述文件里定义探针。
startupProbe、livenessProbe、readinessProbe这三种探针的配置方式都是一样的,关键字段有这么几个:
- periodSeconds,执行探测动作的时间间隔,默认是10秒探测一次。
- timeoutSeconds,探测动作的超时时间,如果超时就认为探测失败,默认是1秒。
- successThreshold,连续几次探测成功才认为是正常,对于startupProbe和livenessProbe来说它只能是1。
- failureThreshold,连续探测失败几次才认为是真正发生了异常,默认是3次。
至于探测方式,Kubernetes 支持3种:Shell、TCP Socket、HTTP GET,它们也需要在探针里配置:
- exec,执行一个Linux命令,比如ps、cat等等,和container的command字段很类似。
- tcpSocket,使用TCP协议尝试连接容器的指定端口。
- httpGet,连接端口并发送HTTP GET请求。
要使用这些探针,我们必须要在开发应用时预留出“检查口”,这样Kubernetes才能调用探针获取信息。这里我还是以Nginx作为示例,用ConfigMap编写一个配置文件:
apiVersion: v1
kind: ConfigMap
metadata:
name: ngx-conf
data:
default.conf: |
server {
listen 80;
location = /ready {
return 200 'I am ready';
}
}
2
3
4
5
6
7
8
9
10
11
12
13
- 在这个配置文件里,我们启用了80端口,然后用
location
指令定义了HTTP路径/ready
,它作为对外暴露的“检查口”,用来检测就绪状态,返回简单的200状态码和一个字符串表示工作正常。
现在我们来看一下Pod里三种探针的具体定义:
apiVersion: v1
kind: Pod
metadata:
name: ngx-pod-probe
spec:
volumes:
- name: ngx-conf-vol
configMap:
name: ngx-conf
containers:
- image: nginx:alpine
name: ngx
ports:
- containerPort: 80
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: ngx-conf-vol
startupProbe:
periodSeconds: 1
exec:
command: ["cat", "/var/run/nginx.pid"]
livenessProbe:
periodSeconds: 10
tcpSocket:
port: 80
readinessProbe:
periodSeconds: 5
httpGet:
path: /ready
port: 80
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
StartupProbe使用了Shell方式,使用 cat
命令检查Nginx存在磁盘上的进程号文件(/var/run/nginx.pid),如果存在就认为是启动成功,它的执行频率是每秒探测一次。
LivenessProbe使用了TCP Socket方式,尝试连接Nginx的80端口,每10秒探测一次。
ReadinessProbe使用的是HTTP GET方式,访问容器的 /ready
路径,每5秒发一次请求。
现在我们用 kubectl apply
创建这个Pod,可以成功运行,之后探针的检查应该都是正常的,你可以用 kubectl logs
来看一下 nginx 的访问日志,里面记录了 HTTP GET 探针的执行情况:
从截图中你可以看到,Kubernetes正是以大约5秒一次的频率,向URI /ready
发送HTTP请求,不断地检查容器是否处于就绪状态。
如果探针探测失败且 Kubernetes 认为你的程序不健康后,便会采取处理措施。比如当 StartupProbe 探测失败的时候,Kubernetes 就会不停地重启容器,现象就是 kubectl get pod 中打印的 RESTARTS
次数不断增加:
# 2.4 小结
这一章讲了两种为 Pod 配置运行保障的方式:Resources 和 Probe。Resources 就是为容器加上资源限制,而 Probe 就是主动健康检查,让 Kubernetes 实时地监控应用的运行状态。
简单小结一下今天的内容:
- 资源配额使用的是cgroup技术,可以限制容器使用的CPU和内存数量,让Pod合理利用系统资源,也能够让Kubernetes更容易调度Pod。
- Kubernetes定义了Startup、Liveness、Readiness三种健康探针,它们分别探测应用的启动、存活和就绪状态。
- 探测状态可以使用Shell、TCP Socket、HTTP Get三种方式,还可以调整探测的频率和超时时间等参数。
课外小贴士:
- 现在的服务器都是多核 CPU,在 Kubernetes 里的“CPU”指的是“逻辑 CPU”,也就是操作系统里能够看到的 CPU。
- StartupProbe 和 livenessProbe 探测失败后的动作其实是由字段“restartPolicy”决定的,它的默认值“On-Failure”就是重启容器。
- 探针可以配置“initialDelaySeconds”字段,表示容器启动后多久才执行探针动作,适用于某些启动比较慢的应用,它的默认值是 0。
- 在容器里还可以配置 lifecycle 字段,在启动后和终止前安装两个钩子:postStart 和 preStop,执行 Shell 命令或者发送 HTTP 请求做一些初始化和收尾工作。
# 3. 集群管理:如何用名字空间分隔系统资源?
之前学习了如何保障 Pod 很好地运行,那在集群层次,有没有类似的方法来为 Kubernetes 提供运行保障呢?Kubernetes 提供了很多手段来管理、控制集群的资源。
# 3.1 为什么要有 Namespace
Kubernetes 中的 Namespace 并不是一个实体对象,而是一个逻辑上的概念。它可以把集群切分成一个个彼此独立的区域,然后我们把对象放到这些区域里,应用就只能在自己的 Namespace 中分配资源和运行,不会干扰到其他 Namespace 中的应用。
Namespace 是 Kubernetes 面对大规模集群、海量节点时的一种现实考虑,因为当集群很大时,难免出现资源争抢和命名冲突等问题。所以当多团队、多项目共用 Kubernetes 时,为了避免这些问题的出现,我们就需要把集群给适当地“局部化”,为每一类用户创建出只属于它自己的“工作空间”。
# 3.2 如何使用 Namespace
Namespace 也是一种 API 对象,简称为 ns。
命令 kubectl create ns <名称>
就可以很容易地创建一个名字空间:
kubectl create ns test-ns
kubectl get ns
2
Kubernetes 初始化集群的时候也会预设4个 Namespace:default、kube-system、kube-public、kube-node-lease。我们常用的是前两个,default
是用户对象默认的 Namespace,kube-system
是系统组件所在的 Namespace,相信你对它们已经很熟悉了。
想要把一个对象放入特定的名字空间,需要在它的 metadata
里添加一个 namespace
字段,比如我们要在“test-ns”里创建一个简单的Nginx Pod,就要这样写:
apiVersion: v1
kind: Pod
metadata:
name: ngx
namespace: test-ns
spec:
containers:
- image: nginx:alpine
name: ngx
2
3
4
5
6
7
8
9
10
kubectl apply
创建这个对象之后,我们直接用 kubectl get
是看不到它的,因为默认查看的是“default”的 Namespace,想要操作其他 Namespace 的对象必须要用 -n
参数明确指定:
kubectl get pod -n test-ns
因为名字空间里的对象都从属于 Namespace,所以在删除名字空间的时候一定要小心,一旦名字空间被删除,它里面的所有对象也都会消失。
可以执行 kubectl delete ns <名称>
来删除一个 Namespace。
会发现删除 Namespace 后,它里面的Pod也会无影无踪了。
# 3.3 什么是资源配额
有了名字空间,我们就可以像管理容器一样,给名字空间设定配额,把整个集群的计算资源分割成不同的大小,按需分配给团队或项目使用。不过集群和单机不一样,除了限制最基本的CPU和内存,还必须限制各种对象的数量,否则对象之间也会互相挤占资源。
名字空间的资源配额需要使用一个专门的 API 对象,叫做 ResourceQuota,简称是 quota。因为资源配额对象必须依附在某个名字空间上,所以在它的描述文件的 metadata
字段里必须明确写出 namespace
(否则就会应用到default名字空间)。
下面我们先创建一个名字空间“dev-ns”,再创建一个资源配额对象“dev-qt”:
apiVersion: v1
kind: Namespace
metadata:
name: dev-ns
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-qt
namespace: dev-ns
spec:
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ResourceQuota对象的使用方式比较灵活,既可以限制整个名字空间的配额,也可以只限制某些类型的对象(使用scopeSelector),今天我们看第一种,它需要在 spec
里使用 hard
字段,意思就是“硬性全局限制”。
在 ResourceQuota 里可以设置各类资源配额,字段非常多,我简单地归了一下类,你可以课后再去官方文档上查找详细信息:
- CPU和内存配额,使用
request.*
、limits.*
,这是和容器资源限制是一样的。 - 存储容量配额,使
requests.storage
限制的是PVC的存储总量,也可以用persistentvolumeclaims
限制PVC的个数。 - 核心对象配额,使用对象的名字(英语复数形式),比如
pods
、configmaps
、secrets
、services
。 - 其他API对象配额,使用
count/name.group
的形式,比如count/jobs.batch
、count/deployments.apps
。
下面的这个YAML就是一个比较完整的资源配额对象:
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-qt
namespace: dev-ns
spec:
hard:
requests.cpu: 10
requests.memory: 10Gi
limits.cpu: 10
limits.memory: 20Gi
requests.storage: 100Gi
persistentvolumeclaims: 100
pods: 100
configmaps: 100
secrets: 100
services: 10
count/jobs.batch: 1
count/cronjobs.batch: 1
count/deployments.apps: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
我来稍微解释一下它为名字空间加上的全局资源配额:
- 所有Pod的需求总量最多是10个CPU和10GB的内存,上限总量是10个CPU和20GB的内存。
- 只能创建100个PVC对象,使用100GB的持久化存储空间。
- 只能创建100个Pod,100个ConfigMap,100个Secret,10个Service。
- 只能创建1个Job,1个CronJob,1个Deployment。
这个YAML文件比较大,字段比较多,如果你觉得不是太容易阅读的话,也可以把它拆成几个小的YAML,分类限制资源数量,也许会更灵活一些。
# 3.4 如何使用资源配额
现在让我们用 kubectl apply
创建这个资源配额对象,然后用 kubectl get
查看,记得要用 -n
指定名字空间:
kubectl apply -f quota-ns.yml
kubectl get quota -n dev-ns
2
你可以看到输出了ResourceQuota的全部信息,但都挤在了一起,看起来很困难,这时可以再用命令 kubectl describe
来查看对象,它会给出一个清晰的表格:
现在让我们尝试在这个名字空间里运行两个busybox Job,同样要加上 -n
参数:
kubectl create job echo1 -n dev-ns --image=busybox -- echo hello
kubectl create job echo2 -n dev-ns --image=busybox -- echo hello
2
ResourceQuota限制了名字空间里最多只能有一个Job,所以创建第二个Job对象时会失败,提示超出了资源配额。再用命令 kubectl describe
来查看,也会发现Job资源已经到达了上限:
只有删除掉第一个 Job,第二个 Job 才能运行。
# 3.5 默认资源配额
学到这里估计你也发现了,在名字空间加上了资源配额限制之后,它会有一个合理但比较“烦人”的约束:要求所有在里面运行的Pod都必须用字段 resources
声明资源需求,否则就无法创建。
比如说,现在我们想用命令 kubectl run
创建一个Pod:kubectl run ngx --image=nginx:alpine -n dev-ns
,你会发现会给出一个“Forbidden”的错误提示,说不满足配额要求。
Kubernetes这样做的原因也很好理解,上一讲里我们说过,如果Pod里没有 resources
字段,就可以无限制地使用CPU和内存,这显然与名字空间的资源配额相冲突。为了保证名字空间的资源总量可管可控,Kubernetes就只能拒绝创建这样的Pod了。
这个约束对于集群管理来说是好事,但对于普通用户来说却带来了一点麻烦,本来YAML文件就已经够大够复杂的了,现在还要再增加几个字段,再费心估算它的资源配额。如果有很多小应用、临时Pod要运行的话,这样做的人力成本就比较高,不是太划算。
那么能不能让Kubernetes自动为Pod加上资源限制呢?也就是说给个默认值,这样就可以省去反复设置配额的烦心事。这个时候就要用到一个很小但很有用的辅助对象了—— LimitRange,简称是 limits,它能为API对象添加默认的资源配额限制。
你可以用命令 kubectl explain limits
来查看它的YAML字段详细说明,这里说几个要点:
spec.limits
是它的核心属性,描述了默认的资源限制。type
是要限制的对象类型,可以是Container
、Pod
、PersistentVolumeClaim
。default
是默认的资源上限,对应容器里的resources.limits
,只适用于Container
。defaultRequest
默认申请的资源,对应容器里的resources.requests
,同样也只适用于Container
。max
、min
是对象能使用的资源的最大最小值。
这个YAML就示范了一个LimitRange对象:
apiVersion: v1
kind: LimitRange
metadata:
name: dev-limits
namespace: dev-ns
spec:
limits:
- type: Container
defaultRequest:
cpu: 200m
memory: 50Mi
default:
cpu: 500m
memory: 100Mi
- type: Pod
max:
cpu: 800m
memory: 200Mi
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
它设置了每个容器默认申请0.2的CPU和50MB内存,容器的资源上限是0.5的CPU和100MB内存,每个Pod的最大使用量是0.8的CPU和200MB内存。
使用 kubectl apply
创建LimitRange之后,再用 kubectl describe
就可以看到它的状态:
现在我们就可以不用编写 resources
字段直接创建Pod了,再运行之前的 kubectl run
命令:
kubectl run ngx --image=nginx:alpine -n dev-ns
有了这个默认的资源配额作为“保底”,这次就没有报错,Pod顺利创建成功,用 kubectl describe
查看Pod的状态,也可以看到LimitRange为它自动加上的资源配额。
# 3.6 小结
这一章主要讲了如何使用 Namespace 来管理 Kubernetes 的集群资源。在生产环境中,为避免某些用户过度消耗资源,就非常有必要用名字空间做好集群的资源规划了。
简单小结一下内容:
- 名字空间是一个逻辑概念,没有实体,它的目标是为资源和对象划分出一个逻辑边界,避免冲突。
- ResourceQuota 对象可以为名字空间添加资源配额,限制全局的CPU、内存和API对象数量。
- LimitRange 对象可以为容器或者Pod添加默认的资源配额,简化对象的创建工作。
课外小贴士:
- 不是所有的 API 对象都可以划分进名字空间管理的比如 Node、PV 等这样的全局资源就不属于任何名字空间。
- 因为 ResourceQuota 可以使用 scopeSelector 字段限制不同类型的对象,所以我们还可以在名字空间里设置多个不同策略的配额对象,更精细地控制资源。
- 在 LimitRange 对象里设置“max”字段可以有效地防止创建意外申请超量资源的对象。