Kubernetes 之 Pod

164 阅读16分钟

Pod

PodKubernetes 中最小的计算单元,拥有自己独自的 IP,主机名,进程等等。由一个或多个容器组成;这些容器共享存储、网络等资源,可以理解成同一主机上运行多个应用。每一个 Pod 都会有一个特殊的根容器,叫做 pause 容器,pause 容器对应的镜像也是属于 K8s 的一部分。

我们通过 pod 中的 pause 容器 来管理其他容器的, 因为 pause 容器会存储所有的容器状态

PodServiceVolumeNamespaceKubernetes 集群中四大基本对象,它们能够表示系统中部署的应用、工作负载、网络和磁盘资源,共同定义了集群的状态。Kubernetes 中很多其他的资源其实只对这些基本的对象进行了组合。

  • Pod:集群中的基本单元。
  • Service:解决如何访问 Pod 里面服务的问题。
  • Volume:集群中的存储卷。
  • Namespace:命名空间为集群提供虚拟的隔离作用。
apiVersion: v1
kind: Pod
metadata:
  name: busybox
  labels:
    app: busybox
spec:
  containers:
  restartPolicy: Always
    - name: busybox
      image: busybox
      command:
        - sleep
        - "3600"
      imagePullPolicy: IfNotPresent

如何定义一个Pod

K8s 中定义一个 pod 就是写一个 yaml 文件,如下:

apiVersion: v1  // 版本号
kind: Pod		// 类型
metadata: 
  name: pod name
  namespace: pod namespace
  labels:
    - name: pod label
	annotations:
	- name: pod annotation	// 自定义注释列表
spec:	// pod 中容器的详细定义
  container:
  - name: container name
    image: container image
    imagesPullPolicy: [Always|Never|IfNotPresent] // 镜像拉取策略
    command: command list
    agrs: app start params
    workingDir: work dir
    volumeMounts:
    - name: volume name
      mountPath: volume absolutely path
      readonly: boolean
	ports:
	- name: ports list name
	  containerPort: 8888
	  hostPort: 9999
	  protocol: TCP  // 可以是 TCP  UDP
	env:
	- name: env name
	  value: string
	resources:
	  limits:
	    cpu: string
	    memory: string
	  requeste:
	    cpu: string
	    memory: string
	livenessProbe:		// 健康检查设置
      exec:
        command: [string]
      httpGet: 			// 通过 httpGet  方式检查
        path: string
        port: string
        host: string
        scheme: string
        httpHeaders: 
        - name: httpHeaders name
          value: string	
      tcpSocket:		// 通过 tcpSocket 方式检查
        port: 80
      initialDelaySeconds: 0	// 首次检查时间
      timeoutSeconds: 0			// 超时检查时间
      periodSeconds: 0			// 检查间隔时间
      successTreshold: 0
      failuerTreshold: 0
      securityContext:			// 安全配置
        privileged: false
    restartPolicy: [Always|Never|OnFailure]    //重启策略
    nodeSelector: object
    imagePullSecrets:
    - name: string
    hostNetwork: false			// 是否使用主机网络模式?
  volumes:						// 存储卷
  - name: volumes list name
    emptyDir: {}
    hostPath:				// pod 所在主机的目录,用于挂载
      path: string
    secret:					// secret 类型 存储卷
      secretName: secret name
      item:
      - key: specific key
        path: key path
    configMap:				// configmap 类型存储卷
      name: string
      items:
      - key: specific key
        path: string

Pod 的基本分类

静态 Pod

  • Kubelet 进行管理。
  • 存在于特定 Node 上的 Pod
  • 不能通过 Api Server 管理。
  • 无法 ReplicationControllerDeploymentDaemonset 进行关联。
  • Kubelet 无法对该 Pod 进行健康检查。

普通 Pod

  • 一旦创建,就会被放到 etcd 存储中。
  • 会被 K8s 中的 Master 调度到某个 Node 上面并绑定,该 Node 上的 Kubelet 会实例化成 Docker 容器 运行起来。
  • K8s 会对 Pod 做健康检查,若 Pod 中的容器暂停或者异常,K8s 会将他们重启。
  • Pod 所在的 Node 宕机了,那么 K8s 会将 该 Node 的所有 Pod 重新调度到别的节点上面。

