Pod对象是一组容器的集合,这些容器共享Network、UTS及IPC名称空间,因此具有相同的域名、主机名和网络接口,并可通过IPC直接通信。为一个Pod对象中的各容器提供网络名称空间等共享机制的是底层基础容器Pause。

1.管理Pod对象的容器

一个Pod对象中至少要存在一个容器,因此,containers字段是定义Pod是其嵌套字段Spec中的必选项,用于为Pod指定要创建的容器列表。

name为必选字段,用于指定容器名称,image字段是为可选,以方便更高级别的管理类资源(如Deployment)等能覆盖此字段,于是自主式的Pod并不可省略此字段。因此,定义一个容器的基础框架如下:

  1. name: CONTAINER_NAME
  2. image: IMAGE_FILE_NAME

1.2 镜像及其获取策略

k8s系统支持用户自定义镜像文件的获取策略,例如在网络资源较为紧张时可以禁止从仓库中获取镜像文件等。

imagePullPolicy:

  • Always: 镜像标签为”latest“或镜像不存在时总是从指定的仓库中获取镜像;
  • IfNotPresent: 仅当本地镜像缺失时才从目标仓库下载镜像;
  • Never:仅使用本地镜像;

如:

apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
spec:
  containers:
  - name: nginx
    image: nginx
    imagePullPolicy: Always

对于标签为”latest”的镜像,默认策略为Always,对于其它标签的镜像, 默认为IfNotPresent。

需要注意的是,使用私有仓库中的镜像需要在相关节点上交互式执行docker login命令来登录,或者将认证信息定位专有的Secret资源,并配置Pod通过”imagePullSecretes”字段调用此认证信息完成。

使用kubectl创建名字为regcred的secret:

kubectl create secret docker-registry regcred \
  --docker-server=<你的镜像仓库服务器> \
  --docker-username=<你的用户名> \
  --docker-password=<你的密码> \
  --docker-email=<你的邮箱地址>

查看regcred Secret的内容,可以用YAML格式查看:

kubectl get secret regcred -o yaml

输出和下面类似:

apiVersion: v1
data:
  .dockerconfigjson: eyJodHRwczovL2luZGV4L ... J0QUl6RTIifX0=
kind: Secret
metadata:
  ...
  name: regcred
  ...
type: kubernetes.io/dockerconfigjson

然后将其部分内容写成资源清单,就不必每次都使用命令创建。

secret-test.yml:

apiVersion: v1
data:
  .dockerconfigjson: eyJhdXRocyI6eyJkb2NrZXJodWIuZGF0YWdyYW5kLmNvbSI6eyJ1c2VybmFtZSI6InpoZW5na2FpeXVhbiIsInBhc3N3b3JkIjoiWmhlbmcxMjM0NTYiLCJlbWFpbCI6InpoZW5na2FpeXVhbkBkYXRhZ3JhbmQuY29tIiwiYXV0aCI6ImVtaGxibWRyWVdsNWRXRnVPbHBvWlc1bk1USXpORFUyIn19fQ==
kind: Secret
metadata:
  name: harbor
  namespace: default
type: kubernetes.io/dockerconfigjson

# kubectl apply -f secret-test.yml

1.3 暴露端口

k8s系统的网络模型中,各Pod的IP地址处于同一网络平面,无论是否为容器指定了要暴露的端口,都不会影响集群中其它节点之上的Pod客户端对其进行访问,这就意味着,任何监听在非lo接口上的端口都可以通过Pod网络直接被请求。从这个角度来看,容器端口只是信息性数据,它只是为集群用户提供一个快速了解相关Pod对象的可访问端口的途径,而且显式指定容器端口,还能为其赋予一个名称以方便调用。

容器的ports字段的值是一个列表,由一到多个端口对象组成,它的常用嵌套字段包括如下几个:

  • containerPort : 必选字段,指定在Pod对象的IP地址上暴露的容器端口,有效范围(0,65536);
  • name : 当前端口的名称,此端口名可被Service资源调用;
  • protocol:端口相关协议,可为TCP或UDP,默认为TCP;
  • hostPort : 主机端口,它将接收到的请求通过NAT机制转发至由containerPort字段指定的容器端口;
  • hostIP : 主机端口要绑定的主机IP,默认为0.0.0.0,即主机之上所有可用的IP地址;考虑到Pod对象工作节点的IP地址难以明确指定,因此此字段通常使用默认值;

需要注意的是,hostPort与NodePort类型的Service对象暴露端口的方式不通,NodePort是通过所有节点暴露容器服务,而hostPort则是经由Pod对象所在节点的IP地址来进行。

如:

apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
spec:
  containers:
  - name: nginx
    image: nginx
    ports:
    - name: http
      containerPort: 80
      protocol: TCP

1.4 自定义运行的容器化应用

由Docker镜像启动容器时运行的应用程序在相应的Dockerfile中由ENTRYPOINT指令进行定义,传递给程序的参数则通过CMD指令指定,ENTRYPOINT指令不存在时,CMD可用于同时指定程序及其参数。

获取镜像中定义的CMD和ENTRYPOINT:

docker inspect nginx:latest -f {{.Config.Cmd}}

docker inspect nginx:latest -f {{.Config.Entrypoint}}

k8s的containers的command字段能够指定不同于镜像默认运行的应用程序,并且可以同时使用args字段进行参数传递,它们将覆盖镜像中的默认定义。

不过,如果仅为容器定义了args字段,那么它将作为参数传递给镜像中默认指定运行的应用程序;如果仅定义command字段,那么它将覆盖镜像中定义的程序及参数,并以无参数方式运行应用程序。

如:

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-custom-command
spec:
  containers:
  - name: test
    image: alpine:latest
    command: ["/bin/sh"]
    args: ["-c","while true; do sleep 30; done"]

自定义args,也是向容器中的应用程序传递配置信息的常用方式之一,对于非原生(cloud native)的应用程序,这几乎也是最简单的配置方式。另一个常用的方式是使用环境变量。

1.5 环境变量

向Pod对象中的容器环境变量传递数据的方法有两种:env和envForm,这里重点介绍第一种,第二种在ConfigMap和Secret资源时说明

  • name : 环境变量的名称,必选字段;
  • value : 传递给环境变量的值,通过$(VAR_NAME)引用,逃逸格式为$$(VAR_NAME),默认值为空;

示例配置清单中定义的Pod对象为其容器filebeat传递了两个环境变量,REDIS_HOST定义了filebeat要发往Redis主机地址,LOG_LEVEL定义了filebeat日志级别:

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-env
spec:
  containers:
  - name: filebeat
    image: filebeat:5.6.5
    env:
    - name: REDIS_HOST
      value: db.ilinux.io:6379
    - name: LOG_LEVEL
      value: info

1.6 共享节点的网络名称空间

同一个Pod对象的各容器均运行于一个独立的、隔离的Network名称空间中,共享同一个网络协议栈及相关的网络设备,也有一些特殊的Pod对象需要运行于所在节点的名称空间中,执行系统级的管理任务,例如查看和操作节点的网络资源甚至是网络设备等;

通常,以kubeadm部署的k8s集群中的kube-apiserver、kube-controller-manager、kube-scheduler,以及kube-proxy和kube-flannel等通常都是第二种类型的Pod对象。事实上,仅需要设置spec.hostNetwork的属性为true即可创建共享节点网络名称空间的Pod对象;

另外,在Pod对象中时还可以分别使用spec.hostPID和spec.hostIPC来共享工作节点的PID和IPC名称空间。

如:

apiVersion: v1
kind: Pod
metadata:
  name: busybox
spec:
  containers:
  - name: busybox
    image: busybox
    command:
    - /bin/sh
    - -c
    - sleep 3600
  hostPID: true

# kubectl apply -f busybox.yml
# kubectl exec -it busybox -- /bin/sh
# ps
  能看到宿主机的进程信息

1.7 设置Pod对象的安全上下文

Pod对象的安全上下文用于设定Pod或容器的权限和访问控制功能,其支持设置的常用属性包括以下几方面:

  • 基于用户ID(UID)和组ID(GID)控制访问对象时的权限;
  • 以特权或非特权的方式运行;
  • 通过Linux Capabilities为其提供部分特权;
  • 基于Seccomp过滤进程的系统调用;
  • 基于SELinux的安全标签;
  • 是否能够进行权限升级;

Pod对象的安全上下文定义在spec.securtiyeContext字段中,而容器的安全上下文则定义在spec.containers[].securityContext字段中,且二者可嵌套使用的字段还有所不同。

下面的配置清单示例为busybox容器定义了安全上下文,它以uid为1000的非特权用户运行容器,并禁止权限升级:

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-securitycontext
spec:
  containers:
  - name: busybox
    image: busybox
    command: ["/bin/sh","-c","sleep 6400"]
    securityContext:
      runAsNonRoot: true
      runAsUser: 1000
      allowPrivilegeEscalation: false
# kubectl exec pod-with-securitycontext -- ps aux
PID   USER     TIME  COMMAND
    1 1000      0:00 sleep 6400
    7 1000      0:00 ps aux

2.标签与标签选择器

标签能够附加于Kubernetes的任何资源对象之上,简单来说,标签就是键值类型的数据,它可于资源创建时直接指定,也可随时按需添加到活动对象中,而后即可由标签选择器进行匹配度检查从而完成资源挑选。

