Fork me on GitHub
Fork me on GitHub

Kubernetes Pod控制器

Pod控制器资源配置清单重要字段详解

资源的清单格式:
一级字段:apiVersion(group/version), kind, metadata(name,namespace,labels,annotations,…), spec, status(只读)

Pod资源:
spec.containers <[]object> # kubectl explain pods.spec.containers

  • name
  • image

  • imagePullPolicy
    可用值:Always, Never, IfNotPresent。Always:本地有或者没有这个镜像,就算有,也不用本地已有的这个镜像,而是去registry中下载。Never:如果有,就用。如果没有,它也不会去下载。IfNotPresent:如果本地存在就直接使用,如果不存在就去下载。如果你的镜像的标签是latest,那么默认值就是Always。如果不是latest标签,默认值都是IfNotPresent。另外在创建完这个资源后,imagePullPolicy这个字段是不可以修改的。除非把这个资源删了,然后去修改这个文件,在创建。很多时候,从一个无法确定其可信与否的主机之上运行容器时,我们把imagePullPolicy设置为Always时一定会浪费我们拖这个对应的对象的带宽,同时也会导致容器启动速度较慢。但是带来的好处是,当你试图启动一个容器时,那个目标主机之上正好存在了启动容器的这个镜像,但是那个镜像可能不是你想要运行的镜像,但是它精巧地伪装成了你要运行的镜像的名称,比如有人在每一个公有云的主机之上都放了某一个镜像,这个镜像里面内建了挖矿的代码,当你试图启动某一个容器时,因为本地已经有了这个镜像,就不用从registry上下载了,这样就中招了。所以 Always 在某些情况下还是有用的。

  • ports <[]Object>
    定义容器内的要暴露的端口时,可以暴露多个端口。而且每一个端口还应该有多个属性来定义。比如端口的名称,将来可以基于名称(在service中映射时可以用到)来引用它。端口号,还有协议。【注意】:暴露一个端口仅仅是提供额外信息的,并不会限制系统是否会真的会暴露。这和docker不太一样,这儿的网络是叠加网络,pod和pod可以通过ip地址直接通信的,所以不需要手动绑定在节点的IP地址上才能访问。在文件中明确地、显式地定义,至少让用户知道我们的应用程序到底监听在哪个端口。只要你容器里那个应用程序监听的端口,一定会暴露出去。

  • args <[]string> 和 commad <[]string> 修改镜像中的默认启动的命令。
    如果我们想不去运行镜像中定义的默认要运行的应用程序,此前的例子中用的是command。其实应该是 args 和 command 两个一起配合。command表示要运行的程序,args表示传递给程序的参数。而在dockerfile中,里面有entrypoint和cmd,如果只有cmd,那就运行cmd中的程序。如果既有entrypoint,又有cmd,cmd的内容将作为参数被传递给entrypoint中的命令。
    Kubernetes清单文件中的 command 是一个 Entrypoint array,有点相像于dockerfile中的entrypoint。而且command给定的代码是不会运行在shell中的。因此如果你想运行在shell中,必须指定/bin/sh。如果你没有提供command,docker镜像在制作时,它有entrypoint那个指令,那么就会运行镜像中的entrypoint。说白了就是当你不提供命令时,就默认运行镜像中的命令。
    args 用来向 command 传参数。如果在args中引用了变量,变量引用格式为:$(VAR_NAME)。如果不想在这被替换,就是想用shell中命令引用的效果,使用方式为:$$(VAR_NAME)。

关于清单文件中定义Pod时使用的command和args字段与dockerfile中的entrypoint和cmd的关系,在 https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/ 文档中有详细的说明。

标签和标签选择器

标签简介

标签是附加在对应对象之上的“键值对”。一个资源之上可存在多个标签,一个标签也可以在多个资源对象上。这每一个标签都可以对标签选择器进行匹配度检查,从而完成资源挑选。标签既可以在对象创建时指定,也可以在资源创建之后使用命令来添加、删除、修改。实践中,我们通常可以给资源附加多个不同维度的标签来进行不同维度的管理。常用的维度有:

  • release,比如stable,beta, alpha, canary等。
  • environment:dev/test/prod
  • 架构层面:接入层、前台等
  • 应用维度:应用名

标签:
key=value
key:字母、数字、、-、.
value:也不可以超过63个字符,可以为空。只能以 字母或数字开头及结尾,中间可使用 字母、数字、
、-、.

示例1:利用标签选择器查看资源

