《Kubernetes权威指南》学习笔记第九篇-Pod调度

1、控制器使用场景

一般Pod不会单独进行创建,而是通过控制器来进行创建,但是控制器有很多种,针对Pod需要调度的使用场景需要使用不同的控制器,下面总结了kubernetes中常见控制器

控制器 描述 场景
Deployment 管理ReplicaSet来创建无状态Pod副本 可以调度Pod副本到任一节点
StatefulSet 创建有状态的Pod副本 各类有数据变化的集群,如数据库集群、消息队列集群
DaemonSet 每个节点上调度有且仅有一个Pod副本 常用于系统监控的Pod
CronJob 创建多个Pod副本协同工作 批处理作业

通过控制器创建的Pod归属这些控制器,如果删除了这些控制器,默认情况下归属的相关Pod也会被删除,如果只想删除控制器而不想删除Pod请添加参数--cascade=false

2、Deployment全自动调度

原理:Deployment通过ReplicaSet创建Pod副本,自动部署一个容器应用的多份副本并持续监控副本数量并维持该副本数量

cat nginx-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment   # Deployment控制器名称
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80

kubectl create -f nginx-deployment.yaml
kubectl get pods

nginx-deployment-7848d4b86f-m4dtx 1/1 Running 0 3m42s
nginx-deployment-7848d4b86f-mhffq 1/1 Running 0 3m42s
nginx-deployment-7848d4b86f-sb5vv 1/1 Running 0 3m42s

kubectl get deployments 查看Deployment

nginx-deployment 3/3 3 3 7m7s

kubectl get rs查看ReplicaSet

nginx-deployment-7848d4b86f 3 3 3 9m38s

从调度策略上讲,以上3个nginxPod副本又master上的Scheduler经过一系列算法来调度,人为无法干预,但如果用户想自定义调度就需要在yaml文件中进行一些字段的设置,下面一一进行介绍

也可以使用以前版本的RC
以redis为例,利用RC来创建
cat redis-master-controller.yaml

apiVersion: v1
kind: ReplicationController
metadata:
  name: redis-master-v1
  labels: redis-master
spec:
  replicas: 1
  selector:
    name: redis-master
  template:
    metadata:
      labels:
        name: redis-master
    spec:
      containers:
      - name: master
        image: kubeguide/redis-master:1.0
        ports:
        - containerPort: 6379