Pod 的多种类型

图片

ReplicationController

ReplicationController 用来确保容器应用的副本数量始终保持在用户定义的副本数,即如果有容器异常退出,会自动创建新的 Pod 来代替,而如果异常多出现的容器会自动回收。

ReplicaSet

在新版本(相对而言的较优方式)的 Kubernetes 中建议使用 ReplicaSet 来取代 ReplicationController 来管理 Pod。虽然 ReplicaSetReplicationController 并没有本质上的不同,只是名字不一样而已,唯一的区别就是 ReplicaSet 支持集合式的 selector,可供标签筛选。

虽然 ReplicaSet 可以独立使用,但一般还是建议使用 Deployment 来自动管理 ReplicaSet 创建的 Pod,这样就无需担心跟其他机制的不兼容问题。比如 ReplicaSet 自身并不支持滚动更新(rolling-update),但是使用 Deployment 来部署就原生支持。

Deployment

DeploymentPodReplicaSet 提供了一个声明式定义方法,用来替代以前使用 ReplicationController 来方便且便捷的管理应用。主要的应用场景,包括:滚动升级和回滚应用、扩容和缩容、暂停和继续。

HPA

HPA 仅仅适用于 DeploymentReplicaSet,在 V1 版本中仅支持根据 Pod 的 CPU 利用率扩缩容,在新版本中,支持根据内存和用户自定义的 metric 动态扩缩容。

StatefulSet

StatefulSet 是为了解决有状态服务的问题,相对于 DeploymentReplicaSet 而已。其主要的使用场景,包括:稳定的持久化存储、稳定的网络标识、有序部署、有序收缩。

DaemonSet

DaemonSet 确保全部或者一些 Node 上面运行一个 Pod 副本。当有 Node 加入集群的时候,也会为它们新加一个 Pod。当有 Node 从集群中移除的时候,这些 Pod 也会被回收。删除 DaemonSet 将会删除它所创建的所有 Pod

使用 DaemonSet 的典型场景就是,在每个节点运行日志收集、运行监控系统、运行集群存储等服务,只要新加进来的节点都需要运行该服务。

Job

Job 负责批处理任务,仅执行一次的任务,它保证批处理任务的一个或者多个 Pod 成功结束,才会返回成功。

Cront Job

Cront Job 管理是基于时间的 Job,即在给定时间点只运行一次,且周期性的在给定时间点运行特定任务。

Pod的状态

Pending

Pending状态表示Api Server已经创建好了该Pod,但是Pod中的一个或者多个容器还没有被创建或者现在正在下载镜像。 Running Pod内所有的容器已经创建好了,至少有一个容器是运行状态、正在启动状态或者是正在重启状态。 Completed Pod内的容器均已执行正常退出,且不会再次重启了 。 Failed Pod内的容器均已执行正常退出,至少有一个容器是退出失败的。 Unknown 由于某种原因无法获取到Pod的状态,例如网络问题等等。

Pod 的内部结构

Pod 代表着集群中运行的进程:共享网络、共享存储。

在同一个 Pod 中,有几个概念特别值得关注,首先就是容器,在 Pod 中其实可以同时运行一个或者多个容器,这些容器能够共享网络、存储以及 CPU/内存等资源。

首先,我们需要知道的是,每个 Pod 都有一个特殊的被称为根容器的Pause容器。Pause容器对应的镜像属于 Kubernetes平台的一部分,通过 Pause 容器使工作在对应 Pod 的容器之间可以共享网络、共享存储。 图片

Pod 共享资源

使用 Pause 容器作为 Pod 根容器,以它的状态代表整个容器组的状态;其次,Pod 里的多个业务容器共享 Pause 容器的 IP 地址,共享 Pause 容器挂接的 Volume 资源。

图片

共享存储资源

可以为一个 Pod 指定多个共享的 Volume 资源。Pod 中的所有容器都可以访问共享的 volume 资源。Volume 也可以用来持久化 Pod 中的存储资源,以防容器重启后文件丢失。

共享网络资源

