深入理解 Pod


深入掌握POD

1. Pod 定义详解

YAML格式的Pod定义文件的完整内容如下:

apiVersion: v1                    # 必选,版本号,例如v1
kind: Pod                         # 必选,Pod
metadata:                         # 必选,元数据
  name: string                    # 必选,Pod名称
  namespace: string               # 必选,Pod所属的命名空间
  labels:                         # 自定义标签
    - name: string                # 自定义标签名字
  annotations:                    # 自定义注释列表
    - name: string
spec:                             # 必选,Pod中容器的详细定义
  containers:                     # 必选,Pod中容器列表
  - name: string                  # 必选,容器名称
    image: string                 # 必选,容器的镜像名称
    imagePullPolicy: [Always | Never | IfNotPresent]# 镜像拉取的策略,默认为Always:Alawys 表示每次都尝试重新拉取镜像;IfNotPresent 表示如果本地有该镜像,则使用本地镜像,本地不存在时拉取镜像;Nerver表示仅使用本地镜像
    command: [string]             # 容器的启动命令列表,如不指定,使用打包时使用的启动命令
    args: [string]                # 容器的启动命令参数列表
    workingDir: string            # 容器的工作目录
    volumeMounts:                 # 挂载到容器内部的存储卷配置
    - name: string                # 引用pod定义的共享存储卷的名称,需用volumes[]部分定义的的卷名
      mountPath: string           # 存储卷在容器内mount的绝对路径,应少于512字符
      readOnly: boolean           # 是否为只读模式,默认为读写模式
    ports:                        # 需要暴露的端口库号列表
    - name: string                # 端口号名称
      containerPort: int          # 容器需要监听的端口号
      hostPort: int               # 容器所在主机需要监听的端口号,默认与Container相同。
                                  # 当设置hostPort时,同一台宿主机将无法启动该容器的第二个副本
      protocol: string            # 端口协议,支持TCP和UDP,默认TCP
    env:                          # 容器运行前需设置的环境变量列表
    - name: string                # 环境变量名称
      value: string               # 环境变量的值
    resources:                    # 资源限制和请求的设置
      limits:                     # 资源限制的设置
        cpu: string               # Cpu的限制,单位为core数,将用于docker run --cpu-shares参数
        memory: string            # 内存限制,单位可以为Mib/Gib,将用于docker run --memory参数
      requests:                   # 资源请求的设置
        cpu: string               # Cpu请求,容器启动的初始可用数量
        memory: string            # 内存清楚,容器启动的初始可用数量
    livenessProbe:# 对Pod内个容器健康检查的设置,当探测无响应几次后将自动重启该容器;检查方法有exec、httpGet和tcpSocket,对一个容器只需设置其中一种方法即可
      exec:                       # 对Pod容器内检查方式设置为exec方式
        command: [string]         # exec方式需要制定的命令或脚本
      httpGet:                    # 对Pod内个容器健康检查方法设置为HttpGet,需要制定Path、port
        path: string
        port: number
        host: string
        scheme: string
        HttpHeaders:
        - name: string
          value: string
      tcpSocket:                  # 对Pod内个容器健康检查方式设置为tcpSocket方式
         port: number
       initialDelaySeconds: 0     # 容器启动完成后首次探测的时间,单位为秒
       timeoutSeconds: 0          # 对容器健康检查探测等待响应的超时时间,单位秒,默认1秒
       periodSeconds: 0           # 对容器监控检查的定期探测时间设置,单位秒,默认10秒一次
       successThreshold: 0
       failureThreshold: 0
       securityContext:
         privileged: false
    restartPolicy: [Always | Never | OnFailure]# Pod的重启策略: Always表示不管以何种方式终止运行,kubelet都将重启;OnFailure表示只有Pod以非0退出码退出才重启; Nerver表示不再重启该Pod
    nodeSelector: obeject         # 设置NodeSelector表示将该Pod调度到包含这个label的node上,以key:value的格式指定
    imagePullSecrets:             # Pull镜像时使用的secret名称,以key:secretkey格式指定
    - name: string
    hostNetwork: false            # 是否使用主机网络模式,默认为false,如果设置为true,表示使用宿主机网络
    volumes:                      # 在该pod上定义共享存储卷列表
    - name: string                # 共享存储卷名称 (volumes类型有很多种)
      emptyDir: {}                # 类型为emtyDir的存储卷,与Pod同生命周期的一个临时目录。为空值
      hostPath: string            # 类型为hostPath的存储卷,表示挂载Pod所在宿主机的目录
        path: string              # Pod所在宿主机的目录,将被用于同期中mount的目录
      secret:                     # 类型为secret的存储卷,挂载集群与定义的secre对象到容器内部
        scretname: string  
        items:     
        - key: string
          path: string
      configMap:                  # 类型为configMap的存储卷,挂载预定义的configMap对象到容器内部
        name: string
        items:
        - key: string
          path: string

Pod YAML定义文件详解

2. Pod 的基本用法

Pod可以由1个或多个容器组合而成。举例说明:

Pod由一个容器组成YAML文件:

以下文件启动后将生成一个名为“frontend”的Pod,该Pod中包含一个容器。

# cat frontend-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: frontend
  labels:
    name: frontend
spec:
  containers:
  - name: frontend
    image: ygego/php-frontend
    env:
    - name: GET_HOSTS_FROM
      value: env
    ports:
    - containerPort: 80

另一个场景是:假如frontend 和 Redis 两个容器应用为紧耦合的关系,并且组合成一个整体对外提供服务时,应该将这两个容器打包为一个Pod,属于同一个Pod的多个容器应用之间相互访问时仅需要通过localhost就可以通信。如图:

Pod由两个个容器组成YAML文件:

# cat frontend-redis-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: php-redis
  labels:
    name: php-redis
spec:
  containers:
  - name: frontend
    image: ygego/php-frontend
    env:
    - name: GET_HOSTS_FROM
      value: env
    ports:
    - containerPort: 80
  - name: redis
    image: ygego/redis-master
    ports:
    - containerPort: 6379

使用Kubectl 创建该Pod:

# kubectl apply -f frontend-redis-pod.yaml
pod/php-redis created

查看创建好的Pod:

# kubectl get pods
NAME 		 READY 	 STATUS 	  RESTARTS  AGE
php-redis 	 2/2 	 running	  0         1h

可以看到READY信息为2/2,表示Pod中的两个容器都成功的运行起来了。

查看Pod的详细信息:
可以看到两个容器的定义以及创建的过程(Event事件信息)