1
2
3
4
5
6
7
# 查看对应资源下含有标签app的资源
[root@spark32 manifests]# kubectl get pods -l app
NAME READY STATUS RESTARTS AGE
pod-demo 2/2 Running 0 28s
[root@spark32 manifests]# kubectl get pods -l app --show-labels
NAME READY STATUS RESTARTS AGE LABELS
pod-demo 2/2 Running 0 35s app=myapp,tier=frontend
1
2
3
4
5
6
7
8
9
# -L 标签名,表示显示对应资源下有相应标签的值。
[root@spark32 manifests]# kubectl get pods -L app,run
NAME READY STATUS RESTARTS AGE APP RUN
client-69fd89d767-95nqt 1/1 Running 1 5d17h client
myapp-5bc569c47d-qq8lb 1/1 Running 0 5d20h myapp
myapp-5bc569c47d-stvj9 1/1 Running 0 5d20h myapp
myapp-5bc569c47d-tzlsf 1/1 Running 0 5d20h myapp
nginx-deploy-55d8d67cf-wf28j 1/1 Running 0 5d21h nginx-deploy
pod-demo 2/2 Running 0 2m48s myapp

示例2:添加标签

1
2
3
4
5
6
[root@spark32 manifests]# kubectl label --help
...
Usage:
kubectl label [--overwrite] (-f FILENAME | TYPE NAME) KEY_1=VAL_1 ... KEY_N=VAL_N [--resource-version=version]
[options]
...

TYPE NAME:表示什么类型的、什么名字的资源。

给Pod pod-demo添加一个标签:release=canary

1
2
3
4
5
[root@spark32 manifests]# kubectl label pods pod-demo release=canary
pod/pod-demo labeled
[root@spark32 manifests]# kubectl get pods pod-demo --show-labels
NAME READY STATUS RESTARTS AGE LABELS
pod-demo 2/2 Running 0 10m app=myapp,release=canary,tier=frontend

示例3:修改标签

1
2
3
4
5
6
7
[root@spark32 manifests]# kubectl label pods pod-demo release=stable
error: 'release' already has a value (canary), and --overwrite is false
[root@spark32 manifests]# kubectl label pods pod-demo release=stable --overwrite
pod/pod-demo labeled
[root@spark32 manifests]# kubectl get pods pod-demo --show-labels
NAME READY STATUS RESTARTS AGE LABELS
pod-demo 2/2 Running 0 13m app=myapp,release=stable,tier=frontend

示例4:删除标签

1
2
3
4
5
6
7
8
[root@spark32 manifests]# kubectl get pods pod-demo --show-labels
NAME READY STATUS RESTARTS AGE LABELS
pod-demo 2/2 Running 0 38m app=myapp,release=stable,tier=frontend
[root@spark32 manifests]# kubectl label pod pod-demo release-
pod/pod-demo labeled
[root@spark32 manifests]# kubectl get pods pod-demo --show-labels
NAME READY STATUS RESTARTS AGE LABELS
pod-demo 2/2 Running 0 38m app=myapp,tier=frontend

标签选择器介绍

k8s的标签选择器支持两类:

  • 等值关系的标签选择器:=、==、!=
  • 基于集合关系的标签选择器:
    • KEY in (VALUE1,VALUE2,…)
    • KEY notin (VALUE1,VALUE2,…)
    • KEY
    • !KEY

有很多类型的资源都要基于 标签选择器 来关联其他资源的,比如说Pod控制器和service。Pod控制器和service来实现标签关联时,通常使用另外两个字段来嵌套。Deployment、service等许多资源通常支持使用 matchLabels来让你去定义,就是里面内嵌一字段来定义怎么使用标签选择器。Deployment、ReplicaSet等支持两种 matchLabels 和 matchExpressions,service只支持第一种 matchLabels。

  • matchLabels: 直接给定键值,相当于使用等值关系一样
  • matchExpressions: 基于给定的表达式来定义使用的标签选择器。{key:”KEY”, operator:”OPERATOR”, values:[VALUE1,VALUE2,…]}
    • OPERATOR:
      • In、NotIn:values字段的值必须为非空列表
      • Exists、NotExists:values字段的值必须为空列表
1
2
3
4
5
6
7
8
9
10
11
12
13
[root@spark32 manifests]# kubectl get pods -l release
NAME READY STATUS RESTARTS AGE
pod-demo 2/2 Running 0 22m
# ,表示与关系,必须都满足
[root@spark32 manifests]# kubectl get pods -l release,app
NAME READY STATUS RESTARTS AGE
pod-demo 2/2 Running 0 22m
[root@spark32 manifests]# kubectl get pods -l release=stable
NAME READY STATUS RESTARTS AGE
pod-demo 2/2 Running 0 22m
[root@spark32 manifests]# kubectl get pods -l release=stable --show-labels
NAME READY STATUS RESTARTS AGE LABELS
pod-demo 2/2 Running 0 22m app=myapp,release=stable,tier=frontend
1
2
3
4
5
6
7
8
9
# !=
# 没有release标签也视为不等于
[root@spark32 manifests]# kubectl get pods -l release!=stable
NAME READY STATUS RESTARTS AGE
client-69fd89d767-95nqt 1/1 Running 1 5d17h
myapp-5bc569c47d-qq8lb 1/1 Running 0 5d20h
myapp-5bc569c47d-stvj9 1/1 Running 0 5d20h
myapp-5bc569c47d-tzlsf 1/1 Running 0 5d20h
nginx-deploy-55d8d67cf-wf28j 1/1 Running 0 5d21h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# in、notin
[root@spark32 manifests]# kubectl get pods -l "release in (canary,alpha,beta)"
No resources found.
[root@spark32 manifests]# kubectl get pods -l "release in (canary,alpha,beta,stable)"
NAME READY STATUS RESTARTS AGE
pod-demo 2/2 Running 0 25m
# 没有release标签也视为不在集合内
[root@spark32 manifests]# kubectl get pods -l "release notin (canary,alpha,beta,stable)"
NAME READY STATUS RESTARTS AGE
client-69fd89d767-95nqt 1/1 Running 1 5d17h
myapp-5bc569c47d-qq8lb 1/1 Running 0 5d20h
myapp-5bc569c47d-stvj9 1/1 Running 0 5d20h
myapp-5bc569c47d-tzlsf 1/1 Running 0 5d20h
nginx-deploy-55d8d67cf-wf28j 1/1 Running 0 5d21h

能使用标签的不仅仅包括Pod,各种资源对象都可以打标,包括节点。

1
2
3
4
5
6
[root@spark32 manifests]# kubectl get nodes --show-labels
NAME STATUS ROLES AGE VERSION LABELS
hadoop16 Ready <none> 19h v1.14.1 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=hadoop16,kubernetes.io/os=linux
spark17 Ready <none> 117d v1.14.1 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=spark17,kubernetes.io/os=linux
spark32 Ready master 117d v1.14.1 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=spark32,kubernetes.io/os=linux,node-role.kubernetes.io/master=
ubuntu31 Ready <none> 117d v1.14.1 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=ubuntu31,kubernetes.io/os=linux

给节点ubuntu31新增一个标签:

1
2
3
4
5
6
7
8
[root@spark32 manifests]# kubectl label node ubuntu31 disktype=ssd
node/ubuntu31 labeled
[root@spark32 manifests]# kubectl get nodes --show-labels
NAME STATUS ROLES AGE VERSION LABELS
hadoop16 Ready <none> 19h v1.14.1 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=hadoop16,kubernetes.io/os=linux
spark17 Ready <none> 117d v1.14.1 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=spark17,kubernetes.io/os=linux
spark32 Ready master 117d v1.14.1 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=spark32,kubernetes.io/os=linux,node-role.kubernetes.io/master=
ubuntu31 Ready <none> 117d v1.14.1 beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,disktype=ssd,kubernetes.io/arch=amd64,kubernetes.io/hostname=ubuntu31,kubernetes.io/os=linux

当节点有标签以后,我们随后在添加资源时,可以让资源对节点有倾向性。

假设我们想让pod-demo这个pod运行在node01上,node01上有个标签是disktype=ssd

1
2
3
4
5
6
7
[root@spark32 manifests]# kubectl delete -f pod-demo.yaml
pod "pod-demo" deleted
[root@spark32 manifests]# kubectl create -f pod-demo.yaml
pod/pod-demo created
[root@spark32 manifests]# kubectl get pods pod-demo -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod-demo 2/2 Running 0 2m41s 10.244.2.31 ubuntu31 <none> <none>

注解

资源除了可以使用标签外,还可以使用注解,annotations。资源注解和标签很相像,都是键值。区别是annotations不能用于挑选资源对象,仅用于为对象提供“元数据”。这些元数据有些时候可能被某些程序所用到,而且很重要,会作为基本判断条件。键值长度不再受字符数量限制。labels键值必须是不超过63个字符,键前缀使用域名的话,不超过253字符。
annotations其实可以动态编辑的,它不是特别重要的属性字段。

1
2
3
4
[root@spark32 manifests]# kubectl delete -f pod-demo.yaml
pod "pod-demo" deleted
[root@spark32 manifests]# kubectl create -f pod-demo.yaml
pod/pod-demo created

Pod的生命周期

在Pod创建时,它可能要做初始化。所以在创建启动之前需要一点时间做初始化。所谓的Pod秒级启动,这个秒级不具有实际意义,有的应用程序初始化有的时候需要很长时间,不可能1s之内完成。另外,在写dockerfile的时候,我们应该使用entrypoint脚本来对容器做环境初始化,这个初始化估计都不止1s。
Pod能站起来,容器启动起来之前,要经历那么一点点时间。如果这点时间忽略不计的话,那么Pod的生命周期从Pod被创建开始。一个容器只用来运行一个进程,一个Pod中可以运行多个容器,但是一般我们只运行一个容器。Pod内的这个主容器,在运行之前,可能需要做一些环境设定。所以一般来讲,主容器启动之前,可以把这段时间哪来去做别的事。比如运行另外一个容器,这个容器专门为主容器做环境初始化,所以通常被称为init container。这个容器内部程序运行完,就可以退出了,它不会一直存在。初始化容器可以有多个,多个初始化容器是串行执行的。主容器退出了,Pod也就退出了,这两个是一个时间点。
在主容器启动时,它也需要把应用程序环境初始化,比如说要用entrypoint生成一个配置文件,再比如要运行一个tomcat,tomcat要把war文件内容展开部署等。所以在主容器刚刚启动之后,用户可以手动嵌入做一些操作,可以做一个 post start,比如执行完一个命令就退出。
如果我们的主容器要退出,在结束之前,也可以做一些事情,pre stop。类似于awk的BEGIN和END。
另外,在整个主容器的执行过程当中,我们还可以做两类操作,一般而言,在post start执行之后,我们就可以做两类检测了,在k8s中,支持两类对Pod中容器的检测。第一种检测:liveness probe,主进程还是否处于存活状态。但是就算这个主进程在运行,也不一定意味着能正常提供服务。liveness probe是做存活状态检测。因为一个Pod内是可以有多个容器的,通常我们称为 主容器 和 辅助容器。所以每一个容器,尤其是主容器,我们应该对它正常与否、以及就绪与否做状态检测。分别是liveness probe和readiness probe。无论是哪种probe,都可以支持三种探测行为:

  • 1.执行自定义命令
  • 2.向指定的套接字发TCP请求
  • 3.向指定的HTTP服务发请求

liveness probe只要用来判断主容器是否处于运行状态的,readiness probe用于判定容器中的主进程是否准备就绪并可以对外提供服务。

整个Pod从创建到结束之前,Pod一定会处于某种状态之中。所以Pod有多种状态:

  • Pending:挂起。我们请求创建这个Pod时,条件不能满足,调度没完成,就更别提启动了
  • Running:运行中。
  • Failed:失败。
  • Successed: 有些状态时间很短。
  • Unknown: 为什么会未知呢?因为所谓一个Pod处于什么状态,是apiserver跟运行这个Pod的节点上的kubelet通信来获取Pod的状态信息的。如果kubelet本身出故障了,那么很显然此时apiserver肯定连不上kubelet,也就得不到信息了。类似于这样

创建Pod经历的阶段:
当用户创建Pod时,这个请求要提交给apiserver,apiserver先把创建请求的目标状态保存在etcd中,而后apiserver之后会请求Scheduler进行调度。如果调度成功了,会把调度结果保存在etcd中。随后目标节点,比如node01,node01上的kubelet通过apiserver当中的状态变化,就知道有一个新的任务给自己了。所以kubelet会通过apiserver拿到此前用户创建的清单,根据清单在当前节点上去创建启动这个Pod。然后将Pod的状态发回给apiserver,apiserver在更新到etcd中。
而Pod整个生命周期中有非常多的重要行为,如上面的图及分析。
Pod生命周期中的重要行为:

  • 初始化容器
  • 启动前钩子、结束前钩子
  • 容器探测:
    • liveness
    • readiness

restartPolicy: Pod的重启策略。比如做liveness探测后发现容器挂了,Pod还在。此时容器要不要重启,要取决于这个字段。

  • Always:
  • OnFailure:状态为错误时才会重启,正常停止是不重启的。
  • Never.:从不重启,挂了就挂了。Default to Always.

重启逻辑:第一次重启是一停止,就立即重启。第二次重启就要延时了,比如第二次重启是终止后等10s在重启。第三次重启等20s,第四次重启等40s…300s是最长的等待时间。为了防止不断重启给系统造成压力。

1
2
3
4
5
6
7
[root@spark32 manifests]# kubectl explain pods.spec
...
restartPolicy <string>
Restart policy for all containers within the pod. One of Always, OnFailure,
Never. Default to Always. More info:
https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy
...

当我们想终止某个Pod,我们应该对其平滑终止。这样才会确保数据不会丢失。比如MariaDB,我们想要结束这个Pod,强行kill掉有可能会导致数据丢失。因此在提交删除一个Pod的时候,它是不会上来就kill掉而删除的,而是像Pod内的每一个容器发送终止term信号,让Pod中的容器正常进行终止。这个终止会给个宽限期,比如30s。宽限期到了,那就只能发送kill信号了。Pod的宽限期一般是30s,可以自己指定时长。