一个对象可拥有不止一个标签,而同一个标签也可被添加至多个资源之上。

标签中的键名称通常由键前缀和键名组成,其中键前缀可选,其格式形如”KEY_PREFIX/KEY_NAME”。省略键前缀时,键将被设为用户的私有数据,不过由Kubernetes系统组建或第三方组建自动为用户资源添加的键必须使用键前缀,而”kubernetes.io/“前缀则预留给Kubernetes的核心组件使用。

2.1 管理资源标签

创建资源时,可直接在其metadata中嵌套使用”labels”字段以定义要附加的标签项。

如:

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-labels
  labels:
    env: qa
    tier: frontend
spec:
  containers:
  - name: nginx
    image: nginx

基于此资源清单创建后查看标签信息:

# kubectl get pod --show-labels
NAME                READY   STATUS    RESTARTS   AGE    LABELS
pod-with-labels   1/1     Running     0          14s    env=qa,tier=frontend

标签较多时,使用”-L key1,key2,…”选项可指定显示有着特定键的标签信息:

# kubectl get pod -L env,tier
NAME                READY   STATUS    RESTARTS   AGE    ENV   TIER
pod-with-labels   1/1     Running     0          118s   qa    frontend

kubectl label命令可以直接管理活动对象的标签,以按需进行添加或修改等操作,当修改已存在的键值是需要使用”—overwrite”:

# kubectl label pods pod-with-labels env=testing --overwrite
pod/pod-with-labels labeled

# kubectl get pod --show-labels
NAME                READY   STATUS    RESTARTS   AGE     LABELS
pod-with-labels   1/1     Running     0          4m23s   env=testing,tier=frontend

2.2 标签选择器

标签选择器用于表达标签的查询条件或选择标准,Kubernetes API目前支持两个选择器:基于等值关系(equality-based)以及基于集合关系(set-based)。使用标签选择器时还将遵循一下逻辑:

  • 同时指定的多个选择器之间的逻辑关系为”与”操作;
  • 使用空值的标签选择器意味着每个资源对象都将被选中;
  • 空的标签选择器将无法选出任何资源;

2.2.1 等值关系

基于等值关系的标签选择器的可用操作符有”=” “==”和”!=”三种,kubectl get 命令的 -l 选项能够指定使用标签选择器,例如,显示键名env的值不为qa的所有Pod对象:

# kubectl get pod -l "env!=qa" -L env
NAME                READY   STATUS    RESTARTS   AGE     ENV
nginx-with-labels   1/1     Running   0          2d22h   testing
test-secret         1/1     Running   0          3d

再例如,显示标签键名env的值不为qa,且标签名tier的值为frontend的所有Pod对象:

# kubectl get pods -l "env!=qa,tier=frontend" -L env,tier
NAME                READY   STATUS    RESTARTS   AGE     ENV       TIER
nginx-with-labels   1/1     Running   0          3d23h   testing   frontend

2.2.2 集合关系

基于集合关系的标签选择器支持in、notin和exists三种操作符,它们的使用格式:

  • KEY in (VALUE1,VALUE2,…): 指定的键名的值存在于给定的列表中即满足条件;
  • KEY notin (VALUE1,VALUE2,…): 指定的键名的值不存在于给定的列表中即满足条件;
  • KEY: 所有存在此键名标签的资源;
  • !KEY: 所有不存在此键名标签的资源;

例如:显示标签键名env的值为testing或dev的所有Pod对象:

# kubectl get pods -l "env in (testing,dev)" -L env
NAME                READY   STATUS    RESTARTS   AGE   ENV
nginx-with-labels   1/1     Running   0          4d    testing

再如,列出标签键名env的值为testing或dev,且不存在键名为tier的标签的所有Pod对象:

# kubectl get pods -l 'env in (testing,dev),!tier' -L env,tier

2.2.3 其它资源对象的标签选择器

此外,Kubernetes的诸多资源对象必须以标签选择器的方式关联到Pod资源对象,例如Service、Deployment和ReplicaSet类型的资源等,它们在spec字段中嵌套使用嵌套的”selector”字段,通过”matchLabels”来指定标签选择器,有的甚至还支持使用”matchExpressions”构造复杂的标签选择机制;

  • matchLabels: 通过直接给定键值对来指定标签选择器;
  • matchExpressions: 基于表达式指定的标签选择器列表,每个选择器都形如”{key:KEY_NAME,operator:OPERAOR,values:[VALUE1,VALUE2,…]}”,选择器列表为”逻辑与”关系;使用In或NotIn操作符时,其values不强制要求为非空的字符串列表,而使用Exists或DostNotExist时,其values必须为空。
selector:
  matchLabels:
    component: redis
  matchExpressions:
    - {key: tier, operator: In, value: [cache]}
    - {key: environment, operator: Exists, values:}

