k8s 读书笔记 - 深入掌握 Pod

什么是 Pod

Pod 是可以在 Kubernetes 中创建和管理的、最小的可部署的计算单元。

在同一个 context 下,应用可能还会有独立的 cgroup 隔离机制,一个 Pod 是一个容器环境下的 “逻辑主机”

Pod 是一组容器单元, 这些容器共享存储、网络、以及怎样运行这些容器的声明。 Pod 中的内容总是并置(colocated)的并且一同调度,在共享的 context(上下文) 中运行。而 Pod 中的这些应用容器可能还有独立的 cgroup 隔离机制,一个 Pod 相当于应用的 “逻辑主机”,其中包含一个或多个应用容器, 这些容器相对紧密地耦合在一起。 在非云环境中,在相同的物理机或虚拟机(VM)上运行的应用类似于在同一逻辑主机上运行的云应用。

k8s 读书笔记 - 深入掌握 Pod_第1张图片

我们可以借助上面这张图(豌豆荚)来形象的理解 Pod,豌豆荚的整体外壳就等同于 Pod,豌豆荚里面的豆子就等同于应用容器(App Container),豆子被豌豆荚包裹这就类似于应用容器运行在 Pod 中。

除了应用容器,Pod 还可以包含在 Pod 启动期间运行的 Init Container。 也可以在集群中支持 临时性容器 的情况下,为调试注入 临时性容器 进行故障排查。

Pod 的设计思想

  • k8s 中创建和管理的、最小的可部署的计算单元。
  • 一组容器(Container)的单元集合,可以包含一个或多个应用容器。
  • 同一个 Pod 中的 Container 共享的是同一个网络命名空间(Network Namespace),并且可以声明共享同一个 Volume。
  • Pod 是短暂的,用完即销毁。

Pod 的组成结构

每个 Pod 创建的时候都会包含一个 Pause 容器和用户的业务容器(或者应用容器,App Container),Pod 的基本组成结构如下图所示:
k8s 读书笔记 - 深入掌握 Pod_第2张图片

既然 Pod 是 k8s 中最小的运行单元,那么在一个 Pod 中包含几个容器呢?

答:至少两个,分别是 1 个基础容器(pause)+ 应用容器(1个或者多个)。

Pod 的 YAML 文件定义示例

此处我们以 asp.net core webapi 项目为例,定义 Pod 的 yaml 文件结构如下:

apiVersion: v1                     # API 版本号
kind: Pod                          # 资源类型
metadata:                          # meta 信息
  name: myweb
  labels:                          # 标签 
    name: myweb
spec:
  containers:
  - name: myweb-aspnetcore           # 容器名称
    image: aspnetcore-6.0:v1.0.0     # 镜像:版本
    imagePullPolicy: IfNotPresent    # 如果镜像不存在则拉取
    ports:
    - containerPort: 5001            # 容器暴露端口
    env:                             # 环境变量
    - name: ASPNETCORE_ENVIRONMENT   # asp.net core 运行时环境
      value: Development             # 开发环境
    - name: ASPNETCORE_URLS          # 设置 asp.net core 启动端口
      value: http://localhost:5001/  # 本地主机 5001 端口

Pod IP:containerPort】就组成了一个新的概念 —— Endpoint,它代表此 Pod 里的一个服务进程的对外通信地址。之前的文章介绍过,这里不再叙述。

Pod 存在的意义

容器的设计是单进程原则,推荐一个容器里只有一个应用在运行。然而单进程模型在某些特定场景下,满足不了应用的需求,因此在这种情况下有了 Pod 的存在,这也很好的诠释了 Pod 相当于应用的 “逻辑主机”

例如,你可能有一个容器,为共享卷中的文件提供 Web 服务器支持,以及一个单独的 “sidecar” 容器负责从远端更新这些文件,这两个容器在功能上进行紧密的协调,如下图所示:

k8s 读书笔记 - 深入掌握 Pod_第3张图片

Pod 的 context 可以理解成多个 linux 命名空间的联合

  • IPC 命名空间(同一个 Pod 中的应用可以通过 VPC 或者 POSIX 进行通信)
  • Network Namespace / 网络命名空间(同一个 Pod 的中的应用对相同的 IP 地址和端口有权限)
  • PID 命名空间(同一个 Pod 中应用可以看到其它进程)
  • UTS 命名空间(同一个 Pod 中的应用共享一个主机名称 Hostname )

前面我们介绍了 Pod 的基本概念和组成结构,那么 Kubernetes 设计这样的 Pod 概念和特殊组成结构有什么用意?

  • 原因一:在一组容器作为一个单元的情况下,难以对整体的容器简单地进行判断及有效地进行管理。比如,一个容器死亡了,此时是算整体挂了么?那么引入与业务无关的 Pause 容器作为 Pod 的根容器,以它的状态代表着整个容器组的状态,这样就可以解决该问题。
  • 原因二:Pod里的多个业务容器共享pause容器的IP,共享Pause容器挂载的volume,这样简化了业务容器之间的通信问题,也解决了容器之间的文件共享问题。

Pod 对象分类

按照 Pod 的运行方式,可以把 Pod 分为以下两类:

  • 自主式 Pod(也叫静态 Pod/ Static Pod),自我管理,不能自我修复,不会自愈。
  • 普通 Pod,即控制器管理的 Pod,刚好和上面的形成对立面。

自主式 Pod(Static Pod)

自我管理的 Pod,创建以后仍然需要提交给 kube-apiserver,由 kube-apiserver 接收以后借助于 Scheduler(调度器) 将其调度至指定的 node 节点,再由该 node 启动被调度过来的 Pod。
如果该 Pod 出现故障,需要重启容器则由 kubelet 来完成,本身不能自我修复。
如果 node 节点故障了,那么该 Pod 将会消失,Pod 不会自愈。这种方式的 Pod 无法实现全局调度,所以 不推荐使用此种 Pod