创建RC
`kubectl create -f redis-master-controller.yaml

ps:要注意比较Deployment与ReplicationController之间字段apiVersion与selector的区别

3、自定义调度

3.1.NodeSelector

两步走

  • 给想要调度Pod的目标节点设置标签
  • 编写yaml文件时添加nodeSelector字段

kubectl label nodes 192.168.0.160 zone=north给节点192.168.0.160打上标签zone=north

cat redis-master-controller.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis-deployment
  labels:
    name: redis-master
spec:
  replicas: 1
  selector:
    matchLabels:
      name: redis-master
  template:
    metadata:
      labels:
        name: redis-master
    spec:
      containers:
      - name: master
        image: kubeguide/redis-master
        ports:
        - containerPort: 6379
      nodeSelector:
        zone: north

创建Deployment后可以看到Pod副本在192.168.0.160上创建了
kubectl get pods -o wide返回

NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
redis-deployment-565bd84bdb-5rhwj 1/1 Running 0 4m1s 172.17.0.4 192.168.0.160

可以看到Pod在192.168.0.160这个我们打了标签的节点创建

ps1:使用nodeselector调度策略可以将Pod调度到我们自定义了标签的节点上去,这个特性在实际生产环境中很有用,比如可以给计划部署数据库的节点打上nodename=database之类的标签,然后将数据库Pod部署到该节点上

ps2:如果yaml文件里使用了字段nodeselector,但是节点上都没有打上相应的标签,即使此时集群中有可用的节点,Pod也无法被成功调度

ps3:除了自定义的节点标签,也可以使用kubernetes给节点预定义的标签,查看节点标签使用命令
kubectl get nodes --show-labels

3.2.NodeAffinity亲和性调度

NodeAffinity比NodeSelector在调度层面更为灵活强大,可以用来替代NodeSelector
节点亲和性有两个表达方式

表达方式 描述
RequiredDuringSchedulingIgnoredDuringExecution 必须满足指定规则Pod才会被调度,类似于nodeselector
PreferredDuringSchedulingIgnoredDuringExecution 优先满足指定规则就会尝试进行调度,但不强制

ps:IgnoreDuringExecution时表示如果某个Pod运行的节点标签有变动,但不会影响该Pod继续在此节点上运行

下面做个测试,给192.168.0.159这个节点打上标签disk-type=ssd
kubectl label node 192.168.0.159 disk-type=ssd

cat with-node-affinity.yaml

apiVersion: v1
kind: Pod
metadata:
  name: with-node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: beta.kubernetes.io/arch
            operator: In
            values:
            - amd64
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: disk-type
            operator: In
            values:
            - ssd
  containers:
  - name: with-node-affinity
    image: k8s.gcr.io/pause:3.2

创建Pod后查看
kubectl get pods with-node-affinity -o wide

with-node-affinity 1/1 Running 0 7m1s 172.17.0.6 192.168.0.159
与我们预期的一样,该Pod被调度到了节点192.168.0.159上

在使用过程中以下几点要注意

  • 如果同时定义了nodeselector和nodeaffinity,则必须同时满足这两个规则才能完成调度
  • 如果nodeaffinity指定了多个nodeSelectorTerms,则匹配其中一个即可完成调度
  • 如果nodeSelectorTerms有多个matchExpressions则必须有节点能同时满足所有matchExpressions规则才可以完成调度

3.3.PodAffinity调度

原理:根据已运行的pod标签而不是节点标签进行调度,通俗一点讲,如果在具有标签X的node上运行了一个或者多个符合条件Y的Pod,如果设置为互斥则不把将要创建Pod调度到这些节点,如果亲和则调度到这些节点

先创建一个参照Pod
cat pod-flag.yaml

apiVersion: v1
kind: Pod
metadata:
  name: pod-flag
  labels:
    security: "S1"
    app: "nginx"
spec:
  containers:
  - name: nginx
    image: nginx

3.3.1.亲和性调度

针对参照Pod,创建一个例子来说明亲和性调度
cat pod-affinity.yaml

apiVersion: v1
kind: Pod
metadata:
  name: pod-affinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: security
            operator: In
            values:
            - S1
        topologyKey: kubernetes.io/hostname
  containers:
  - name: with-pod-affinity
    image: k8s.gcr.io/pause:3.2

创建Pod后查看
kubectl get pods -o wide

NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod-affinity 1/1 Running 0 22m 172.17.0.3 192.168.0.160
pod-flag 1/1 Running 0 38m 172.17.0.2 192.168.0.160
with-node-affinity 1/1 Running 0 2d23h 172.17.0.4 192.168.0.159

从上面得结果说明创建得Pod与参照Pod是处于同一个节点上
注意这里的topologyKey值,为了验证其作用,做一个测试
先删除目前运行的Pod
kubectl delete -f pod-affinity.yaml
kubectl labels nodes 192.168.0.160 kubernetes.io/hostname-删除节点192.168.0.160上的标签kubernetes.io/hostname
kubectl create -f pod-affinity.yaml再次创建Pod
kubectl get pods返回

NAME READY STATUS RESTARTS AGE
pod-affinity 0/1 Pending 0 2m11s

从上面发现Pod一直处于Pending状态,这说明没有满足条件topologyKey值时Pod也是无法成功调度的

3.3.2.互斥性调度

将上面删掉标签kubernetes.io/hostname=192.168.0.160恢复
kubectl label node 192.168.0.160 kubernetes.io/hostname=192.168.0.160

cat anti-affinity.yaml

apiVersion: v1
kind: Pod
metadata:
  name: anti-affinity
spec:
  affinity:
    podAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: security
            operator: In
            values:
            - S1
        topologyKey: beta.kubernetes.io/arch
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - nginx
        topologyKey: kubernetes.io/hostname
  containers:
  - name: anti-affinity
    image: k8s.gcr.io/pause:3.2

kubectl create -f anti-affinity.yaml创建Pod
kubectl get pods -o wide

NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
anti-affinity 1/1 Running 0 13m 172.17.0.2 192.168.0.159
pod-affinity 1/1 Running 0 70m 172.17.0.3 192.168.0.160
pod-flag 1/1 Running 0 121m 172.17.0.2 192.168.0.160

从上面的返回结果可以看出新创建的Pod基于Pod互斥性特性没有调度到与参照Pod同一节点上,这里虽然也有亲和性调度配置,匹配节点标签名beta.kubernetes.io/arch和pod标签security: S1,但是当存在互斥性的时候也需要满足互斥性配置,如果两个规则之间有所交集以互斥配置优先,比如本文就是

ps1:在Pod亲和性调度和RequiredDuringScheduling类型的互斥性定义中不允许使用空的topologyKey

ps2:在PreferredDuringScheduling类型的Pod互斥性定义中,空的topologyKey默认为kubernetes.io/hostname、failure-domain.beta.kubernetes.io/zone及failure-domain.beta.kubernetes.io/region的组合

ps3:除了设置labelSelector和topologyKey还可以设置Namespace来限制,这里默认使用了要创建的Pod所在的namespace,设置为空则表示所有的namespace

ps4:与节点亲和性类似,在requireDuringScheduling类型的matchExpressions要全部满足才能调度Pod到目标节点

3.4.污点与容忍调度

与nodeaffinity相反,污点与容忍是让pod不调度到某些节点
Taints需要与Toleration结合使用,Taint是节点属性,Toleration是pod属性,两者均可以设置多个

  • 给node设置Taint
    kubectl taint node nodename key=value:Noschedule
    该命令表示node上配置了一个taint,键值为key、value,效果是Noschedule,除非在pod声明可以容忍这个taint,否则pod不会被调度到该node上
  • pod上声明Tolerations
tolerations:
- key: "key"
  operator: "Equal"
  value: "value"
  effect: "NoSchedule"

或者写成

tolerations:
- key: "key"
  operator: "Exists"
  effect: "NoSchedule"

下面是针对效果的表,描述了三种效果

效果值 结果
NoSchedule 不会将pod调度到节点
PreferNoSchedule 尽量不把Pod调度到节点
NoExecute 不会将Pod调度到节点,如果某个节点已经运行该Pod,会进行驱逐

下面用一个例子来简单说明以下
对某个节点进行Taint设置
kubectl taint nodes node1 key1=value1:NoSchedule
kubectl taint nodes node1 key1=value1:NoExecute
kubectl taint nodes node1 key2=value2:NoSchedule

pod上设置Toleration

tolerations:
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoSchedule"
- key: "key1"
  operator: "Equal"
  value: "value1"
  effect: "NoExcute"

经过上面的设置后,pod还是不可能调度到节点上,这是因为还缺少一个toleration设置,但是若该pod已经运行在该节点,那么在运行时设置第3个Taint时该pod还是能够继续在节点上运行的,因为pod容忍前两个taint了,什么意思呢,这就是说Noschedule只对调度前的pod生效,但是NoExecute不一样,它会直接驱逐,但是这里kubernetes还有一种针对NoExecute的机制,可以在Tolerations中设置一个字段tolerationSeconds,它可以让正在节点运行的Pod在节点被设置taint后仍然可以继续运行的时间范围,在这个时间内Pod不会马上被驱逐,同时如果在这个时间范围取消了NoExecute的taint,那么不会触发驱逐事件

tolerations:
- key: "key1"
  operator: "Exists"
  effect: "NoExecute"
  tolerationSeconds: 6000

上面这个设置的意思是给pod设置了在节点运行的最大时间,虽然设置了对应节点taint的容忍,但是这个容忍不是无限期的,当pod运行时间超过6000s就会被驱逐

应用场景

  • 独占节点
    比如想让应用app1 app2 app3调度到某一些固定的几个节点,并且这些节点只提供给这些应用,这种情况就可以给这几个节点添加相应的taint,并给封装这些应用的pod添加toleration设置,而且这些taint节点需要打上标签,然后pod利用节点亲和性来匹配这些标签进行调度,达到应用只会调度到这批节点,这批节点只允许特定pod调度过来

  • 具有特殊硬件设备的节点
    比如GPU芯片设备等,实现方式同上

  • 定义驱逐行为来应对节点故障
    当运行着大量本地应用的节点网络出现故障,比如节点unreachable或者notready,这个时候可以设置相关的toleration来让应用能继续运行在该节点,为什么呢,既然节点失效应该会马上被驱逐的,这不合理啊,其实原因就是kubernetes在这种情况是先把故障节点标记为Taint,由此设置toleration及tolerationSeconds来保证应用可以继续在有限的时间内运行在故障节点,但是这里要集群将节点在发生故障的时候标记为taint,这需要满足两个条件
    首先是节点故障状态只能是unreachable或notready,其次必须在kubelet启动项添加参数--feature-gates=TaintBasedEvictions=true

至于toleration设置如下

tolerations:
- key: "node.alpha.kubernetes.io/unreachable"
  operator: "Exists"
  effect: "NoExecute"
  tolerationSeconds: 6000   
- key: "node.alpha.kubernetes.io/notReady"
  operator: "Exists"
  effect: "NoExecute"
  tolerationSeconds: 6000

如果没有自定义相关的toleration,集群会自动给pod添加相应的toleration,但是tolerationSeconds的值只有300

3.5.pod优先级调度

pod优先级调度的行为分两类,一类叫驱逐(Eviction),一类叫抢占(Preemption)
触发这两种行为的情况有区别

  • 驱逐调度为kubelet进程的行为,当目标节点资源不足时会根据pod优先级、资源申请量与实际使用量综合计算哪些pod被驱逐
  • 抢占式调度主要是针对没有资源申请量的pod(qos等级为best effort),这无法通过计算资源申请量,这种就会发生抢占式调度,会选择驱逐部分低优先级的pod来释放资源

下面简单说一下优先级设置

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000
globalDefault: false
description: "This priority class should be used for xyz service pods only."

在需要的设置优先级调度的yaml中添加参数

apiVersion: v1
...
  spec:
  ...
  priorityClassName: high-priority

ps:优先级调度有时候会导致很多问题,特别是抢占式优先级调度容易造成调度进入死循环,这一点可以查找书籍《kubernetes权威指南第四版》的384页的相关举例,所以正常来讲当集群资源不够时第一时间想到的应该是增加节点给集群扩容,优先级调度设置只能当作没有资源可增加时的备选

4、DaemonSet

其特点是对于某个pod副本每一个节点只运行一个pod
使用场景:

  • 每个节点运行ceph或gluster进程
  • 每个节点运行一个日志采集器进程
  • 每个节点运行一个监控agent
  • DaemonSet控制器也可以在pod中通过定义NodeSelector或者nodeAffinity来调度

下面使用DaemonSets在每一个工作节点部署一个pod
cat fluentd-ds.yaml

apiVersion: apps/v1
metadata: DaemonSet
  name: fluentd-cloud-logging
  namespace: kube-system
  labels:
    k8s-app: fluentd-cloud-logging
spec:
  selector:
    matchLaels:
      k8s-app: fluentd-cloud-logging
  template:
    metadata:
      namespace: kube-system
      labels:
        k8s-app: fluentd-cloud-logging
    spec:
      containers:
      - name: fluentd-cloud-logging
        image: istOne/fluentd-elasticsearch
        resources:
          limits:
            cpu: 100m
            memory: 200Mi
        env:
        - name: FLUENTD_ARGS
          value: -q
        volumeMounts:
        - name: varlog
          mountPath: /var/log
          readOnly: false
        - name: containers
          mountPath: /var/lib/docker/containers
          readOnly: false
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: containers
        hostPath:
          path: /var/lib/docker/containers

这里有两点稍微注意一下,首先在selector部分是没有replicas的,它会在指定的而每一个节点部署有且仅有一个pod的pod副本,同时这里采用的镜像为istOne/fluentd-elasticsearch,如果使用其他镜像可能导致pod部署失败
下面创建pod
kubect create -f fluentd-ds.yaml
查看
kubectl describe daemonset fluentd-cloud-logging --namespace=kube-system
kubectl get daemonset fluentd-cloud-logging --namespace=kube-system
kubectl get pods --namespace=kube-system

5、批处理

job的三种模式

mode description
job template expansion 一个job处理一个工作项
queue with pod per work item 首先通过一个任务列存放所有工作项,然后使用一个job启动N个pod,并且每个pod都对应一个work item
queue with variable pod count 采用一个任务队列存放所有工作项,再使用一个job来启动动态个pod完成

job template expansion

这种模式下最常见的做法是先写一个模板文件,通过这个模板文件去生成不同的job文件,进而批量创建job

创建模板
cat job.yaml.txt

apiVersion: batch/v1
kind: Job
metadata:
  name: process-item-$ITEM
  labels:
    jobgroup: jobexample
spec:
  template:
    metadata:
      name: jobexample
      labels:
         jobgroup: jobexample
    spec:
      containers:
      - name: c
        image: busybox
        command: ["sh","-c","echo Processing item $ITEM && sleep 5"]
      restartPolicy: Never

利用这个模板生成多个Job文件去处理工作
cat job.sh

#!/bin/bash
    
for i in apple banana cherry
do
    cat job.yaml.txt | sed "s/\$ITEM/$i/" > ./jobs/job-$i.yaml  
done

创建Job
kubectl create -f jobs

查看结果
kubectl get jobs -l jobgroup=jobexample

queue with pod per work item
简单说一下原理,首先通过一个任务队列存放work item,比如RabbitMQ,具体的实现逻辑是work程序从任务队列拉取一个工作项处理,处理完后结束进程
03db1

queue with variable pod count
这种模式也是将工作项存放在任务队列,但是程序在进行处理的时候是动态去任务队列里拉取工作项生成pod,直到队列里没有等待处理的工作项后结束,所以程序需要直到队列中是否还有等待处理的工作项,一般这种模式的任务队列采用redis
03db2

6、cronjob

定时任务格式和linux系统计划任务类似
|格式域|可用字符|范围|
|分|,- * /|0~59|
|时|,- * /|0~23|
|天|,- * /|0~31|
|月|,- * /|1~12|
|周|,- * /|1~7|
简单说下字符的意思
* 表示任何时刻都发生,比如在分这个域表示每分钟都执行计划
/表示从起始时间出发,然后每隔一段时间触发,比如分域5/20表示第五分钟开始第一次触发,后面每隔20分钟触发一次

下面是一个基本的Cron Job例子
cat cron.yaml

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date;echo hello from the kubernetes cluster.
          restartPolicy: OnFailure

kubect get cronjob hello
持续查看job的持续时间验证job运行的间隔时间

还可以使用命令kubectl get jobs --watch查看任务的具体调度情况

7、自定义调度

如果不使用kubernetes自带的调度器,单独给一个pod编写一个yaml文件的时候,其实kubernetes还是会通过系统默认的调度器来调度,但是除此之外我们还可以自定义调度器,并在pod的yaml文件中指定自定义的调度器以此来调度pod

先创建一个pod
cat test-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  schedulerName: personal-scheduler
  containers:
  - name: nginx
    image: nginx

创建pod后该pod因为缺少控制器而处于pending状态

创建控制器
cat controller.sh

#!/bin/bash

server='localhost:8080'
while true
do
    for podname in $(kubectl --server $server get pods -o json | jq '.items[] | select(.spec.schedulerName == "personal-scheduler") | select(.spec.nodeName == null) | .metadata.name' | tr -d '"')
    do
        nodes=($(kubectl --server $server get nodes -o json | jq '.items[].metadata.name' | tr -d '"'))
        numnodes=${#nodes[@]}
        chosen=${nodes[$[$RANDOM % $numnodes]]}
        curl --header "Content-Type:application/json" --request POST --data '{"apiVersion":"v1","kind":"Binding","metadata":{"name":"'$podname'"},"target":{"apVersion":"v1","kind":"Node","name":"'$chosen'"}}' http://$server/api/v1/namespaces/default/pods/$podname/binding/
        echo "Assigned $podname to $chosen"
    done
    sleep 1
done

这里的脚本中利用了bash中的jq工具对获取的json做过滤处理,第一个for循环时过滤出使用调度器personal-scheduler来调度的所有pod名字,第二个for循环时获取所有节点名,将每一个pod随机的调度到节点中去,同个调度器会无限循环

调度成功会返回

{
"kind": "Status",
"apiVersion": "v1",
"metadata": {
},
"status": "Success",
"code": 201

注意jq工具需要使用epel源来安装
wget -P /etc/yum.repos.d/ http://mirrors.aliyun.com/repo/epel-7.repo
yum makechache && yum -y install jq

8、容器初始化

在启动应用容器前一般会有一启用初始化容器来完成应用的一系列前置条件,而且这些前置条件都成功了才会启动应用容器,这里有一个重要的参数restartPolicy,如果为Never,那么一旦前置容器初始化有失败的,那么pod启动失败;如果为Always,pod会重启

下面是一个含有init容器的pod文件
cat nginx-init.yaml

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  annotations:
spec:
  initcontainers:
  - name: install
    image: busybox
    command:
    - wget
    - "-O"
    - "/work-dir/index.html"
    - https://linuxwt.com
    volumeMounts:
    - name: workdir
      mountPath: "/work-dir"
  containers:
  - name: nginx
    image: nginx
    ports:
    - containerPort: 80
    volumeMounts:
    - name: workdir
      mountPath: /usr/share/nginx/html
  dnsPolicy: Default
    volumes:
    - name: workdir
      empty: {}

创建pod
kubectl create -f nginx-init.yaml
查看
kubectl get pod nginx

nginx 0/1 Init:0/1 0 1s

一段时间后

nginx 1/1 Running 0 37s

9、Pod升级与回滚

升级更新应用服务是生产中常见的操作,一般情况是停掉服务相关的pod,然后更新新版镜像启动服务,但如果是集群且集群节点过多这会导致服务升级时间过长,而kubernetes提供了滚动升级的功能

Deployment升级Pod
先运行一个nginx的pod副本
cat nginx-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLables:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPorts: 80

创建pod副本
kubectl create -f nginx-deployment.yaml

升级nginx镜像为1.9.1,只需要修改镜像名就可以触发pod的滚动升级,修改镜像有两种方式

  • kubectl set image deployment/nginx-deployment nginx=nginx:1.9.1
  • kubectl edit deployment/nginx-deployment修改spec.template.spec.containers[0].image中的版本

当然在升级过程当中可以通过命令查看升级详细过程
kuebctl rollout status deployment/nginx-deployment

更新原理:
更新时Deployment会创建新的ReplicaSet,通过该ReplicaSet创建一个新的pod,旧的ReplicaSet减少一个旧的Pod,依次更换直到新的ReplicaSet创建三个新额Pod,旧的ReplicaSet下无旧的Pod,以此完成服务的滚动升级
04rs1

可以查看一下rs
kubectl get rs
返回

NAME DESIRED CURRENT READY AGE
nginx-deployment-5d59d67564 0 0 0 61m
nginx-deployment-69c44dfb78 3 3 3 52m

滚动升级最大的好处就是可以保证服务不中断,deployment会控制整个pod副本数量至少为所需副本数量减1,至多为所需副本数量加1,控制这两个数值的也有相应参数,分别为maxUnavailable、maxSurge

当然kubernetes中滚动更新的策略有两种,滚动跟新rollingupdate是一种,还有一种是重建recreate,顾名思义就是将所有旧pod全部杀掉,在重建新的pod,这两种策略可以通过字段spec.strategy.type

maxUnavailable、maxSurge这连个参数是滚动更新过程中需要重点关注的两个参数,在最新的版本中可以是数值也可以是百分比

字段 含义
spec.stragety.rollingUpdate.maxUnavailable deployment在滚动更新中不可用pod数最大值
spec.stragety.rollingUpdate.maxSurge deployment在滚动更新中超过期望的pod数最大值

ps:更新的发生是在deployment进行部署时触发的,而且只有当模板的内容发生变化才会触发,比如更新镜像版本或模板标签,非模板变化比如更改副本数目不会触发更新

Deployment回滚Pod

有时候因为新版服务可能不稳定想退回到旧版本这个时候需要进行回滚
kubectl rollout history deployment/nginx-deployment查看各个版本信息
kubectl rollout history deployment/nginx-deployment --revision=版本号查看具体信息

kubectl rollout history deployment/nginx-deployment --revision=1

deployment.apps/nginx-deployment with revision #1
Pod Template:
Labels: app=nginx
pod-template-hash=5d59d67564
Containers:
nginx:
Image: nginx:1.7.9
Port: 80/TCP
Host Port: 0/TCP
Environment:
Mounts:
Volumes:

现在我们回滚到这个版本
kubectl rollout undo deployment/nginx-deployment回滚到上一个版本
也可以指定版本号回滚到固定的版本
kubectl rollout undo deployment/nginx-deployment --to-revision=1

暂停和恢复deployment部署操作
有时候我们需要频繁更改deployment中的模板内容,这会导致频繁触发更新操作,这个时候可以先让deploymnet暂时禁用更新触发的功能,待所有模板变更配置完了后开启更新触发功能一次将所有变更更新
kubectl rollout pause deployment/nginx-deployment停止更新触发
kubectl set image deployment/nginx-deployment nginx=nginx:1.10.0修改模板内容
kubectl rollout history deployment/nginx-deployment查看版本发现没有新版本出现,这说变更没有触发更新
kubectl rollout resume deployment/nginx-deployment恢复更新触发
kubectl rollout history deployment/nginx-deployment或者kubectl rollout status deployment/nginx-deployment就可以看到有新版本出现

DaemonSet升级Pod
两种升级策略

  • RollingUpdate
  • OnDelete (default)

Ondelete策略下及时更新了DaemonSet配置也是无法触发升级的,只有手动删除了旧pod后才会触发

StatefulSet升级Pod
三种升级策略

  • RollingUpdate
  • OnDelete
  • Paritioned

10、Pod扩容缩容

扩缩容也分为手动扩容和自动扩容

10.1.手动扩缩容

现在对前面的nginx-deployment控制器下的nginx副本进行扩容,目标为5个Pod
kubectl scale deployment nginx-deployment --replicas 5
现在对前面的nginx-deployment控制器下的nginx副本进行缩容,目标为1个Pod
kubectl scale deployment nginx-deployment --replicas 1

10.2.HPA自动扩缩容

自动扩缩容是通过HPA控制器来实现的,HPA需要通过kube-controller-manager服务带上参数--horizontal-pod-autoscaler-sync-period,该参数会定义一个探测周期,周期性监测目标pod的某个资源性能指标,与HPA资源对象扩缩容条件对比,满足条件后对pod副本数量进行调整

10.2.1.HPA原理

HPA是什么原理?

  • Metrics Server持续采集Pod副本的指标数据
  • HPA控制器通过Metrics Server的API获取数据
  • HPA根据定义的扩缩容规则进行计算获取目标Pod副本数量
  • 将计算得到的值与当前pod副本数比较,如不同HPA控制器就向Pod副本控制器发起scale操作调整Pod副本数量

04k8
那么HPA可以管理哪些指标类型呢?

  • pod级的性能指标,比如cpu使用率,注意这个指标通常是一个比值
  • pod的自定义指标,通常是一个数值,比如接收的请求数
  • 外部服务自定义指标,也是一个数值,需要容器应用以外的方式来提供,比如http url “/metrics”
    HPA通过metrics server获取相应性能指标数据后会基于算法计算目标副本数,然后与当前的pod副本数作比较,那么这个算法是什么呢?
    目标副本数 = 当前副本数 * (当前cpu比值/目标cpu比值)
    这里以cpu使用比率来做例子,比如当前cpu使用为200,但期望是100,当前pod副本数为2,那么会自动扩容到4个,如果期望值是400,那么会缩容至1个

这里比较重要的就是当前cpu指标值/目标比值,这个值如果处在1的左右,进行扩容缩容都有点不划算,我们如果想在这个小范围中不触发自动扩容缩容,应该怎么做呢?

当计算结果非常接近1的时候可以通过设定一个容忍值来控制系统不自动触发扩缩容操作,可以通过设定启动参数--horizontal-pod-autoscaler-tolerance来设置,比如设置为0.1,则HPA计算的值的-10%~10%范围,即当前cpu指标值/目标比值在0.9至1.1之间不要触发扩缩容操作

10.2.2.HPA配置

HPA资源对象配置有两个版本:v1\v2
v1只支持根据cpu使用率来进行自动扩缩容,v2能配置任意的性能指标,下面分别为两种版本的配置

cat hpa-v1.yaml

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
spec:
  scaleTargetRef:     # 目标作用对象,可以是各类控制器如Deployment\RC等
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1      # Pod副本的最小值
  maxReplicas: 10     # Pod副本的最大值
  targetCPUUtilizationPercentage: 50  # 期望每个Pod的cpu使用率为50%,该使用率基于给pod设置的cpu request值进行计算

v2版本就支持任意指标了
cat hpa-v2.yaml

apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
spec:
  scaleTargetRef:    # 目标作用对象
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1
  maxReplicas: 10
  metrics:    # 目标指标值
  - type: Resource  # 指标类型
    resource:
      name: cpu    # 指标名字
      target:     # 指标目标值
        type: Utilization
        averageUtilization: 50

这里重点说明一下指标类型

  • Resource 包括cpu ram
  • Pods pod副本的指标值的平均值
  • Object 基于资源对象或应用系统的任意自定义指标

如果是ram指标,要将averageUtilization替换为averageValue

Pods与Object其实都属于自定义指标
Pods指标示例如下

metrics:
- type: Pods  
  pods:
    metric:
      name: packets-per-second
    target:
      type: AverageValue
      averagevalue: 1K 

上面这个pod指标示例表示该hpa配置的指标为pod类型的自定义指标,当目标指标平均值达到1000时就会触发扩缩容操作

下面十几个常见的Objects指标
Ingress每秒请求数

metrics:
- type: Object 
  object:
    metric:
      name: requests-per-second
    describedObject:
      apiVersion: extensions/v1beta1
      kind: Ingress
      name: main-route
    target:
      type: Value
      value: 2k

http_requests

- type: Object
  object:
    metric:
      name: 'http_requests'
      selector: 'verb=GET'
    target:
      type: AverageValue
      aerageValue: 500

以上hpa配置例子均是单指标,hpa也可以设置多指标,然后针对每一种指标计算目标Pod数,最后使用最大值进行扩缩容

apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
  name: php-apache
  namespace: default
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: php-apache
  minReplicas: 1
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: AverageUtilization
        averageUtilization: 50
  - type: Pods
    pods:
      metric:
        name: packet-per-second
      taretAverageValue: 1k
  - type: Object
    object:
      metric:
        name: request-per-second
      describedObject:
        apiVersion: extensions/v1beta1
        kind: Ingress
        name: main-route
      target:
        kind: Value
        value: 10k

除了使用Object类型来定义外部指标外还可以使用External类型指标来定义外部指标

- type: External
  external:
    metric:
      name: queue_messages_ready
      selector: "queue=worker_tasks"
    target:
      type: AverageValue
      averageValue: 30

那么Object与External类型区别在哪?

在使用external时,需要部署能够对接到hpa模型的监控系统,通过理解监控系统采集这些指标的机制后,才方便配置自动扩缩容,而Object类型可以通过Operator模式,将外部指标通过CRD(自定义资源)定义为API资源对象来实现。

11、StatefulSet

有状态控制器,一般用于数据库服务,下面以mongodb为例来使用kubernetes部署一个复制集群,并使用GlusterFS作为集群的后端存储

组件架构
06mongo

先规划集群后端存储,可以参考GlusterFS+Heketi实现动态存储

这里使用动态模式来自动创建pv
创建storageclass
这里就直接使用前面文章 利用heketi+glusterfs创建的共享存储 文中有创建storageclass:gluster-heketi

部署mongo集群
下面依次创建mongo集群需要的yaml文件,这里采用待认证的集群,方式为keyFile

openssl rand -base64 741 > mongodb-keyfile

cat Dockerfile


FROM mongo:3.6.4

RUN rm -f /etc/localtime && ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime RUN echo "Asia/Shanghai" > /etc/timezone 
ADD mongodb-keyfile /data/config/mongodb-keyfile
RUN chown mongodb:mongodb /data/config/mongodb-keyfile && chmod 600 /data/config/mongodb-keyfile

docker build -t mongo:v3.6.4-keyfile .

cat mongo-rbac.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: mongo
  namespace: default
  labels:
    k8s-app: mongo
    addonmanager.kubernetes.io/mode: Reconcile
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: default
  name: mongo
  labels:
    k8s-app: mongo
    addonmanager.kubernetes.io/mode: Reconcile
subjects:
  - kind: ServiceAccount
    name: mongo
    namespace: default
roleRef:
  kind: ClusterRole
  name: view
  apiGroup: rbac.authorization.k8s.io

cat mongo-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: mongodb
  labels:
    name: mongo
spec:
  ports:
  - port: 27017
    targetPort: 27017
  selector:
    role: mongo

cat mongo-sts.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongodb
spec:
  serviceName: "mongodb"
  replicas: 3
  selector:
    matchLabels:
      role: mongo      
  template:
    metadata:
      labels:
        role: mongo
        environment: test
    spec: 
      serviceAccountName: mongo
      terminationGracePeriodSeconds: 10
      containers:
        - name: mongodb
          image: mongo:v3.6.4-keyfile
          imagePullPolicy: IfNotPresent
          env:
            - name: MONGO_INITDB_ROOT_USERNAME
              value: admin
            - name: MONGO_INITDB_ROOT_PASSWORD
              value: Qazwsx123
          args: 
            - mongod
            - "--replSet"
            - rs0
            - "--bind_ip"
            - 0.0.0.0
            - "--smallfiles"
            - "--noprealloc"
            - --clusterAuthMode
            - keyFile
            - --keyFile
            - /data/config/mongodb-keyfile
          ports:
            - containerPort: 27017
          volumeMounts:
            - name: mongo-persistent-storage
              mountPath: /data/db
        - name: mongo-sidecar
          image: cvallance/mongo-k8s-sidecar
          env:
            - name: MONGO_SIDECAR_POD_LABELS
              value: "role=mongo,environment=test"
            - name: KUBERNETES_MONGO_SERVICE_NAME
              value: mongodb
            - name: MONGODB_USERNAME
              value: admin
            - name: MONGODB_PASSWORD
              value: Qazwsx123
            - name: MONGODB_DATABASE
              value: admin
  volumeClaimTemplates:
  - metadata:
      name: mongo-persistent-storage
      annotations:
        volume.beta.kubernetes.io/storage-class: "gluster-heketi"
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 4Gi
  selector:
    matchLabels:
      role: mongo
      environment: test

ps:service一定不要命名为mongo,这会导致集群配置失败

依次执行

kubectl create -f mongo-rbac.yaml   
kubectl create -f mongo-service.yaml   
kubectl create -f mongo-sts.yaml   

简单操作mongo集群
随机登录mongo的某个pod
kubectl exec -it mongodb-0 -c mongodb -- bash
use admin
db.auth("admin","Qazwsx123")
rs.status()
返回

rs0:PRIMARY> rs.status()
{
        "set" : "rs0",
        "date" : ISODate("2021-07-08T09:09:34.375Z"),
        "myState" : 1,
        "term" : NumberLong(1),
        "heartbeatIntervalMillis" : NumberLong(2000),
        "optimes" : {
                "lastCommittedOpTime" : {
                        "ts" : Timestamp(1625735371, 1),
                        "t" : NumberLong(1)
                },
                "readConcernMajorityOpTime" : {
                        "ts" : Timestamp(1625735371, 1),
                        "t" : NumberLong(1)
                },
                "appliedOpTime" : {
                        "ts" : Timestamp(1625735371, 1),
                        "t" : NumberLong(1)
                },
                "durableOpTime" : {
                        "ts" : Timestamp(1625735371, 1),
                        "t" : NumberLong(1)
                }
        },
        "members" : [
                {
                        "_id" : 0,
                        "name" : "mongodb-0.mongodb.default.svc.cluster.local:27017",
                        "health" : 1,
                        "state" : 1,
                        "stateStr" : "PRIMARY",
                        "uptime" : 3554,
                        "optime" : {
                                "ts" : Timestamp(1625735371, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDate" : ISODate("2021-07-08T09:09:31Z"),
                        "electionTime" : Timestamp(1625731838, 2),
                        "electionDate" : ISODate("2021-07-08T08:10:38Z"),
                        "configVersion" : 5,
                        "self" : true
                },
                {
                        "_id" : 1,
                        "name" : "mongodb-1.mongodb.default.svc.cluster.local:27017",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 3519,
                        "optime" : {
                                "ts" : Timestamp(1625735371, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDurable" : {
                                "ts" : Timestamp(1625735371, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDate" : ISODate("2021-07-08T09:09:31Z"),
                        "optimeDurableDate" : ISODate("2021-07-08T09:09:31Z"),
                        "lastHeartbeat" : ISODate("2021-07-08T09:09:33.884Z"),
                        "lastHeartbeatRecv" : ISODate("2021-07-08T09:09:33.924Z"),
                        "pingMs" : NumberLong(1),
                        "syncingTo" : "mongodb-0.mongodb.default.svc.cluster.local:27017",
                        "configVersion" : 5
                },
                {
                        "_id" : 2,
                        "name" : "mongodb-2.mongodb.default.svc.cluster.local:27017",
                        "health" : 1,
                        "state" : 2,
                        "stateStr" : "SECONDARY",
                        "uptime" : 3494,
                        "optime" : {
                                "ts" : Timestamp(1625735371, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDurable" : {
                                "ts" : Timestamp(1625735371, 1),
                                "t" : NumberLong(1)
                        },
                        "optimeDate" : ISODate("2021-07-08T09:09:31Z"),
                        "optimeDurableDate" : ISODate("2021-07-08T09:09:31Z"),
                        "lastHeartbeat" : ISODate("2021-07-08T09:09:34.104Z"),
                        "lastHeartbeatRecv" : ISODate("2021-07-08T09:09:32.616Z"),
                        "pingMs" : NumberLong(1),
                        "syncingTo" : "mongodb-0.mongodb.default.svc.cluster.local:27017",
                        "configVersion" : 5
                }
        ],
        "ok" : 1,
        "operationTime" : Timestamp(1625735371, 1),
        "$clusterTime" : {
                "clusterTime" : Timestamp(1625735371, 1),
                "signature" : {
                        "hash" : BinData(0,"BWW6idxueMPbBgaqhI4WdDBKIgw="),
                        "keyId" : NumberLong("6982465084865904641")
                }
        }
}

应用程序访问集群需要使用下面的uri

mongodb://mongodb-0.mongodb.default.svc.cluster.local:27017,mongodb-1.mongodb.default.svc.cluster.local:27017,mongodb-2.mongodb.default.svc.cluster.local:27017/?replicaSet=rs0 

扩容
kubectl scale --replicas=4 statefulset mongodb

故障恢复
删除某个pod会自动重建,比如mongodb-0这个pod是集群的primary节点,重建后就会变成secondary,但是他仍然会使用原来的数据,也就是说kubernetes保证了StatefulSet控制器部署的应用在运行中是具有固定身份标识和独立的后端存储的,这一点可以通过重建前后使用命令kubectl get po mongodb-0 -o yaml | grep persistentVolumeClaim进行比较而得出

GitHub Repository