大家好,我是腾意。

Pod是Kubernetes系统的基础单元,是资源对象模型中可由用户创建或部署的最小组件,也是在Kubernetes系统上运行容器化应用的资源对象。其他的大多数资源对象都是用于支撑和扩展Pod对象功能的,例如,用于管控Pod运行的StatefulSet和Deployment等控制器对象,用于暴露Pod应用的Service和Ingress对象,为Pod提供存储的PersistentVolume存储资源对象等。这些资源对象大体可分为有限的几个类别,并且可基于资源清单作为资源配置文件进行陈述式或声明式管理。本篇将描述这些类别,并详细介绍Pod资源的基础应用。

1.1 容器与Pod资源对象

现代的容器技术被设计用来运行单个进程(包括子进程)时,该进程在容器中PID名称空间中的进程号为1,可直接接收并处理信号,于是,在此进程终止时,容器即终止退出。若要在一个容器内运行多个进程,则需要为这些进程提供一个类似于Linux操作系统init进程的管控类进程,以树状结构完成多进程的生命周期管理,例如,崩溃后回收相应的系统资源等。单容器运行多进程时,通常还需要日志进程来管理这些进程的日志,例如,将它们分别保存于不同的目标日志文件等,否则用户就不得不手动来分拣日志信息。因此,绝大多数场景中都应该于一个容器中仅运行一个进程,它将日志信息直接输出至容器的标准输出,支持用户直接使用命令(kubectl logs)进行获取,这也是Docker及Kubernetes使用容器的标准方式。

不过,分别运行于各自容器的进程之间无法实现基于IPC的通信机制,此时,容器间的隔离机制对于依赖于此类通信方式的进程来说却又成了阻碍。Pod资源抽象正是用来解决此类问题的组件,前文已然多次提到,Pod对象是一组容器的集合,这些容器共享Network、UTS及IPC名称空间,因此具有相同的域名、主机名和网络接口,并可通过IPC直接通信。为一个Pod对象中的各容器提供网络名称空间等共享机制的是底层基础容器pause,图4-1所示为一个由三个容器组成的Pod资源,各容器共享Network、IPC和UTS名称空间,但分别拥有各自的MNT、USR和PID名称空间。需要特别强调的是,一个Pod对象中的多个容器必须运行于同一工作节点之上。

img

图4-1  Pod内的容器共享Network、IPC和UTS名称空间

尽管可以将Pod类比为物理机或VM,但一个Pod内通常仅应该运行一个应用,除非多个进程之间具有密切的关系。这也意味着,实践中应该将多个应用分别构建到多个而非单个Pod中,这样也更能符合容器的轻量化设计、运行之目的。

例如,一个有着前端(application server)和后端(database server)的应用,其前、后端应该分别组织于各自的Pod中,而非同一Pod的不同容器中。这样做的好处在于,多个Pod可被调度至多个不同的主机运行,提高了资源利用率。另外,Pod也是Kubernetes进行系统规模伸缩的基础单元,分别运行于不同Pod的多个应用可独立按需进行规模变动,这就增强了系统架构的灵活性。事实上,前、后端应用的规模需求通常不会相同,而且无状态应用(application server)的规模变动也比有状态应用(database server)容易得多,将它们组织于同一Pod中时将无法享受这种便利。

不过,有些场景要求必须于同一Pod中同时运行多个容器。此时,这些分布式应用(尤其是微服务架构中的多个服务)必须遵循某些最佳实践机制或基本准则。事实上,Kubernetes并非期望成为一个管理系统,而是一个支持这些最佳实践的向开发人员或管理人员提供更高级别服务的系统。分布式系统设计通常包含以下几种模型。

1)Sidecar pattern(边车模型或跨斗模型):即为Pod的主应用容器提供协同的辅助应用容器,每个应用独立运行,最为典型的代表是将主应用容器中的日志使用agent收集至日志服务器中时,可以将agent运行为辅助应用容器,即sidecar。另一个典型的应用是为主应用容器中的database server启用本地缓存,如图4-2所示。

img

图4-2 Sidecar pattern

2)Ambassador pattern(大使模型):即为远程服务创建一个本地代理,代理应用运行于容器中,主容器中的应用通过代理容器访问远程服务,如图4-3所示。一个典型的使用示例是主应用容器中的进程访问“一主多从”模型的远程Redis应用时,可在当前Pod容器中为Redis服务创建一个Ambassador container,主应用容器中的进程直接通过localhost接口访问Ambassador container即可。即便是Redis主从集群架构发生变动时,也仅需要将Ambassador container加以修改即可,主应用容器无须对此做出任何反应。

img

图4-3 Ambassador pattern

3)Adapter pattern(适配器模型):此种模型一般用于将主应用容器中的内容进行标准化输出,例如,日志数据或指标数据的输出,这有助于调用者统一接收数据的接口,如图4-4所示。另外,某应用滚动升级后的版本不兼容旧的版本时,其报告信息的格式也存在不兼容的可能性,使用Adapter container有助于避免那些调用此报告数据的应用发生错误。

img

图4-4 Adapter pattern

Kubernetes系统的Pod资源对象用于运行单个容器化应用,此应用称为Pod对象的主容器(main container),同时Pod也能容纳多个容器,不过额外的容器一般工作为Sidecar模型,用于辅助主容器完成工作职能。

1.2 管理Pod对象的容器

一个Pod对象中至少要存在一个容器,因此,containers字段是定义Pod时其嵌套字段Spec中的必选项,用于为Pod指定要创建的容器列表。进行容器配置时,name为必选字段,用于指定容器名称,image字段是为可选,以方便更高级别的管理类资源(如Deployment)等能覆盖此字段,于是自主式的Pod并不可省略此字段。因此,定义一个容器的基础框架如下:


name: CONTAINER_NAME
image: IMAGE_FILE_NAME

此外,定义容器时还有一些其他常用的字段,例如,定义要暴露的端口、改变镜像运行的默认程序、传递环境变量、定义可用的系统资源配额等。

1.2.1 镜像及其获取策略

各工作节点负责运行Pod对象,而Pod的核心功用在于运行容器,因此工作节点上必须配置容器运行引擎,如Docker等。启动容器时,容器引擎将首先于本地查找指定的镜像文件,不存在的镜像则需要从指定的镜像仓库(Registry)下载至本地,如图4-5所示。

img

图4-5 Docker及其Registry

Kubernetes系统支持用户自定义镜像文件的获取策略,例如在网络资源较为紧张时可以禁止从仓库中获取镜像文件等。容器的“imagePullPolicy”字段用于为其指定镜像获取策略,它的可用值包括如下几个。

·Always:镜像标签为“latest”或镜像不存在时总是从指定的仓库中获取镜像。

·IfNotPresent:仅当本地镜像缺失时方才从目标仓库下载镜像。

·Never:禁止从仓库下载镜像,即仅使用本地镜像。

下面的资源清单中的容器定义了如何使用nginx:latest镜像,其获取策略为Always,这意味着每次启动容器时,它都会到镜像仓库中获取最新版本的镜像文件:


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