控制器管理 Pod(普通 Pod)

在 k8s 环境中,通常大多数情况下都使用该方式来管理 Pod,常见的 Pod 控制器有以下这些:

  • ReplicationController(副本控制器,简称 RC),当启动一个 Pod 时,这个 Pod 如果不够用可以再启一个副本,而后由该控制器来管理同一类 Pod 的各种副本与对象。一旦副本达不到预期目标数量就会自动增加或减少。采取多退少补的规则,精确符合我们所定义的期望。RC 同时还支持滚动更新。
  • ReplicaSet(复制集,简称 RS),下一代 RC ,是 RC 的替换,由一个名叫 Deployment 的声明式更新的控制器来管理。
  • Deployment(部署,简称 deploy),用于管理无状态的应用。
  • StatefulSet(状态集),有状态副本集可以用于管理有状态的应用。
  • DaemonSet(守护程序) 如果需要在每个 node 上运行一个副本即可用该对象。
  • Job(任务) 用于运行批处理任务、运维 / ad-hoc 任务等。
  • Cronjob(定时任务) 在 Job 的基础上,增加了时间周期,用于执行周期性的动作,例如备份、邮件、报告生成等。cron 时间配置与 linux crontab 相似

以上每种控制器都是用来实现一种独特的应用管理的。

关于 Job 的应用场景
批处理任务: 比如说你想每天运行一次批处理任务,或者在指定日程中运行。它可能是像从存储库或数据库中读取文件那样,将它们分配给一个服务来处理文件。
运维 / ad-hoc 任务: 比如你想要运行一个脚本/代码,该脚本/代码会运行一个数据库清理活动,甚至备份一个Kubernetes集群。

静态 Pod(Static Pod)

静态 Pod(Static Pod) 直接由特定节点上的 kubelet 守护进程管理, 不需要通过控制面(例如,Deployment )来管理的,对于静态 Pod 而言,kubelet 直接监控每个 Pod,并在其失效时重启。

静态 Pod 通常绑定到某个 node 节点上的 kubelet。 其主要用途是运行自托管的控制面(master)。 在自托管场景中,使用 kubelet 来管理各个独立的 控制面组件。

kubelet 自动尝试为每个静态 Pod 在 Kubernetes API 服务器上创建一个 镜像 Pod。 这意味着在节点上运行的 Pod 在 API 服务器(kube-apiserver)上是可见的,但不可以通过 API 服务器来控制。

说明:静态 Pod 的 spec 不能引用其他的 API 对象(例如: ServiceAccount、 ConfigMap、 Secret 等)。

Static Pod 创建示例

1、配置文件方式

  1. 选择一个要运行静态 Pod 的节点。
ssh k8s-node1
  1. 选择 /etc/kubernetes/manifests 目录来保存 Web 服务 Pod 的 YAML 定义文件,例如 /etc/kubernetes/manifests/static-web.yaml
# 在 kubelet 运行的节点上执行以下命令
mkdir -p /etc/kubernetes/manifests/
cat <<EOF >/etc/kubernetes/manifests/static-web.yaml
apiVersion: v1
kind: Pod
metadata:
  name: static-web
  labels:
    role: myrole
spec:
  containers:
    - name: web
      image: nginx
      ports:
        - name: web
          containerPort: 80
          protocol: TCP
EOF
  1. 配置该 node 节点上的 kubelet,使用参数 --pod-manifest-path=/etc/kubelet.d/ 执行。(或者在 Kubelet 配置文件 中添加 staticPodPath: <目录>字段)
KUBELET_ARGS="--cluster-dns=10.254.0.10 --cluster-domain=kube.local --pod-manifest-path=/etc/kubernetes/manifests/"
  1. 重启该 node 节点上的 kubelet ,使 yaml 配置文件生效。
# 在 kubelet 运行的节点上执行以下命令
systemctl restart kubelet

KubeletConfiguration 中包含 kubelet 的配置。

字段 描述
staticPodPath string staticPodPath 是指向要运行的本地(静态)Pod 的目录, 或者指向某个静态 Pod 文件的路径。默认值:“”

2、HTTP 方式

设置 kubelet 的启动参数 “–manifest-url”,kubelet 将会定期从该 URL 地址下载 Pod 的定义文件,并以 .yaml 和 .json 文件的格式进行解析,然后创建 Pod。

以上两种方式创建的 Pod 是一致的。

关于 KubeletConfiguration 更多信息,请查看:https://kubernetes.io/zh-cn/docs/reference/config-api/kubelet-config.v1beta1/#kubelet-config-k8s-io-v1beta1-KubeletConfiguration

如何查看创建的 Pod ?

方式一:

在 kubelet 运行的 node 节点上来查看正在运行的容器(包括静态 Pod):

crictl ps

输出信息:

CONTAINER       IMAGE                                 CREATED           STATE      NAME    ATTEMPT    POD ID
129fd7d382018   docker.io/library/nginx@sha256:...    11 minutes ago    Running    web     0          34533c6729106

说明:
crictl 会输出镜像 URI 和 SHA-256 校验和。NAME 看起来像: docker.io/library/nginx@sha256:0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31。

方式二:

可以在 API 服务上看到镜像 Pod:

kubectl get pods

输出信息:

NAME         READY   STATUS    RESTARTS        AGE
static-web   1/1     Running   0               2m

说明:要确保 kubelet 在 API 服务上有创建镜像 Pod 的权限。如果没有,创建请求会被 API 服务拒绝。

关于创建 Static Pod 的更多信息,请查看:https://kubernetes.io/zh-cn/docs/tasks/configure-pod-container/static-pod/

Pod 中容器分类

Pod 中的容器又能分三类:基础容器(Pause)、初始化容器(Init Container)和应用容器(App Container)。

  • 基础容器(pause):又叫 “根容器” 或 “父容器” ,给 pod 中的所有应用容器提供网络和存储资源的共享;提供 init 进程。管理整个 Pod 里的容器组的生命周期。
  • 初始化容器(Init Container):是在应用容器之前完成所有 init 容器的启动,多个 init 容器是串行启动。每个 Init 容器都必须在下一个 Init 容器启动之前成功完成启动。
  • 应用容器(App Container):提供应用程序业务。并行启动

Pod 中容器共享 Volume

由于 Pod 的设计是临时性的,用完即销毁的理念,那么 Pod 中容器 Container 运行产生的数据也会随着 Pod 的销毁而消失,为了保证 Container 里面产生的数据可以持久化保存,Pod 引入了 Volume(数据卷) 的概念,简单的理解类似于电脑磁盘的挂载。

k8s 读书笔记 - 深入掌握 Pod_第4张图片

Pod 使用 Volume 示例

示例1:配置 Pod 使用临时卷

这里我们以 redis 的 Pod 为例,使用 Volume 的 yaml 定义文件如下:

apiVersion: v1
kind: Pod
metadata:
  name: redis
spec:
  containers:
  - name: redis
    image: redis
    volumeMounts:
    - name: redis-storage
      mountPath: /data/redis
  volumes:
  - name: redis-storage
    emptyDir: {}

上面的定义的 yaml 文件中,使用的是 emptyDir 临时卷,其中包含一个 挂载点 volumeMounts,将 redis 容器产生的数据挂载到该容器内的 /data/redis 路径。

在 k8s 中 Volume 支持的类型有很多,这里不再列举,更多 Volume 的信息请查看:

  • https://kubernetes.io/zh-cn/docs/concepts/storage/volumes/
  • https://feisky.gitbooks.io/kubernetes/content/concepts/volume.html

示例2:使用 Sidecar 容器运行日志代理

你可以通过以下方式之一使用边车(Sidecar)容器:

  • 边车容器将应用程序日志传送到自己的标准输出。
  • 边车容器运行一个日志代理,配置该日志代理以便从应用容器收集日志。

2.1 使用传输数据流的 Sidecar 容器

利用边车容器向自己的 stdoutstderr 传输流的方式, 你就可以利用每个节点上的 kubelet 和日志代理来处理日志。 边车容器从文件、套接字或 journald 读取日志。 每个边车容器向自己的 stdout 和 stderr 流中输出日志。

k8s 读书笔记 - 深入掌握 Pod_第5张图片
这种方法允许你将日志流从应用程序的不同部分分离开,其中一些可能缺乏对写入 stdoutstderr 的支持。重定向日志背后的逻辑是最小的,因此它的开销几乎可以忽略不计。 另外,因为 stdout 和 stderr 由 kubelet 处理,所以你可以使用内置的工具 kubectl logs

例如,某 Pod 中运行一个容器,该容器向两个文件写不同格式的日志。 下面是这个 Pod 的配置文件:

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox:1.28
    args:
    - /bin/sh
    - -c
    - >
      i=0;
      while true;
      do
        echo "$i: $(date)" >> /var/log/1.log;
        echo "$(date) INFO $i" >> /var/log/2.log;
        i=$((i+1));
        sleep 1;
      done      
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  volumes:
  - name: varlog
    emptyDir: {}

不建议在同一个日志流中写入不同格式的日志条目,即使你成功地将其重定向到容器的 stdout 流。 推荐创建两个边车容器。每个边车容器可以从共享卷跟踪特定的日志文件, 并将文件内容重定向到各自的 stdout 流

下面是运行两个边车(Sidecar)容器的 Pod 的配置文件:

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox:1.28
    args:
    - /bin/sh
    - -c
    - >
      i=0;
      while true;
      do
        echo "$i: $(date)" >> /var/log/1.log;
        echo "$(date) INFO $i" >> /var/log/2.log;
        i=$((i+1));
        sleep 1;
      done      
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-log-1
    image: busybox:1.28
    args: [/bin/sh, -c, 'tail -n+1 -F /var/log/1.log']
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-log-2
    image: busybox:1.28
    args: [/bin/sh, -c, 'tail -n+1 -F /var/log/2.log']
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  volumes:
  - name: varlog
    emptyDir: {}

2.2 使用具有日志代理(logging-agent)功能的 Sidecar 容器

如果节点级日志记录代理程序对于你的场景来说不够灵活, 你可以创建一个带有单独日志记录代理的边车容器,将代理程序专门配置为与你的应用程序一起运行。

k8s 读书笔记 - 深入掌握 Pod_第6张图片

说明:
在边车容器中使用日志代理会带来严重的资源损耗。 此外,你不能使用 kubectl logs 命令访问日志,因为日志并没有被 kubelet 管理。

下面是两个配置文件,可以用来实现一个带日志代理的边车容器。

1、创建名称为 fluentd 的 Pod 的 ConfigMap,yaml 定义文件如下:

apiVersion: v1
kind: ConfigMap
metadata:
  name: fluentd-config
data:
  fluentd.conf: |
    
      type tail
      format none
      path /var/log/1.log
      pos_file /var/log/1.log.pos
      tag count.format1
    

    >
      type tail
      format none
      path /var/log/2.log
      pos_file /var/log/2.log.pos
      tag count.format2
    >

    **>
      type google_cloud
    >    

说明:要进一步了解如何配置 fluentd,请参考 fluentd 官方文档:https://docs.fluentd.org/

2、创建 fluentd 边车容器的 Pod , flutend 通过 Pod 的挂载卷(ConfigMap)获取它的配置数据。

apiVersion: v1
kind: Pod
metadata:
  name: counter