每个 Pod 都会被分配一个唯一的 IP 地址。Pod 中的所有容器共享网络空间,包括 IP 地址和端口。Pod 内部的容器可以使用 localhost 互相通信。Pod 中的容器与外界通信时,必须分配共享网络资源,例如使用宿主机的端口映射。

veth 设备的特点

一个设备收到协议栈的数据发送请求后,会将数据发送到另一个设备上去。

  • veth 和其它的网络设备都一样,一端连接的是内核协议栈。
  • veth 设备是成对出现的,另一端两个设备彼此相连。
# 物理网卡eth0配置的IP为192.168.1.11
# 而veth0和veth1的IP分别是192.168.2.11192.168.2.10

+----------------------------------------------------------------+
|                                                                |
|       +------------------------------------------------+       |
|       |             Newwork Protocol Stack             |       |
|       +------------------------------------------------+       |
|              ↑               ↑               ↑                 |
|..............|...............|...............|.................|
|              ↓               ↓               ↓                 |
|        +----------+    +-----------+   +-----------+           |
|        |   eth0   |    |   veth0   |   |   veth1   |           |
|        +----------+    +-----------+   +-----------+           |
|192.168.1.11  ↑               ↑               ↑                 |
|              |               +---------------+                 |
|              |         192.168.2.11     192.168.2.10           |
+--------------|-------------------------------------------------+
               ↓
         Physical Network

Pod的网络通信

集群网络解决方案: Kubernetes + Flannel

Kubernetes 的网络模型假定了所有 Pod 都在一个直接连通的扁平的网络空间中,这在 GCE(Google Compute Engine)里面是现成的网络模型,Kubernetes 假定这个网络已经存在了。而在私有云搭建 Kubernetes 集群,就不能假定这个网络已经存在了。我们需要自己实现这个网络假设,将不同节点上的 Docker 容器之间的互相访问先打通,然后才能正常运行 Kubernetes 集群。

  • 同一个 Pod 内多个容器之间通过回环网络(lo - 127.0.0.1)进行通信。
  • Pod 之间的通讯,则是通过 Overlay Network 网络进行通信。
  • PodService 之间的通讯,则是各节点的 iptableslvs 规则。

FlannelCoreOS 团队针对 Kubernetes 设计的一个网络规划服务,简单来说,它的功能就是让集群中的不同节点主机创建的 Docker 容器都具有全集群唯一的虚拟 IP 地址。而且它还能在这些 IP 地址之间建立一个覆盖的网络(Overlay Network),通过这个覆盖网络,将数据包原封不动地传递给目标容器内。 图片 图片

Pod网络通信方式

同一个 Pod 内部通讯:

同一个 Pod 共享同一个网络命名空间,共享同一个 Linux 协议栈。

不同 Pod 之间通讯:

Pod1Pod2 在同一台 Node 主机,由 docker0 网桥直接转发请求到 Pod2 上面,- 不经过 Flannel 的转发。

Pod1Pod2 不在同一台 Node 主机,Pod 的地址是与 docker0 在同一个网段的,但 docker0 网络与宿主机网卡是两个完全不同的 IP 网段,并且不同的 Node 之间的通讯只能通过宿主机的物理网卡进行。将 PodIP 地址和所在 NodeIP 地址关联起来,通过这个关联让 Pod 可以互相访问。

PodService 的网络

目前基于性能考虑,全部为 iptableslvs 维护和转发。

Pod 到外网

Pod 向外网发送请求,查找路由表,转发数据包到宿主机的网卡,宿主机网卡完成路由选择之后,iptableslvs 执行 Masquerade,把源 IP 地址更改为宿主机的网卡的 IP 地址,然后向外网服务器发送请求。

外网访问 Pod

通过 Service 服务来向外部提供 Pod 服务。

ETCD 之于 Flannel 提供说明:

  • 存储管理 Flannel 可分配的 IP 地址段资源。
  • 监控 ETCD 中每一个 Pod 的实际 IP 地址,并在内存中建立维护 Pod 节点的路由表。 图片

Pod的生命周期

生命周期 Pod的生命周期涉及以下几个概念:

  • Init Container(初始化容器)。
  • 容器的两个钩子函数:PostStart 以及 PreStop
  • 容器健康检查的两个探针:liveness probe(存活探针)和 readiness probe(就绪探针)

Init Container(初始化容器)

Init Container 就是用来做初始化工作的容器,可以是一个或者多个,如果有多个的话,这些容器会按定义的顺序依次执行,只有所有的 Init Container 执行完后,主容器才会被启动。我们知道一个 Pod 里面的所有容器是共享数据卷和网络命名空间的,所以 Init Container 里面产生的数据可以被主容器使用到的。

Pod的生命周期图中我们可以直观的看到 PostStartPreStop 包括 livenessreadiness 是属于主容器的生命周期范围内的,而 Init Container 是独立于主容器(Main C)之外的,当然他们都属于 Pod 的生命周期范畴之内的。

Init Container(初始化容器)应用场景

解决服务依赖

可以用来解决服务之间的依赖问题,比如我们有一个 Web 服务,该服务又依赖于另外一个数据库服务,但是在我们启动这个 Web 服务的时候我们并不能保证依赖的这个数据库服务就已经启动起来了,所以可能会出现一段时间内 Web 服务连接数据库异常。 要解决这个问题的话我们就可以在 Web 服务的 Pod 中使用一个Init Container,在这个初始化容器中去检查数据库是否已经准备好了,准备好了过后初始化容器就结束退出,然后我们的主容器 Web 服务被启动起来,这个时候去连接数据库就不会有问题了。

做初始化配置

比如集群里检测所有已经存在的成员节点,为主容器准备好集群的配置信息,这样主容器起来后就能用这个配置信息加入集群。

其它场景

如将 pod 注册到一个中央数据库、配置中心等。

服务依赖场景应用示例

下面以服务依赖的场景下初始化容器的使用方法,Pod 的定义方法:

apiVersion: v1
kind: Pod
metadata:
name: init-pod-demo
labels:
  app: init
spec:
containers:
- name: init-container
  image: busybox
  command: ['sh', '-c', 'echo app is running! && sleep 600']
initContainers:
- name: init-myservice
  image: busybox
  command: ['sh', '-c', 'until nslookup myservice; do echo waiting for myservice; sleep 3; done;']
- name: init-mydb
  image: busybox
  command: ['sh', '-c', 'until nslookup mydb; do echo waiting for mydb; sleep 3; done;']

Service 对应的 yaml 文件 内容如下:

kind: Service
apiVersion: v1
metadata:
name: myservice
spec:
ports:
- protocol: TCP
  port: 80
  targetPort: 6376
---
kind: Service
apiVersion: v1
metadata:
name: mydb
spec:
ports:
- protocol: TCP
  port: 80
  targetPort: 6377

可以先创建上面的 Pod,查看 Pod 的状态,然后再创建 Service,看下前后状态有什么不一样。

Pod 启动过程中,初始化容器会按顺序在网络和数据卷初始化之后启动。每个容器必须在下一个容器启动之前成功退出。

如果由于运行时或失败退出,导致容器启动失败,它会根据 PodrestartPolicy 策略进行重试。 如果 PodrestartPolicy 设置为 AlwaysInit 容器失败时会不断的重启。

Pod 钩子函数

Pod 中通常有两种钩子函数可以使用:

  • PostStart:这个钩子在容器创建后会被执行。通常用于一些环境准备和资源部署等。不过需要注意的是如果钩子执行太长时间会导致容器不能正常运行,以至于被挂起的状态。
  • PreStop:这个钩子在容器终止之前会被调用。它是阻塞的,意味着它是同步的, 所以它必须在删除容器的调用发出之前完成。主要用于优雅关闭应用程序、通知其他系统等。

如果 PostStart 或者 PreStop 钩子执行过程中失败, 它会杀死相应的容器。所以我们应该让钩子函数尽可能的轻量。

实现 PostStartPreStop 的方式:

  • Exec 方式 :- 通过执行一段特定的命令来实现,该命令消耗的资源会被计入容器。
  • HTTP 方式 :对容器上特定的端点执行 HTTP 请求。

案例一:PostStart

在以下示例中,定义了一个 NginxPod,其中设置了 PostStart 钩子函数,即在容器创建成功后,写入一句话到 /usr/share/message 文件中。

apiVersion: v1
kind: Pod
metadata:
  name: hook-demo1
spec:
  containers:
  - name: hook-demo1
    image: nginx
    lifecycle:
      postStart:
        exec:
          command: ["/bin/sh", "-c", "echo hello postStart handler > /usr/share/message"]

案例二:PreStop

当有请求需要删除含有 Pod 的资源对象时(比如 Deployment 等),为了让应用程序可以优雅的关闭(即让正在处理的程序完成后再关闭),K8S 提供两种信息通知来处理:

  • 默认情况下 K8s 会通知 node 执行 docker stop 命令,然后 docker 会先向容器中 PID 为 1 的进程发送系统信号 SIGTERM,等待容器中的应用程序终止执行。如果等待时间达到设定的超时时间,或者默认超时时间(30s),则会继续发送 SIGKILL 的系统信号强行 kill 掉进程。
  • 使用 PreStop 钩子函数:在发送终止信号之前会执行该钩子函数进行关闭资源。

比如我们定义了一个 NginxPod,其中设置了 PreStop 钩子函数,即在容器退出之前,优雅的关闭 Nginx:

apiVersion: v1
kind: Pod
metadata:
  name: hook-demo2
spec:
  containers:
  - name: hook-demo2
    image: nginx
    lifecycle:
      preStop:
        exec:
          command: ["/usr/sbin/nginx","-s","quit"]

---
apiVersion: v1
kind: Pod
metadata:
  name: hook-demo2
  labels:
    app: hook
spec:
  containers:
  - name: hook-demo2
    image: nginx
    ports:
    - name: webport
      containerPort: 80
    volumeMounts:
    - name: message
      mountPath: /usr/share/
    lifecycle:
      preStop:
        exec:
          command: ['/bin/sh', '-c', 'echo hello preStop Handler > /usr/share/message']

Pod 健康检查(指针)

除了PostStart 与 PreStop这两个钩子函数以外,还有一项配置会影响到容器的生命周期的,那就是健康检查的探针,在 Kubernetes 集群当中,我们可以通过配置 liveness probe(存活探针)和 readiness probe(就绪探针)来影响容器的生存周期。

exec 方式检测容器存活

下面先来看下liveness probe(存活探针)的使用方法,首先我们用 exec 执行命令的方式来检测容器的存活,如下:

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

这里用到了 livenessProbe,然后通过 exec 执行一段命令,其中 periodSeconds 属性表示让 kubelet 每隔3s执行一次存活探针,也就是每3s执行一次上面的 cat /tmp/healthy 命令,如果命令执行成功了,将返回 0,那么 kubelet 就会认为当前这个容器是存活的并且是可监控,如果返回的是非 0 值,那么 kubelet 就会把该容器杀掉然后重启它。

再来看下 initialDelaySeconds 表示在第一次执行探针的时候要等待 6 秒,这样能够确保我们的容器能够有足够的时间启动起来。试想下,如果第一次执行探针等候的时间太短,就很有可能容器还没正常启动起来就进行存活探针检查,这样检查始终都是失败的,从而会不断的重启下去。所以设置一个合理的 initialDelaySeconds 就非常重要。

HTTP方式配置存活探针

另外我们可以使用 HTTP GET 请求来配置我们的存活探针,这里使用一个liveness镜像来验证下:

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

同样的,根据 periodSeconds 可以知道 kubelet 需要每隔3s执行一次 liveness probe,该探针将向容器中的 server8080 端口发送一个 HTTP GET 请求。如果 server/healthz 路径的 handler 返回一个成功的返回码,kubelet 就会认定该容器是健康存活的。

如果返回失败的返回码,kubelet 将杀掉该容器并重启。initialDelaySeconds 指定 kubelet 在该执行第一次探测之前需要等待3s。

从上面的 YAML 文件我们可以看出 readiness probe 的配置跟 liveness probe 大同小异。不同是使用 readinessProbe 而不是 livenessProbe

两者如果同时使用的话就可以确保流量不会到达还未准备好的容器,如果应用程序出现了错误,则会重新启动容器。

这就是 liveness probe(存活探针)和 readiness probe(就绪探针)的使用方法。