# kubectl describe pod php-redis 
Name:         php-redis
Namespace:    default
Priority:     0
Node:         k8s-node03/10.4.148.228
Start Time:   Tue, 07 Jan 2020 17:12:12 +0800
Labels:       name=php-redis
Annotations:  cni.projectcalico.org/podIP: 10.100.7.161/32
              kubectl.kubernetes.io/last-applied-configuration:
                {"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"labels":{"name":"php-redis"},"name":"php-redis","namespace":"default"},"spec...
Status:       Running
IP:           10.100.7.161
Containers:
  frontend:
    Container ID:   docker://7b40ddbaed450da56f4ae9aeb7ddc46a1da0e5630327b41973718a075b805cd5
    Image:          ygego/php-frontend
    Image ID:       docker://sha256:d014f67384a11186e135b95a7ed0d794674f7ce258f0dce47267c3052a0d0fa9
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Tue, 07 Jan 2020 17:12:15 +0800
    Ready:          True
    Restart Count:  0
    Environment:
      GET_HOSTS_FROM:  env
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-c6t97 (ro)
  redis:
    Container ID:   docker://3483cbd386fa788302e6d6b1d6bace0233e8a972dfcc14b9a2bb131320d36737
    Image:          ygego/redis-master
    Image ID:       docker://sha256:405a0b586f7ebeb545ec65be0e914311159d1baedccd3a93e9d3e3b249ec5cbd
    Port:           6379/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Tue, 07 Jan 2020 17:12:16 +0800
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-c6t97 (ro)
Conditions:
  Type              Status
  Initialized       True 
  Ready             True 
  ContainersReady   True 
  PodScheduled      True 
Volumes:
  default-token-c6t97:
    Type:        Secret (a volume populated by a Secret)
    SecretName:  default-token-c6t97
    Optional:    false
QoS Class:       BestEffort
Node-Selectors:  <none>
Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
                 node.kubernetes.io/unreachable:NoExecute for 300s
Events:
  Type    Reason     Age   From                 Message
  ----    ------     ----  ----                 -------
  Normal  Scheduled  17s   default-scheduler    Successfully assigned default/php-redis to k8s-node03
  Normal  Pulled     15s   kubelet, k8s-node03  Container image "ygego/php-frontend" already present on machine
  Normal  Created    15s   kubelet, k8s-node03  Created container frontend
  Normal  Started    14s   kubelet, k8s-node03  Started container frontend
  Normal  Pulled     14s   kubelet, k8s-node03  Container image "ygego/redis-master" already present on machine
  Normal  Created    14s   kubelet, k8s-node03  Created container redis
  Normal  Started    13s   kubelet, k8s-node03  Started container redis

3. Pod 容器共享Volume

同一个Pod中的多个容器能够共享Pod级别的存储卷Volume。Volume可以被定义为各种类型,多个容器各自进行挂载操作,将一个Volume挂载为容器内部需要的目录,如图所示:

如下图所示的例子中,在Pod中包含两个容器: Tomcat 和 busybox,在Pod级别设置Volume:“app-logs”,用于 Tomcat向其中写入日志文件,busybox 读取日志文件。

这里设置的Volume名为app-logs,类型为emptyDir,挂载在Tomcat容器内的/usr/local/tomcat/logs目录,同时挂载到busybox容器内的/logs目录。Tomcat容器启动后会向/usr/local/tomcat/logs目录写入文件,busybox容器就可以读取其中的文件了,busybox 容器的启动命令为“tail -f /logs/catalina*.log”。

# cat pod-volume-applogs.yaml
apiVersion: v1
kind: Pod
metadata:
  name: volume-pod
spec:
  containers:
  - name: tomcat
  	image: tomcat
  	imagePullPolicy: Never
  	ports:
  	- containerPort: 8080
  	volumeMounts:
  	- name: app-logs
  	  mountPath: /usr/local/tomcat/logs
  - name: busybox
    image: busybox
    imagePullPolicy: Never
    command: ["sh", "-c", "tail -f /logs/catalina*.log"]
    volumeMounts:
    - name: app-logs
      mountPath: /logs
  volumes:
  - name: app-logs
  	emptyDir: {}

使用Kubectl 创建该Pod:

# kubectl apply -f pod-volume-applogs.yaml 
pod/volume-pod created

查看创建好的Pod:

# kubectl get pods
NAME          READY   STATUS    RESTARTS   AGE
volume-pod    2/2     Running   0          2m13s

登录Tomcat容器查看生成的日志文件:

# kubectl exec -it volume-pod  -c tomcat -- ls /usr/local/tomcat/logs
catalina.2020-01-07.log  host-manager.2020-01-07.log  localhost.2020-01-07.log  localhost_access_log.2020-01-07.txt  manager.2020-01-07.log

查看Tomcat日志中的内容:

# kubectl exec -it volume-pod  -c tomcat -- tail /usr/local/tomcat/logs/catalina.2020-01-07.log
07-Jan-2020 09:30:43.227 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]
07-Jan-2020 09:30:43.237 INFO [main] org.apache.tomcat.util.net.NioSelectorPool.getSharedSelector Using a shared selector for servlet write/read
07-Jan-2020 09:30:43.250 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["ajp-nio-8009"]
07-Jan-2020 09:30:43.251 INFO [main] org.apache.tomcat.util.net.NioSelectorPool.getSharedSelector Using a shared selector for servlet write/read
07-Jan-2020 09:30:43.251 INFO [main] org.apache.catalina.startup.Catalina.load Initialization processed in 751 ms
07-Jan-2020 09:30:43.274 INFO [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]
07-Jan-2020 09:30:43.274 INFO [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet Engine: Apache Tomcat/8.5.50
07-Jan-2020 09:30:43.288 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
07-Jan-2020 09:30:43.297 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["ajp-nio-8009"]
07-Jan-2020 09:30:43.300 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 48 ms

使用 kubectl logs 命令查看busybox容器的输出内容:

# kubectl logs volume-pod  -c busybox            
07-Jan-2020 09:30:43.227 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["http-nio-8080"]
07-Jan-2020 09:30:43.237 INFO [main] org.apache.tomcat.util.net.NioSelectorPool.getSharedSelector Using a shared selector for servlet write/read
07-Jan-2020 09:30:43.250 INFO [main] org.apache.coyote.AbstractProtocol.init Initializing ProtocolHandler ["ajp-nio-8009"]
07-Jan-2020 09:30:43.251 INFO [main] org.apache.tomcat.util.net.NioSelectorPool.getSharedSelector Using a shared selector for servlet write/read
07-Jan-2020 09:30:43.251 INFO [main] org.apache.catalina.startup.Catalina.load Initialization processed in 751 ms
07-Jan-2020 09:30:43.274 INFO [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]
07-Jan-2020 09:30:43.274 INFO [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet Engine: Apache Tomcat/8.5.50
07-Jan-2020 09:30:43.288 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
07-Jan-2020 09:30:43.297 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["ajp-nio-8009"]
07-Jan-2020 09:30:43.300 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 48 ms

可以看到,busybox容器的日志内容就是Tomcat容器的日志内容。

4. Pod 的配置管理

应用部署的一个最佳实践是将应用所需的配置信息与程序进行分离,这样可以使应用程序被更好地复用,通过不同的配置也能实现更灵活的功能。将应用打包为容器镜像后,可以通过环境变量或者外挂文件的方式在创建容器时进行配置注入,但在大规模容器集群的环境中,对多个容器进行不同的配置将变得非常复杂。Kubernetes 从 1.2 版本开始提供了一种统一的应用配置管理方案---ConfigMap。

4.1 ConfigMap 概述

ConfigMap 供容器使用的典型用法如下:

  1. 生成为容器内的环境变量
  2. 设置容器启动命令的启动参数(需设置为环境变量)
  3. 以Volume的形式挂载为容器内部的文件或目录

ConfigMap以一个或多个"Key:Value"的形式保存在 Kubernetes 系统中给应用使用,既可以用于表示一个变量的值(例如:loglevel=info),也可以用于表示一个完整配置文件的内容(例如:server.xml=<?xml...>...)

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

4.2 创建ConfigMap资源对象

4.2.1. 通过YAML配置文件方式创建

下面的文件描述了将应用所需的变量定义为ConfigMap的配置:

# cat cm-appvars.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-appvars
data:
  apploglevel: info
  appdatadir: /var/data

使用Kubectl 创建该ConfigMap:

# kubectl apply -f cm-appvars.yaml
configmap/cm-appvars created

查看创建的ConfigMap:

# kubectl get configmap
NAME         DATA   AGE
cm-appvars   2      18s

# kubectl describe configmap cm-appvars 
Name:         cm-appvars
Namespace:    default
Labels:       <none>
Annotations:  kubectl.kubernetes.io/last-applied-configuration:
                {"apiVersion":"v1","data":{"appdatadir":"/var/data","apploglevel":"info"},"kind":"ConfigMap","metadata":{"annotations":{},"name":"cm-appva...

Data
====
appdatadir:
----
/var/data
apploglevel:
----
info
Events:  <none>

# kubectl get configmap -o yaml
apiVersion: v1
items:
- apiVersion: v1
  data:
    appdatadir: /var/data
    apploglevel: info
  kind: ConfigMap
  metadata:
    annotations:
      kubectl.kubernetes.io/last-applied-configuration: |
        {"apiVersion":"v1","data":{"appdatadir":"/var/data","apploglevel":"info"},"kind":"ConfigMap","metadata":{"annotations":{},"name":"cm-appvars","namespace":"default"}}
    creationTimestamp: "2020-01-07T11:28:13Z"
    name: cm-appvars
    namespace: default
    resourceVersion: "1896678"
    selfLink: /api/v1/namespaces/default/configmaps/cm-appvars
    uid: b93e6852-4b51-49c7-ac12-5a0833d04025
kind: List
metadata:
  resourceVersion: ""
  selfLink: ""

4.2.2 通过Kubectl命令行方式创建

通过命令行“kubectl create configmap“创建ConfigMap,可以使用参数“--from-file“或“--from-literal“指定内容,并且可以在一行命令中指定多个参数。

准备配置文件

# cat /root/configmap/cm1.cfg 
env=dev1
app.name=cm1
log.level=info

# cat /root/configmap/cm2.cfg 
env=alpha
app.name=cm2
log.level=debug

1、从文件创建(--from-file)
通过--from-file参数从文件中进行创建,可以指定key的名称,也可以在一个命令行中创建包含多个key的ConfigMap。
语法:

# kubectl create configmap NAME --from-file=[key=]CONFIG_FILE 

创建ConfigMap

# kubectl create configmap cm-test --from-file=/root/configmap/cm1.cfg --from-file=/root/configmap/cm2.cfg
configmap/cm-test created

查看创建的ConfigMap

# kubectl get configmap
NAME         DATA   AGE
cm-test      2      8s

# kubectl describe configmap cm-test 
Name:         cm-test
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
cm1.cfg:
----
env=dev1
app.name=cm1
log.level=info

cm2.cfg:
----
env=alpha
app.name=cm2
log.level=debug

Events:  <none>

# kubectl get configmap cm-test -o yaml
apiVersion: v1
data:
  cm1.cfg: |
    env=dev1
    app.name=cm1
    log.level=info
  cm2.cfg: |
    env=alpha
    app.name=cm2
    log.level=debug
kind: ConfigMap
metadata:
  creationTimestamp: "2020-01-07T12:37:25Z"
  name: cm-test
  namespace: default
  resourceVersion: "1903226"
  selfLink: /api/v1/namespaces/default/configmaps/cm-test
  uid: a03188d3-f0d1-437c-b0ce-b95bf8d3520f

创建时指定Data中的Key

# kubectl create configmap cm-test1 --from-file=cm-test1=/root/kube/kube-book/configmap/cm1.cfg 
configmap/cm-test1 created

# kubectl describe configmap cm-test1
Name:         cm-test1
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
cm-test1:
----
env=dev1
app.name=cm1
log.level=info

Events:  <none>

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

# kubectl create configmap NAME --from-file=CONFIG_FILE_DIR 

创建ConfigMap

# kubectl create configmap cm-test2 --from-file=/root/configmap/
configmap/cm-test2 created

查看创建的ConfigMap

# kubectl get configmap
NAME         DATA   AGE
cm-test2     2      6s

# kubectl describe configmap cm-test2 
Name:         cm-test2
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
cm1.cfg:
----
env=dev1
app.name=cm1
log.level=info

cm2.cfg:
----
env=alpha
app.name=cm2
log.level=debug

Events:  <none>

# kubectl get configmap cm-test2 -o yaml
apiVersion: v1
data:
  cm1.cfg: |
    env=dev1
    app.name=cm1
    log.level=info
  cm2.cfg: |
    env=alpha
    app.name=cm2
    log.level=debug
kind: ConfigMap
metadata:
  creationTimestamp: "2020-01-07T12:45:26Z"
  name: cm-test2
  namespace: default
  resourceVersion: "1903985"
  selfLink: /api/v1/namespaces/default/configmaps/cm-test2
  uid: 72983c0c-df9a-42de-8d3a-7cabbb348fe2

3、指定键值创建(--from-literal)
通过 --from-literal 时会从文本中进行创建,直接将指定的 key=value 创建为ConfigMap的内容。
语法为:

# kubectl creata configmap NAME --from-literal=KEY1=VALUE1 --from-literal=KEY2=VALUE2

创建ConfigMap

# kubectl create configmap mysql-env --from-literal=mysql.user=admin --from-literal=mysql.passwd=admin@123
configmap/mysql-env created

查看创建的ConfigMap

# kubectl get configmap
NAME         DATA   AGE
cm-test      2      14h
cm-test1     1      14h
cm-test2     2      14h
mysql-env    2      54s

# kubectl describe configmap mysql-env
Name:         mysql-env
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
mysql.passwd:
----
admin@123
mysql.user:
----
admin
Events:  <none>

# kubectl get configmap mysql-env -o yaml
apiVersion: v1
data:
  mysql.passwd: admin@123
  mysql.user: admin
kind: ConfigMap
metadata:
  creationTimestamp: "2020-01-08T03:03:45Z"
  name: mysql-env
  namespace: default
  resourceVersion: "1985164"
  selfLink: /api/v1/namespaces/default/configmaps/mysql-env
  uid: 3268d53e-35b4-41ed-9820-b7afcaedb10f

4.3 在Pod 中使用ConfigMap

Pod中的容器应用使用ConfigMap有两种方法:

  1. 通过环境变量获取ConfigMap中内容
  2. 通过Volume挂载的方式将ConfigMap中的内容挂载为容器内部的文件或目录

4.3.1 通过环境变量方式使用ConfigMap

以上面创建的ConfigMap“mysql-env”为例,该ConfigMap中有两个键值对:mysql.user=admin 、mysql.passwd=admin@123

# kubectl get configmap mysql-env -o yaml
apiVersion: v1
data:
  mysql.passwd: admin@123
  mysql.user: admin
kind: ConfigMap
metadata:
  creationTimestamp: "2020-01-08T03:03:45Z"
  name: mysql-env
  namespace: default
  resourceVersion: "1985164"
  selfLink: /api/v1/namespaces/default/configmaps/mysql-env
  uid: 3268d53e-35b4-41ed-9820-b7afcaedb10f

通过定义文件“cm-test-pod.yaml”创建Pod,将ConfigMap“mysql-env”中的内容以环境变量方式设置为容器内部的环境变量。

# cat cm-test-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: cm-test-pod
spec:
  containers:
  - name: cm-pod
    image: busybox
    imagePullPolicy: Never
    command: ["/bin/sh", "-c", "sleep 3600"]
    env:
    - name: MYSQL_USER        # 定义容器内环境变量名称:MYSQL_USER
      valueFrom:
        configMapKeyRef:
          name: mysql-env     # “MYSQL_USER”的值取自“mysql-env”
          key: mysql.user     # MYSQL_USER对应的key为“mysql.user”
    - name: MYSQL_PASSWD      # 定义容器内环境变量名称:MYSQL_PASSWD
      valueFrom:
        configMapKeyRef:
          name: mysql-env     # “MYSQL_PASSWD”的值取自“mysql-env”
          key: mysql.passwd   # MYSQL_PASSWD对应的key为“mysql.passwd”
  restartPolicy: Never

执行Kubectl创建POD

# kubectl apply -f cm-test-pod.yaml 
pod/cm-test-pod created

# kubectl get pods
NAME                                READY   STATUS    RESTARTS   AGE
cm-test-pod                         1/1     Running   0          10s

进入POD内的容器查看环境变量

# 使用 kubectl exec 进入POD内部容器,执行命令“env |grep MYSQL”查看MYSQL相关环境变量
# kubectl exec -it cm-test-pod env |grep MYSQL 
MYSQL_USER=admin
MYSQL_PASSWD=admin@123

可以看到POD内部容器的环境变量已经设置成ConfigMap“mysql-env”中的值。

Kubernetes从1.6版本开始,引入一个新的字段“envFrom”,实现了在POD环境中将ConfigMap(也可用于Secret资源)中所定义的 key=value自动生成为环境变量。
示例如下:

# cat cm-envfrom-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: cm-envfrom-pod
spec:
  containers:
  - name: cm-envfrom
    image: busybox
    imagePullPolicy: Never
    command: ["/bin/sh", "-c", "sleep 3600"]
    envFrom:
    - configMapRef:
        name: mysql-env     # 根据mysql-env中的key=value自动生成环境变量
  restartPolicy: Never


# kubectl apply -f cm-envfrom-pod.yaml                                 
pod/cm-envfrom-pod created

# kubectl get pods
NAME                                READY   STATUS    RESTARTS   AGE
cm-envfrom-pod                      1/1     Running   0          25s

# kubectl exec -it cm-envfrom-pod env |grep mysql
mysql.passwd=admin@123
mysql.user=admin

需要注意的是,环境变量的名称需要遵循格式" [a-zA-Z_][a-zA-Z0-9_]*",不能以数字开头。如果包含非法字符,则系统将跳过该条环境变量的创建,并记录一个Event来提示环境变量无法生成,但并不会阻止Pod的启动。

4.3.2 通过volumeMount使用ConfigMap

准备变量配置文件

# cat /root/configmap/cm1.cfg 
env=dev1
app.name=cm1
log.level=info

# cat /root/configmap/cm2.cfg 
env=alpha
app.name=cm2
log.level=debug

创建ConfigMap

# kubectl create configmap cm-volume --from-file=/root/configmap/
configmap/cm-volume created

查看创建好的ConfigMap

# kubectl get cm
NAME        DATA   AGE
cm-volume   2      5s

# kubectl get cm cm-volume -o yaml
apiVersion: v1
data:
  cm1.cfg: |
    env=dev1
    app.name=cm1
    log.level=info
  cm2.cfg: |
    env=alpha
    app.name=cm2
    log.level=debug
kind: ConfigMap
metadata:
  creationTimestamp: "2020-01-08T07:01:20Z"
  name: cm-volume
  namespace: default
  resourceVersion: "2007689"
  selfLink: /api/v1/namespaces/default/configmaps/cm-volume
  uid: 4c9545b6-4060-4850-bedf-728e6af38295

创建POD定义文件

# cat cm-volume-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: cm-volume-pod
spec:
  containers:
  - name: cm-volume
    image: busybox
    imagePullPolicy: Never
    command: ["/bin/sh", "-c", "sleep 3600"]
    volumeMounts:
    - name: config-volume       # 引用Volume的名称
      mountPath: /etc/config    # 挂载到容器内的目录
  volumes:
  - name: config-volume         # 定义Volume的名称
    configMap: 
      name: cm-volume           # 引用的ConfigMap为cm-volume
      items:
      - key: cm1.cfg            # key=cm1.cfg
        path: cm1-cfg           # cm1.cfg的value将以文件名cm1-cfg进行挂载
      - key: cm2.cfg            # ey=cm2.cfg
        path: cm2-cfg           # cm2.cfg的value将以文件名cm2-cfg进行挂载

根据定义文件生产Pod

# kubectl apply -f cm-volume-pod.yaml 
pod/cm-volume-pod created

进入POD容器内部查看挂载配置文件

# kubectl get pods
NAME             READY   STATUS    RESTARTS   AGE
cm-volume-pod    1/1     Running   0          17s

# kubectl exec -it cm-volume-pod /bin/sh
/ # cd /etc/config/
/etc/config # ls
cm1-cfg  cm2-cfg
/etc/config # cat cm1-cfg 
env=dev1
app.name=cm1
log.level=info
/etc/config # cat cm2-cfg 
env=alpha
app.name=cm2
log.level=debug
/etc/config #

执行“kubectl exec”登录POD内部容器,切换到/etc/config目录下,可以看到存在cm1-cfg和cm2-cfg两个文件,它们的内容就是ConfigMap“cm-volume”中两个key定义的内容。

如果在引用configMap时不指定items,POD内部容器在使用volumeMount挂载ConfigMap时,会根据ConfigMap中数据(data)条目(item)的数量,在容器挂载目录下为每个条目生成一个以条目名称为文件名的文件。下面举例说明:

创建POD定义文件

# cat cm-volume-pod2.yaml
apiVersion: v1
kind: Pod
metadata:
  name: cm-volume-pod2
spec:
  containers:
  - name: cm-volume2
    image: busybox
    imagePullPolicy: Never
    command: ["/bin/sh", "-c", "sleep 3600"]
    volumeMounts:
    - name: config-volume2      # 引用Volume的名称
      mountPath: /etc/config    # 挂载到容器内的目录
  volumes:
  - name: config-volume2        # 定义Volume的名称
    configMap: 
      name: cm-volume           # 引用的ConfigMap为cm-volume

根据定义文件生产Pod

# kubectl apply -f cm-volume-pod2.yaml 
pod/cm-volume-pod2 created

进入POD容器内部查看挂载配置文件

# kubectl get pods
NAME               READY   STATUS    RESTARTS   AGE
cm-volume-pod2     1/1     Running   0          113s

# kubectl exec -it cm-volume-pod2 /bin/sh
/ # cd /etc/config/
/etc/config # ls
cm1.cfg  cm2.cfg
/etc/config # cat cm1.cfg 
env=dev1
app.name=cm1
log.level=info
/etc/config # cat cm2.cfg 
env=alpha
app.name=cm2
log.level=debug
/etc/config #

执行“kubectl exec”登录POD内部容器,切换到/etc/config目录下,可以看到存在cm1.cfg和cm2.cfg两个文件,它们的内容就是ConfigMap“cm-volume”中两个key定义的内容。

4.3.3 使用ConfigMap的限制条件

使用ConfigMap的限制条件如下:

  • ConfigMap必须在Pod之前创建。
  • ConfigMap受Namespace限制,只有处于相同Namespace中的Pod才可以引用它。
  • ConfigMap中定义的key的值都是明文的,对于一些密码类型的值不安全。
  • 在Pod对ConfigMap进行挂载(volumeMount)操作时,在容器内部只能挂载为“目录”,无法挂载为“文件”。挂载到容器内部后,在目录下将包含ConfigMap定义的每个item,如果在该目录下原来还有其他文件,则容器内的该目录将被挂载的ConfigMap覆盖。如果应用程序需要保留原来的其他文件,则需要进行额外的处理。可以将ConfigMap挂载到容器内部的临时目录,再通过启动脚本将配置文件复制或者链接到(cp或link命令)应用所用的实际配置目录下。

5. 容器内读取Pod的信息(Downward API)

待完善

6. Pod 生命周期和重启策略

Pod在整个生命周期中被系统定义为各种状态,熟悉Pod的各种状态对于理解如何设置Pod的调度策略、重启策略是很有必要的。

Pod的状态如表格所示:

状态值 描述
Pending API Server 已经创建该Pod,但是在Pod内还有一个或多个容器的镜像没有创建,包括正在下载镜像的过程
Running Pod 内所有容器均已创建,且至少有一个容器处于运行状态、正在启动状态或正在重启状态
Succeeded Pod 内所有容器均成功执行后退出,且不会在重启
Failed Pod 内所有容器均已退出,但至少有一个容器退出为失败状态
Unknow 由于某种原因无法获取该Pod的状态,可能由于网络通信不畅导致

Pod的重启策略(RestartPolicy)应用于Pod内的所有容器,并且仅在Pod所处的Node上由kubelet进行判断和重启操作。当某个容器异常退出或者健康检查失败时,kubelet将根据RestartPolicy的设置来进行相应的操作。

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

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

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

Pod的重启策略与控制方式息息相关,当前可用于管理Pod的控制器包括ReplicationController、Job、DaemonSet及直接通过kubelet管理(静态Pod)。
每种控制器对Pod的重启策略要求如下。

  • RC和DaemonSet:必须设置为Always,需要保证该容器持续运行。
  • Job:OnFailure或Never,确保容器执行完成后不再重启。
  • kubelet:在Pod失效时自动重启它,不论将RestartPolicy设置为什么值,也不会对Pod进行健康检查。

结合Pod的状态和重启策略,下面表格列出一些常见的状态转换场景:

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

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

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

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

  1. ExecAction:在容器内部执行一个命令,如果该命令的返回码为0,则表明容器健康。
  2. TCPSocketAction:通过容器的IP地址和端口号执行TCP检查,如果能够建立TCP连接,则表明容器健康。
  3. HTTPGetAction:通过容器的IP地址、端口号及路径调用HTTP Get方法,如果响应的状态码大于等于200且小于400,则认为容器健康。

对于每种探测方式,都需要设置initialDelaySeconds和timeoutSeconds两个参数,它们的含义分别如下:

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

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

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

8. 玩转Pod调度

在Kubernetes平台上,我们很少会直接创建一个Pod,在大多数情况下会通过RC、Deployment、DaemonSet、Job等控制器完成对一组Pod副本的创建、调度及全生命周期的自动控制任务。

在最早的Kubernetes版本里是没有这么多Pod副本控制器的,只有一个Pod副本控制器RC(Replication Controller),这个控制器是这样设计实现的:RC独立于所控制的Pod,并通过Label标签这个松耦合关联关系控制目标Pod实例的创建和销毁,随着Kubernetes的发展,RC也出现了新的继任者——Deployment,用于更加自动地完成Pod副本的部署、版本更新、回滚等功能。

严谨地说,RC的继任者其实并不是Deployment,而是ReplicaSet,因为 ReplicaSet进一步增强了RC标签选择器的灵活性。之前RC的标签选择器只能选择一个标签,而ReplicaSet拥有集合式的标签选择器,可以选择多个Pod标签,如下所示:

......
    selector:
      matchLabels:
        tier: frontend
      matchExpressions:
        - {key: tier, operator: In, values: [frontend]}

与RC不同,ReplicaSet被设计成能控制多个不同标签的Pod副本。一种常见的应用场景是,应用MyApp目前发布了v1与v2两个版本,用户希望MyApp的Pod副本数保持为3个,可以同时包含v1和v2版本的Pod,就可以用ReplicaSet来实现这种控制,写法如下:

......
    selector:
      matchLabels:
        version: v2
      matchExpressions:
        - {key: version, operator: In, values: [v1, v2]}

其实,Kubernetes的滚动升级就是巧妙运用ReplicaSet的这个特性来实现的,同时,Deployment也是通过ReplicaSet来实现Pod副本自动控制功能的。我们不应该直接使用底层的ReplicaSet来控制Pod副本,而应该使用管理ReplicaSet的Deployment对象来控制副本,这是来自官方的建议。

在大多数情况下,我们希望Deployment创建的Pod副本被成功调度到集群中的任何一个可用节点,而不关心具体会调度到哪个节点。但是,在真实的生产环境中的确也存在一种需求:希望某种Pod的副本全部在指定的一个或者一些节点上运行,比如希望将MySQL数据库调度到一个具有SSD磁盘的目标节点上,此时Pod模板中的NodeSelector属性就开始发挥作用了,上述MySQL定向调度案例的实现方式可分为以下两步:

  1. 把具有SSD磁盘的Node都打上自定义标签“disk=ssd”;
  2. 在Pod模板中设定NodeSelector的值为“disk: ssd”.

如此一来,Kubernetes在调度Pod副本的时候,就会先按照Node的标签过滤出合适的目标节点,然后选择一个最佳节点进行调度。

上述逻辑看起来既简单又完美,但在真实的生产环境中可能面临以下问题:

  • 如果NodeSelector选择的Label不存在或者不符合条件,比如这些目标节点此时宕机或者资源不足,该怎么办?
  • 如果要选择多种合适的目标节点,比如SSD磁盘的节点或者超高速硬盘的节点,该怎么办?

为此,Kubernates引入了NodeAffinity(节点亲和性设置)来解决该需求。

在真实的生产环境中还存在如下所述的特殊需求:

  • 不同Pod之间的亲和性(Affinity)。比如MySQL数据库与Redis中间件不能被调度到同一个目标节点上,或者两种不同的Pod必须被调度到同一个Node上,以实现本地文件共享或本地网络通信等特殊需求,这就是PodAffinity要解决的问题。
  • 有状态集群的调度。对于ZooKeeper、Elasticsearch、MongoDB、Kafka等有状态集群,虽然集群中的每个Worker节点看起来都是相同的,但每个Worker节点都必须有明确的、不变的唯一ID(主机名或IP地址),这些节点的启动和停止次序通常有严格的顺序。此外,由于集群需要持久化保存状态数据,所以集群中的Worker节点对应的Pod不管在哪个Node上恢复,都需要挂载原来的Volume,因此这些Pod还需要捆绑具体的PV。针对这种复杂的需求,Kubernetes提供了StatefulSet这种特殊的副本控制器来解决问题,在Kubernetes 1.9版本发布后,StatefulSet才可用于正式生产环境中。
  • 在每个Node上调度并且仅仅创建一个Pod副本。这种调度通常用于系统监控相关的Pod,比如主机上的日志采集、主机性能采集等进程需要被部署到集群中的每个节点,并且只能部署一个副本,这就是DaemonSet这种特殊Pod副本控制器所解决的问题。
  • 对于批处理作业,需要创建多个Pod副本来协同工作,当这些Pod副本都完成自己的任务时,整个批处理作业就结束了。这种Pod运行且仅运行一次的特殊调度,用常规的RC或者Deployment都无法解决,所以Kubernates引入了新的Pod调度控制器Job来解决问题,并继续延伸了定时作业的调度控制器CronJob。

与单独的Pod实例不同,由RC、ReplicaSet、Deployment、DaemonSet等控制器创建的Pod副本实例都是归属于这些控制器的,这就产生了一个问题:控制器被删除后,归属于控制器的Pod副本该何去何从?在Kubernates 1.9之前,在RC等对象被删除后,它们所创建的Pod副本都不会被删除;在Kubernates1.9以后,这些Pod副本会被一并删除。如果不希望这样做,则可以通过kubectl命令的--cascade=false参数来取消这一默认特性。

# kubectl delete replicaset my-repset --cascade=false

8.1 全自动调度(Deployment)

Deployment的主要功能之一就是自动部署一个容器应用的多份副本,以及持续监控副本的数量,在集群内始终维持用户指定的副本数量。

以下是一个Deployment的例子,该配置文件将创建一个RepicaSet,该ReplicaSet会创建3个Nginx应用的Pod:

# cat nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 80

运行kubectl apply 命令创建该Deployment:

# kubectl apply -f nginx-deployment.yaml
deployment/nginx-deployment created

查看Deployment的状态:

  • -o wide:查看更详细的信息
# kubectl get deployments -o wide
NAME               READY   UP-TO-DATE   AVAILABLE   AGE     CONTAINERS   IMAGES       SELECTOR
nginx-deployment   3/3     3            3           2m22s   nginx        nginx:1.14   app=nginx

通过以上信息可以看到:Deployment已创建好所有3个副本,并且所有副本都是最新的可用的。

通过运行“kubectl get pods”和“kubectl get rs”查看已经创建的ReplicaSet和Pod的信息。

# kubectl get rs -o wide           
NAME                          DESIRED   CURRENT   READY   AGE     CONTAINERS   IMAGES       SELECTOR
nginx-deployment-6cfc444fb    3         3         3       2m30s   nginx        nginx:1.14   app=nginx,pod-template-hash=6cfc444fb

# kubectl get pods -o wide  
NAME                                READY   STATUS    RESTARTS   AGE     IP              NODE         NOMINATED NODE   READINESS GATES
nginx-deployment-6cfc444fb-lqbjv    1/1     Running   0          2m36s   10.100.14.221   k8s-node02   <none>           <none>
nginx-deployment-6cfc444fb-q72lc    1/1     Running   0          2m36s   10.100.0.228    k8s-node01   <none>           <none>
nginx-deployment-6cfc444fb-zxf7d    1/1     Running   0          2m37s   10.100.7.164    k8s-node03   <none>           <none>

以上信息可以看到:ReplicaSet和Pods的名称之间是有所关联的。从调度策略上看,这3个Nginx Pod由系统全自动完成调度。它们各自最终运行在哪个节点上,完全由Master的Scheduler经过一系列算法计算得出,用户无法干预调度过程和结果。

除了使用系统自动调度算法完成一组Pod的部署,Kubernetes也提供了多种丰富的调度策略,用户只需在Pod的定义中使用NodeSelector、NodeAffinity、PodAffinity、Pod驱逐等更加细粒度的调度策略设置,就能完成对Pod的精准调度。以下是这些策略的详细说明。

8.1.1 Node定向调度(NodeSelector)

Kubernetes Master上的Scheduler服务(kube-scheduler进程)负责实现Pod的调度,整个调度过程通过执行一系列复杂的算法,最终为每个Pod都计算出一个最佳的目标节点,这一过程是自动完成的,通常我们无法知道Pod最终会被调度到哪个节点上。在实际情况下,也可能需要将Pod调度到指定的一些Node上,可以通过Node的标签(Label)和Pod的nodeSelector属性相匹配,来达到上述目的。

首先,我们给目标Node打上标签,下面我们给节点k8s-node01打上一个“role=middleware”的标签。

# kubectl label nodes k8s-node01 role=middleware

# kubectl get nodes --show-labels
NAME         STATUS   ROLES    AGE   VERSION   LABELS
k8s-master   Ready    master   17d   v1.15.5   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-master,kubernetes.io/os=linux,node-role.kubernetes.io/master=
k8s-node01   Ready    <none>   17d   v1.15.5   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-node01,kubernetes.io/os=linux,role=middleware
k8s-node02   Ready    <none>   17d   v1.15.5   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-node02,kubernetes.io/os=linux
k8s-node03   Ready    <none>   17d   v1.15.5   beta.kubernetes.io/arch=amd64,beta.kubernetes.io/os=linux,kubernetes.io/arch=amd64,kubernetes.io/hostname=k8s-node03,kubernetes.io/os=linux

然后,在Pod的资源定义文件总加上nodeSelector的配置,下面以一个Redis的Pod为例:

apiVersion: v1
kind: Pod
metadata:
  name: php-redis
  labels:
    name: php-redis
spec:
  containers:
  - name: frontend
    image: ygego/php-frontend
    env:
    - name: GET_HOSTS_FROM
      value: env
    ports:
    - containerPort: 80
  - name: redis
    image: ygego/redis-master
    ports:
    - containerPort: 6379
  nodeSelector:
    role: middleware

运行命令创建此Pod,Scheduler就会将给Pod调度到拥有标签“role=middleware”的Node上。

# kubectl apply -f frontend-redis-pod.yaml 
pod/php-redis created
 
# kubectl get pods -o wide
NAME         READY   STATUS    RESTARTS   AGE   IP             NODE         NOMINATED NODE   READINESS GATES
php-redis    2/2     Running   0          8s    10.100.0.229   k8s-node01   <none>           <none>

如果我们给多个Node都定义了相同的标签(例如zone=north),则scheduler会根据调度算法从这组Node中挑选一个可用的Node进行Pod调度。

通过基于Node标签的调度方式,我们可以把集群中具有不通特点的Node都加上不同的标签,例如“role=frontedn”、“role=backend”、“role=database”等标签,在部署应用是就可以根据应用的需求设置NodeSelector来进行指定Node范围的调度。

需要注意的是,如果我们指定了Pod的nodeSelector条件,且在集群中不存在包含相应标签的Node,则即使在集群中还有其他可供使用的Node,这个Pod也无法被成功调度。

除了用户可以给Node添加标签,Kubernetes集群也会给Node预定义一些标签,用户也可以使用这些系统标签进行Pod的定向调度,包括:

  • kubernetes.io/hostname
  • beta.kubernetes.io/os(从1.14版本开始更新为稳定版,到1.18版本删除)
  • beta.kubernetes.io/arch(从1.14版本开始更新为稳定版,到1.18版本删除)
  • kubernetes.io/os(从1.14版本开始启用)
  • kubernetes.io/arch(从1.14版本开始启用)

NodeSelector通过标签的方式,简单实现了限制Pod所在节点的方法。亲和性调度机制则极大扩展了Pod的调度能力,主要的增强功能如下:

  • 更具表达力(不仅仅是“符合全部”的简单情况)。
  • 可以使用软限制、优先采用等限制方式,代替之前的硬限制,这样调度器在无法满足优先需求的情况下,会退而求其次,继续运行该Pod。
  • 可以依据节点上正在运行的其他Pod的标签来进行限制,而非节点本身的标签。这样就可以定义一种规则来描述Pod之间的亲和或互斥关系。

亲和性调度功能包括节点亲和性(NodeAffinity)和Pod亲和性(PodAffinity)两个维度的设置。节点亲和性与NodeSelector类似,增强了上述前两点优势;Pod的亲和与互斥限制则通过Pod标签而不是节点标签来实现,也就是上面第3点内容所陈述的方式,同时具有前两点提到的优点。

NodeSelector将会继续使用,随着节点亲和性越来越能够表达nodeSelector的功能,最终NodeSelector会被废弃。

8.1.2 Node亲和性调度(NodeAffinity)

NodeAffinity意为Node亲和性的调度策略,是用于替换NodeSelector的全新调度策略。目前有两种节点亲和性表达:

  • RequiredDuringSchedulingIgnoredDuringExecution:必须满足指定的规则才可以调度Pod到Node上(功能与nodeSelector很像,但是使用的是不同的语法),相当于硬限制。
  • PreferredDuringSchedulingIgnoredDuringExecution:强调优先满足指定规则,调度器会尝试调度Pod到Node上,但并不强求,相当于软限制。多个优先级规则还可以设置权重(weight)值,以定义执行的先后顺序。

IgnoredDuringExecution的意思是:如果一个Pod所在的节点在Pod运行期间标签发生了变更,不再符合该Pod的节点亲和性需求,则系统将忽略Node上Label的变化,该Pod能继续在该节点运行。

NodeAffinity语法支持的操作符包括In、NotIn、Exists、DoesNotExist、Gt、Lt。虽然没有节点排斥功能,但是用NotIn和DoesNotExist就可以实现排斥的功能了。

NodeAffinity调度规则示例,具体需求如下:

  • requiredDuringSchedulingIgnoredDuringExecution要求只运行在amd64的节点上(beta.kubernetes.io/arch In amd64)
  • preferredDuringSchedulingIgnoredDuringExecution要求尽量运行在磁盘类型为ssd的节点上(disk-type In ssd)。

Pod资源定义文件

apiVersion: v1
kind: Pod
metadata:
  name: node-affinity
spec:
  containers:
  - name: node-affinity
    image: nginx:1.14
    imagePullPolicy: IfNotPresent
  affinity:
    nodeAffinity:
      requireDuringSchedulingIgnoreDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: beta.kubernetes.io/arch
            opertator: In
            value:
            - amd64
      preferredDuringSchedulingIgnoreDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: disk-type
            operator: In
            values;
            - ssd

NodeAffinity规则设置的注意事项如下:

  • 如果同时定义了nodeSelector和nodeAffinity,那么必须两个条件都得到满足,Pod才能最终运行在指定的Node上。
  • 如果nodeAffinity指定了多个nodeSelectorTerms,那么其中一个能够匹配成功即可。
  • 如果nodeSelectorTerms中有多个matchExpressions,则一个节点必须满足所有matchExpressions才能运行该Pod。

8.1.3 Pod亲和与互斥调度(PodAffinity)

出于高效通信的需求,偶尔需要把一些 Pod对象组织在相近的位置(同一节点、机架、区域或地区等),如某业务的前端 Pod和后端Pod等,此时可以将这些Pod对象间的关系称为亲和性。偶尔,出于安全或分布式等原因也有可能需要将一些Pod对象在其运行的位置上隔离开来,如在每个区域运行一个应用代理 Pod对象等,此时可把这些 Pod对象间的关系称为反亲和性(anti-affinity)。

当然,也可以通过节点亲和性来定义Pod对象间的亲和或反亲和特性,但用户必须为此明确指定Pod可运行的节点标签,显然这并非较优的选择。较理想的实现方式是,允许调度器把第一个 Pod放置于任何位置,而后与其有亲和或反亲和关系的 Pod据此动态完成位置编排,这就是 Pod亲和性调度和反亲和性调度的功用。 Pod的亲和性定义也存在“硬”(required)亲和性和“软”(preferred)亲和性的区别,它们表示的约束意义同节点亲和性相似。

Kubemetes调度器通过内建的MatchlnterPodAffinity预选策略为这种调度方式完成节点预选,并基于InterPodAffinityPriority优选函数进行各节点的优选级评估。

8.1.3.1 位置拓扑

Pod亲和性调度需要各相关的 Pod对象运行于“同一位置”,而反亲和性调度则要求它们不能运行于“同一位置”。何谓同一位置?事实上,它们取决于节点的位置拓扑,拓扑的方式不同,对于如图 8-1所示的Pod-A和Pod-B是否在同一位置的判定结果也可能有所不同。
如果以基于各节点的 kubernetes.io/hostname标签作为评判标准,那么很显然,“同一位置”意味着同一个节点,不同节点即不同的位置,如图 8-2所示。

而如果是基于如图 8-3所划分的故障转移域来进行评判,那么 serverl和 server4属于同一位置,而 server2和 server3属于另一个意义上的同一位置

故此,在定义Pod对象的亲和性与反亲和性时,需要借助于标签选择器来选择被依赖的Pod对象,并根据选出的Pod对象所在节点的标签来判定“同一位置”的具体意义。

8.1.3.2 Pod硬亲和调度

Pod强制约束的亲和性调度也使用 requiredDuringSchedulinglgnoredDuringExecution属性进行定义。 Pod亲和性用于描述一个 Pod对象与具有某特征的现存 Pod对象运行位置的依赖关系,因此,测试使用 Pod亲和性约束,需要事先存在被依赖的 Pod对象,它们具有特别的识别标签。下面创建一个有着标签“ app=nginx”的 Deployment资源部署一个目标 Pod对象:

# kubectl run nginx -l app=nginx --image nginx:1.14
deployment.apps/nginx created

# kubectl get pods -o wide
NAME                     READY   STATUS    RESTARTS   AGE   IP             NODE         NOMINATED NODE   READINESS GATES
nginx-699bd9c545-jhwhs   1/1     Running   0          47s   10.100.7.166   k8s-node03   <none>           <none>

下面的资源配置清单中定义了一个 Pod对象,它通过labelSlector定义的标签选择器挑选感兴趣的现存 Pod对象,而后根据挑选出的 Pod对象所在节点的标签“kubernetes.io/hostname”来判断同一位置的具体含义,并将当前 Pod对象调度至这一位置的某节点之上:

# cat required-podAffinity-pod.yaml
apiVersion: vl 
kind: Pod
metadata:
  name: pod-affinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoreDuringExecution:
      - labelSelector:
          matchExpressions:
          - {key: app, operator: In, values: ["nginx"]}
        topologyKey: kubernetes.io/hostname
  containers:
  - name: myapp
    image: ikubernetes/myapp:v1
    imagePullPolicy: IfNotPresent

事实上,kubernetes.io/hostname标签是Kubernetes集群节点的内建标签,它的值为当前节点的节点主机名称标识,对于各个节点来说,各有不同。因此,新建的 Pod对象将被部署至被依赖的 Pod对象的同一节点之上, requiredDuringSchedulinglgnoredDuringExecution表示这种亲和性为强制约束。

# kubectl apply -f required-podAffinity-pod.yaml 
pod/pod-affinity created

# kubectl get pods -o wide
NAME                     READY   STATUS    RESTARTS   AGE   IP             NODE         NOMINATED NODE   READINESS GATES
nginx-699bd9c545-jhwhs   1/1     Running   0          16m   10.100.7.166   k8s-node03   <none>           <none>
pod-affinity             1/1     Running   0          12s   10.100.7.167   k8s-node03   <none>           <none>

基于单一节点的 Pod亲和性只在极个别的情况下才有可能会用到,较为常用的通常是基于同一地区(region)、区域(zone)或机架( rack)的拓扑位置约束。例如部署应用程序服务(myapp)与数据库(db)服务相关的 Pod时,db Pod可能会部署于如图 8-4所示的 foo或 bar这两个区域中的某节点之上,依赖于数据服务的 myapp Pod对象可部署于 db Pod所在区域内的节点上。当然,如果 db Pod在两个区域 foo和 bar中各有副本运行,那么 myapp Pod将可以运行于这两个区域的任何节点之上。

例如,创建两个拥有标签为“app=db”的副本Pod作为被依赖的资源,它们可能运行于类似图8-5所示的三个节点中的任何一个或两个节点之上,本示例中它们凑巧运行于两个 zone标签值不同的节点之上:

# kubectl run db -l app=db --image=ygego/redis-master:latest --image-pull-policy=Never --replicas=2
deployment.apps/db created

# kubectl get pods -o wide
NAME                 READY   STATUS    RESTARTS   AGE   IP             NODE         NOMINATED NODE   READINESS GATES
db-d47c9597c-n2pq8   1/1     Running   0          7s    10.100.0.232   k8s-node01   <none>           <none>
db-d47c9597c-vjdt8   1/1     Running   0          7s    10.100.7.169   k8s-node03   <none>           <none>

因此,依赖亲和于这两个 Pod的其他 Pod对象可运行于 zone标签值为 foo和 bar的区域内的所有节点之上。以下资源配置清单中定义了这样一些Pod资源,它们由 Deployment控制器所创建:

# cat deploy-required-podAffinity.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-required-podaffinity
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      name: myapp
      labels:
        app: myapp
    spec:
      affinity:
        podAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - {key: app, operator: In, values: ["db"]}
            topologyKey: zone
      containers:
      - name: myapp
        image: ygego/myapp:v1
        imagePullPolicy: IfNotPresent
# kubectl apply -f deployment-required-affinity.yaml 
deployment.apps/myapp-required-podaffinity created

# kubectl get pods -o wide --show-labels
NAME                                  READY   STATUS    RESTARTS   AGE     IP              NODE         NOMINATED NODE   READINESS GATES   LABELS
myapp-required-podaffinity-57ff997984-4snvl   1/1     Running   0          11s     10.100.0.233    k8s-node01   <none>           <none>            app=myapp,pod-template-hash=57ff997984
myapp-required-podaffinity-57ff997984-mpgx4   1/1     Running   0          11s     10.100.14.222   k8s-node02   <none>           <none>            app=myapp,pod-template-hash=57ff997984
myapp-required-podaffinity-57ff997984-w9mwn   1/1     Running   0          11s     10.100.7.170    k8s-node03   <none>           <none>            app=myapp,pod-template-hash=57ff997984

在调度示例中的 Dep loym巳nt控制器创建的 Pod资源时,调度器首先会基于标签选择器查询拥有标签“ app=db”的所有Pod资源,接着获取到它们分别所属的节点的 zone标签值,接下来再查询拥有匹配这些标签值的所有节点,从而完成节点预选。而后根据优选函数计算这些节点的优先级,从而挑选出运行新建Pod对象的节点。

需要注意的是,如果节点上的标签在运行时发生了更改,以致它不再满足Pod上的亲和性规则,但该Pod还将继续在该节点上运行,因此它仅会影响新建的Pod资源;另外,labelSelector属性仅匹配与被调度器的 Pod在同一名称空间中的 Pod资源,不过也可以通过为其添加 namespace字段以指定其他名称空间。

8.1.3.3 Pod软亲和调度


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM