Kubernetes系列学习文章 - Pod的深入理解(四)

| 导语 从这篇文章开始,我们将详细介绍K8S的各个组件。学习一项技术,理论先行,只有充分的了解了内在原理才能在日后的维护和调优方面有所思路。

一、什么是Pod

1. Pod是什么

Pod的介绍,我们前面在第二篇文章里有个大概的解释。你可以把它想象成一个“豆荚” ,然后里面包着一组有关联关系的“豆子”(容器)。

一个豆荚里的豆子,它们共同吸收着同一个养分,Pod也是如此,里面的容器共有着同一组资源。K8S官方文档对Pod的描述是:A Pod is the basic building block of Kubernetes–the smallest and simplest unit in the Kubernetes object model that you create or deploy. A Pod represents processes running on your Cluster. 翻译成中文:Pod是K8S的基本构建模块,它是你在K8S集群里能创建或部署的最小和最简单的单元

刚学习K8S的同学一般会认为容器Docker是最小单元,其实不是,Pod才是K8S里最小的单元。其实,你把Pod想成虚拟机,然后容器想成虚拟机里面的应用你就明白很多了。我们还是拿K8S跟OpenStack对比就明白了:

K8S

OpenStack

Pod

VM

Docker

application 应用、进程

OpenStack管理的VM可以说是OpenStack里的最小单元,虚拟机我们知道有隔离性,里面部署的应用只跑在虚拟机里面,他们共享这个VM的CPU、Mem、网络、存储资源。那么,同理Pod也是如此,Pod里面的容器共享着Pod里面的CPU、Mem、网络和存储资源。那么Pod是如何做到的呢?我们接着看下面的知识点你就明白了。

2. Pod的特点

集群里的最小单元

注意K8S集群的最小单元是Pod,而不是容器;K8S直接管理的也是Pod,而不是容器。

Pod里可以运行一个或多个容器

如果Pod里只运行一个容器,那就是 “one-container-per-Pod” 模式。当然你也可以把一组有关联关系的容器放在一个Pod里面,这些容器共享着同一组网络命名空间和存储卷。比如K8S官网文档里就举了一个例子,把一个Web Server的容器跟File Puller的容器放在一起(Web Server对外提供web服务,File Puller负责内容存储和提供),然后它们就形成了一个Pod,构成了一个统一的服务单元。

另外,我们也可以想复杂点,比如我们在传统运维里搭建的LAMP环境,如果把它们容器化部署在K8S集群里,那么是怎样个模式?IAAS层里面部署LAMP环境,如果是规模小的话,用一个虚拟机就可以把apache + mysql + PHP环境部署起来,规模大的话多用几个虚拟机,然后做分布式。那么如果是PAAS层Pod容器化部署呢?是否是apache + mysql + PHP分别跑一个docker,然后打包放在一个Pod里,然后规模大就多起几个Pod?

Pod里的容器共用相同的网络和存储资源

在K8S里,每个Pod会被分配唯一的IP地址,然后里面的容器都会共享着这个网络空间,这个网络空间包含了IP地址和网络端口。Pod容器内部通信用的是localhost,如果要跟外面通信,就需要用到共享的那个IP和端口。

Pod可以指定共享的存储Volume,然后Pod里的所有容器都有权限访问这个Volume。Volume是持久化数据用的,Pod的重启都不会影响Volume里的数据。

Pod里的容器共用相同的依赖关系

前面提到过有关联关系的容器可以放在一个Pod里,那么这里的关联关系怎么理解?通常,我们会把有紧耦合的服务部署在一个Pod里,比如LAMP应用栈。这里做的目的就是能做到共同调度和协调管理。所以,没有关联的容器最好不要放一个Pod里,没有规则的乱放,你将无法体会到K8S的强大之处——编排。

二、Pod内部机制

1. Pod实现原理

我们在学习容器的时候就了解到因为Linux提供了Namespace和Cgroup两种机制才让容器的出现提供了可能。Namespace用于进程之间的隔离,Cgroup用于控制进程资源的使用;Namespace由hostname、PID、filesystem、network、IPC组成。在K8S里,Pod的生成也是基于Namespace和Cgroup的,所以Pod内的架构合成,我们可以用下面这张图画出来:

Pod架构

那这些要素是通过什么机制组合在一起呢?这里是通过一个叫Pause(gcr.io/google_containers/pause-amd64)的容器完成的。K8S在初始化一个Pod的时候会先启动一个叫Pause的容器,然后再启动用户自定义的业务容器。这个Pause容器我们认为它是一个“根容器”,它主要有两方面作用:

  • 扮演PID 1的角色,处理僵尸进程
  • 在Pod里为其他容器共享Linux namespace的基础

首先我们了解下Linux系统下PID为 1 的进程它的作用和意义。在Linux里,PID 为 1的进程,叫超级进程,也叫根进程,它是系统的第一个进程,是其他进程的父进程,所有的进程都会被挂在这个进程下。如果一个子进程的父进程退了,那么这个子进程会被挂到 PID 1 下面。

其次,我们知道容器本身就是一个进程。在一个Namespace下,Pause作为PID为1的进程存在于一个Pod里,其他的业务容器都挂载这个Pause进程下面。这样,一个Namespace下的进程就会以Pause作为根,呈树状的结构存在一个Pod下。

最后,Pause还有个功能是负责处理僵尸进程。僵尸进程:一个进程使用fork函数创建子进程,如果子进程退出,而父进程并没有来得及调用wait或waitpid获取其子进程的状态信息,那么这个子进程的描述符仍然保存在系统中,其进程号会一直存在被占用(而系统的进程号是有限的),这种进程称之为僵尸进程(Z开头)。

Pause这个容器代码是用C写的(代码见下),其中Pause的代码里,有个无限循环的for(;;)函数,函数里面执行的是pause( )函数,pause() 函数本身是在睡眠状态的, 直到被信号(signal)所中断。因此,正是因为这个机制,Pause容器会一直等待SIGCHLD信号,一旦有了SIGCHLD信号(进程终止或者停止时会发出这种信号),Pause就会启动sigreap方法,sigreap方法里就会调用waitpid获取其子进程的状态信息,这样自然就不会在Pod里产生僵尸进程了。

## Pause代码
static void sigdown(int signo) {
  psignal(signo, "Shutting down, got signal");
  exit(0);
}

static void sigreap(int signo) {
  while (waitpid(-1, NULL, WNOHANG) > 0);
}

int main() {
  if (getpid() != 1)
    /* Not an error because pause sees use outside of infra containers. */
    fprintf(stderr, "Warning: pause should be the first process\n");

  if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 1;
  if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
    return 2;
  if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
                                             .sa_flags = SA_NOCLDSTOP},
                NULL) < 0)
    return 3;

## 关注这下面的for循环代码
  for (;;)
    pause();
  fprintf(stderr, "Error: infinite loop terminated\n");
  return 42;
}

2. Pod的生命周期

这一点我们来分析下Pod的生命周期 lifecycle,在K8S官方文档里,分别通过phase、Conditions、Container probes、Container States、Pod readiness gate、Restart policy、Pod lifetime等几方面来讲述。

2.1 Pod的状态值有哪些 - phase

Pod的状态是PodStatus对象里的phase字段来表示的,这个phase字段有以下一些值:

阶段名

描述

Pending

K8S集群里已经发起创建Pod请求,里面的Pod还没有容器。这个阶段一般是发生在Pod被调度之前或者Pod里的镜像正在下载。

Running

Pod已经调度落地在了一个Node里,并且里面的容器已经创建好了。里面至少一个容器正在运行或者正在启动或者重新启动。

Succeeded

Pod里面的所有容器成功运行,也没发生重启等行为。

Failed

Pod里面的所有容器终止,至少有一个容器以失败方式终止。也就是说,这个容器要么已非 0 状态退出,要么被系统终止。

Unknown

由于一些原因,Pod 的状态无法获取,通常是与 Pod 通信时有出错。

2.2 Pod Conditions

前面说到PodStatus对象里有phase字段,那么PodStatus里也包含Pod Conditions,这是一个数组,里面包含的属性有:

字段

描述

lastProbeTime

最后一次探测 Pod Condition 的时间戳。

lastTransitionTime

上次 Condition 从一种状态转换到另一种状态的时间。

message

上次 Condition 状态转换的详细描述。

reason

Condition 最后一次转换的原因。

status

Condition 状态类型,可以为 “True”, “False”, and “Unknown”.

type

Condition 类型,包括以下方面: - PodScheduled(Pod已经被调度到其他node里) - Ready(Pod能够提供服务请求,可以被添加到所有可匹配服务的负载平衡池中) - Initialized(所有的init containers已经启动成功) - Unschedulable(调度程序现在无法调度Pod,例如由于缺乏资源或其他限制;) - ContainersReady(Pod里的所有容器都是ready状态)

2.3 Container probes

probes中文就是探针的意思,所以container probes翻译成中文就是“容器探针”。这是K8S的一种诊断容器状态的机制。我们知道Node里会运行kubelet进程,它有一个作用是收集容器的状态,然后汇报给master节点。“容器探针” 这种机制就是通过kubelet来实现的。那么kubelet是怎么知道节点里容器状态信息的呢?具体的主要是kubelet调用容器提供的Handler(钩子)三种处理程序:

  • ExecAction :在容器内执行指定的命令。 如果命令以状态代码 0 退出,则认为诊断成功,容器是健康的。
  • TCPSocketAction :通过容器的 IP 地址和端口号执行 TCP 检查。 如果端口存在,则认为诊断成功,容器是健康的。
  • HTTPGetAction :通过容器的 IP 地址和端口号及路径调用 HTTP GET 方法,如果响应的状态码大于等于 200 且小于 400,则认为容器状态健康。

每个Container probes都会获得三种结果:

  • 成功:容器通过了诊断。
  • 失败:容器未通过诊断。
  • 未知:诊断失败,不应采取任何措施。

另外,kubelet可以在运行的Containers(容器)里,有两种探针方式:

  • livenessProbe :存活探针,这个是为了表明容器是否正在运行,服务是否正常。如果LivenessProbe探测到容器不健康,则kubelet会杀死Container,并且根据Container的重启策略来重启。 如果Container未提供livenessProbe,则默认状态为Success
  • readinessProbe :就绪探针,这个是为了表明Container是否已准备好提供服务(是否启动成功)。 如果readinessProbe探测失败,则Container的Ready将为False,控制器将此Pod的Endpoint从对应的service的Endpoint列表中移除,从此不再将任何请求调度此Pod上,直到下次探测成功。 如果Container未提供readinessProbe,则默认状态为Success

为什么会有这两种探针机制,主要是POD的生命周期会受到很多环境条件的影响,比如POD内部各个容器的状态、容器依赖的上游或者周边服务的状态等等。所以需要有一个机制来根据容器不同的状态来决定POD是否健康。所以,liveness和readiness探测就是为了解决这个问题产生的。

下面我们用两张动态图来介绍下这两种探针具体的工作方式(图网上借用,侵删):

比如有个Pod通过LivenessProbe探测发现无法再提供服务了,那么LivenessProbe会根据容器重启策略判断它是否重启,策略通过后,执行新Pod替代操作。

Liveness探针机制工作动态图

有时候一些应用需要一段时间来预热和启动,比如一个后端项目的启动需要先启动消息队列或者数据库等才能提供服务。那么这样的情况,使用就绪探针比较合适。

Readiness探针机制工作动态图

那么在具体的生产环境实践中,什么时候用Liveness?什么时候用Readiness?这里总结了一些经验,可以参考下:

(1)Liveness 跟 Readiness 应该直接探测程序,不要走间接拐弯的探测方式。

(2)Liveness 探测的程序里不要做任何其他逻辑,它很简单就是探测服务是否运行正常。如果主线程是正常的,那就直接返回200,不是的话就返回5xx。如果有其他逻辑存在,则探测程序会把握不准。

(3)Readiness 探测的程序里有相关的处理逻辑。Readiness主要是探测判断容器是否已经准备好对外提供服务。因此,实现一些逻辑来检查目标程序后端所有依赖组件的可用性非常重要。实现Readiness探测时,需要清楚的知道所探测的程序依赖于哪些功能,并且这些依赖的功能什么时候准备OK。例如,如果应用程序需要先建立与数据库的连接然后才能提供服务,那么在“Readiness”的处理程序中就必须检查是否已建立与数据库连接才能最终确认程序是否就绪。

(4)Readiness不要嵌套使用。也就是说某个程序已经使用了Readiness做探测,那么外面不要再套一层Readiness。

最后,Liveness 跟 Readiness YAML配置语法上是一样的,也就是说同样的YAML配置文件,你把Liveness设置成Readiness即可使用。

2.4 Container States 容器状态

一旦Pod落地Node被创建了,kubelet就会在Pod里创建容器了。容器在K8S里有三种状态:Waiting,Running和Terminated。如果你要检查容器的状态,我们可以使用命令 kubectl describe pod [POD_NAME] ,这个命令会显示该Pod里每个容器的状态。另外,K8S在创建资源对象时,可以使用lifecycle来管理容器在运行前和关闭前的一些动作。lifecycle有两种回调函数:

  • PostStart:容器创建成功后,运行前的任务,用于资源部署、环境准备等。
  • PreStop:在容器被终止前的任务,用于优雅关闭应用程序、通知其他系统等等。

Waiting:这是容器的默认状态。如果容器未处于“正在运行”或“已终止”状态,则它就是“Waiting”状态。处于Waiting状态的容器仍然可以运行其所需的操作,如拉图像,应用秘密等。在这个状态下,Reason字段将显示一些原因表示为什么会是Waiting状态。

...
  State:          Waiting
   Reason:       ErrImagePull
  ...

Running:表示容器正在运行。一旦容器进入Running,如果有postStart的话,将会执行。另外,Started字段会显示容器启动的具体时间

   ...
      State:          Running
       Started:      Wed, 30 Jan 2019 16:46:38 +0530
   ...

Terminated:表示容器已终止运行。容器在成功完成执行或由于某种原因失败就出现此状态。容器终止的原因、退出代码以及容器的开始和结束时间都会一起显示出来(如下示例所示)。另外在容器进入Terminated之前,如果有preStop则会执行。

   ...
      State:          Terminated
        Reason:       Completed
        Exit Code:    0
        Started:      Wed, 30 Jan 2019 11:45:26 +0530
        Finished:     Wed, 30 Jan 2019 11:45:26 +0530
    ...

2.5 Pod 生命周期控制方法

一般情况下,Pod如果不被人为干预或被某个控制器删除,它是不会消失的。不过,例外情况就是处于"Succeeded"或者"Failed"的Pod,如果处于此种状态超过一定的时间,比如terminated-pod-gc-threshold的设定值,就会被垃圾回收机制清除。

注:terminate-pod-gc-threshold 在master节点里,它的作用是设置gcTerminated的阈值,默认是12500s。

三种类型的控制器控制Pod生命周期的方法:

  • Job:适用于一次性任务如批量计算,任务结束后Pod会被此类控制器清除。Job的重启策略只能是"OnFailure"或者"Never"。
  • ReplicationController, ReplicaSet, or Deployment,此类控制器希望Pod一直运行下去,它们的restart policy只能是"always"。
  • DaemonSet:每个node一个Pod,很明显此类控制器的restart policy应该是"always"。

3. Pod资源使用机制

我们前面提到过Pod好比一个虚拟机,虚拟机我们是能分配固定的CPU、Mem、Disk、网络资源的。同理,Pod也是如此,那么Pod如何使用和控制这些分配的资源呢?

首先,我们先了解下CPU资源的分配模式:

计算机里CPU的资源是按“时间片”的方式分配给请求的,系统里的每一个操作都需要CPU的处理,我们知道CPU的单位是Hz、GHz(1Hz = 1/s,即在单位时间内完成振动的次数,1GHz = 1 000 000 000 Hz = 1 000 000 000 次/s),频率越大,单位时间内完成的处理次数就越多。所以,哪个任务要是申请的CPU时间片越多,那么它得到的CPU资源就越多。

其次,我们再了解一些Cgroup里资源的换算单位:

CPU换算单位

1 CPU = 1000 millicpu(1 Core = 1000m)

0.5 CPU = 500 millicpu (0.5 Core = 500m)

这里的 m 就是毫、毫核的意思,K8S集群中的每一个节点可以通过操作系统的命令来确认本节点的CPU内核数量,然后将这个数量乘以1000,得到的就是节点总CPU总毫数。比如一个节点有四核,那么该节点的CPU总毫量为4000m。如果你要使用0.5 core,则你要求的是 4000*0.5 = 2000m。

K8S里是通过以下两个参数来限制和请求CPU的资源的:

spec.containers[].resources.limits.cpu CPU上限值,可以短暂超过,容器也不会被停止。

spec.containers[].resources.requests.cpu CPU请求值,K8S调度算法里的依据值,可以超过。

这里需要明白的是,如果resources.requests.cpu设置的值大于集群里每个Node的最大CPU核心数,那么这个Pod将无法调度(很容易理解,没有Node能满足它)。

例子,我们在YAML里定义一个容器CPU资源如下:

resources:
  requests:
    memory: 50Mi
    cpu: 50m
  limits:
    memory: 100Mi
    cpu: 100m

这里,CPU我们给的是50m,也就是0.05core,这0.05 core也就是占了1 CPU里的5%的资源时间。

另外,我们还要知道K8S CPU资源这块,它是一个可压缩性的资源。如果容器达到了CPU设定值会开始限制,容器性能会下降,但是不会终止和退出。

最后我们了解下MEM这块的资源控制:

单位换算:1 MiB = 1024 KiB , 这里注意的是MiB ≠ MB,MB是十进制单位,MiB 是二进制,平时我们以为MB等于1024KB,其实1MB=1000KB,1MiB才等于1024KiB。中间带字母 i 的是国际电工协会(IEC)定的,走1024乘积;KB、MB、GB是国际单位制,走1000乘积。

内存这块在K8S里一般用的是Mi单位,当然你也可以使用Ki、Gi甚至Pi,看具体的业务需求和资源容量。

这里要注意的是,内存这里不是可压缩性资源,如果容器使用内存资源到达了上限,那么会OOM,造成内存溢出,容器就会终止和退出。

三、Pod基本操作命令

说明

具体命令

创建

kubectl create -f xxx.yaml

查询

kubectl get pod PodName / kubectl describe pod PodName

删除

kubectl delete pod PodName

更新

kubectl replace /path/to/newPodName.yaml (当然也可以加--force 强制替换)

查看logs输出

kubectl logs PodName

命令这东西其实多半是要多用才能熟练,很多敲着敲着你就会了。根据本人个人经验,计算机IT里的命令都离不开这些关键词:create、get、delete ... 当然,还有万能的--help。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券