notebook notebook
首页
  • 计算机网络
  • 计算机系统
  • 数据结构与算法
  • 计算机专业课
  • 设计模式
  • 前端 (opens new window)
  • Java 开发
  • Python 开发
  • Golang 开发
  • Git
  • 软件设计与架构
  • 大数据与分布式系统
  • 常见开发工具

    • Nginx
  • 爬虫
  • Python 数据分析
  • 数据仓库
  • 中间件

    • MySQL
    • Redis
    • Elasticsearch
    • Kafka
  • 深度学习
  • 机器学习
  • 知识图谱
  • 图神经网络
  • 应用安全
  • 渗透测试
  • Linux
  • 云原生
面试
  • 收藏
  • paper 好句
GitHub (opens new window)

学习笔记

啦啦啦,向太阳~
首页
  • 计算机网络
  • 计算机系统
  • 数据结构与算法
  • 计算机专业课
  • 设计模式
  • 前端 (opens new window)
  • Java 开发
  • Python 开发
  • Golang 开发
  • Git
  • 软件设计与架构
  • 大数据与分布式系统
  • 常见开发工具

    • Nginx
  • 爬虫
  • Python 数据分析
  • 数据仓库
  • 中间件

    • MySQL
    • Redis
    • Elasticsearch
    • Kafka
  • 深度学习
  • 机器学习
  • 知识图谱
  • 图神经网络
  • 应用安全
  • 渗透测试
  • Linux
  • 云原生
面试
  • 收藏
  • paper 好句
GitHub (opens new window)
  • Linux

  • 云原生

    • Docker

    • 专栏-深入容器

    • Kubernetes入门实战课-罗剑锋

      • 初识容器
      • Kubernetes 的安装与基本架构
      • YAML、Pod、Job、CronJob、ConfigMap、Secret
      • Deployment、Daemonset、Service、Ingress
      • PV、PVC、StorageClass、Provisioner、StatefulSet
      • 滚动更新、应用保障、集群管理
        • 1. 滚动更新:如何做到平滑的应用升级降级?
          • 1.1 Kubernetes 如何定义应用版本
          • 1.2 Kubernetes 如何实现应用更新
          • 1.3 Kubernetes 如何管理应用更新
          • 1.4 Kubernetes 如何添加更新描述
          • 1.5 小结
        • 2. 应用保障:如何让 Pod 运行得更健康?
          • 2.1 容器资源配额
          • 2.2 容器状态探针
          • 2.3 如何使用容器状态探针
          • 2.4 小结
        • 3. 集群管理:如何用名字空间分隔系统资源?
          • 3.1 为什么要有 Namespace
          • 3.2 如何使用 Namespace
          • 3.3 什么是资源配额
          • 3.4 如何使用资源配额
          • 3.5 默认资源配额
          • 3.6 小结
      • MetricsServer、Prometheus、CNI
    • 深入剖析 Kubernetes-张磊

    • Jenkins

  • 运维
  • 云原生
  • Kubernetes入门实战课-罗剑锋
yubin
2023-04-23
目录

滚动更新、应用保障、集群管理

参考: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 作为例子:

20230610194021

名字中的随机数"6796..."就是 Pod 模板的 Hash 值,也就是 Pod 的“版本号”。如果你变动了 Deployment 下 template 中 Pod 的 YAML 任一描述,都会生成一个新的应用版本,kubectl apply 后就会重新创建 Pod:

20230610194221

可以看到,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
  ... ...
1
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
1
2
20230610195012

更新完成之后,再去执行 kubectl get pod 就可以看到,Pod 已经全部替换成了新的版本号。

仔细观察 kubectl rollout status 的输出信息可以发现,Kubernetes 并不是把旧的 Pod 全部销毁然后再一次性创建出来新的 Pod,而是逐个创建新 Pod,同时也在销毁旧 Pod,保证系统里始终有足够数量的 Pod 在运行,不会有“空窗期”中断服务。这就是滚动更新。

使用命令 kubectl describe 可以更清楚地看到 Pod 的变化情况:

kubectl describe deploy ngx-dep
1
20230610200026
  • 一开始的时候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,同时新版本扩容到指定值,是一个”此消彼长“的过程。

这个滚动更新的过程可以参考下图:

20230610200244

# 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
1
20230610200947

