单实例应用Pod
in DevOps with 0 comment

单实例应用Pod

in DevOps with 0 comment

实验介绍

本节将介绍 kubernetes 中的最基本和核心的概念:Pod。它与 docker 中的 container 概念比较类似,但比 container 的概念更为丰富。本实验将通过对 Pod 基本概念的介绍以及练习来掌握 Pod 的使用方法。

Pod 的概念

我们知道 Docker 容器(container)的基本概念,那么简单来讲,Pod 是一组(也可以为一个)container 的集合,这些 container 一起调度,视为一个基本单元。那么为什么要有 Pod 这个概念呢?

首先来讲,kubernetes 为了提供服务,需要有这么一个基本的计算单元,但它对这个“基本单元”的定位,Docker 的 container 并不十分适合。kubernetes 的需求是:

下面以 unit 代指 “基本单元”这个概念

结合以上的考量,Kubernetes 将 Pod 作为其最基本的运算单元。

本实验中,也会大量地使用应用/服务的概念。这是一个业务上的概念,泛指用户想要在容器平台(kubernetes) 运行的程序,比如 MySQL、Nginx…最终这些应用/服务都会以容器(Pod)的形式存在。

Pod 基本结构

在上一个实验中,我们已经介绍了 Resource 的基本概念以及如何用 yaml/json 文件来创建 Resource。Pod 也是一种 Resource。

/home/shiyanlou 目录下新建 pod.yaml 文件并向其中写入如下的内容,下面的内容是用于创建一个 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
    - name: myapp-container
      image: busybox
      command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']

这是一个非常简单的 Pod 样例。它的主要字段解释如下:

这个 yaml 展示了 Pod 的基本结构,虽然信息不多,但是已经足够运行一个 Pod 了。在命令行执行:

kubectl create -f pod.yaml

我们可以看到创建的结果:

image-1655176142449

接下来我们看一下新创建的 myapp-pod 的详细信息,在命令中执行如下命令(截图不完整):

kubectl get pod myapp-pod -o yaml

image-1655176155260

可以看到,我们使用的 yaml 在创建之后包含了更多的字段。这些都是 kubernetes 帮助填写的默认值;metadata 字段前面实验已经介绍过。下面主要说下 pod 的 spec 和 status 字段。

一般来说,很多资源都有这两个字段,而且含义类似。spec 是具体的属性描述,status 是状态信息,会在创建后不断变化。Pod 的 spec 字段是一个 containers 列表,因为它支持多容器。每个 container 内部的信息与 docker 和 docker compose 包含的信息是类似的,只是字段不同。因为最终目的都是要配置应用、运行应用,在这方面二者的目的是一致的。所以表现的主要差别只体现在语法上。

container 里主要包含的基本信息有:

Pod 的状态

Pod 创建完之后,一直到持久运行起来,中间有很多步骤,也就有很多出错的可能,因此会有很多不同的状态。一般来说,pod 这个过程包含以下几个步骤:

  1. 调度到某台机器上。kubernetes 根据一定的优先级算法选择一台机器将其作为 pod 运行的机器
  2. 拉取镜像
  3. 挂载存储配置等
  4. 运行起来。如果有健康检查,会根据检查的结果来设置其状态。

把刚才的输出拉到最下方,我们看下刚才的 pod 的状态结果:

image-1655176170692

分别包含了 pod 级别的信息以及各个 container 的信息。pod 部分的信息如下:

image-1655176182265

conditions 部分的信息比较多,包含了每个 container 的运行信息。比较重要的有:

image-1655176193263

至于 containerStatuses 部分,则提供了 pod 下各个容器的基本信息。比较重要的信息有:

一般来说,这些信息并不需要特别关注,pod 的 phase 字段大部分时间都能比较明确地提供大致的状态信息,但出错的时候,我们就需要综合各种信息来判断问题出在哪里。

kubectl 在展示状态的时候,就做了一个特殊处理,将 pod 的 phase 字段以及 container 的状态信息结合起来计算出一个状态展示出来。我们可以通过创建一个有问题的 pod 来看下。

/home/shiyanlou 目录下新建 bad-pod.yaml 文件,并向其中写入如下代码:

apiVersion: v1
kind: Pod
metadata:
  name: bad-pod # 换了名字,避免与之前的pod名字冲突
  labels:
    app: myapp
spec:
  containers:
    - name: myapp-container
      image: busybox:error-tag # 加了一个不存在的tag
      command: ['sh', '-c', 'echo Hello Kubernetes! && sleep 3600']

在命令行中执行如下命令:

kubectl create -f bad-pod.yaml   # 创建 bad-pod
kubectl get pods -w   # 执行