2.3 Pod节点选择器NodeSelector

Pod节点选择器是标签及标签选择器的一种应用,它能够让Pod对象基于集群中工作节点的标签来挑选倾向运行的目标节点。

比如仅有副本节点拥有被Pod对象依赖到的特殊硬件设备的情况,如GPU和SSD等。即便如此,用户也不应该静态指定Pod对象的运行位置,而是让scheduler基于标签和标签选择器为Pod挑选匹配的工作节点。

Pod对象的spec.nodeSelector可用于定义节点标签选择器,用户事先为特定部分的Node资源对象设定好标签,而后配置Pod对象通过节点标签选择器进行匹配检测,从而完成节点亲和性调度。

为Node资源对象附加标签的方法同Pod资源,使用kubectl label nodes/NODE命令即可。例如为node节点设置”disktype=ssd”标签以标识其拥有SSD设备:

# kubectl label nodes node disktype=ssd
node/node labeled

# kubectl get nodes -l "disktype" -L disktype
NAME   STATUS   ROLES    AGE   VERSION   DISKTYPE
node   Ready    <none>   14d   v1.18.8   ssd

Pod需要调度至具有SSD设备的节点上,如pod-with-nodeselector.yaml:

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-nodeselector
  labels:
    env: testing
  spec:
    containers:
    - name: test-nginx
      image: nginx
    nodeSelector:
      disktype: ssd

集群中的每个节点默认以及附带了多个标签,如 kubernetes.io/hostname、beta.kubernetes.io/os和beta.kubernetes.io/arch 等。这些标签也可以直接由nodeSelector使用,尤其是希望将Pod调度至特定节点时,可以使用kubernete.io/hostname直接绑定相应的主机即可。不过,这种绑定至特定主机的需求还有一种更为简单的实现方式,即使用spec.nodeName字段直接指定目标节点。

2.4 资源注解

除了标签(label)之外,Pod与其它各种资源还能使用资源注解(annotation)。与标签类似,注解也是键值类型的数据,不过它不能用于标签及挑选kubernetes对象,仅可用于为资源提供”元数据”信息。另外,注解中的元数据不受字符数量的限制,它可大可小,可以为结构化或非结构化形式,也支持使用在标签中禁止使用的其它字符。

annotations可在资源创建时使用”metadata.annotations”字段指定,也可随时按需在活动的资源上使用”kubectl annotate”命令进行附加。例如,为pod-example重新进行注解:

# kubectl annotate pods pod-example ilinux.io/created-by="cluster admin"
pod/nginx-with-labels annotated

# kubectl describe pods pod-example

清单中指定:

apiVersion: v1
kind: Pod
metadata:
  name: pod-example
  annotations:
    ilinux.io/created-by: cluster admin
spec:
  ...

3.Pod对象的生命周期

Pod对象自从其创建开始至其终止退出的时间范围称为其生命周期。在这段时间中,Pod会处于多种不通的状态:

  • 创建住容器(main container) — 必须的操作
  • 运行初始化容器(init container)
  • 容器启动后钩子(post start hook)
  • 容器的存活性探测(liveness probe)
  • 就绪性探测(readiness probe)
  • 容器终止前钩子(pre stop hook)

3.1 Pod的相位

无论是用户手动创建,还是通过Deployment等控制器创建,Pod对象总是应该处于其生命进程中以下几个相位(phase)之一:

  • Pending: API Server创建了Pod资源对象并已存入etcd中,但它尚未被调度完成,或者仍处于从仓库下载镜像的过程中;
  • Running: Pod已经被调度至某节点,并且所有容器都已经被kubelet创建完成;
  • Succeeded: Pod中的所有容器都已经成功终止并且不会被重启;
  • Failed: 所有容器都已经终止,但至少有一个容器终止失败,即容器返回了非0值的退出状态或已经被系统终止;
  • Unknown: API Server无法正常获取到Pod对象的状态信息,通常是由于其无法与所在工作节点的kubelet通信所致;

3.2 Pod生命周期中的重要行为

3.2.1 初始化容器

初始化容器(init container)即应用程序的主容器启动之前要运行的容器,常用于为主容器执行一些预置操作,它们具有两种典型特性:

  • 初始化容器必须运行完成直至结束,若某初始化容器运行失败,那么Kubernetes需要重启它直到成功完成;
  • 每个初始化容器都必须按定义的顺序串行运行;

初始化容器的典型应用需求具体包含如下:

  • 用于运行特定的工具程序,处于安全等方面的原因,这些程序不适于包含在主容器镜像中;
  • 提供主容器镜像中不具备的工具程序或自定义代码;
  • 为容器镜像的构建和部署人员提供了分离、独立工作的途径,使得他们不必协同起来制作单个镜像文件;
  • 初始化容器和主容器处于不通的文件系统视图中,因此可以分别安全地使用敏感数据,例如Secrets资源;
  • 初始化容器要先于应用容器串行启动并运行完成,因此可用于延后应用容器的启动直至其依赖的条件得到满足;