但 kubectl rollout history 的列表输出的有用信息太少,你可以在命令后加上参数 --revision 来查看每个版本的详细信息,包括标签、镜像名、环境变量、存储卷等等,通过这些就可以大致了解每次都变动了哪些关键字段:

20230610201238

假设我们认为刚刚更新的 nginx:1.22-alpine 不好,想要回退到上一个版本,就可以使用命令 kubectl rollout undo,也可以加上参数 --to-revision 回退到任意一个历史版本:

20230610201330

kubectl rollout undo 的操作过程其实和 kubectl apply 是一样的,执行的仍然是“滚动更新”,只不过使用的是旧版本Pod模板,把新版本Pod数量收缩到0,同时把老版本Pod扩展到指定值。

这个V2到V1的“版本降级”的过程如下图,它和“版本升级”过程是完全一样的,不同的只是版本号的变化方向:

20230610201411

# 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
...
1
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
...
1
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
...
1
2
3
4
5
6
7

注意以上三个版本的 metadata 部分,使用 kubernetes.io/change-cause 描述了版本更新的情况,这样更新版本之后,我们在使用 kubectl rollout history 命令就可以看到更新历史:

20230610201936

这次显示的列表信息就好看多了,每个版本的主要变动情况列得非常清楚,和Git版本管理的感觉很像。

# 1.5 小结

这一章主要讲的滚动更新,小结一下要点:

  1. Kubernetes 使用 template 的 hash 值作为版本号。
  2. Kubernetes 的更新过程是滚动更新,保证了在更新过程中服务的始终可用。
  3. 管理应用更新使用的命令是 kubectl rollout,子命令有 status、 history、 undo 等。
  4. 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
1
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" 状态,实际上没有被真正调度:

20230610204510

如果我们使用命令 kubectl describe 来查看具体原因,会发现有这么一句提示:

20230610204541

这就很明确地告诉我们Kubernetes调度失败,当前集群里的所有节点都无法运行这个Pod,因为它要求的CPU实在是太多了。

# 2.2 容器状态探针

当一个程序正常启动后,也有可能在运行时发生死锁或死循环等故障,这时在外部看进程一切正常,但在内部已经是一团糟了。所以,我们还希望Kubernetes这个“保姆”能够更细致地监控Pod的状态,除了保证崩溃重启,还必须要能够探查到Pod的内部运行状态,定时给应用做“体检”,让应用时刻保持“健康”,能够满负荷稳定工作。

那应该用什么手段来检查应用的健康状态呢?

由于各种程序对外界来说就是一个黑盒子,只能看到启动、执行、停止这三个基本状态,此外就没有什么好的办法来知道它内部是否正常了。所以我们必须把应用变成灰盒子,让部分内部信息对外可见,这样Kubernetes才能够探查到内部的状态。

这里的检查过程就有点像核酸检测,Kubernetes 使用探针(Probe)在应用的检查口去提取信息,然后根据这些信息来判断应用是否健康。

Kubernetes 为检查应用状态定义了三种探针,它们分别对应容器不同的状态:

  • Startup,启动探针,用来检查应用是否已经启动成功,适合那些有大量初始化工作要做,启动很慢的应用。
  • Liveness,存活探针,用来检查应用是否正常运行,是否存在死锁、死循环。
  • Readiness,就绪探针,用来检查应用是否可以接收流量,是否能够对外提供服务。

你需要注意这三种探针是递进的关系:应用程序先启动,加载完配置文件等基本的初始化数据就进入了Startup状态,之后如果没有什么异常就是Liveness存活状态,但可能有一些准备工作没有完成,还不一定能对外提供服务,只有到最后的Readiness状态才是一个容器最健康可用的状态。

这三个状态与探针的关系如下图:

20230610205028

那 Kubernetes 具体是如何使用状态和探针来管理容器的呢?

如果一个 Pod 里的容器配置了探针,Kubernetes 在启动容器后就会不断地调用探针来检查容器的状态:

  • 如果Startup探针失败,Kubernetes会认为容器没有正常启动,就会尝试反复重启,当然其后面的Liveness探针和Readiness探针也不会启动。
  • 如果Liveness探针失败,Kubernetes就会认为容器发生了异常,也会重启容器。
  • 如果Readiness探针失败,Kubernetes会认为容器虽然在运行,但内部有错误,不能正常提供服务,就会把容器从Service对象的负载均衡集合中排除,不会给它分配流量。