结果如下:

image-1655176208952

可以看到,过了一小会之后,kubernetes 发现镜像拉取失败, kubectl 展示的状态是 ImagePullBackOff,这个状态不在 pod 的 yaml 中,是 kubectl 根据 pod 以及 containers 的状态综合计算出来的。我们可以使用 Ctrl + c 来退出当前命令。kubectl 作为一个使用频率非常高的交互工具,用这样的状态能极大地增强易用性。

这里可以回想一下删除的命令,把这个错误的 Pod 删除。

资源申请

我们知道使用容器有一个优点,可以自行控制每个容器使用资源的大小。docker 提供了参数来控制 cpu 和内存的使用,pod 也同样,但仍然是容器级别的,需要对 pod 的每个容器做设置。在之前的例子中,我们没有设置这个字段,表示默认不限制资源,但是在正式的环境中使用,还是建议对资源进行限制,防止某个 Pod 超量使用资源,影响其他 Pod 甚至主机运行。

Requests and Limits

Pod 中的资源限制也主要是针对 cpu 和 内存。与 docker 不同的是,它提供了 requests 和 limits 两个设置。具体的含义为:

注意,limits 的数值不能小于 requests,否则 Pod 会启动失败。

我们找一个例子来测试下,在 /home/shiyanlou 目录下新建 wp.pod 文件,并向其中写入如下代码:

apiVersion: v1
kind: Pod
metadata:
  name: wp
spec:
  containers:
    - name: wp
      image: wordpress
      resources:
        requests:
          memory: '64Mi'
          cpu: '250m'
        limits:
          memory: '128Mi'
          cpu: '500m'

注: cpu 仅支持数字(如:1)或者 milli CPU(如:500m) 这样的写法。其中 500m = 0.5(核),且这两个数值都是绝对数值,即无论是在 2 核的主机还是 10 核的主机上,Pod 都只占用 0.5 个核心,而不是所有核心数量的一半。

内存分配有很多单位,我们常用的有 KB,MB,GB 等,注意在这里我们的单位是 Ki,Mi,Gi。1Mi = 1024 x 1024,而 1M = 1000 x 1000,其他单位以此类推。具体有哪些可用在使用时查看官方文档即可。

执行如下命令创建:

kubectl create -f wp.pod

这个 pod 明确设置了 requests 和 limits,并且 requests <= limits。这里我们要介绍一个 kubectl 的命令,叫 describe。 它与 get 类似,都是查看某个资源的信息,但是包含的信息更多。它重新排版了资源的 yaml,凸显了重要信息,并且将这个资源创建以来的事件信息也展示出来了,在排查问题时很有用。

我们看下运行结果(等 Pod 变为运行中):

kubectl describe pods wp

我们可以在这里以更直观的方式看到 pod 的信息以及 events。

image-1655176226317

pod 在调度到某个节点上之后,我们也可以在这个节点的状态信息中看到资源的占用情况:

node 的名字从上面 describe pods 的结果中可以看到,也可以使用命令 kubectl get pod --output=wide 查看 Pod 被调度到哪个节点上了。不同环境被调度的节点可能不同,请根据实际情况执行。

kubectl describe node kubernetes-worker

image-1655176236187

这里面我们可以看到当前 Non-terminated Pods 一共有三个,两个是集群的,还有我们刚才创建的 wp。后面也列出了它的资源申请量,以及所占百分比。下图中的 Allocated Resources 部分也显示此机器目前的资源使用情况。这些信息在我们查看集群状态,机器状态以及排查问题时非常有用。

Qos

刚才我们创建的 Pod 在 describe 的信息里有一个字段叫 Qos Class,它的值是 Burstable。这是由 Pod 的 requests 和 limits 设计所决定的一个字段。表示了 Kubernetes 对不同的 Pod 因其 requests/limits 设置而对其运行情况的保障。

Qos 全称是 Quality of Service,服务质量的意思。熟悉计算机网络的同学应该也看到过 Qos 的概念,K8S 里的 Qos 名称一致,但是表示的具体内容不同。

K8S 的 Qos 具体分为以下几类:

由此可见,K8S 的 Qos 其实是类似于 Linux 中进程 priority 的一个概念,通过 requests 值和 limits 值的设置,让用户自己选择去将其应用划分为不同的优先级。我们上面使用的例子就是一个 Burstable 的 pod。下面我们看看其他的例子。

Guaranteed

/home/shiyanlou 目录下新建 qos-demo.yaml 文件,并向其中写入如下代码:

apiVersion: v1
kind: Pod
metadata:
  name: qos-demo