Pod资源的spec.initContainers字段以列表的形式定义可用的初始容器,其嵌套可用字段类似于spec.containers

一个简单示例:

apiVersion: v1
kind: Pod
metadata:
  name: pod-init-test
  labels:
    test: init
spec:
  containers:
  - name: myapp-container
    image: ikubernetes/myapp:v1
  initContainers:
  - name: init-test
    image: busybox
    command: ['sh','-c','sleep 10']

3.2.2 生命周期钩子函数

容器生命周期钩子使它能够感知其自身生命周期管理中的事件,并在相应的时刻到来时运行由用户指定的处理程序代码。

  • postStart: 于容器创建完成之后立即运行的钩子处理器(handler),不过Kubernetes无法确保它一定会于容器中的ENTRYPOINT之前运行;
  • preStop: 于容器终止操作之前立即运行的钩子处理器,它以同步的方式调用,因此在其完成之前会阻塞删除容器的操作的调用;

钩子处理器的实现方式有”Exec”和”HTTP”两种,前一种在钩子事件触发时直接在当前容器中运行由用户定义的命令,后一种则是在当前容器中向某URL发起HTTP请求;

postStart和preStop处理器定义在容器的spec.containers.lifecycle嵌套字段中,示例:

apiVersion: v1
kind: Pod
metadata:
  name: lifycycle-test
spec:
  containers:
  - name: lifecycle-test-container
    image: ikubernetes/myapp:v1
    lifecycle:
      postStart:
        exec:
          command: [/bin/sh,-c,echo 'lifecycle hooks handler'> /usr/share/nginx/html/test.html]
# kubectl apply -f pod-lifecycle-test.yml
pod/lifycycle-test created

# kubectl get pods
NAME                READY   STATUS    RESTARTS   AGE
lifycycle-test      1/1     Running   0          5s

# kubectl get pods -o wide
NAME                READY   STATUS    RESTARTS   AGE     IP               NODE   NOMINATED NODE   READINESS GATES
lifycycle-test      1/1     Running   0          11s     10.244.167.149   node   <none>           <none>

# curl 10.244.167.149/test.html
lifecycle hooks handler

3.2.3 容器探测

容器探测(container probe)是kubelet对容器周期性执行的健康状态检测,诊断操作由容器的处理器(handler)进行定义;

  • ExecAction: 在容器中执行一个命令,并根据其返回的状态码进行诊断,状态码为0表示成功,否则即为不健康状态;
  • TCPSocketAction: 通过与容器的某TCP端口尝试建立连接进行诊断,端口能够成功打开即为正常,否则为不健康状态;
  • HTTPGetAction: 通过向容器IP地址的某指定端口的指定path发起HTTP GET请求进行诊断,响应码为2xx或3xx时即为成功,否则为失败;

kubelet可在活动容器上执行两种类型的检测:

  • 存活性检测(livenessProbe): 用于判定容器是否处于Running状态;一旦此类检测未通过,kubelet将杀死容器并根据其restartPolicy决定是否将其重启;未定义存活性检测的容器的默认状态为”Success”;
  • 就绪性检测(readinessProbe): 用于判断容器是否就绪并可对外提供服务;未通过检测的容器意味着其尚未准备就绪,端点控制器(如Service)会将其IP从所有匹配到此Pod对象的Service对象的端点列表中移除;检测通过之后,会再次将其IP添加至端点列表中;

3.2.4 设置exec探针

spec.containers.livenessProbe.exec字段用于定义此类检测,它只有一个可用属性”command”,用于指定要执行的命令。

示例:

apiVersion: v1
kind: Pod
metadata:
  name: liveness-exec
  labels:
    test: liveness-exec
spec:
  containers:
  - name: liveness-exec-test
    image: busybox
    args: [/bin/sh,-c,"touch /tmp/healthy; sleep 60; rm -rf /tmp/healthy; sleep 600"]
    livenessProbe:
      exec:
        command: [test, -e, /tmp/healthy]

在60秒之内使用kubectl describe pods liveness-exec查看其详细信息,其存活性探测不会出现错误。而超过60秒之后,再次运行查看可以发现,存活性探测出现了故障,并且隔更长一段时间之后再查看甚至还可以看到容器重启的相关信息:

Events:
  Type     Reason     Age                  From               Message
  ----     ------     ----                 ----               -------
  Normal   Scheduled  2m49s                default-scheduler  Successfully assigned default/liveness-exec to node
  Warning  Unhealthy  72s (x3 over 92s)    kubelet, node      Liveness probe failed:
  Normal   Killing    72s                  kubelet, node      Container liveness-exec-test failed liveness probe, will be restarted
  Normal   Pulling    42s (x2 over 2m49s)  kubelet, node      Pulling image "busybox"
  Normal   Pulled     32s (x2 over 2m33s)  kubelet, node      Successfully pulled image "busybox"
  Normal   Created    32s (x2 over 2m33s)  kubelet, node      Created container liveness-exec-test
  Normal   Started    32s (x2 over 2m33s)  kubelet, node      Started container liveness-exec-test

另外,输出信息的Containers一段中还清晰显示了容器监控检测及状态变化的相关信息:容器当前处于Running,但前一次为Terminated,原因是退出码为137的错误信息,它表示进程是被外部信号所终止的。137事实上是由两部分数字之和生成的:128+signum,其中signum是导致进程终止的信号的数字标识,9表示SIGKILL,这意味着进程是被强制终止的:

Containers:
  liveness-exec-test:
    Container ID:  docker://e3a150ffaa02869c69c6cba93ba3b6cd5da050ae68e67caea836f91dfc2dd55a
    Image:         busybox
    Image ID:      docker-pullable://busybox@sha256:9f1c79411e054199210b4d489ae600a061595967adb643cd923f8515ad8123d2
    Port:          <none>
    Host Port:     <none>
    Args:
      /bin/sh
      -c
      touch /tmp/healthy; sleep 60; rm -rf /tmp/healthy; sleep 600
    State:          Running
      Started:      Wed, 25 Nov 2020 17:07:32 +0800
    Last State:     Terminated
      Reason:       Error
      Exit Code:    137
      Started:      Wed, 25 Nov 2020 17:05:31 +0800
      Finished:     Wed, 25 Nov 2020 17:07:22 +0800
    Ready:          True
    Restart Count:  1
    Liveness:       exec [test -e /tmp/healthy] delay=0s timeout=1s period=10s #success=1 #failure=3

需要特别说明的是,exec指定的命令运行于容器中,会消耗容器的可用资源配额,另外,考虑到探测操作的效率本身等因素,探测操作的命令应该尽可能简单和轻量;

3.2.5 设置HTTP探针

基于HTTP的探测向目标容器发起一个HTTP请求,根据其响应码进行结果判定,响应码形如2xx或3xx表示检测通过。spec.containers.livenessProbe.httpGet字段用于定义此类检测:

  • host : 请求的主机地址,默认为Pod IP,也可以在httpHeaders中使用”Host:”来定义;
  • port : 请求的端口,必选字段;
  • httpHeaders <[]Object>: 自定义的请求报文首部;
  • path : 请求的HTTP资源路径,即 URL path;
  • scheme: 建立连接使用的协议,仅可为HTTP或HTTPS,默认为HTTP;

示例:

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-http
spec:
  containers:
  - name: liveness-http-test
    image: nginx
    ports:
    - name: http
      containerPort: 80
    lifecycle:
      postStart:
        exec:
          command: [bin/sh,-c,"echo Healthy > /usr/share/nginx/html/healthz"]
    livenessProbe:
      httpGet:
        path: /healthz
        port: http
        scheme: HTTP

使用 kubectl exec 删除pod的测试页面healthz:

# kubectl exec liveness-http -- rm /usr/share/nginx/html/healthz

查看其详细的状态信息,可以看到探针探测失败,容器被杀掉后重新创建:

Events:
  Type     Reason     Age                  From               Message
  ----     ------     ----                 ----               -------
  Normal   Scheduled  3m24s                default-scheduler  Successfully assigned default/liveness-http to node
  Normal   Pulling    83s (x2 over 3m23s)  kubelet, node      Pulling image "nginx"
  Warning  Unhealthy  83s (x3 over 103s)   kubelet, node      Liveness probe failed: HTTP probe failed with statuscode: 404
  Normal   Killing    83s                  kubelet, node      Container liveness-http-test failed liveness probe, will be restarted
  Normal   Pulled     65s (x2 over 3m6s)   kubelet, node      Successfully pulled image "nginx"
  Normal   Created    65s (x2 over 3m6s)   kubelet, node      Created container liveness-http-test
  Normal   Started    65s (x2 over 3m6s)   kubelet, node      Started container liveness-http-test

需要注意的是,这种检测方式仅对分层架构中的当前一层有效,例如,它能检测应用程序工作正常与否的状态,但重启操作却无法解决其后端服务(如数据库或缓存服务)导致的故障;

3.2.6 设置TCP探针