spec:
  containers:
  - name: count
    image: busybox:1.28
    args:
    - /bin/sh
    - -c
    - >
      i=0;
      while true;
      do
        echo "$i: $(date)" >> /var/log/1.log;
        echo "$(date) INFO $i" >> /var/log/2.log;
        i=$((i+1));
        sleep 1;
      done      
    volumeMounts:
    - name: varlog
      mountPath: /var/log
  - name: count-agent
    image: k8s.gcr.io/fluentd-gcp:1.30
    env:
    - name: FLUENTD_ARGS
      value: -c /etc/fluentd-config/fluentd.conf
    volumeMounts:
    - name: varlog
      mountPath: /var/log
    - name: config-volume
      mountPath: /etc/fluentd-config
  volumes:
  - name: varlog
    emptyDir: {}
  - name: config-volume
    configMap:
      name: fluentd-config

在上面的示例配置中,fluentd 可以被替换为任何日志代理,从应用容器内的任何来源读取数据。

关于 k8s 中日志架构的更多信息,请查看:https://kubernetes.io/zh-cn/docs/concepts/cluster-administration/logging/

Pod 的配置管理

应用部署的最佳实践是将应用所需的配置信息与程序进行分离,这样可以使应用程序被更好地复用,通过不同的配置能实现更灵活的功能。

将应用打包为容器镜像后,可以通过环境变量(env)或外挂文件(volume)这两种方式在创建容器时进行配置的注入,但在大规模化容器集群的环境中,对多个容器进行不同的配置会变得非常的复杂,多个容器配置出错的概率大幅提升,随之管理成本上升,难以方便的管理。

面对以上问题,k8s v1.2 版本开始提供了一种统一的集中化应用配置管理方案 —— ConfigMap(也叫集中化配置中心)。

ConfigMap 简介

ConfigMap 是一个 API 对象,使用键-值(key-value)对形式保存非机密性的数据。 可以存储其他对象所需要使用的配置。 不同的是,其他 k8s 对象都有一个 spec ,而 ConfigMap 使用 databinaryData 字段。这些字段能够接收 键-值(key-value) 对作为其取值。

data 和 binaryData 字段都是可选的

  • data 字段设计用来保存 UTF-8 字符串。
  • binaryData 则被设计用来保存 二进制数据作为 base64 编码的字串

注意:
ConfigMap 并不提供保密或者加密功能。 如果你想存储的数据是机密的,请使用 Secret, 或者使用其他第三方工具来保证你的数据的私密性,而不是用 ConfigMap。

ConfigMap 在设计上不是用来保存大量数据的。在 ConfigMap 中保存的数据不可超过 1 MiB。如果你需要保存超出此尺寸限制的数据,考虑使用 挂载存储卷 或者使用 独立的数据库或者文件服务

ConfigMap 供 Pod 容器使用的典型用法,有以下四种方式:

  1. 生成为容器内的环境变量(env)。
  2. 设置容器启动命令的启动参数(在容器命令和参数内,需要设置为环境变量)。
  3. 在只读卷里面添加一个文件(以 Volume 的形式挂载为容器内部的文件或目录),让应用来读取。
  4. 编写代码在 Pod 中运行,使用 Kubernetes API 来读取 ConfigMap。

这些不同的方法适用于不同的数据使用方式。 对前三个方法,kubelet 使用 ConfigMap 中的数据在 Pod 中启动容器。

第四种方法意味着你必须编写代码才能读取 ConfigMap 和它的数据。然而, 由于你是直接使用 Kubernetes API,因此只要 ConfigMap 发生更改, 你的应用就能够通过订阅来获取更新,并且在这样的情况发生的时候做出反应。 通过直接进入 Kubernetes API,这个技术也可以让你能够获取到不同的Namespace 里的 ConfigMap。

创建 ConfigMap 资源对象

可以通过 YAML 配置文件 方式或者直接使用 kubectl create configmap 命令行的方式来创建 ConfigMap。

1、通过 yaml 文件方式创建

上面的示例(使用 Sidecar 容器运行日志代理)中已经展示过,接下来我们再创建一个 game-demo 的 ConfigMap。

apiVersion: v1
kind: ConfigMap
metadata:
  name: game-demo
data:
  # 类属性键;每一个键都映射到一个简单的值
  player_initial_lives: "3"
  ui_properties_file_name: "user-interface.properties"

  # 类文件键
  game.properties: |
    enemy.types=aliens,monsters
    player.maximum-lives=5    
  user-interface.properties: |
    color.good=purple
    color.bad=yellow
    allow.textmode=true    

注意区分 key 键的类型:类属性键类文件键

执行如下命令创建上面定义的 ConfigMap:

kubectl create -f game-demo.yaml

输出信息:

configmap "game-demo" created

查看创建好的 ConfigMap:

kubectl get configmap

查看 ConfigMap 的详细信息:

kubectl describe configmap game-demo

除了上面的方式,还可以将 配置文件(xml 或 json 文件)定义为 ConfigMap 的用法,设置 key 为配置文件的别名,value 为该配置文件的全部文本内容。比如:asp.net core webapi 里面的 appsettings.json 文件

2、通过 kubectl 命令方式创建

除了上面的 YAML 文件方式,还可以通过 kubectl 命令行方式创建 ConfigMap,并且还可以在一行命令中指定多个参数。
2.1 通过 --from-file 参数从文件中进行创建,可以指定 key 的名称,也可以在一个命令行中创建包含多个 key 的 ConfigMap ,语法格式:

kubectl create configmap NAME --from-file=[key=]source --from-file=[key=]source

2.2 通过 --from-file 参数从目录中进行创建,该目录下的每个配置文件名都被设置为 key,文件的内容被设置为 value ,语法格式:

kubectl create configmap NAME --from-file=config-files-dir

2.3 使用 --from-literal 时会从文本中进行创建,直接将指定的 key#=value# 创建为 ConfigMap 内容,语法格式:

kubectl create configmap NAME --from-literal=key1=value1 --from-literal=key2=value2

分别对上面这几种命令方式创建 ConfigMap 举例说明:

示例1:比如在 asp.net core 项目中有配置文件 appsettings.json,可以创建一个包含该文件内容的 ConfigMap