spec:
  containers:
    - name: qos-demo-ctr
      image: nginx
      resources:
        limits:
          memory: '200Mi'
          cpu: '700m'
        requests:
          memory: '200Mi'
          cpu: '700m'

这个示例中,requests = limits,在命令行中执行如下命令进行创建:

kubectl create -f qos-demo.yaml
kubectl describe pods qos-demo

image-1655176251706

可以看到其 Qos Class 是 Guaranteed。

BestEffort

/home/shiyanlou 目录下新建 qos-demo-2.yaml 文件,并向其中写入如下代码:

apiVersion: v1
kind: Pod
metadata:
  name: qos-demo-2
spec:
  containers:
    - name: qos-demo-3-ctr
      image: nginx

在命令行中执行如下命令进行创建:

kubectl create -f qos-demo-2.yaml
kubectl describe pods qos-demo-2

image-1655176265045

可以看到其 QosClass 为 BestEffort,当资源不足时这个 Pod 会被优先杀死。

启动命令

我们在使用 docker run 的时候可以指定启动命令,在 Dockerfile 里也可以设置 ENETRYPOINT 和 RUN 指令。在 pod 中,kubernetes 使用了更加简单的 command / args 参数来设置,它与 docker 使用的参数对应如下:

image-1655176275284

之所以要介绍这部分,是因为本身镜像层面的 entrypoint 和 cmd 就容易混淆。再加上 pod 又抽象出了新的参数,很容易误用出 bug。上面的几条规则大概意思如下:

一般来说,我们最好将启动命令在镜像里设置好,这样就不用在 pod 里设置。如果要在 pod 里面设置,最好填上 command,这样就完全以 pod 的参数为准,方便理解。下面看个例子:

/home/shiyanlou 目录下新建 commands.yaml 文件,并向其中写入如下代码:

apiVersion: v1
kind: Pod
metadata:
  name: command-demo
  labels:
    purpose: demonstrate-command
spec:
  containers:
    - name: command-demo-container
      image: debian
      command: ['printenv']
      args: ['HOSTNAME', 'KUBERNETES_PORT']
  restartPolicy: OnFailure

在命令行中执行如下命令:

kubectl create -f commands.yaml
# 查看 command-demo 是否是 Running 或 Completed 状态
kubectl get pods -w
kubectl logs command-demo

image-1655176289253

image-1655176296354

这个例子中我们就是将 command 和 args 都设置了,默认的 debian 的启动命令是 bash,在 pod 里面没有什么实际的用途。这里我们使用了 printenv 命令来打印出来 HOSTNAMEKUBERNETES_PORT 两个环境变量的值。

健康检查

早期的 Docker 是没有原生的健康检查的,在 1.12 版本之后也加入了健康检查的配置,可见它也意识到了健康检查的重要性,而 pod 在一开始就引入了对健康检查的支持。通常我们会使用容器来运行长时间运行的服务,比如 http 服务、cache 等,如果没有健康检查,很有可能容器仍在运行,但是服务已经不能正常工作了。要知道,容器运行正常并不等于容器里运行的应用或服务也正常,所以我们需要健康检查来统一两者的状态。

在基础的健康检查的概念之上,kubernetes 提供了更加精细的概念。分别是存活性检查和可用性检查,分别介绍如下:

因为在一般的应用场景下,都会用负载均衡后面挂上多个实例来达到分担流量以及稳定性保障的目的。可用性检查就是用来保证这个 Pod 是否可以提供服务,并被挂载在负载均衡后面。

一般的应用不会有这么精细的区分,这时候存活性检查和可用性检查用同样的配置即可。

通常来讲,应用对外提供服务都是通过 tcp 端口或者 http 端口,比如 Nginx 提供 http 服务,Redis 通过 6379 端口提供服务。对于这样的服务,我们可以通过检查其端口是否开启,http endpoint 是否可以访问来判断其健康状态。

有的服务只提供集群内部访问,不需要对提供对外的网络端口,这时候可以通过 exec 命令进去 Pod 之后执行相应的命令来检查。kuberentes 提供了对这几种方式的支持,通过 pod 所在节点的 kubelet 组件来执行这些检查,下面我们可以逐一实验。

EXEC

exec 就是指通过 exec 到容器中执行命令来进行健康检查,我们看一个示例。

/home/shiyanlou 目录下新建 liveness-exec.yaml 文件,并向其中写入如下代码:

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-exec
spec:
  containers:
    - name: liveness
      image: busybox
      args:
        - /bin/sh
        - -c
        - touch /tmp/healthy; sleep 30; rm -rf /tmp/healthy; sleep 600
      livenessProbe:
        exec:
          command:
            - cat
            - /tmp/healthy
        initialDelaySeconds: 5
        periodSeconds: 5