基于TCP的存活性探测用于向容器的特定端口发起TCP请求并尝试建立连接进行结果判定,连接建立成功即为通过检测。相比较来说,它比基于HTTP的探测要更高效、更节约资源,但精准度略低,毕竟连接建立成功未必意味着页面资源可用。spec.containers.livenessProbe.tcpSocket字段用于定义此类检测,它有两个可用属性:

  • host : 请求连接的目标IP地址,默认为Pod IP;
  • port : 请求连接的目标端口,必选字段;

示例:

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-tcp
spec:
  containers:
  - name: liveness-tcp-test
    image: nginx
    ports:
    - name: http
      containerPort: 80
    livenessProbe:
      tcpSocket:
        port: http

3.2.7 存活性探测行为属性

使用kubectl describe会得到类似如下一行的内容:

Liveness:       exec [test -e /tmp/healthy] delay=0s timeout=1s period=10s #success=1 #failure=3

它给出了探测方式及其额外的配置属性delay、timeout、period、success和failure及其各自的相关属性值。用户没有明确定义这些属性字段时,它们会使用各自的默认值,例如上面显示出的设定。这些属性信息可通过spec.containers.livenessProbe

  • initialDelaySeconds : 存活性探测延迟时长,即容器启动多久之后再开始第一次探测操作,显示为delay属性;默认为0秒,即容器启动后立刻便开始进行探测;
  • timeoutSeconds : 存活性探测的超时时长,显示为timeout属性,默认为1s,最小值也为1s;
  • periodSeconds : 存活性探测的频度,显示为period属性,默认为10s,最小值为1s;过高的频率会对Pod对象带来较大的额外开销,而过低的频率又会使得对错误的反应不及时;
  • successThreshold : 处于失败状态时,探测操作至少连续多少次的成功才被认为是通过检测,显示为#success属性,默认值为1,最小值为1;
  • failureThreshold: 处于成功状态时,探测操作至少连续多少次的失败才被认为是检测不通过,显示为#failure属性,默认值为3,最小值为1;

例如:

spec:
  containers:
  ...
    livenessProbe:
      exec:
        command: [test, -e, /tmp/healthy]
      initialDelaySeconds: 5
      periodSeconds: 5

3.2.7 容器的重启策略

Pod资源的spec.restartPolicy字段定义了Pod的重启策略

  • Always: 但凡Pod对象终止就将其重启,此为默认设定;
  • OnFailure: 仅在Pod对象出现错误时方才将其重启;
  • Never:从不重启;

首次需要重启的容器,将在其需要时立即进行重启,随后再次需要重启的操作将由kubelet延迟一段时间后进行,且反复的重启操作的延迟时长依次为10秒、20秒、40秒、80秒、160秒和300秒,300秒是最大延迟时长。

4. 资源需求及资源限制

在Kubernetes上,可由容器或Pod请求或消费的计算资源是指CPU和内存,GPU资源处于实验阶段,需使用额外的插件来支持,详情可见官网-GPU使用

目前来说,资源隔离尚且属于容器级别,CPU和内存资源的配置需要在Pod中的容器上进行,每种资源均可由requests属性定义其请求的确保可用值,即容器运行可能用不到这些额度的资源,但用到时必须要确保有如此多的资源可用,而limit属性则用于限制资源可用的最大值,即硬限制;

在k8s系统上,1个单位的CPU相当于虚拟机上1颗虚拟CPU(vCPU)或物理机上的一个超线程(Hyperthread,或称为一个逻辑CPU),它支持分数计量方式,500m相当于是0.5个核心。内存的计量方式与日常使用方式相同,默认单位是字节,也可以使用E、P、T、G、M和K作为单位后缀,或Ei、Pi、Ti、Gi、Mi和Ki形式的单位后缀。

4.1 资源需求

下面的示例中,Pod要求为stress容器确保128Mi的内存及五分之一个CPU核心(200m)资源可用:

apiVersion: v1
kind: Pod 
metadata:
  name: stress-pod
spec:
  containers:
  - name: stress
    image: ikubernetes/stress-ng
    command: ["/usr/bin/stress-ng","-m 1","-c 1","-metrics-brief"]
    resources:
      requests:
        memory: "128Mi"
        cpu: "200m"

对于压缩型的资源CPU来说,未定义其请求用量以确保其最小的可用资源时,它可能会被其他的Pod资源压缩至极低的水平,甚至会达到Pod不能被调度运行的境地。而对于非压缩型资源来说,内存资源在任何原因导致的紧缺情形下都有可能导致相关的进程被杀死。因此,在k8s系统上运行关键型业务相关的Pod时必须使用requests属性为容器定义资源的确保可用量。

而对于一个节点的资源来说,每运行一个Pod对象,其requests中定义的请求量都要被预留,直到被所有Pod对象瓜分完毕为止。

4.2 资源限制