对于标签为“latest”的镜像文件,其默认的镜像获取策略即为“Always”,而对于其他标签的镜像,其默认策略则为“IfNotPresent”。需要注意的是,使用私有仓库中的镜像时通常需要由Registry服务器完成认证后才能进行。认证过程要么需要在相关节点上交互式执行docker login命令来进行,要么就是将认证信息定义为专有的Secret资源,并配置Pod通过“imagePullSecretes”字段调用此认证信息完成。

1.2.2 暴露端口

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

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

·containerPort :必选字段,指定在Pod对象的IP地址上暴露的容器端口,有效范围为(0,65536);使用时,应该总是指定容器应用正常监听着的端口。

·name :当前端口的名称,必须符合IANA_SVC_NAME规范且在当前Pod内必须是唯一的;此端口名可被Service资源调用。

·protocol:端口相关的协议,其值仅可为TCP或UDP,默认为TCP。

提示 可以通过“kubectl explain pods.spec.containers.ports”获取ports对象可用的字段列表。

下面的资源配置清单示例(pod-example-with-port.yaml)中定义的pod-example指定了要暴露容器上TCP的80端口,并将之命名为http:


apiVersion: v1
kind: Pod
metadata:
  name: pod-example
spec:
  containers:
  - name: myapp
    image: ikubernetes/myapp:v1
    ports
    - name: http
      containerPort: 80
       protocol: TCP

然而,Pod对象的IP地址仅在当前集群内可达,它们无法直接接收来自集群外部客户端的请求流量,尽管它们的服务可达性不受工作节点边界的约束,但依然受制于集群边界。一个简单的解决方案是通过其所在的工作节点的IP地址和端口将其暴露到集群外部,如图4-6所示。

img

图4-6 通过hostIP和hostPort暴露容器服务

·hostPort :主机端口,它将接收到的请求通过NAT机制转发至由containerPort字段指定的容器端口。

·hostIP :主机端口要绑定的主机IP,默认为0.0.0.0,即主机之上所有可用的IP地址;考虑到托管的Pod对象是由调度器调度运行的,工作节点的IP地址难以明确指定,因此此字段通常使用默认值。

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

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

由Docker镜像启动容器时运行的应用程序在相应的Dockerfile中由ENTRYPOINT指令进行定义,传递给程序的参数则通过CMD指令指定,ENTRYPOINT指令不存在时,CMD可用于同时指定程序及其参数。例如,在某工作节点上运行下面的命令获取ikubernetes/myapp:v1镜像中定义的CMD和ENTRYPOINT,命令如下:


~]$ docker inspect ikubernetes/myapp:v1 -f {{.Config.Cmd}}
[nginx -g daemon off;]
~]$ docker inspect ikubernetes/myapp:v1  -f {{.Config.Entrypoint}}
[]

容器的command字段能够指定不同于镜像默认运行的应用程序,并且可以同时使用args字段进行参数传递,它们将覆盖镜像中的默认定义。不过,如果仅为容器定义了args字段,那么它将作为参数传递给镜像中默认指定运行的应用程序;如果仅为容器定义了command字段,那么它将覆盖镜像中定义的程序及参数,并以无参数方式运行应用程序。例如下面的资源清单文件将镜像ikubernetes/myapp:v1的默认应用程序修改为了“/bin/sh”,传递应用的参数修改为了“-c while true;do sleep 30;done”:


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

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

1.2.4 环境变量

非容器化的传统管理方式中,复杂应用程序的配置信息多数由配置文件进行指定,用户可借助于简单的文本编辑器完成配置管理。然而,对于容器隔离出的环境中的应用程序,用户就不得不穿透容器边界在容器内进行配置编辑并进行重载,这种方式复杂且低效。于是,由环境变量在容器启动时传递配置信息就成为一种备受青睐的方式。

注意 这种方式依赖于应用程序支持通过环境变量进行配置的能力,否则,用户在制作Docker镜像时需要通过entrypoint脚本完成环境变量到程序配置文件的同步。

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

通过环境变量配置容器化应用时,需要在容器配置段中嵌套使用env字段,它的值是一个由环境变量构成的列表。环境变量通常由name和value字段构成。

·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: ikubernetes/filebeat:5.6.5-alpine
    env:
    - name: REDIS_HOST
      value: db.ilinux.io:6379
    - name: LOG_LEVEL
      value: info

这些环境变量可直接注入容器的shell环境中,无论它们是否真正被用到,使用printenv一类的命令都能在容器中获取到所有环境变量的列表。

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

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

img

图4-7 Pod对象的网络名称空间

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


apiVersion: v1
kind: Pod
metadata:
  name: pod-use-hostnetwork
spec:
  containers:
  - name: myapp
    image: ikubernetes/myapp:v1
  hostNetwork: true

将上面的配置清单保存于配置文件中,如pod-use-hostnetwork.yaml,将其创建于集群上,并查看其网络接口的相关属性信息以验证它是否能共享使用工作节点的网络名称空间:


~]$ kubectl apply -f pod-use-hostnetwork.yaml
~]$ kubectl exec -it pod-use-hostnetwork -- sh
/ # ifconfig
eth0      Link encap:Ethernet  HWaddr 00:16:3E:08:1B:F4
          inet addr:172.16.0.68  Bcast:172.31.143.255  Mask:255.255.240.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1

如上述命令的结果显示所示,它打印出的是工作节点的网络设备及其相关的接口信息。这就意味着,Pod对象中运行的容器化应用也将监听于其所在的工作节点的IP地址之上,这可以通过直接向node03.ilinux.io节点发起请求来验证:


~]$ curl node03.ilinux.io
Hello MyApp | Version: v1 | <a href="hostname.html">Pod Name</a>

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

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

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

·基于用户ID(UID)和组ID(GID)控制访问对象(如文件)时的权限。

·以特权或非特权的方式运行。

·通过Linux Capabilities为其提供部分特权。

·基于Seccomp过滤进程的系统调用。

·基于SELinux的安全标签。

·是否能够进行权限升级。

Pod对象的安全上下文定义在spec.securityContext字段中,而容器的安全上下文则定义在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 86400"]
    securityContext:
      runAsNonRoot: true
      runAsUser: 1000
      allowPrivilegeEscalation: false

将上面的配置清单保存于配置文件(如pod-with-securitycontext.yaml文件)中,而后创建于集群中即可验证容器进程的运行者身份:


$ kubectl apply -f pod-with-securitycontext.yaml
$ kubectl exec pod-with-securitycontext -- ps aux
PID   USER     TIME  COMMAND
    1 1000      0:00 sleep 86400
   14 1000      0:00 ps aux

另外,可设置的安全上下文属性还有fsGroup、seLinuxOptions、supplementalGroups、sysctls、capabilities和privileged等,且Pod和容器各自支持的字段也有所不同,感兴趣的读者可按需对各属性进行测试。

1.3 标签与标签选择器

实践中,随着同类型资源对象的数量越来越多,分类管理也变得越来越有必要:基于简单且直接的标准将资源对象划分为多个较小的分组,无论是对开发人员还是对系统工程师来说,都能提升管理效率,这也正是Kubernetes标签(Label)的核心功能之一。对于附带标签的资源对象,可使用标签选择器(Label Selector)挑选出符合过滤条件的资源以完成所需要的操作,如关联、查看和删除等。

1.3.1 标签概述

标签是Kubernetes极具特色的功能之一,它能够附加于Kubernetes的任何资源对象之上。简单来说,标签就是“键值”类型的数据,它们可于资源创建时直接指定,也可随时按需添加于活动对象中,而后即可由标签选择器进行匹配度检查从而完成资源挑选。一个对象可拥有不止一个标签,而同一个标签也可被添加至多个资源之上。

实践中,可以为资源附加多个不同纬度的标签以实现灵活的资源分组管理功能,例如,版本标签、环境标签、分层架构标签等,用于交叉标识同一个资源所属的不同版本、环境及架构层级等,如图4-8所示。下面是较为常用的标签。

·版本标签:"release":"stable","release":"canary","release":"beta"。

·环境标签:"environment":"dev","environment":"qa","environment":"production"。

·应用标签:"app":"ui","app":"as","app":"pc","app":"sc"。

·架构层级标签:"tier":"frontend","tier":"backend","tier":"cache"。

·分区标签:"partition":"customerA","partition":"customerB"。

·品控级别标签:"track":"daily","track":"weekly"。

img

图4-8 多维度标签使用示例(图片来源:《Kubernetes in action》)

标签中的键名称通常由键前缀和键名组成,其中键前缀可选,其格式形如“KEY_PREFIX/KEY_NAME”。键名至多能使用63个字符,可使用字母、数字、连接号(-)、下划线(_)、点号(.)等字符,并且只能以字母或数字开头。键前缀必须为DNS子域名格式,且不能超过253个字符。省略键前缀时,键将被视为用户的私有数据,不过由Kubernetes系统组件或第三方组件自动为用户资源添加的键必须使用键前缀,而“kubernetes.io/”前缀则预留给Kubernetes的核心组件使用。

标签中的键值必须不能多于63个字符,它要么为空,要么是以字母或数字开头及结尾,且中间仅使用了字母、数字、连接号(-)、下划线(_)或点号(.)等字符的数据。

提示 实践中,建议键名及键值能做到“见名知义”,且尽可能保持简单。

1.3.2 管理资源标签

创建资源时,可直接在其metadata中嵌套使用“labels”字段以定义要附加的标签项。例如,下面的Pod资源清单文件示例pod-with-labels.yaml中使用了两个标签env=qa和tier=frontend:


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

基于此资源清单创建出定义的Pod对象之后,即可在“kubectl get pods”命令中使用“--show-labels”选项,以额外显示对象的标签信息:


~]$ kubectl apply -f pod-with-labels.yaml
pod "pod-with-labels" created
~]$ kubectl get pods --show-labels
NAME             READY     STATUS    RESTARTS   AGE     LABELS
pod-example      1/1       Running   0          3h      <none>
pod-with-labels  1/1       Running   0          13s     env=qa,tier=frontend

标签较多时,在“kubectl get pods”命令上使用“-L key1,key2,…”选项可指定显示有着特定键的标签信息。例如,仅显示各pods之上的以env和tier为键名的标签:


~]$ kubectl get pods -L env,tier
NAME             READY     STATUS    RESTARTS   AGE     ENV      TIER
pod-example      1/1       Running   2          3h
pod-with-labels  1/1       Running   0          5m      qa       frontend

“kubectl label”命令可以直接管理活动对象的标签,以按需进行添加或修改等操作。例如,为pod-example添加env=production标签:


~]$ kubectl label pods/pod-example env=production
pod "pod-example" labeled

不过,对于已经附带了指定键名的标签,使用“kubectl label”为其设定新的键值时需要为命令同时使用“--overwrite”命令以强制覆盖原有的键值。例如,将pod-with-labels的env的值修改为“testing”:


~]$ kubectl label pods/pod-with-labels env=testing --overwrite
pod "pod-with-labels" labeled

用户若期望对某标签之下的资源集合执行某类操作,例如,查看或删除等,则需要先使用“标签选择器”挑选出满足条件的资源对象。

1.3.3 标签选择器

标签选择器用于表达标签的查询条件或选择标准,Kubernetes API目前支持两个选择器:基于等值关系(equality-based)以及基于集合关系(set-based)。例如,env=production和env!=qa是基于等值关系的选择器,而tier in(frontend,backend)则是基于集合关系的选择器。另外,使用标签选择器时还将遵循以下逻辑。

1)同时指定的多个选择器之间的逻辑关系为“与”操作。

2)使用空值的标签选择器意味着每个资源对象都将被选中。

3)空的标签选择器将无法选出任何资源。

基于等值关系的标签选择器的可用操作符有“=”“==”和“!=”三种,其中前两个意义相同,都表示“等值”关系,最后一个表示“不等”关系。“kubectl get”命令的“-l”选项能够指定使用标签选择器,例如,显示键名env的值不为qa的所有Pod对象:


~]$ kubectl get pods -l "env!=qa" -L env
NAME             READY     STATUS    RESTARTS   AGE   ENV
pod-example      1/1       Running   2          4h    production
pod-with-labels  1/1       Running   0          40m   testing

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


~]$ kubectl get pods -l "env!=qa,tier=frontend" -L env,tier
NAME             READY     STATUS    RESTARTS   AGE   ENV       TIER
pod-with-labels  1/1       Running   0          43m   testing   frontend

基于集合关系的标签选择器支持in、notin和exists三种操作符,它们的使用格式及意义具体如下。

·KEY in(VALUE1,VALUE2,…):指定的键名的值存在于给定的列表中即满足条件。

·KEY notin(VALUE1,VALUE2,…):指定的键名的值不存在于给定的列表中即满足条件。

·KEY:所有存在此键名标签的资源。

·!KEY:所有不存在此键名标签的资源。

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


~]$ kubectl get pods -l "env in (production,dev)" -L env
NAME          READY     STATUS    RESTARTS   AGE       ENV
pod-example   1/1       Running   2          4h        production

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


~]$ kubectl get pods -l 'env in (production,dev),!tier' -L env,tier
NAME     READY   STATUS  RESTARTS  AGE     ENV          TIER
pod-example  1/1     Running   2          4h        production

注意 为了避免shell解释器解析叹号(!),必须要为此类表达式使用单引号。

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

·matchLabels:通过直接给定键值对来指定标签选择器。

·matchExpressions:基于表达式指定的标签选择器列表,每个选择器都形如“{key:KEY_NAME,operator:OPERATOR,values:[VALUE1,VALUE2,…]}”,选择器列表间为“逻辑与”关系;使用In或NotIn操作符时,其values不强制要求为非空的字符串列表,而使用Exists或DostNotExist时,其values必须为空。

下面所示的资源清单片断是一个示例,它同时定义了两类标签选择器:


selector:
  matchLabels:
    component: redis
  matchExpressions:
    - {key: tier, operator: In, values: [cache]}
    - {key: environment, operator: Exists, values:}

标签赋予了Kubernetes灵活操作资源对象的能力,它也是Service和Deployment等核心资源类型得以实现的基本前提。

1.3.4 Pod节点选择器nodeSelector

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

Kubernetes的kube-scheduler守护进程负责在各工作节点中基于系统资源的可用性等标签挑选一个来运行待创建的Pod对象,默认的调度器是default-scheduler。Kubernetes可将所有工作节点上的各系统资源抽象成资源池统一分配使用,因此用户无须关心Pod对象的具体运行位置也能良好工作。不过,事情总有例外,比如仅有部分节点拥有被Pod对象依赖到的特殊硬件设备的情况,如GPU和SSD等。即便如此,用户也不应该静态指定Pod对象的运行位置,而是让scheduler基于标签和标签选择器为Pod挑选匹配的工作节点。

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

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


~]$ kubectl label nodes node01.ilinux.io disktype=ssd
node "node01.ilinux.io" labeled
~]$ kubectl label nodes node03.ilinux.io disktype=ssd
node "node03.ilinux.io" labeled

查看具有键名SSD的标签的Node资源:


~]$ kubectl get nodes -l 'disktype' -L disktype
NAME             STATUS  ROLES   AGE  VERSION   DISKTYPE
node01.ilinux.io  Ready     <none>    15d     v1.12.1    ssd
node03.ilinux.io  Ready     <none>    15d     v1.12.1    ssd

如果某Pod资源需要调度至这些具有SSD设备的节点之上,那么只需要为其使用spec.nodeSelector标签选择器即可,例如下面的资源清单文件pod-with-nodeselector.yaml示例:


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

将如上资源清单中定义的Pod资源创建于集群中,通过查看其运行的结点即可判定调度效果。

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

1.4 资源注解

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

资源注解可由用户手动添加,也可由工具程序自动附加并使用它们。在Kubernetes的新版本中(alpha或beta阶段)为某资源引入新字段时,常以注解的方式提供,以避免其增删等变动对用户带来困扰,一旦确定支持使用它们,这些新增字段就将再引入到资源中并淘汰相关的注解。另外,为资源添加注解也可让其他用户快速了解资源的相关信息,例如其创建者的身份等。以下为常用的场景案例。

·由声明式配置层(如apply命令)管理的字段:将这些字段定义为注解有助于识别由服务器或客户端设定的默认值、系统自动生成的字段以及由自动伸缩系统生成的字段。

·构建、发行或镜像相关的信息,例如,时间戳、发行ID、Git分支、PR号码、镜像哈希及仓库地址等。

·指向日志、监控、分析或审计仓库的指针。

·由客户端库或工具程序生成的用于调试目的的信息:如名称、版本、构建信息等。

·用户或工具程序的来源地信息,例如,来自其他生态系统组件的相关对象的url。

·轻量化滚动升级工具的元数据,如config及checkpoints。

·相关人员的电话号码等联系信息,或者指向类似信息的可寻址的目录条目,如网站站点。

1.4.1 查看资源注解

“kubectl get-o yaml”和“kubectl describe”命令均能显示资源的注解信息。例如下面的命令显示的pod-example的注解信息:


~]$ kubectl describe pods pod-example
Name:         pod-example
Namespace:    default
Node:         node02.ilinux.io/172.16.0.67
Start Time:   Mon, 26 Feb 2018 18:40:53 +0800
Labels:       env=production
Annotations:  kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"v
    1","kind":"Pod","metadata":{"annotations":{},"name":"pod-example","namespa
    ce":"default"},"spec":{"containers":[{"image":"ikubernetes/m..
Status:       Running
……

pod-example此前由声明式配置命令apply创建,因此它在注解中保存了如上的相关信息以便在下次资源变动时进行版本对比。

1.4.2 管理资源注解

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


~]$ kubectl annotate pods pod-example ilinux.io/created-by="cluster admin"
pod "pod-example" annotated

查看生成的注解信息:


~]$ kubectl describe pods pod-example | grep "Annotations"
Annotations:  ilinux.io/created-by=cluster admin

如果需要在资源创建时的清单中指定,那么使用类似如下的方式即可:


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

1.5 Pod对象的生命周期

Pod对象自从其创建开始至其终止退出的时间范围称为其生命周期。在这段时间中,Pod会处于多种不同的状态,并执行一些操作;其中,创建主容器(main container)为必需的操作,其他可选的操作还包括运行初始化容器(init container)、容器启动后钩子(post start hook)、容器的存活性探测(liveness probe)、就绪性探测(readiness probe)以及容器终止前钩子(pre stop hook)等,这些操作是否执行则取决于Pod的定义,如图4-9所示。

1.5.1 Pod的相位

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

·Pending:API Server创建了Pod资源对象并已存入etcd中,但它尚未被调度完成,或者仍处于从仓库下载镜像的过程中。

·Running:Pod已经被调度至某节点,并且所有容器都已经被kubelet创建完成。

·Succeeded:Pod中的所有容器都已经成功终止并且不会被重启。

·Failed:所有容器都已经终止,但至少有一个容器终止失败,即容器返回了非0值的退出状态或已经被系统终止。

img

图4-9 Pod的生命周期(图片来源:https://blog.openshift.com)

·Unknown:API Server无法正常获取到Pod对象的状态信息,通常是由于其无法与所在工作节点的kubelet通信所致。

Pod的相位是在其生命周期中的宏观概述,而非对容器或Pod对象的综合汇总,而且相位的数量和含义被严格界定,它仅包含上面列举的相位值。

1.5.2 Pod的创建过程

Pod是Kubernetes的基础单元,理解它的创建过程对于了解系统运作大有裨益。图4-10描述了一个Pod资源对象的典型创建过程。

1)用户通过kubectl或其他API客户端提交Pod Spec给API Server。

2)API Server尝试着将Pod对象的相关信息存入etcd中,待写入操作执行完成,API Server即会返回确认信息至客户端。

3)API Server开始反映etcd中的状态变化。

4)所有的Kubernetes组件均使用“watch”机制来跟踪检查API Server上的相关的变动。

5)kube-scheduler(调度器)通过其“watcher”觉察到API Server创建了新的Pod对象但尚未绑定至任何工作节点。

6)kube-scheduler为Pod对象挑选一个工作节点并将结果信息更新至API Server。

img

图4-10 Pod资源对象创建过程(图片来源:https://blog.heptio.com/ )

7)调度结果信息由API Server更新至etcd存储系统,而且API Server也开始反映此Pod对象的调度结果。

8)Pod被调度到的目标工作节点上的kubelet尝试在当前节点上调用Docker启动容器,并将容器的结果状态回送至API Server。

9)API Server将Pod状态信息存入etcd系统中。

10)在etcd确认写入操作成功完成后,API Server将确认信息发送至相关的kubelet,事件将通过它被接受。

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

除了创建应用容器(主容器及其辅助容器)之外,用户还可以为Pod对象定义其生命周期中的多种行为,如初始化容器、存活性探测及就绪性探测等。

1.初始化容器

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

1)初始化容器必须运行完成直至结束,若某初始化容器运行失败,那么Kubernetes需要重启它直到成功完成。

2)每个初始化容器都必须按定义的顺序串行运行。

注意 如果Pod的spec.restartPolicy字段值为“Never”,那么运行失败的初始化容器不会被重启。

有不少场景都需要在应用容器启动之前进行部分初始化操作,例如,等待其他关联组件服务可用、基于环境变量或配置模板为应用程序生成配置文件、从配置中心获取配置等。初始化容器的典型应用需求具体包含如下几种。

1)用于运行特定的工具程序,出于安全等方面的原因,这些程序不适于包含在主容器镜像中。

2)提供主容器镜像中不具备的工具程序或自定义代码。

3)为容器镜像的构建和部署人员提供了分离、独立工作的途径,使得他们不必协同起来制作单个镜像文件。

4)初始化容器和主容器处于不同的文件系统视图中,因此可以分别安全地使用敏感数据,例如Secrets资源。

5)初始化容器要先于应用容器串行启动并运行完成,因此可用于延后应用容器的启动直至其依赖的条件得到满足。

Pod资源的“spec.initContainers”字段以列表的形式定义可用的初始容器,其嵌套可用字段类似于“spec.containers”。下面的资源清单仅是一个初始化容器的使用示例,读者可自行创建并观察初始化容器的相关状态:


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

2.生命周期钩子函数

生命周期钩子函数(lifecycle hook)是编程语言(如Angular)中常用的生命周期管理的组件,它实现了程序运行周期中的关键时刻的可见性,并赋予用户为此采取某种行动的能力。类似地,容器生命周期钩子使它能够感知其自身生命周期管理中的事件,并在相应的时刻到来时运行由用户指定的处理程序代码。Kubernetes为容器提供了两种生命周期钩子。

·postStart:于容器创建完成之后立即运行的钩子处理器(handler),不过Kubernetes无法确保它一定会于容器中的ENTRYPOINT之前运行。

·preStop:于容器终止操作之前立即运行的钩子处理器,它以同步的方式调用,因此在其完成之前会阻塞删除容器的操作的调用。

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

postStart和preStop处理器定义在容器的spec.lifecycle嵌套字段中,其使用方法如下面的资源清单所示,读者可自行创建相关的Pod资源对象,并验证其执行结果:


apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
spec:
  containers:
  - name: lifecycle-demo-container
    image: ikubernetes/myapp:v1
    lifecycle:
      postStart:
        exec:
          command: ["/bin/sh","-c","echo 'lifecycle hooks handler' > /usr/share/
              nginx/html/test.html"]

3.容器探测

容器探测(container probe)是Pod对象生命周期中的一项重要的日常任务,它是kubelet对容器周期性执行的健康状态诊断,诊断操作由容器的处理器(handler)进行定义。Kubernetes支持三种处理器用于Pod探测。

·ExecAction:在容器中执行一个命令,并根据其返回的状态码进行诊断的操作称为Exec探测,状态码为0表示成功,否则即为不健康状态。

·TCPSocketAction:通过与容器的某TCP端口尝试建立连接进行诊断,端口能够成功打开即为正常,否则为不健康状态。

·HTTPGetAction:通过向容器IP地址的某指定端口的指定path发起HTTP GET请求进行诊断,响应码为2xx或3xx时即为成功,否则为失败。

任何一种探测方式都可能存在三种结果:“Success”(成功)、“Failure”(失败)或“Unknown”(未知),只有第一种结果表示成功通过检测。

·kubelet可在活动容器上执行两种类型的检测:存活性检测(livenessProbe)和就绪性检测(readinessProbe)。

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

提示 存活性检测和就绪性检测相关的话题在后文的章节中还会有进一步的介绍。

1.5.4 容器的重启策略

容器程序发生崩溃或容器申请超出限制的资源等原因都可能会导致Pod对象的终止,此时是否应该重建该Pod对象则取决于其重启策略(restartPolicy)属性的定义。

1)Always:但凡Pod对象终止就将其重启,此为默认设定。

2)OnFailure:仅在Pod对象出现错误时方才将其重启。

3)Never:从不重启。

需要注意的是,restartPolicy适用于Pod对象中的所有容器,而且它仅用于控制在同一节点上重新启动Pod对象的相关容器。首次需要重启的容器,将在其需要时立即进行重启,随后再次需要重启的操作将由kubelet延迟一段时间后进行,且反复的重启操作的延迟时长依次为10秒、20秒、40秒、80秒、160秒和300秒,300秒是最大延迟时长。事实上,一旦绑定到一个节点,Pod对象将永远不会被重新绑定到另一个节点,它要么被重启,要么终止,直到节点发生故障或被删除。

1.5.5 Pod的终止过程

Pod对象代表了在Kubernetes集群节点上运行的进程,它可能曾用于处理生产数据或向用户提供服务等,于是,当Pod本身不再具有存在的价值时,如何将其优雅地终止就显得尤为重要了,而用户也需要能够在正常提交删除操作后可以获知其何时开始终止并最终完成。操作中,当用户提交删除请求之后,系统就会进行强制删除操作的宽限期倒计时,并将TERM信息发送给Pod对象的每个容器中的主进程。宽限期倒计时结束后,这些进程将收到强制终止的KILL信号,Pod对象随即也将由API Server删除。如果在等待进程终止的过程中,kubelet或容器管理器发生了重启,那么终止操作会重新获得一个满额的删除宽限期并重新执行删除操作。

如图4-11所示,一个典型的Pod对象终止流程具体如下。

1)用户发送删除Pod对象的命令。

2)API服务器中的Pod对象会随着时间的推移而更新,在宽限期内(默认为30秒),Pod被视为“dead”。

3)将Pod标记为“Terminating”状态。

4)(与第3步同时运行)kubelet在监控到Pod对象转为“Terminating”状态的同时启动Pod关闭过程。

5)(与第3步同时运行)端点控制器监控到Pod对象的关闭行为时将其从所有匹配到此端点的Service资源的端点列表中移除。

img

图4-11 Pod的终止过程

6)如果当前Pod对象定义了preStop钩子处理器,则在其标记为“terminating”后即会以同步的方式启动执行;如若宽限期结束后,preStop仍未执行结束,则第2步会被重新执行并额外获取一个时长为2秒的小宽限期。

7)Pod对象中的容器进程收到TERM信号。

8)宽限期结束后,若存在任何一个仍在运行的进程,那么Pod对象即会收到SIGKILL信号。

9)Kubelet请求API Server将此Pod资源的宽限期设置为0从而完成删除操作,它变得对用户不再可见。

默认情况下,所有删除操作的宽限期都是30秒,不过,kubectl delete命令可以使用“--grace-period=”选项自定义其时长,若使用0值则表示直接强制删除指定的资源,不过,此时需要同时为命令使用“--force”选项。

1.6 Pod存活性探测

有不少应用程序长时间持续运行后会逐渐转为不可用状态,并且仅能通过重启操作恢复,Kubernetes的容器存活性探测机制可发现诸如此类的问题,并依据探测结果结合重启策略触发后续的行为。存活性探测是隶属于容器级别的配置,kubelet可基于它判定何时需要重启一个容器。

Pod spec为容器列表中的相应容器定义其专用的探针(存活性探测机制)即可启用存活性探测。目前,Kubernetes的容器支持存活性探测的方法包含以下三种:ExecAction、TCPSocketAction和HTTPGetAction。

1.6.1 设置exec探针

exec类型的探针通过在目标容器中执行由用户自定义的命令来判定容器的健康状态,若命令状态返回值为0则表示“成功”通过检测,其值均为“失败”状态。“spec.containers.livenessProbe.exec”字段用于定义此类检测,它只有一个可用属性“command”,用于指定要执行的命令。下面是定义在资源清单文件liveness-exec.yaml中的示例:


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

上面的资源清单中定义了一个Pod对象,基于busybox镜像启动一个运行“touch/tmp/healthy;sleep 60;rm-rf/tmp/healthy;sleep 600”命令的容器,此命令在容器启动时创建/tmp/healthy文件,并于60秒之后将其删除。存活性探针运行“test-e/tmp/healthy”命令检查/tmp/healthy文件的存在性,若文件存在则返回状态码0,表示成功通过测试。

首先,执行类似如下的命令,创建Pod对象liveness-exec:


~]$ kubectl apply -f liveness-exec.yaml
pod "liveness-exec" created

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


Events:
  Type     Reason          Age               From                 Message
  ----     ------          ----              ----                 -------
……
  Warning  Unhealthy       45s (x3 over 1m)  kubelet, node02.ilinux.io  Liveness 
      probe failed:
  Normal   Pulling         14s (x2 over 2m)  kubelet, node02.ilinux.io  pulling 
      image "busybox"
  Normal    Killing        14s               kubelet, node02.ilinux.io  Killing 
      container with id docker://liveness-demo:Container failed liveness probe.. 
      Container will be killed and recreated.
  Normal   Pulled          10s (x2 over 2m)  kubelet, node02.ilinux.io  
      Successfully pulled image "busybox"
……

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


  liveness-exec-demo:
……
    State:          Running
      ……
    Last State:     Terminated
      Reason:       Error
      Exit Code:    137
      ……
    Ready:          True
    Restart Count:   1
    Liveness:       exec [test -e /tmp/healthy] delay=0s timeout=1s period=10s 
        #success=1 #failure=3
……

待容器重启完成后再次查看,容器已经处于正常运行状态,直到文件再次被删除,存活性探测失败而重启。从下面的命令显示可以看出,liveness-exec在4分钟时间内已然重启了两次:


~]$ kubectl get pods liveness-exec
NAME            READY     STATUS       RESTARTS    AGE
liveness-exec   1/1       Running      2           4m

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

1.6.2 设置HTTP探针

基于HTTP的探测(HTTPGetAction)向目标容器发起一个HTTP请求,根据其响应码进行结果判定,响应码形如2xx或3xx时表示检测通过。“spec.containers.livenessProbe.httpGet”字段用于定义此类检测,它的可用配置字段包括如下几个。

·host :请求的主机地址,默认为Pod IP;也可以在httpHeaders中使用“Host:”来定义。

·port :请求的端口,必选字段。

·httpHeaders<[]Object>:自定义的请求报文首部。

·path :请求的HTTP资源路径,即URL path。

·scheme:建立连接使用的协议,仅可为HTTP或HTTPS,默认为HTTP。

下面是一个定义在资源清单文件liveness-http.yaml中的示例,它通过lifecycle中的postStart hook创建了一个专用于httpGet测试的页面文件healthz:


apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-http
spec:
  containers:
  - name: liveness-http-demo
    image: nginx:1.12-alpine
    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

上面清单文件中定义的httpGet测试中,请求的资源路径为“/healthz”,地址默认为Pod IP,端口使用了容器中定义的端口名称HTTP,这也是明确为容器指明要暴露的端口的用途之一。首先创建此Pod对象:


~]$ kubectl apply -f liveness-http.yaml
pod "liveness-http" created

而后查看其健康状态检测相关的信息,健康状态检测正常时,容器也将正常运行:


~]$ kubectl describe pods liveness-http
……
Containers:
  liveness-http-demo:
……
    Port:           80/TCP
    State:          Running
      Started:      Thu, 21 Aug 2018 12:55:41 +0800
    Ready:          True
    Restart Count:  0
    Liveness:       http-get http://:http/healthz delay=0s timeout=1s period=10s 
        #success=1 #failure=3
……

接下来借助于“kubectl exec”命令删除经由postStart hook创建的测试页面healthz:


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

而后再次使用“kubectl describe pods liveness-http”查看其详细的状态信息,事件输出中的信息可以表明探测测试失败,容器被杀掉后进行了重新创建:


Events:
……
  Warning  Unhealthy         28s (x3 over 48s)  kubelet, node02.ilinux.io  Liveness 
      probe failed: HTTP probe failed with statuscode: 404
  Normal   Killing           28s            kubelet, node02.ilinux.io  Killing 
      container with id docker://liveness-demo:Container failed liveness probe.. Container will be killed and recreated.

一般来说,HTTP类型的探测操作应该针对专用的URL路径进行,例如,前面示例中特别为其准备的“/healthz”。另外,此URL路径对应的Web资源应该以轻量化的方式在内部对应用程序的各关键组件进行全面检测以确保它们可正常向客户端提供完整的服务。

需要注意的是,这种检测方式仅对分层架构中的当前一层有效,例如,它能检测应用程序工作正常与否的状态,但重启操作却无法解决其后端服务(如数据库或缓存服务)导致的故障。此时,容器可能会被一次次的重启,直到后端服务恢复正常为止。其他两种检测方式也存在类似的问题。

1.6.3 设置TCP探针

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

1)host:请求连接的目标IP地址,默认为Pod IP。

2)port:请求连接的目标端口,必选字段。

下面是一个定义在资源清单文件liveness-tcp.yaml中的示例,它向Pod IP的80/tcp端口发起连接请求,并根据连接建立的状态判定测试结果:


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

这里不再给出其具体的创建与测试过程,有兴趣的读者可自行进行测试。

1.6.4 存活性探测行为属性

使用kubectl describe命令查看配置了存活性探测的Pod对象的详细信息时,其相关容器中会输出类似如下一行的内容:


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。

例如,这里可将1.6.1节中清单文件中定义的探测示例重新定义为如下所示的内容:


spec:
  containers:
……
    livenessProbe:
      exec:
        command: ["test", "-e", "/tmp/healthy"]
      initialDelaySeconds 5s
       timeoutSeconds 2s
       periodSeconds 5s

根据修改的清单再次创建Pod对象并进行效果测试,可以从输出的详细信息中看出Liveness已经更新到自定义的属性,其内容如下所示。具体过程这里不再给出,请感兴趣的读者自行测试。


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

1.7 Pod就绪性探测

Pod对象启动后,容器应用通常需要一段时间才能完成其初始化过程,例如加载配置或数据,甚至有些程序需要运行某类的预热过程,若在此阶段完成之前即接入客户端的请求,势必会因为等待太久而影响用户体验。因此,应该避免于Pod对象启动后立即让其处理客户端请求,而是等待容器初始化工作执行完成并转为“就绪”状态,尤其是存在其他提供相同服务的Pod对象的场景更是如此。

与存活性探测机制类似,就绪性探测是用来判断容器就绪与否的周期性(默认周期为10秒钟)操作,它用于探测容器是否已经初始化完成并可服务于客户端请求,探测操作返回“success”状态时,即为传递容器已经“就绪”的信号。

与存活性探测机制相同,就绪性探测也支持Exec、HTTP GET和TCP Socket三种探测方式,且各自的定义机制也都相同。但与存活性探测触发的操作不同的是,探测失败时,就绪性探测不会杀死或重启容器以保证其健康性,而是通知其尚未就绪,并触发依赖于其就绪状态的操作(例如,从Service对象中移除此Pod对象)以确保不会有客户端请求接入此Pod对象。不过,即便是在运行过程中,Pod就绪性探测依然有其价值所在,例如Pod A依赖到的Pod B因网络故障等原因而不可用时,Pod A上的服务应该转为未就绪状态,以免无法向客户端提供完整的响应。

将容器定义中的livenessProbe字段名替换为readinessProbe即可定义出就绪性探测的配置,一个简单的示例如下面的配置清单(readiness-exec.yaml)所示,它会在Pod对象创建完成5秒钟后使用test-e/tmp/ready命令来探测容器的就绪性,命令执行成功即为就绪,探测周期为5秒钟:


apiVersion: v1
kind: Pod
metadata:
  labels:
    test: readiness-exec
  name: readiness-exec
spec:
  containers:
  - name: readiness-demo
    image: busybox
    args: ["/bin/sh", "-c", "while true; do rm -f /tmp/ready; sleep 30; touch 
        /tmp/ready; sleep 300; done"]
    readinessProbe:
      exec:
        command: ["test", "-e", "/tmp/ready"]
      initialDelaySeconds: 5
      periodSeconds: 5

首先,使用“kubectl create”命令将资源配置清单定义的资源创建到集群中:


~]$ kubectl create -f readiness-exec.yaml
pod/readiness-exec created

接着,运行“kubectl get-w”命令监视其资源变动信息,由如下命令结果可知,尽管Pod对象处于“Running”状态,但直到就绪探测命令执行成功后,Pod资源才转为“就绪”:


~]$ kubectl get pods -l test=readiness-exec  -w
NAME       READY    STATUS    RESTARTS    AGE
readiness-exec      0/1       Running     0        15s
readiness-exec      1/1       Running     0        41s

另外,还可从Pod对象的详细信息中得到类似如下的表示其已经处于就绪状态的信息片断:


Ready:        True
Restart Count:  0
Readiness:      exec [test -e /tmp/ready] delay=5s timeout=1s period=5s #success=1 
    #failure=3

这里需要特别提醒读者的是,未定义就绪性探测的Pod对象在Pod进入“Running”状态后将立即就绪,在容器需要时间进行初始化的场景中,在应用真正就绪之前必然无法正常响应客户端请求,因此,生产实践中,必须为关键性Pod资源中的容器定义就绪性探测机制。其探测机制的定义请参考1.6节中的定义。

1.8 资源需求及资源限制

在Kubernetes上,可由容器或Pod请求或消费的“计算资源”是指CPU和内存(RAM),这也是目前仅有的受支持的两种类型。相比较来说,CPU属于可压缩(compressible)型资源,即资源额度可按需收缩,而内存(当前)则是不可压缩型资源,对其执行收缩操作可能会导致某种程度的问题。

目前来说,资源隔离尚且属于容器级别,CPU和内存资源的配置需要在Pod中的容器上进行,每种资源均可由“requests”属性定义其请求的确保可用值,即容器运行可能用不到这些额度的资源,但用到时必须要确保有如此多的资源可用,而“limits”属性则用于限制资源可用的最大值,即硬限制,如图4-12所示。不过,为了表述方便,人们通常仍然把资源配置称作Pod资源的请求和限制,只不过它是指Pod内所有容器上某种类型资源的请求和限制的总和。

img

图4-12 容器资源需求及资源限制示意图

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

1.8.1 资源需求

下面的示例中,自主式Pod要求为stress容器确保128Mi的内存及五分之一个CPU核心(200m)资源可用,它运行stress-ng镜像启动一个进程(-m 1)进行内存性能压力测试,满载测试时它也会尽可能多地占用CPU资源,另外再启动一个专用的CPU压力测试进程(-c 1)。stress-ng是一个多功能系统压力测试具,master/worker模型,Master为主进程,负责生成和控制子进程,worker是负责执行各类特定测试的子进程,例如测试CPU的子进程,以及测试RAM的子进程等:


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资源大小为200m,这意味着一个CPU核心足以确保其以期望的最快方式运行。另外,配置清单中期望使用的内存大小为128Mi,不过其运行时未必真的会用到这么多。考虑到内存为非压缩型资源,其超出指定的大小在运行时存在被OOM killer杀死的可能性,于是请求值也应该就是其理想中使用的内存空间上限。

接下来创建并运行此Pod对其资源限制效果进行检查。需要特别说明的是,笔者当前使用的系统环境中,每个节点的可用CPU核心数均为8,物理内存空间为16GB:


~]$ kubectl create -f pod-resources-test.yaml

而后在Pod资源的容器内运行top命令观察其CPU及内存资源的占用状态,如下所示,其中{stress-ng-vm}是执行内存压测的子进程,它默认使用256m的内存空间,{stress-ng-cpu}是执行CPU压测的专用子进程:


~]$ kubectl exec stress-pod -- top
Mem: 2884676K used, 13531796K free, 27700K shrd, 2108K buff, 1701456K cached
CPU:  25% usr   0% sys   0% nic  74% idle   0% io   0% irq   0% sirq
Load average: 0.57 0.60 0.71 3/435 15
PID   PPID USER  STAT  VSZ   %VSZ CPU %CPU COMMAND
9     8 root     R     262m   2%   6  13% {stress-ng-vm} /usr/bin/stress-ng
7     1 root     R     6888   0%   3  13% {stress-ng-cpu} /usr/bin/stress-ng
1     0 root     S     6244   0%   1   0% /usr/bin/stress-ng -c 1 -m 1 --met
……

top命令的输出结果显示,每个测试进程的CPU占用率为13%(实际为12.5%),{stress-ng-vm}的内存占用量为262m(VSZ),此两项资源占用量都远超其请求的用量,原因是stress-ng会在可用的范围内尽量多地占用相关的资源。两个测试线程分布于两个CPU核心以满载的方式运行,系统共有8个核心,因此其使用率为25%(2/8)。另外,节点上的内存资源充裕,虽然容器的内存用量远超128M,但它依然可运行。一旦资源紧张时,节点仅保证容器有五分之一个CPU核心可用,对于有着8个核心的节点来说,它的占用率为2.5%,于是每个进程为1.25%,多占用的资源会被压缩。内存为非可压缩型资源,所以此Pod在内存资源紧张时可能会因OOM被杀死(killed)。

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

集群中的每个节点都拥有定量的CPU和内存资源,调度Pod时,仅那些被请求资源的余量可容纳当前被调度的Pod的请求量的节点才可作为目标节点。也就是说,Kubernetes的调度器会根据容器的requests属性中定义的资源需求量来判定仅哪些节点可接收运行相关的Pod资源,而对于一个节点的资源来说,每运行一个Pod对象,其requests中定义的请求量都要被预留,直到被所有Pod对象瓜分完毕为止。

1.8.2 资源限制

容器的资源需求仅能达到为其保证可用的最少资源量的目的,它并不会限制容器的可用资源上限,因此对因应用程序自身存在Bug等多种原因而导致的系统资源被长时间占用的情况则无计可施,这就需要通过limits属性为容器定义资源的最大可用量。资源分配时,可压缩型资源CPU的控制阀可自由调节,容器进程无法获得超出其CPU配额的可用时间。不过,如果进程申请分配超出其limits属性定义的硬限制的内存资源时,它将被OOM killer杀死,不过,随后可能会被其控制进程所重启,例如,容器进程的Pod对象会被杀死并重启(重启策略为Always或OnFailure时),或者是容器进程的子进程被其父进程所重启。

下面的配置清单文件(memleak-pod.yaml)中定义了如何使用saadali/simmemleak镜像运行一个Pod对象,它模拟内存泄漏操作不断地申请使用内存资源,直到超出limits属性中memory字段设定的值而导致“OOMKillled”为止:


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"

下面测试其运行效果,首先将配置清单中定义的资源复用下面的命令创建于集群中:


~]$ kubectl apply -f memleak-pod.yaml
pod/memleak created

Pod资源的默认重启策略为Always,于是在memleak因内存资源达到硬限制而被终止后会立即重启,因此用户很难观察到其因OOM而被杀死的相关信息。不过,多次重复地因为内存资源耗尽而重启会触发Kubernetes系统的重启延迟机制,即每次重启的时间间隔会不断地拉长。于是,用户看到的Pod资源的相关状态通常为“CrashLoopBackOff”:


~]$ kubectl get pods -l app=memleak
NAME          READY     STATUS             RESTARTS   AGE
memleak-pod   0/1       CrashLoopBackOff   1          24s

Pod资源首次的重启将在crash后立即完成,若随后再次crash,那么其重启操作会延迟10秒进行,随后的延迟时长会逐渐增加,依次为20秒、40秒、80秒、160秒和300秒,随后的延迟将固定在5分钟的时长之上而不再增加,直到其不再crash或者delete为止。describe命令可以显示其状态相关的详细信息,其部分内容如下所示:


~]$ kubectl describe pods memleak-pod
Name:         memleak-pod
……
Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137
      Started:      Wed, 02 May 2018 12:42:50 +0800
      Finished:     Wed, 02 May 2018 12:42:50 +0800
    Ready:          False
    Restart Count:  3
……

如上述命令结果所显示的,OOMKilled表示容器因内存耗尽而被终止,因此,为limits属性中的memory设置一个合理值至关重要。与requests不同的是,limits并不会影响Pod的调度结果,也就是说,一个节点上的所有Pod对象的limits数量之和可以大于节点所拥有的资源量,即支持资源的过载使用(overcommitted)。不过,这么一来一旦资源耗尽,尤其是内存资源耗尽,则必然会有容器因OOMKilled而终止。

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

1.8.3 容器的可见资源

细心的读者可能已经发现了这一点:于容器中运行top等命令观察资源可用量信息时,即便定义了requests和limits属性,虽然其可用资源受限于此两个属性中的定义,但容器中可见的资源量依然是节点级别的可用总量。例如,为前面定义的stress-pod添加如下limits属性定义:


limits:
   memory: "512Mi"
   cpu: "400m"

重新创建stress-pod对象,并于其容器内分别列出容器可见的内存和CPU资源总量,命令及结果如下所示:


~]$ kubectl exec stress-pod -- cat /proc/meminfo | grep ^MemTotal
MemTotal:       16416472 kB
$ kubectl exec stress-pod -- cat /proc/cpuinfo | grep -c ^processor
8

命令结果中显示其可用内存资源总量为16416472KB(16GB),CPU核心数为8个,这是节点级的资源数量,而非由容器的limits所定义的512Mi和400m。其实,这种结果不仅仅使得其查看命令的显示结果看起来有些奇怪,而且对有些容器应用的配置也会带来不小的负面影响。

较为典型的是在Pod中运行Java应用程序时,若未使用“-Xmx”选项指定JVM的堆内存可用总量,它默认会设置为主机内存总量的一个空间比例(如30%),这会导致容器中的应用程序申请内存资源时将会达到上限而转为OOMKilled。另外,即便使用了“-Xmx”选项设置其堆内存上限,但它对于非堆内存的可用空间不会产生任何限制作用,结果是仍然存在达到容器内存资源上限的可能性。

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

1.8.4 Pod的服务质量类别

前面曾提到过,Kubernetes允许节点资源对limits的过载使用,这意味着节点无法同时满足其上的所有Pod对象以资源满载的方式运行。于是,在内存资源紧缺时,应该以何种次序先后终止哪些Pod对象?Kubernetes无法自行对此做出决策,它需要借助于Pod对象的优先级完成判定。根据Pod对象的requests和limits属性,Kubernetes将Pod对象归类到BestEffort、Burstable和Guaranteed三个服务质量(Quality of Service,QoS)类别下,具体说明如下。

·Guaranteed:每个容器都为CPU资源设置了具有相同值的requests和limits属性,以及每个容器都为内存资源设置了具有相同值的requests和limits属性的Pod资源会自动归属于此类别,这类Pod资源具有最高优先级。

·Burstable:至少有一个容器设置了CPU或内存资源的requests属性,但不满足Guaranteed类别要求的Pod资源将自动归属于此类别,它们具有中等优先级。

·BestEffort:未为任何一个容器设置requests或limits属性的Pod资源将自动归属于此类别,它们的优先级为最低级别。

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

每个运行状态容器都有其OOM得分,得分越高越会被优先杀死。OOM得分主要根据两个纬度进行计算:由QoS类别继承而来的默认分值和容器的可用内存资源比例。同等类别的Pod资源的默认分值相同,下面的代码片段取自pkg/kubelet/qos/policy.go源码文件,它们定义的是各种类别的Pod资源的OOM调节(Adjust)分值,即默认分值。其中,Guaranteed类别的Pod资源的Adjust分值为-998,而BestEffort类别的默认分值为1000,Burstable类别的Pod资源的Adjust分值则经由相应的算法计算得出:


const (
    PodInfraOOMAdj        int = -998
    KubeletOOMScoreAdj    int = -999
    DockerOOMScoreAdj     int = -999
    KubeProxyOOMScoreAdj  int = -999
    guaranteedOOMScoreAdj int = -998
    besteffortOOMScoreAdj int = 1000
)

因此,同等级别优先级的Pod资源在OOM时,与自身的requests属性相比,其内存占用比例最大的Pod对象将被首先杀死。例如,图4-13中的同属于Burstable类别的Pod A将先于Pod B被杀死,虽然其内存用量小,但与自身的requests值相比,它的占用比例95%要大于Pod B的80%。

需要特别说明的是,OOM是内存耗尽时的处理机制,它们与可压缩型资源CPU无关,因此CPU资源的需求无法得到保证时,Pod仅仅是暂时获取不到相应的资源而已。

img

图4-13 资源需求、资源限额及OOM

1.9 小结

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

·Pod就是联系紧密的一组容器,它们共享Network、UTS和IPC名称空间及存储卷资源。

·分布式系统设计主要有Sidecar、Ambassador和Adapter三种主要模式。

·Kubernetes资源对象的管理操作基本上是由增、删、改和查等操作组成的,并且支持陈述式命令、陈述式对象配置和声明式对象配置三种管理方式。

·Pod的核心目标在于运行容器,容器的定制配置常见的包括暴露端口及传递环境变量等。

·标签是附加在Kubernetes系统上的键值类型的元数据,而标签选择器是基本等值或集合关系的标签过滤机制;注解类似于标签,但不能被用于标签选择器。

·Pod的生命周期中可能存在多种类型的操作,但运行主容器是其核心任务。

·存活性探测及就绪性探测是辅助判定容器状态的重要工具。

·资源需求及资源限制是管理Pod对象系统资源分配的有效方式。

版权声明:如无特殊说明,文章均为本站原创,版权所有,转载需注明本文链接

本文链接:http://www.bianchengvip.com/article/Kubernetes-Managed-Pod-Resource-Objects/