kubectl create configmap appsettings.json --from-file=appsettings.json
configmap "appsettings.json" created

示例2:假如,在 asp.net core 项目中的 configfiles 目录下包含两个配置文件 appconfig.xml 和 appsettings.json,创建一个包含两个文件内容的 ConfigMap

kubectl create configmap cm-appconf --from-file=configfiles 

示例3:使用 --from-literal 参数进行创建 ConfigMap

kubectl create configmap cm-appenv --from-literal=loglevel=info --from-literal=appdatadir=/var/data

配置 Pod 使用 ConfigMap

配置 Pod 使用 ConfigMap 有以下两种方式:

  1. 通过环境变量(env)方式使用 ConfigMap
  2. 通过 volumeMount 方式使用 ConfigMap

这里我们使用上面创建的 game-demo 的 ConfigMap,通过两种方式配置 Pod 使用 game-demo

apiVersion: v1
kind: Pod
metadata:
  name: configmap-demo-pod
spec:
  containers:
    - name: demo
      image: alpine
      command: ["sleep", "3600"]
      env:
        # 定义环境变量
        - name: PLAYER_INITIAL_LIVES      # 请注意这里和 ConfigMap 中的键名是不一样的
          valueFrom:
            configMapKeyRef:
              name: game-demo             # 这个值来自 ConfigMap
              key: player_initial_lives   # 需要取值的键
        - name: UI_PROPERTIES_FILE_NAME
          valueFrom:
            configMapKeyRef:
              name: game-demo
              key: ui_properties_file_name
      volumeMounts:
      - name: config
        mountPath: "/config"
        readOnly: true
  volumes:
    # 你可以在 Pod 级别设置卷,然后将其挂载到 Pod 内的容器中
    - name: config
      configMap:
        # 提供你想要挂载的 ConfigMap 的名字
        name: game-demo
        # 来自 ConfigMap 的一组键,将被创建为文件
        items:
        - key: "game.properties"
          path: "game.properties"
        - key: "user-interface.properties"
          path: "user-interface.properties"

说明:
环境变量的名称遵循 POSIX 命名规范([a-zA-Z_][a-zA-Z0-9_]*),不能以数字开头。如果包含非法字符,则系统将跳过该条环境变量的创建,并记录一个 Event 来提示环境变量无法创建,但并不阻止 Pod 的启动。

使用 ConfigMap 的限制条件

  1. ConfigMap 必须在 Pod 创建之前已经创建(ConfigMap 必须已经存在)。
  2. ConfigMap 受 Namespace 限制,只有处于相同 Namespace 中的 Pod 才可以引用。多个 Pod 可以引用同一个 ConfigMap。
  3. ConfigMap 中的配额管理还未能实现。
  4. kubelet 只支持可以被 kube-apiserver 管理的 Pod 使用 ConfigMap,kubelet 在宿主的 node 上通过 --manifest-url--config 自动创建的 Static Pod 将无法引用 ConfigMap。
  5. 在 Pod 对 ConfigMap 进行挂载(volumeMount)操作时,在容器内部只能挂载为 “目录”,无法挂载为 “文件”。在挂载到容器内部后,在目录下包含 ConfigMap 定义的每个 item ,如果应用程序需要保留原来的其他文件,则需要进行额外的处理。可以将 ConfigMap 挂载到容器内部的临时目录,再通过启动脚本将配置文件复制或者链接到(cp 或 link 命令)应用所用的实际配置目录下。

在容器内获取 Pod 信息(Downward API)

当 Pod 被成功创建之后,都会被系统分配唯一的名字、IP 地址,并且处于某个 namespace 中,那么这些信息如何在 Pod 的容器内获取该 Pod 这些重要信息呢?

对于容器来说,在不与 Kubernetes 过度耦合的情况下,拥有关于自身的信息有时是很有用的。 Downward API 允许容器在不使用 Kubernetes 客户端(kubectl)或 API 服务器(kube-apiserver)的情况下获得自己或集群的信息。

Downward API 可以通过以下两种方式将 Pod 信息注入容器内部:

  1. 环境变量(env):用于单个变量,可以将 Pod 信息注入容器内部。
  2. volumeMount:将数组类信息生成为文件并挂载到容器内部。

这两种暴露 Pod 和容器字段的方式统称为 Downward API

Downward API 的用法举例说明

1、环境变量方式:将 Pod 信息注入为环境变量

通过 Downward API 将 Pod 的 IP、名称、所在的 Namespace 和 node 名称注入容器的环境变量中,容器应用使用 env 命令将全部环境变量打印到标准输出中,dapi-envars-fieldref.yaml 文件定义如下:

apiVersion: v1
kind: Pod
metadata:
  name: dapi-envars-fieldref
spec:
  containers:
    - name: test-container
      image: k8s.gcr.io/busybox
      command: [ "sh", "-c", "env"]
      args:
      - while true; do
          echo -en '\n';
          printenv MY_NODE_NAME MY_POD_NAME MY_POD_NAMESPACE;
          printenv MY_POD_IP MY_POD_SERVICE_ACCOUNT;
          sleep 10;
        done;
      env:
        - name: MY_NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName # Pod 所在的 Node 名称
        - name: MY_POD_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name # Pod 的名称,当 Pod 通过 Deployment 创建时,其名称是 RS 随机产生的唯一名称
        - name: MY_POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace # Pod 所在的 Namespace
        - name: MY_POD_IP
          valueFrom:
            fieldRef:
              fieldPath: status.podIP # Pod 的 IP 地址,之所以叫做 status.podIP 而非 metadata.podIP,是因为 Pod 的 IP 属于状态数据,而非元数据(metadata)
        - name: MY_POD_SERVICE_ACCOUNT
          valueFrom:
            fieldRef:
              fieldPath: spec.serviceAccountName # Pod 的服务账号名称
  restartPolicy: Never # Pod 重启策略,从不重启

执行以下命令创建 Pod:

kubectl create -f dapi-envars-fieldref.yaml
pod "dapi-envars-fieldref" created

查看 “dapi-envars-fieldref” 的日志信息:

kubectl logs dapi-envars-fieldref
...
MY_NODE_NAME=xxx
MY_POD_NAME=dapi-envars-fieldref
MY_POD_NAMESPACE=default
MY_POD_IP=172.17.1.2
MY_POD_SERVICE_ACCOUNT=yyy
...

注意,上面 valueFrom 这种特殊的语法是 Downward API 的写法。

2、环境变量方式:将 Container 资源信息注入为环境变量

用 Container 字段作为环境变量的值,dapi-envars-resourcefieldref.yaml 文件定义如下:

apiVersion: v1
kind: Pod
metadata:
  name: dapi-envars-resourcefieldref
spec:
  containers:
    - name: test-container
      image: k8s.gcr.io/busybox:1.24
      command: [ "/bin/sh", "-c", "env"]
      args:
      - while true; do
          echo -en '\n';
          printenv MY_CPU_REQUEST MY_CPU_LIMIT;
          printenv MY_MEM_REQUEST MY_MEM_LIMIT;
          sleep 10;
        done;
      resources:
        requests:
          memory: "32Mi"
          cpu: "125m"
        limits:
          memory: "64Mi"
          cpu: "250m"
      env:
        - name: MY_CPU_REQUEST
          valueFrom:
            resourceFieldRef:
              containerName: test-container
              resource: requests.cpu  # 容器的 CPU 请求值(最小值)
        - name: MY_CPU_LIMIT
          valueFrom:
            resourceFieldRef:
              containerName: test-container
              resource: limits.cpu  # 容器的 CPU 限制值(最大值)
        - name: MY_MEM_REQUEST
          valueFrom:
            resourceFieldRef:
              containerName: test-container
              resource: requests.memory  # 容器的内存请求值(最小值)
        - name: MY_MEM_LIMIT
          valueFrom:
            resourceFieldRef:
              containerName: test-container
              resource: limits.memory  # 容器的内存限制值(最大值)
  restartPolicy: Never

这个配置文件中,可以看到四个环境变量。env 字段是一个 EnvVars. 对象的数组。数组中第一个元素指定 MY_CPU_REQUEST 这个环境变量从 Containerrequests.cpu 字段获取变量值。同样,其它环境变量也是从 Container 的字段获取它们的变量值。

说明: 本例中使用的是 Container 的字段而不是 Pod 的字段。

3、volumeMounts(Volume 挂载)方式

将创建一个包含一个容器的 Pod,并将 Pod 级别的字段作为文件映射到正在运行的容器中。kubernetes-downwardapi-volume-example.yaml 定义文件如下:

apiVersion: v1
kind: Pod
metadata:
  name: kubernetes-downwardapi-volume-example
  labels:
    zone: us-est-coast
    cluster: test-cluster1
    rack: rack-22
  annotations:
    build: two
    builder: john-doe
spec:
  containers:
    - name: client-container
      image: k8s.gcr.io/busybox
      command: ["/bin/sh", "-c", "env"]
      args:
      - while true; do
          if [[ -e /etc/podinfo/labels ]]; then
            echo -en '\n\n'; cat /etc/podinfo/labels; fi;
          if [[ -e /etc/podinfo/annotations ]]; then
            echo -en '\n\n'; cat /etc/podinfo/annotations; fi;
          sleep 5;
        done;
      volumeMounts:
        - name: podinfo
          mountPath: /etc/podinfo
  volumes:
    - name: podinfo
      downwardAPI:
        items:
          - path: "labels"
            fieldRef:
              fieldPath: metadata.labels
          - path: "annotations"
            fieldRef:
              fieldPath: metadata.annotations

命令行执行 kubectl create -f kubernetes-downwardapi-volume-example.yaml 创建 Pod,再查看该 Pod 的日志,输出的信息和文件描述的意思相符,这里就不在演示了。

暴露 Pod 和容器的可用字段

只有部分 Kubernetes API 字段可以通过 Downward API 使用

你可以使用 fieldRef 传递来自可用的 Pod 级字段的信息。在 API 层面,一个 Pod 的 spec 总是定义了至少一个 Container。 你可以使用 resourceFieldRef 传递来自可用的 Container 级字段的信息。

可以使用的字段如下表:

  • 通过 fieldRef 获得的信息
字段 描述信息 是否可作为环境变量获取
metadata.name Pod 的名称
metadata.namespace Pod 的命名空间
metadata.uid Pod 的唯一 ID
metadata.annotations[‘’] Pod 的注解 的值(例如:metadata.annotations[‘myannotation’])
metadata.labels[‘’] Pod 的标签 的值(例如:metadata.labels[‘mylabel’])
spec.serviceAccountName Pod 的服务账号名称
spec.nodeName Pod 运行时所处的节点名称
status.hostIP Pod 所在节点的主 IP 地址
status.podIP Pod 的主 IP 地址(通常是其 IPv4 地址)
metadata.labels Pod 的所有标签,格式为 标签键名=“转义后的标签值”,每行一个标签
metadata.annotations Pod 的全部注解,格式为 注解键名=“转义后的注解值”,每行一个注解
  • 通过 resourceFieldRef 获得的信息
字段 描述信息 是否可作为环境变量获取
resource: limits.cpu 容器的 CPU 限制值
resource: requests.cpu 容器的 CPU 请求值
resource: limits.memory 容器的内存限制值
resource: requests.memory 容器的内存请求值
resource: limits.hugepages-* 容器的巨页限制值(前提是启用了 DownwardAPIHugePages 特性门控)
resource: requests.hugepages-* 容器的巨页请求值(前提是启用了 DownwardAPIHugePages 特性门控)
resource: limits.ephemeral-storage 容器的临时存储的限制值
resource: requests.ephemeral-storage 容器的临时存储的请求值

资源限制的后备信息

如果没有为容器指定 CPU 和内存限制时尝试使用 Downward API 暴露该信息,那么 kubelet 默认会根据 node 节点可分配资源 计算并暴露 CPU 和内存的最大可分配值。

Downward API 存在的价值

在某些集群环境中,集群中的每个 node 都需要将自身的标识(ID)及进程绑定的 IP 地址等信息先写入配置文件中,进程在启动时会读取这些信息,然后将这些信息发布到某个类似的服务注册中心的地方,以实现集群节点的自动发现功能。这种场景就是 Downward API 发挥价值的地方,具体做法:

先写一个预启动脚本或者 Init Container,然后通过环境变量或文件方式获取 Pod 的自身名称、IP地址等信息,接下来再将这些信息写入主程序的配置文件中,最后启动主程序,即可大功告成。

创建一个 Pod 的工作流程

Kubernetes 基于 list-watch 机制的控制器架构,实现组件间交互的解耦。

Pod 创建的工作流程如下:

k8s 读书笔记 - 深入掌握 Pod_第7张图片

kubectl run nginx --image=nginx
  1. kubectl 将创建pod的请求提交到 kube-apiserver。
  2. kube-apiserver 会将请求的信息写到 etcd 数据库。
  3. kube-apiserver 通知 scheduler 有新的 pod 待创建,收到创建信息之后就会根据调度算法选择一个合适的 node 节点。
  4. 给这个 pod 打一个标记(labels),pod=k8s-node1。
  5. kube-apiserver 收到 scheduler 的调度结果写到 etcd 数据库。
  6. k8s-node1 上的 kubelet 收到事件,从 kube-apiserver 获取 pod 的相关信息。
  7. kubelet 调用 docker api 创建 pod 中所需的容器 container。
  8. kubelet 会把这个 pod 的状态汇报给 kube-apiserver。
  9. kube-apiserver 把状态写入到 etcd 数据库。

查看创建的 Pod:

kubectl get pods

API Server(kube-apiserver) 在 k8s 中的两个角色:

  1. 统一集群的入口;
  2. 负责协作各个组件;

使用 Deployment 对象创建 Pod 的工作流程:

  1. 在上图的 etcd 和 Scheduler 之间增加 controller-manager 控制器对象。
  2. 在上图的 kubelet 和 Docker 之间增加 kube-proxy(通过 service 提供 pod 的外部网络访问)。

Pod 对象应用的自恢复(重启策略+健康检查)

Pod 生命周期和重启策略

k8s 读书笔记 - 深入掌握 Pod_第8张图片

说明:上图中 Init Container 是线性顺序执行 的,必须等待前一个 Init Container 成功结束才开始执行下一个 Init Container ,而后面的 应用容器(Main Container)则是并行执行 的,多个业务容器可以并行执行。

Pod 生命周期(Pod Lifecycle)

Pod 创建的时候可能经历这么几个阶段(phase):

取值 描述
Pending(悬决) Pod 已被 k8s 系统接受,但有一个或者多个容器尚未创建亦未运行。此阶段包括等待 Pod 被调度的时间和通过网络下载镜像的时间。
Running(运行中) Pod 已经绑定到了某个节点,Pod 中所有的容器都已被创建。至少有一个容器仍在运行,或者正处于启动或重启状态。
Succeeded(成功) Pod 中的所有容器都已成功终止,并且不会再重启。
Failed(失败) Pod 中的所有容器都已终止,并且至少有一个容器是因为失败终止。也就是说,容器以非 0 状态退出或者被系统终止。
Unknown(未知) 因为某些原因无法取得 Pod 的状态。这种情况通常是因为与 Pod 所在主机通信失败。

如果某节点 “死掉” 或者与集群中其他节点 “失联”,k8s 会实施一种策略,将失去的节点上运行的所有 Pod 的 phase 设置为 Failed。

Pod 的重启策略(Pod RestartPolicy)

Pod 的重启策略包括 Always、OnFailure 和 Never。默认值是 Always

  • Always:当容器失效时,由 kubelet 自动重启该容器。
  • OnFailure:当容器终止运行且退出码不为 0 时,由 kubelet 自动重启该容器。
  • Never:无论容器运行状态如何,kubelet 都不会重启该容器。

kubelet 重启失效容器的 时间间隔以 ync-frequency 乘以 2n 来计算,例如:1、2、4、8 倍等,最长延时 5min,并且在成功重启后的 10min 后重置该时间。

Pod 状态转换

结合 Pod 的阶段(phase)状态信息和重启策略,列举出一些常见的状态转换场景,如下表所示:

k8s 读书笔记 - 深入掌握 Pod_第9张图片

Pod 健康检查和服务可用性检查

容器配置活跃(Liveness)、就绪(Readiness)和启动(Startup)探测器。
更多信息请查看:https://kubernetes.io/zh-cn/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/

k8s 对 Pod 的健康状态可以通过两类探针来检查:LivenessProbe 和 ReadinessProbe,kubelet 定期执行这两类探针来诊断容器的健康状况。

  • LivenessProbe(存活探针):用于判断容器是否存活(Running)状态,如果该探针探测到容器不健康,则 kubelet 将杀掉该容器,并依据容器的重启策略做相应的处理。如果容器不包含 LivenessProbe 探针,kubelet 则认为该容器的 LivenessProbe 探针返回值永远是 Success。
  • ReadinessProbe(就绪探针):用于判断容器服务是否可用(Ready)状态,达到 Ready 状态的 Pod 才可以接受请求。 对于被 Service 管理的 Pod,Service 与 Pod Endpoint 的关联关系也将基于 Pod 是否 Ready 进行设置。如果在运行中过程中 Pod 的状态由 Ready 变为 False,则系统自动将其从 Service 的后端 Endpoint 列表中隔离出去,后续再把恢复到 Ready 状态的 Pod 加回后端 Endpoint 列表。这样就能保证客户端在访问 Service 时不会被转发到服务不可用的 Pod 副本实例上。