容器通过limits属性为容器定义资源的最大可用量,资源分配时,可压缩型资源CPU的控制阀可自由调节,容器进程无法获得超出其CPU配额的可用时间。不过,如果进程申请分配超出其limits属性定义的硬限制的内存资源时,它将被OOM杀掉,不过,随后可能会被其控制进程所重启,例如,容器进程的Pod对象会被杀死并重启(重启策略为Always或OnFailure时),或者是容器进程的子进程被其父进程所重启。

下面示例,模拟内存泄露操作不断地申请使用内存资源,直到超出limits属性中memory字段设定的值而导致“OOM”为止:

apiVersion: v1
kind: Pod
metadata:
  name: memleak-pod
  labels:
    app: memleak
spec:
  containers:
  - name: simmemleak
    image: saadali/simmemleak
    resources:
      requests:
        memory: "64Mi"
        cpu: "1"
      limits:
        memory: "64Mi"
        cpu: "1"

limits不会影响Pod的调度结果,也就是说,一个节点上的所有Pod对象的limits数量之和可以大于节点所拥有的资源量,即支持资源的过载使用(overcommitted)。不过,这么一来一旦资源耗尽,尤其是内存资源耗尽,则必然会有容器因OOMKilled而终止。

另外需要说明的是,k8s仅会确保Pod能够获得它们请求(requests)的CPU时间额度,它们能否获得额外(throttled)的CPU时间,则取决于其他正在运行的作业对CPU资源的占用情况。例如,对于总数为1000m的cpu资源来说,容器A请求使用200m,容器B请求使用500m,在不超出它们各自的最大限额的前提下,余下的300m在双方都需要时会以2:5的方式进行配置。

4.3 容器的可见资源

容器中运行top等命令观察资源可用量信息时,即便定义了requests和limits属性,虽然其可用资源受限于此两个属性中的定义,但容器中可见的资源依然时节点级别的可用总量。

颇具代表性的场景是于Pod中运行的nginx应用,在配置参数worker_processes的值为auto时,主进程会创建与Pod中能够访问到的CPU核心数相同数量的worker进程。若Pod的实际可用CPU核心远低于主机级别的数量时,那么这种设置在较大的并发访问负荷下会导致严重的资源竞争,并将带来更多的内存资源消耗。一个较为妥当的解决方案是使用Downward API将limits定义的资源量暴露给容器。

4.4 Pod的服务质量类别

k8s允许节点资源对limits的过载使用,这意味着节点无法同时满足其上的所有Pod对象以资源满载的方式运行,在内存资源紧缺时,k8s需要借助于Pod对象的优先级来判断以何种次序先后终止哪些Pod对象。根据Pod对象的requests和limits属性,k8s将Pod对象归类到BestEffort、Burstable和Guaranteed三个服务质量(Quality of Service, QoS);

  • Guaranteed: 每个容器都为CPU资源和内存资源设置了具有相同值的requests和limits属性的Pod资源会自动归属于此类别,这类Pod资源具有最高优先级;
  • Burstable: 至少有一个容器设置了CPU或内存资源的requests属性的Pod资源自动归属于此类别,它们具有中等优先级;
  • BestEffort: 未为任何一个容器设置requests和limits属性的Pod资源将自动归属于此类别,它们的优先级为最低级;

内存资源紧缺时,BestEffort类别的容器将被首当其冲地被终止,因为系统不为其提供任何级别的资源保证,但换来的好处是,它们能够在可用时做到尽可能多地占用资源。若已然不存任何BestEffort类别的容器,则接下来是有着中等优先级的burstable类别的Pod被终止。Guaranteed类别的容器拥有最高优先级,它们不会被杀死,除非其内存资源需求超限,或者OOM时没有其他更低优先级的Pod资源存在。

小结

本篇主要介绍了Pod资源的基础概念、分布式系统的设计模式、Pod的基础管理操作、如何定义和管理容器、资源标签和标签选择器、资源注解等,讲解了Pod生命周期中的事件、容器的存活性探测和就绪性探测机制等话题。

  • Pod就是联系紧密的一组容器,它们共享Network、UTS和IPC名称空间及存储卷资源;
  • K8s资源对象的管理操作基本上是由增、删、查和改等操作组成,并且支持陈述式命令、陈述式对象配置和声明式对象配置三种管理方式;
  • Pod的核心目标在于运行容器,容器的定制配置常见的包括暴露端口及传递环境变量等;
  • 标签是附加在K8s系统上的键值类型的元数据,而标签选择器是基本等值或集合关系的标签过滤机制;注解类似于标签,但不能被用于标签选择器;
  • Pod的生命周期中可能存在多种类型的操作,但运行主容器是其核心任务;
  • 存活性探测及就绪性探测是辅助判定容器状态的重要工具;
  • 资源需求及资源限制是管理Pod对象系统资源分配的有效方式。