2.4 深入理解Pod

从本节开始,我们将具体学习Kubernetes的各个组件。

2.4.1 什么是Pod

2.2节对Pod有过简单的解释。我们可以把它想象成一个“豆荚”,里面包着一组有关联关系的“豆子”(容器),如图2-8所示。

图2-8 容器“豆荚”

同一个豆荚里的豆子吸取着同一个源头的养分,Pod也是如此,里面的容器共用同一组资源。Kubernetes官方文档对Pod的描述是这样的:Pod是Kubernetes的基本构建模块,是你在Kubernetes集群里能创建或部署的最小、最简单单元。

刚学习Kubernetes的人一般会认为Docker是Kubernetes里的最小单元,其实Pod才是。我们将Kubernetes和OpenStack做个对比,如表2-1所示。

表2-1 Kubernetes和OpenStack最小单元对比

OpenStack管理的VM可以说是OpenStack里的最小单元,我们知道虚拟机有隔离性,里面部署的应用只在虚拟机中运行,它们共享这个VM的CPU、MEM、网络和存储资源。Pod也是如此,Pod里面的容器共享着Pod的CPU、MEM、网络和存储资源。那么Pod是如何做到这一点的呢?我们接着学习。

注意,Kubernetes集群的最小单元是Pod,而不是容器;Kubernetes直接管理的也是Pod,而不是容器。Pod里可以运行一个或多个容器。

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

2.4.2 Pod的内部机制

1.Pod的实现原理

我们在学习容器的时候就了解到,因为Linux提供了Namespace和Cgroup两种机制,容器技术的出现才有了可能。Namespace由Hostname、PID、Network、IPC组成,用于隔离进程;Cgroup用于控制进程资源的使用。在Kubernetes里,Pod的生成也是基于Namespace和Cgroup的,图2-9是Pod的内部架构示意图。

图中的IPC即进程中通信、Network即网络访问(包括接口)、PID即容器PID、Hostname即容器主机名,那这些要素是通过什么机制组合在一起的呢?这是通过一个叫Pause的容器完成的。Kubernetes在初始化Pod的时候会先启动容器Pause,然后再启动用户自定义的业务容器。Pause容器可以算作一个“根容器”,它主要有两方面的作用。

图2-9 Pod的内部架构

1)扮演PID 1的角色,处理僵尸进程。

2)在Pod里协助其他容器共享Linux Name-space。

首先,我们了解一下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容器会一直等待信号,一旦有了信号(进程终止或者停止时会发出这种信号),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的生命周期。

(1)Pod所处阶段

Pod所处阶段是用PodStatus对象里的phase字段表示的,phase字段包含的值如表2-2所示。

表2-2 Phase取值

(2)Pod Conditions

PodStatus对象里除了有phase字段,还有Pod Conditions数组,里面包含的属性如表2-3所示。

表2-3 Pod Conditions

(3)Container probes

probes中文就是探针的意思,所以Container probes翻译成中文就是容器探针,这是Kubernetes中的一种诊断容器状态的机制。我们知道节点里会运行Kubelet进程,通过Container probes可以收集容器的状态,然后汇报给master节点。那么Kubelet是怎么知道节点里容器状态信息的呢?主要是通过Kubelet调用容器提供的三种处理程序实现的。

1)ExecAction:在容器内运行指定的命令。如果命令以状态代码0退出,则认为诊断成功,容器是健康的。

2)TCPSocketAction:通过容器的IP地址和端口号运行TCP检查。如果端口存在,则认为诊断成功,容器是健康的。

3)HTTPGetAction:通过容器的IP地址、端口号及路径调用HTTP GET方法,如果响应的状态码大于等于200且小于400,则认为容器健康。

每个Container probes都会获得3种结果。

1)成功:容器通过了诊断。

2)失败:容器未通过诊断。

3)未知:诊断失败,不应采取任何措施。

另外,Kubelet在运行的容器里有两种探针方式。

1)livenessProbe:存活探针,用来表明容器是否正在运行,服务是否正常。如果livenessProbe探测到容器不健康,Kubelet会中断容器,并且根据容器的重启策略来重启容器。如果容器未提供livenessProbe,则默认状态为Success。

2)readinessProbe:就绪探针,用来表明容器是否已准备好提供服务(是否启动成功)。如果readinessProbe探测失败,则容器的Ready将为False,控制器将此Pod的Endpoint从对应服务的Endpoint列表中移除,从此不再将任何请求调度至此Pod上,直到下次探测成功。如果容器未提供readinessProbe,则默认状态为Success。

为什么会有这两种探针机制呢?这是因为Pod的生命周期会受环境条件的影响,比如Pod内部各个容器的状态、容器依赖的上游或者周边服务的状态。所以需要有一个机制,它能根据容器的不同状态判断Pod是否健康,这就是liveness和readiness探测存在的意义。

下面我们来介绍这两种探针的工作方式。

比如通过livenessProbe探测发现某Pod无法再提供服务了,那么livenessProbe会根据容器重启策略判断它是否重启。策略通过后,运行新Pod替代之前的旧Pod,livenessProbe机制如图2-10所示。

图2-10 livenessProbe机制

图2-11 readinessProbe机制

有时候应用需要一段时间来预热和启动,比如一个后端项目需要先启动消息队列或者数据库等才能提供服务,遇到这样的情况,使用就绪探针比较合适。readinessProbe机制如图2-11所示。

在具体的生产环境实践中,如何使用这两种探针呢?这里总结了一些经验,以供参考。

1)livenessProbe和readinessProbe都应直接探测程序。

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

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

4)readinessProbe不能嵌套使用。

livenessProbe和readinessProbe的YAML配置语法是一样的,同样的YAML配置文件,把livenessProbe设置成readinessProbe即可切换探针。

(4)容器状态

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

1)postStart:容器创建成功后,运行前的任务用于资源部署、环境准备。

2)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
...

(5)Pod的生命周期控制方法

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

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

以下是3种控制Pod生命周期的控制器。

1)Job:适用于一次性任务,如批量计算。任务结束后Pod会被此控制器清除。Job的重启策略只能是OnFailure或者Never。

2)ReplicationController、ReplicaSet或Deployment:此类控制器希望Pod一直运行下去,它们的重启策略只能是Always。

3)DaemonSet:每个节点有一个Pod,很明显此类控制器的重启策略应该是Always。

2.4.3 Pod的资源使用机制

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

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

我们再来了解Cgroup里资源的换算单位,CPU换算单位如下所示。

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

2)0.5CPU=500millicpu(0.5core=500m)

这里m是毫核的意思,Kubernetes集群中的每一个节点都可以通过操作系统的命令确认本节点的CPU内核数量,然后将这个数量乘以1000,得到的就是节点CPU总毫数。比如一个节点有4核,那么该节点的CPU总毫数为4000m。

Kubernetes是通过以下两个参数来限制和请求CPU的资源的。

1)spec.containers[].resources.limits.cpu:CPU上限值,短暂超出上限值,容器不会被停止。

2)spec.containers[].resources.requests.cpu:CPU请求值,是Kubernetes调度算法里的依据值,可以超出。

这里需要说明一点,如果resources.requests.cpu设置的值大于集群里每个节点的最大CPU核心数,那么这个Pod将无法调度(没有节点能满足它)。

我们在YAML里定义一个容器CPU资源如下。


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

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

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

最后我们了解一下MEM的资源控制。

单位换算方法:1 MiB=1024KiB,注意MiB≠MB,MB是十进制单位,MiB是二进制,很多人以为1MB=1024KB,其实1MB=1000KB,1MiB=1024KiB。

在Kubernetes里,内存单位一般用的是MiB,当然你也可以根据具体的业务需求和资源容量使用KiB、GiB甚至PiB为单位。

这里要注意的是,内存是可压缩资源,容器使用的内存资源到达了上限会造成内存溢出,容器就会终止运行并退出。

2.4.4 Pod的基本操作命令

下面介绍Pod的基本操作命令,如表2-4所示。

表2-4 Pod的基本操作命令

命令要多用才能熟练,大多数命令离不开create、get、delete这些关键词,当然,还有--help。

如何用Kubernetes Pod容器化部署LAMP环境呢?我们先来了解下面的两个知识点。

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

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

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

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

前面提到有关联关系的容器可以放在同一个Pod里,那么这里的关联关系是什么呢?通常,我们会把有紧耦合的服务部署在一个Pod里,比如LAMP应用栈。这样做的目的是便于调度和协调管理。所以,没有关联的容器最好不要放在同一个Pod里,没有规则地乱放,将无法体会到Kubernetes的强大特性—编排。

LAMP容器化可用图2-12表示。

图2-12 LAMP容器化

现在我们对Pod进行了较深入的学习,包括内部实现机制、资源使用机制、基本操作命令。如果我们在Kubernetes平台部署一个Pod,就需要用到YAML文件,接下来我们学习一下YAML文件的编写语法及参数。