LivenessProbe 和 ReadinessProbe 均可配置以下三种实现方式

  • ExecAction:在容器内部执行一个命令,如果该命令的返回码为 0 ,则说明容器处于健康状态。

示例文件:

apiVersion: v1
kind: Pod
metadata:
  labels:
    test: liveness
  name: liveness-exec
spec:
  containers:
  - name: liveness
    image: k8s.gcr.io/busybox
    args:
    - /bin/sh
    - -c
    - touch /tmp/healthy; sleep 30; rm -f /tmp/healthy; sleep 600
    livenessProbe:
      exec:
        command:
        - cat
        - /tmp/healthy
      initialDelaySeconds: 5
      periodSeconds: 5
  • TCPSocketAction:通过容器的 IP 地址和端口号执行 TCP 检查,如果能够建立 TCP 连接,则说明容器处于健康状态。

示例文件:

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-healthcheck
spec:
  containers:
  - name: nginx
    image: nginx
    ports: 
    - containerPort: 80
    livenessProbe:
      tcpSocket: 
        port: 80
      initialDelaySeconds: 30
      periodSeconds: 5
  • HTTPGetAction:通过容器的 IP 地址、端口号及路径调用 HTTP Get 方法,如果响应的状态码大于等于 200 且小于 400( HTTP Status Code ≥ 200 &&<400),则说明容器处于健康状态。

示例文件:

apiVersion: v1
kind: Pod
metadata:
  name: pod-with-healthcheck
spec:
  containers:
  - name: nginx
    image: nginx
    ports: 
    - containerPort: 80
    livenessProbe:
      httpGet: 
        path: /status/healthz
        port: 80
        httpHeaders:
        - name: Custom-Header # 自定义 http 请求头,比如:Accept
          value: Awesome      # 自定义 http 请求头对应的值,比如:application/json
      initialDelaySeconds: 30
      periodSeconds: 5

说明:HTTPGetAction 方式可以添加 httpHeaders 配置,用户可依据自身业务情况扩展 http 请求头。

对于以上三种探测方式,都需要设置 initialDelaySecondsperiodSeconds 两个参数,他们的含义分别如下:

  • initialDelaySeconds:启动容器后进行首次健康检查的等待时间,单位为秒 s。
  • periodSeconds:健康检查发送请求后等待响应的超时时间,单位为秒 s。当超时发生,kubelet 会认为容器已经无法提供服务,将会重启该容器。

k8s 的 ReadinessProbe 机制可能无法满足某些复杂应用对容器内服务可用状态的判断,从 k8s v1.11 版本开始,引入 Pod Ready++ 特性对 ReadinessProbe 探测机制进行扩展,在 v1.14 版本时达到 GA 稳定版,称其为 PodReadiness Gates

k8s 读书笔记 - 深入掌握 Pod_第10张图片

关于 k8s 更多特性的信息,请查看:https://kubernetes.io/zh-cn/docs/reference/command-line-tools-reference/feature-gates/

通过 Pod Readiness Gates 机制,用户可以将自定义的 ReadinessProbe 探测方式设置在 Pod 上,辅助 k8s 设置 Pod 何时达到服务可用状态(Ready)。为了使用自定义的 ReadinessProbe 生效,用户需要提供一个外部的控制器(Controller)来设置相应的 Condition(条件)状态。

示例文件(伪 yaml 定义结构):

...
kind: Pod
...
spec:
  readinessGates: 
  - conditionType: "www.example.com/feature-1" # 新 Readiness Gate,类型为 www.example.com/feature-1
  status: 
    conditions: 
    - type: Ready  # k8s 系统内置的名为 Ready 的 condition
      status:"True"
      lastProbeTime: null
      lastTransitionTime: 2021-01-01T00:00:00Z
    - type: "www.example.com/feature-1" # 用户自定义的 condition
      status:"True"
      lastProbeTime: null
      lastTransitionTime: 2022-08-28T00:00:00Z
    containerStatuses: 
      - conditionID: docker://abcd...
        ready: true

新增的自定义 Condition(条件)状态(status)将由用户自定义的外部控制器设置,默认值为 False。k8s 将在判断全部 readinessGates 条件都为 True 时,才设置 Pod 为服务可用状态(Ready 为 True)。

使用启动(Startup)探测器保护慢启动容器

有时候,会有一些现有的应用在启动时需要较长的初始化时间。 在这种情况下,若要不影响对死锁作出快速响应的探测,设置存活探测参数是要技巧的。 技巧就是使用相同的命令来设置启动探测,针对 HTTP 或 TCP 检测,可以通过将 failureThreshold * periodSeconds 参数设置为足够长的时间来应对糟糕情况下的启动时间。

前面定义的 yaml 文件示例,修改如下:

ports:
- name: liveness-port
  containerPort: 8080
  hostPort: 8080
# 存活探针
livenessProbe:
  httpGet:
    path: /healthz
    port: liveness-port
  failureThreshold: 1
  periodSeconds: 10
# 启动探针
startupProbe:
  httpGet:
    path: /healthz
    port: liveness-port
  failureThreshold: 30
  periodSeconds: 10

总结

k8s 的核心基本是围绕 Pod 展开的,顺着 Pod 的资源抽象概念,可以延伸出 k8s 基于容器化资源管理系统的各个知识点,比如:RC、RS、Deployment、DaemonSet、Job & CronJob 、StatefulSet、Serivice、Volume、PV/PVC 等资源对象对 Pod 的一系列管控机制,比如 Pod 背后的调度机制等,因此理解并深入掌握 Pod 是进入 k8s 知识体系的基本前提和保障。

你可能感兴趣的:(Kubernetes,&,容器化资源编排管理,kubernetes,container,容器,运维,Pod)