注意:

  1. 例子中为了演示,让 command 不断地创建删除文件,健康检查去检查这个文件。
  2. 这个例子中只配置了 livenessProbe。
  3. command: 就是健康检查执行的命令。如果执行结果状态码为 0,就认为通过,其它的认为失败。
  4. periodSecounds: command 执行的间隔,健康检查是一个持续性的过程,需要反复执行。
  5. initialDelaySeconds:因为好多程序启动时有初始化时间。比如 java 程序,初始化一般就比较慢。这时候如果立马做健康检查就不太合适,initialDelaySeconds 就是设置了一个合理的时间,等过了这个时间再做检查。

在这个例子中,容器启动后,创建 /tmp/healthy 这个文件。30 秒内,健康检查是 ok 的,之后文件被删,健康检查就会出问题了。

在命令行中执行如下命令进行创建:

kubectl create -f liveness-exec.yaml

然后反复执行:

kubectl describe pods liveness-exec

image-1655176312954

可以看到,刚开始的 event 都是 Normal 的,在删除文件之后,变成 Warning,因为 Livenss probe 失败了。失败之后,Pod 也会被重启,我们可以看下:

kubectl get pods | grep liveness

image-1655176320242

这个是过了一段时间之后看到的,已经重启了 3 次了。等的时间越久,重启的次数也就越多。

HTTP

http 检查即是通过调用 http 服务的某个路径,然后根据错误码来判定。 http status code 的 200-400 代表成功,其它代表失败。

/home/shiyanlou 目录下新建 liveness-http.yaml 文件,并向其中写入如下代码:

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-http
spec:
  containers:
    - name: liveness
      image: cnych/liveness
      args:
        - /server
      livenessProbe:
        httpGet:
          path: /healthz
          port: 8080
          httpHeaders:
            - name: X-Custom-Header
              value: Awesome
        initialDelaySeconds: 3
        periodSeconds: 3

依然是 livenessProbe:

这个镜像里,我们依然动态修改了返回结果,用来演示 healtcheck 的不同效果。其实现代码如下图所示:

http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
    duration := time.Now().Sub(started)
    if duration.Seconds() > 10 {
        w.WriteHeader(500)
        w.Write([]byte(fmt.Sprintf("error: %v", duration.Seconds())))
    } else {
        w.WriteHeader(200)
        w.Write([]byte("ok"))
    }
})

前 10s 返回 200, 之后返回 500。

在命令行中执行如下命令进行创建:

kubectl create -f liveness-http.yaml
kubectl describe pods liveness-http

看下 pod 的 event:

image-1655176333116

event 中通过事件就可以看到健康检查已经失败了,kubelet 在重启 pod。

image-1655176339880

TCP

对于监听 tcp 端口的服务,我们可以尝试与这个端口建立连接。如果成功,则认为服务正常。

/home/shiyanlou 目录下新建 liveness-tcp.yaml 文件,并向其中写入如下代码:

apiVersion: v1
kind: Pod
metadata:
  name: goproxy
  labels:
    app: goproxy
spec:
  containers:
    - name: goproxy
      image: googlecontainer/goproxy:0.1
      ports:
        - containerPort: 8080
      readinessProbe:
        tcpSocket:
          port: 8080
        initialDelaySeconds: 5
        periodSeconds: 10
      livenessProbe:
        tcpSocket:
          port: 8080
        initialDelaySeconds: 15
        periodSeconds: 20

这个实例中配置了 livenessProbe 和 readinessProbe,从具体的配置上来看,与 http 的配置是很像的。只是其配置中需要指明的是端口。

在命令行中执行如下命令进行创建查看:

kubectl create -f liveness-tcp.yaml
kubectl describe pods goproxy

image-1655176349582

这个地方因为镜像配置的是一直运行,所以结果 healthcheck 会一直过的。这里着重看下 liveness 和 readiness 的一些其他默认值:

多容器 Pod

刚才我们的例子中都是单容器的 pod,也是最常用的。在某些情况下,多容器的 Pod 更能适应需求。这样的模式通常是一个容器用来主要提供服务,另一个做一些其他的零碎工作。比如:

  1. 收集日志。这样不用修改原来的服务,可以用另外的容器来适配各种日志收集系统。
  2. 做 Proxy。比如我们的程序需要访问外部的服务,可以固定配置为 localhost,由其他的容器来决定如何转发请求,相当于将动态配置的需求交由其他的容器来做。

综合来讲,这样做的好处就是让主要的服务容器不做修改,就能更好地适配各种系统。另外,也能较好地做到职责分离,不需要由一个容器来处理过多的任务。

下面看一个例子,在 /home/shiyanlou 目录下新建 two-containers.yaml 文件,并向其中写入如下代码:

apiVersion: v1
kind: Pod
metadata:
  name: two-containers
spec:
  restartPolicy: Never

  volumes:
    - name: shared-data
      emptyDir: {}

  containers:
    - name: nginx-container
      image: nginx
      volumeMounts:
        - name: shared-data
          mountPath: /usr/share/nginx/html

    - name: debian-container
      image: debian
      volumeMounts:
        - name: shared-data
          mountPath: /pod-data
      command: ['/bin/sh']
      args:
        ['-c', 'echo Hello from the debian container > /pod-data/index.html']

这个例子中我们使用了 volume,docker 中也有这个概念,二者是类似的。 Pod 中的 volume 是各个容器共享的,我们可以用它来让各个容器交互。在这个例子中,debian-container 往 /pod-data/ 里面写入了一个文件,而这个目录是两个 container 共享的 volume,nginx 将其挂载到其配置的目录。

在命令行中执行如下命令:

kubectl create -f two-containers.yaml
kubectl get pods -w

需要注意到 READY 下面的 0/2 ,这个 2 就是表示两个容器的意思。一般情况下,需要等到 2 个容器都是 ready 之后 Pod 才会处于运行中。在这个例子中,因为一个 container 运行完程序就结束了。所以其状态会如下所示:

image-1655176364824

这时候我们可以 exec 进去看看能否读取到 debian-container 写入的文件:

kubectl exec -it two-containers -c nginx-container -- bash
cd /usr/share/nginx/html
ls
cat index.html

image-1655176372710

kubectl 的 exec 命令非常类似于 docker 的 exec,区别之处在于其 exec 的目标是 Pod。当 Pod 是单个容器时,其结构与 docker exec 是一致的。当 Pod 有多个容器时,就需要指明具体的目标 container 是哪个。默认值是 yaml 里的第一个容器,这里为了演示,加上了明确的参数。

可以从输出的结果来看,在 nginx-container 里面,可以读取到 debian-container 写入的数据。

InitContainers

initContainers 是 Pod 提供的另外一个非常有用的功能。它的结构与普通的 container 类似,但是作用上有很大的区别:

那么它的用处在哪呢?想象一下以下的业务场景:

综合来讲,就是说某个服务的运行,需要一些如服务依赖、文件等前置条件才能正常运行。在没有 initContainers 的情况下,我们需要在正常的 container 里做一些 hack 才能做到,这样不好维护并且比较复杂。有了 initContainers,不管是结构上还是可维护性上都会好很多。

我们看一个例子,在 /home/shiyanlou 目录下新建 init.yaml 文件,并向其中写入如下代码:

apiVersion: v1
kind: Pod
metadata:
  name: init-ctr-demo
spec:
  volumes:
    - name: data
      emptyDir: {}
  initContainers:
    - name: init-1
      image: busybox
      command: ['sh', '-c', 'echo start 1 >> /data/file']
      volumeMounts:
        - name: data
          mountPath: /data
    - name: init-2
      image: busybox
      command: ['sh', '-c', 'echo start 2 >> /data/file']
      volumeMounts:
        - name: data
          mountPath: /data
  containers:
    - name: busybox
      image: busybox
      command: ['sh', '-c', 'sleep 1000']
      volumeMounts:
        - name: data
          mountPath: /data

这个例子如上面的多 container 示例一样,用 volume 来共享数据。这个 Pod 一共包含了两个 initContainers,分别往 volume 写数据。执行完之后,我们就应该能在正常的 container 里观察到这些数据。

在命令行中执行如下命令:

kubectl create -f init.yaml
# 继续 watch pods 的变更
kubectl get pods -w

image-1655176387540

在观察的过程中,我们可以看到。 READY 里面是不显示 initContainers 信息的。但是 STATUS 里面的状态,会显示出具体的进度。 Init:0/2 表示有两个 initContainers,正在执行第一个,Init:1/2 表示正在执行第二个。 PodInitializing 表示 initContainers 已经执行完毕,开始执行正常的 containers。

如果大家看不到 STATUS 的状态变化可能是因为其它的 pod 干扰了,可以执行 kubectl delete pod <pod-name> 删除之前创建的多余的 pod。

等到 Running 后,我们可以 exec 到 Pod 中去观察具体的数据是否正确:

kubectl exec -it init-ctr-demo -- sh
cd /data
ls
cat file

image-1655176394910

根据结果可以发现,在 container 中能够看到在 initContainers 里面写入的数据。