知道了Kubernetes对这三种状态的处理方式,我们就可以在开发应用的时候编写适当的检查机制,让Kubernetes用“探针”定时为应用做“体检”了。

下图在刚才图的基础上,又补充上了 k8s 的处理动作,从这张图就可以很好地理解容器探针的工作流程了:

20230610205223

# 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';
      }
    }
1
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
1
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 探针的执行情况:

20230610205659

从截图中你可以看到,Kubernetes正是以大约5秒一次的频率,向URI /ready 发送HTTP请求,不断地检查容器是否处于就绪状态。

如果探针探测失败且 Kubernetes 认为你的程序不健康后,便会采取处理措施。比如当 StartupProbe 探测失败的时候,Kubernetes 就会不停地重启容器,现象就是 kubectl get pod 中打印的 RESTARTS 次数不断增加:

20230610205852

# 2.4 小结

这一章讲了两种为 Pod 配置运行保障的方式:Resources 和 Probe。Resources 就是为容器加上资源限制,而 Probe 就是主动健康检查,让 Kubernetes 实时地监控应用的运行状态。

简单小结一下今天的内容:

  1. 资源配额使用的是cgroup技术,可以限制容器使用的CPU和内存数量,让Pod合理利用系统资源,也能够让Kubernetes更容易调度Pod。
  2. Kubernetes定义了Startup、Liveness、Readiness三种健康探针,它们分别探测应用的启动、存活和就绪状态。
  3. 探测状态可以使用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
1
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
1
2
3
4
5
6
7
8
9
10

kubectl apply 创建这个对象之后,我们直接用 kubectl get 是看不到它的,因为默认查看的是“default”的 Namespace,想要操作其他 Namespace 的对象必须要用 -n 参数明确指定:

kubectl get pod -n test-ns
1

因为名字空间里的对象都从属于 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:
  ... 
1
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
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
1
2
20230610214610

你可以看到输出了ResourceQuota的全部信息,但都挤在了一起,看起来很困难,这时可以再用命令 kubectl describe 来查看对象,它会给出一个清晰的表格:

20230610214628

现在让我们尝试在这个名字空间里运行两个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
1
2
20230610214722

ResourceQuota限制了名字空间里最多只能有一个Job,所以创建第二个Job对象时会失败,提示超出了资源配额。再用命令 kubectl describe 来查看,也会发现Job资源已经到达了上限:

20230610214910

只有删除掉第一个 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
1
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 就可以看到它的状态:

20230610215426

现在我们就可以不用编写 resources 字段直接创建Pod了,再运行之前的 kubectl run 命令:

kubectl run ngx --image=nginx:alpine -n dev-ns
1

有了这个默认的资源配额作为“保底”,这次就没有报错,Pod顺利创建成功,用 kubectl describe 查看Pod的状态,也可以看到LimitRange为它自动加上的资源配额。

# 3.6 小结

这一章主要讲了如何使用 Namespace 来管理 Kubernetes 的集群资源。在生产环境中,为避免某些用户过度消耗资源,就非常有必要用名字空间做好集群的资源规划了。

简单小结一下内容:

  1. 名字空间是一个逻辑概念,没有实体,它的目标是为资源和对象划分出一个逻辑边界,避免冲突。
  2. ResourceQuota 对象可以为名字空间添加资源配额,限制全局的CPU、内存和API对象数量。
  3. LimitRange 对象可以为容器或者Pod添加默认的资源配额,简化对象的创建工作。

课外小贴士:

  • 不是所有的 API 对象都可以划分进名字空间管理的比如 Node、PV 等这样的全局资源就不属于任何名字空间。
  • 因为 ResourceQuota 可以使用 scopeSelector 字段限制不同类型的对象,所以我们还可以在名字空间里设置多个不同策略的配额对象,更精细地控制资源。
  • 在 LimitRange 对象里设置“max”字段可以有效地防止创建意外申请超量资源的对象。
编辑 (opens new window)
上次更新: 2023/06/10, 13:58:30
PV、PVC、StorageClass、Provisioner、StatefulSet
MetricsServer、Prometheus、CNI

← PV、PVC、StorageClass、Provisioner、StatefulSet MetricsServer、Prometheus、CNI→

最近更新
01
Deep Reinforcement Learning
10-03
02
误删数据后怎么办
04-06
03
MySQL 一主多从
03-22
更多文章>
Theme by Vdoing | Copyright © 2021-2024 yubincloud | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×