三、Deployment

之前有个ReplicationController也可以实现扩缩容,但目前他已经过时了。现在通过Deployment+ReplicaSet来实现这一功能,而且功能更加强大,因为Deployment具有更加便捷的滚动更新能力、ReplicaSet可以实现更加复杂的标签选择器等特性。
Deployment + ReplicaSet > ReplicationController

实际上在开发中我们并不会直接手动的去创建Pod,而是创建一个Deployment这样的工作负载(还有其他的工作负载类型,比如StatefulSet、DaemonSet等,后面会一一提到),由他们来创建并管理实际的Pod。我们可能会想,为什么要用Deployment来管理Pod呢,直接创建Pod不香吗?其实真的不香,通过Deployment管理的Pod具有自愈能力、动态扩缩容能力、滚动升级能力。Deployment才是真香!(至于为什么香,感兴趣的可以自己去看看已经过时的ReplicationController的滚动升级的过程。)

1、Deployment的自愈功能

我们可以直接通过一个实验就能明白什么是自愈功能。

  1. 首先部署一个Deployment(这时就会创建一个或多个pod,可以通过命令kubectl get pod -owide查看这个pod部署在哪个节点上)
  2. k8s deployment和StatefulSet一起用 k8s job deployment 区别_Docker

  3. 然后在master上监听这些pod(命令:watch -n 1 kubectl get pod
  4. 接着在部署pod的节点上去删掉这个Pod中部署的容器(docker rm -f 容器id),模拟Pod内容器异常。
  5. 这时我们在查看Pod(kubectl get pod),会发现有一个Pod处于容器创建的状态,因为我们刚刚删除了这个Pod中的容器,所以他现在在重新创建这个Pod中的容器。

这就是Department的自愈能力,只要由Deployment管理的Pod内的容器出现了异常,那么他会重启这个容器。
而如果是Pod被删除了,那么他会重新拉起一个Pod

如果一个被单独创建的Pod被删除了是不会有人再重新拉起这个Pod的。

k8s deployment和StatefulSet一起用 k8s job deployment 区别_云原生_02

需要注意的是:自愈机制不能保证这个Pod还在当前机器上被重启!
通过下面这个图我们再来看看由Deployment管理的Pod的自愈能力:

k8s deployment和StatefulSet一起用 k8s job deployment 区别_云原生_03

2、Deployment升级应用

我们知道Pod中运行的是我们的应用程序,那如果我们对应用程序进行了修改,要怎么做到零停机的更新呢?Deployment可以实现这一操作。

2.1 通过Yaml创建Deployment

这里我们首先创建一个Deployment,部署一组Pod。

apiVersion: apps/v1
kind: Deployment
metadata:
  name:  dep-v2  ### 遵循域名编写规范
  namespace: default
  labels:
    app: test-01
### 期望状态
spec:
  replicas: 5   ### 期望副本你数量
  selector:   ### 选择器,指定Deployment要控制的所有的Pod的共同标签(即帮助Deployment筛选他要控制哪些Pod)
    matchLabels:
      pod-name: ppp    ### 和模板template里面的Pod的标签必须一样
  ### 编写Pod
  template:
    metadata:    ### Pod的metadata
      labels:
        pod-name: ppp
    spec:
      containers:
      - name: nginx-01
        image: nginx

部署:kubectl apply -f dep-v2.yaml --record (–record选项会记录历史版本号,后面会有用)

2.2 Deployment和ReplicaSet、Pod的关系

创建一个Deployment会产生三个资源:Deployment资源、ReplicaSet资源(RS只提供了副本数量的控制功能)、Pod资源。

他们之间的关系是:Deployment控制RS、RS创建和管理Pod

这里可能有一个疑问,为什么非得由Deployment来控制ReplicaSet呢?其实他是为了方便后面的滚动升级的过程。

k8s deployment和StatefulSet一起用 k8s job deployment 区别_容器_04


k8s deployment和StatefulSet一起用 k8s job deployment 区别_容器_05

我们在创建一个Deployment后可以发现,由他产生的5个Pod名称中均包含一串额外的数字,这是什么?

这个数字实际上对应Deployment和ReplicaSet中的Pod模板的哈希值。Pod是由ReplicaSet管理的,所以我们再看看ReplicaSet的样子

k8s deployment和StatefulSet一起用 k8s job deployment 区别_云原生_06

我们发现ReplicaSet的名称中也包含了其Pod模板的哈希值。后面会说到**同一个Deployment会创建多个ReplicaSet,用来对应和管理多组不同版本的Pod模板。**像这样使用Pod模板的哈希值,就可以让Deployment始终对给定版本的Pod模板创建相同的ReplicaSet。

2.3 升级Deployment

Deployment的升级策略有两种
  • 滚动更新(RollingUpdate),这是默认的升级策略

该策略会逐渐地删除旧的Pod,与此同时创建新的Pod,使应用程序在整个升级过程中都处于可用状态,并确保其处理请求的能力没有因为升级而有所影响。

  • 重新创建(Recreate)

该策略在会一次性删除所有旧版本的Pod,之后才开始创建新的Pod。如果你的应用程序不支持多个版本同时对外提供服务,需要在启动新版本之前完全停用旧版本,那么需要使用这种策略。但是使用这种策略的话,会导致应用程序出现短暂的不可用。

有一点我们需要了解:仅当Deployment中定义的Pod模板(即 .spec.template )发生改变时,那么Deployment会使用更新后的Pod模板来创建新的实例,同时会删除旧的Pod。

  • 如果仅仅是修改Deployment的副本数等是不会触发Deployment的升级动作的。
  • 每次滚动升级后产生的新的Pod是会由一个新的RS进行控制,所以一个Deployment可能会对应多个RS。而旧的RS仍然会被保留,这个旧的RS会在下面回滚的时候被用到!

2.4 回滚Deployment

比如我使用V1版本的镜像部署了第一版本的Deployment,然后再对其进行升级,改为V2版本的镜像。但是这时V2版本的镜像存在一个BUG,这个BUG还不是那么好改。为了不让用户感知到升级导致的内部服务器错误,尽快修复问题。我们可以采用回滚,让他先回滚到V1版本,正常为用户提供服务,然后我们再下来慢慢修改V2版本,修改完后重新升级Deployment。

回滚命令(默认回滚到上一个版本)

kubectl rollout undo deployment dep-v2

查看滚动升级历史

kubectl rollout history deployment dep-v2

注意,我们前面在部署Deployment时在命令后面加了个 --record 参数,这样会在版本历史中的CHANGE-CAUSE栏记录部署信息,加上这个 --record参数主要是为了方便用户辨别每次版本做了哪些修改。

k8s deployment和StatefulSet一起用 k8s job deployment 区别_容器_07

回滚到指定的Deployment版本

通过在undo命令中指定一个特定的版本号,便可以回滚到特定的版本。

例如,想要回滚到第一个版本: kubectl rollout undo deployment dep-v2 --to-revision=1

2.5 滚动更新的原理

创建新的RS,准备就绪后,替换旧的RS,最后旧的RS会被保留!

k8s deployment和StatefulSet一起用 k8s job deployment 区别_运维开发_08

回滚升级之所以这么快能完成,主要是因为Deployment始终保持着升级的版本历史记录。这个历史版本号实际上是保存在ReplicaSet中的(每个RS都用特定的版本号来保存Deployment不同版本的完整信息),滚动升级成功后,老版本的ReplicaSet也不会被删掉,这也使得回滚操作可以回滚到任何一个历史版本,而不仅仅是上一个版本。

我们可以通过指定Deployment的revisionHistoryLimit属性来限制一个Deployment所能保存的ReplicaSet(历史版本)数量。

2.6 Deployment升级的一些属性

  • spec.strategy:指定新Pod替换旧Pod的策略
  • spec.strategy.type:指定替换策略,有两个属性值
  • Recreate:重新创建。将之前的Pod直接先杀死再重新创建Pod(这种方式不推荐)
  • RollingUpdate:滚动更新。如果选择的这个属性,则可以通过spec.strategy.rollingUpdate指定滚动更新策略
  • spec.strategy.rollingUpdate:指定滚动更新速率

下面这两个属性用于控制Deployment的滚动升级的速率。他们可以决定在Deployment滚动升级期间一起可以替换多少个Pod。

  • spec.strategy.rollingUpdate.maxSurge:指定除Deployment期望的副本数外,最多允许创建超出的Pod的数量,可以指定百分比,也可以指定数字
  • spec.strategy.rollingUpdate.maxUnavailable:指定在滚动升级期间,最多可以允许有多少个Pod不可用(反过来说就是保证集群中至少有多少Pod是可以处理请求的)

示例:

apiVersion: apps/v1kind: Deployment metadata: name: mydeploy namespace: default labels: dep: test spec: strategy: type: RollingUpdate ### 滚动更新 rollingUpdate: ### 下面这个设置的含义是:在滚动更新时,新创建的Pod数量最多为副本数量的20%个,杀死的Pod数量最多不能超过2个 maxUnavailable: 2 maxSurge: 20% replicas: 10 selector: matchLabels: pod-name: zaq ### 和模板template里面的pod的标签必须一样 template: metadata: labels: pod-name: zaq spec: containers: - name: nginx-test image: nginx

3、Deployment怎么关联Pod?

我们前面也提到了Deployment不直接管理Pod,而是间接的通过ReplicaSet来管理Pod。那么ReplicaSet是怎么知道哪些Pod是需要他来进行管理的呢?

答:他通过标签选择器来选择具有特定标签的Pod

3.1 ReplicaSet的更富表达力的标签选择器

由于创建一个Deployment会默认创建一个RS,所以这里直接创建一个Deployment,在这个Deployment中我们可以指定标签选择器。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: test
  namespace: default
  labels:
    test: zaq
spec:
  selector:
    matchExpressions:   ### 注意这里是匹配表达式而不是匹配标签
    - key: test        ### 该选择器要求该Pod包含名为 test 的标签
      operator: In
      values:
      - zaq                        ### 标签的值必须是zaq
  replicas: 2
  template:
    metadata:
      labels:
        test: zaq
    spec:
      containers:
      - name: nginx
        image: nginx

我们发现,新的标签选择器(matchExpressions)可以采用额外的表达式(这是ReplicationController所不能实现的)。

operator的四个值:
  • In:上面key的值必须与下面其中一个指定的values匹配。
  • NotIn:上面key的值与下面任何指定的values不匹配。
  • Exists:Pod必须包含一个指定名称的标签(这里指的是必须要有指定名称的key,values值是什么不重要),使用此运算符时,不应指定values字段。
matchExpressions:  
- key: test       
  operator: Exists
  • DoesNotExist:Pod不得包含指定名称的标签。values属性不得指定。

如果指定了多个表达式,则所有这些表达式都必须为true才能使选择器与Pod匹配。如果同时指定matchLabels和matchExpressions,则所有标签都必须匹配,并且所有表达式必须计算为true以使该Pod与选择器匹配。

4、动态扩缩容(HPA)


5、蓝绿部署

蓝绿部署:即系统存在两个版本,一个绿版本(正常),一个蓝版本(等待验证的版本)。如果蓝版本经过反复的测试、修改、验证,确定达到上线标准之后,则可以将流量请求切换到蓝版本,绿版本不接受请求,但是还依然存在。如果蓝版本在生产环境出现了问题,则可以立刻将请求转为绿版本。当确定蓝版本可以稳定正常运行时,就可以将原来的绿版本进行销毁,释放资源,然后蓝版本变为绿版本。(注意,蓝绿版本的转换只是我们人为的这么定义的,而并不是说需要配置什么东西才能实现他们角色的转换。)

k8s deployment和StatefulSet一起用 k8s job deployment 区别_Docker_09

6、金丝雀部署

金丝雀部署

金丝雀对瓦斯这种气体十分敏感。空气中哪怕有极其微量的瓦斯,金丝雀也会停止歌唱;而当瓦斯含量超过一定限度时,虽然鲁钝的人类毫无察觉,金丝雀却早已毒发身亡。当时在采矿设备相对简陋的条件下,工人们每次下井都会带上一只金丝雀作为“瓦斯检测指标”,以便在危险状况下紧急撤离。

金丝雀部署的意义在于,同时存在两个版本V1和V2,V1版本和V2版本都能接收到流量请求,但是V2刚开始只能接收到少量的请求,所以这时候V2版本也不需要部署到多台机器上。当到达V2版本的请求都能成功处理了,他不存在任何BUG了,那逐步开始增加V2版本的部署,移除V1版本的部署,让更多的流量请求来到V2版本。直到彻底消除V1版本。

k8s deployment和StatefulSet一起用 k8s job deployment 区别_运维开发_10

蓝绿部署和金丝雀部署的区别:

蓝绿部署的方式两个版本同时存在,但是流量只会发送到一个版本上,而金丝雀部署的方式,两个版本同时存在,流量请求也会发送到这两个版本上。

注意:金丝雀部署和滚动发布是不一样的。

滚动发布的缺点?

  • 他和金丝雀部署的相同点是,滚动发布也同时存在两个版本,都能接收流量。但是他没法控制流量
  • 滚动发布短时间就直接结束,不能直接控制新老版本的存活时间。

6.1 金丝雀部署案例

这里涉及到了Service的概念,可以学完Service再来看这里。

  • 准备一个Service
apiVersion: v1
kind: Service
metadata:
  name: canary-test
  namespace: default
spec:
  selector:
    app: canary-nginx
  type: NodePort
  ports:
  - name: canary-test
    port: 80   
    targetPort: 80   ### 指Pod的访问端口
    protocol: TCP
    nodePort: 8888   ### 机器上开的端口,客户端访问的就是这个端口
  • 准备版本v1的Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: canary-dep-v1
  namespace: default
  labels:
    app: canary-dep-v1
spec:
  selector:
    matchLabels:
      app: canary-nginx
      version: v1
  replicas: 3
  template:
    metadata:
      labels:
        app: canary-nginx
        version: v1
    spec:
      containers:
      - name: nginx
        image: registry.cn-hangzhou.aliyuncs.com/lfy_k8s_images/nginx-test:env-msg   ### 这个版本的nginx访问页会输出 1111111
  • 准备版本v2的Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: canary-dep-v2
  namespace: default
  labels:
    app: canary-dep-v2
spec:
  selector:
    matchLabels:
      app: canary-nginx
      version: v2
  replicas: 1   # 先让v2版本只有一个副本
  template:
    metadata:
      labels:
        app: canary-nginx
        version: v2
    spec:
      containers:
      - name: nginx
        image: nginx   ### v2版本使用默认的nginx,主要是为了打印出默认页面,和v1版本的做区分罢了
  • kubectl apply -f 上面的资源后,接下来访问:主机ip + 8888,我们可以发现3/4的请求会分配到v1版本。
  • 这时候我们再调大v2版本的副本数。
  • 我们发现v2版本基本上稳定了,没什么问题,这时候就可以删除v1版本:kubectl delete -f canary-dev-v1.yaml
  • 这时候v2版本就 上线成功了!
  • 这些复杂的步骤到时候会放在DevOps流水线中自动完成的

四、StatefulSet

学习此处需要先了解Volume和Service的概念,所以之后回过来再看这里。

五、DaemonSet

Deployment用于在Kubernetes集群中部署特定数量的Pod,而且他还不能保证把Pod部署到特定节点上。如果我们现在需要在集群中的所有节点上都仅部署一个Pod,怎么办?

1、使用DaemonSet在每个节点上部署一个Pod

DaemonSet控制器的作用就是确保所有的(或者一部分)节点都运行了一个指定的Pod副本。

上面提到一部分,是通过pod模板中的nodeSelector属性指定的。

  • 它不存在副本数的概念。因为他的工作是确保一个Pod在每个节点(或特定节点)都存在一份
  • 如果一个节点中通过DaemonSet部署的Pod被删除了,那么他会在该节点上重启一个新的Pod。
  • 如果某个节点宕机,DaemonSet不会在其他机器上重新部署一个新的Pod。
  • 当一个新的节点被加入到集群中,DaemonSet会立刻部署一个新的Pod到这个新加入的节点上。

2、DaemonSet 的典型使用场景有:

3、使用DaemonSet只在特定的节点上运行Pod

DaemonSet将Pod部署到集群中的所有节点上,除非指定这些Pod只在部分节点上运行。这是通过Pod模板中的nodeSelector属性指定的。

创建一个简单的DaemonSet

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: ssd-monitor
spec:
  selector:
    matchLabels:
      app: ssd-monitor
  template:
    metadata:
      labels:
        app: ssd-monitor
    spec:
      nodeSelector:     ### 表示只会在有disk:ssd的节点上部署该Pod
        disk: ssd
      containers:
      - name: main
        image: luksa/ssd-monitor

kubectl apply -f test-daemonset.yaml

上面DaemonSet描述的意思是:该DaemonSet只会在标有disk:ssd标签的节点上部署一个Pod。

k8s deployment和StatefulSet一起用 k8s job deployment 区别_容器_11

如果我们这时候再给Node2节点打上disk=app标签,那么这时就会在Node2上启动一个Pod。
如果我们这时候将Node3节点上的disk=app标签移除,那么这时Node3节点上的Pod就会被删除。

六、Job、CronJob

前面,通过Deployment、StatefulSet、DaemonSet创建的Pod都是一个持续运行的Pod,如果我们遇到一个只想运行完工作后就终止的情况怎么办?

1、Job

Kubernetes中的 Job 对象将创建一个或多个 Pod,并确保指定数量的 Pod 可以成功执行到进程正常结束:

  • 当 Job 创建的 Pod 执行成功并正常结束时,Job 将记录成功结束的 Pod 数量
  • 当成功结束的 Pod 达到指定的数量时,Job 将完成执行
  • 删除 Job 对象时,将清理掉由 Job 创建的 Pod
  • 在节点发送故障时,该节点上由Job管理的Pod将会被在其他的节点上被重新拉起。

1.1 一个简单的Job

apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  completions: 4   ### 期望Pod成功运行的次数
  template:
    spec:
      containers:
      - name: pi
        image: perl   ### 注意:用于执行job的镜像都必须是非阻塞的,也就是容器启动后执行完会自己退出(如果用了nginx镜像,那么他会在执行第一次时就一直阻塞着)
        command: ["perl",  "-Mbignum=bpi", "-wle", "print bpi(2000)"]
      restartPolicy: Never #Job情况下,不支持Always
  backoffLimit: 4 #任务4次都没成功就认为Job是失败的
  activeDeadlineSeconds: 10

这里需要特别说明的是:在一个Pod的定义中,可以指定在容器中运行的进程结束时,K8s会做什么,这是通过Pod配置的属性restartPolicy完成的,默认是Always。Job的Pod是不能使用这个默认策略的,因为他们并不是要无限期地运行。所以,需要明确指定Job的Pod的restartPolicy为OnFailure或Never。

2、CronJob

CronJob 按照预定的时间计划(schedule)创建 Job

一个 CronJob 在时间计划中的每次执行时刻,都创建 大约 一个 Job 对象。这里用到了 大约 ,是因为在少数情况下会创建两个 Job 对象,或者不创建 Job 对象。尽管 K8S 尽最大的可能性避免这种情况的出现,但是并不能完全杜绝此现象的发生。因此,Job 程序必须是幂等的。

当以下两个条件都满足时,Job 将至少运行一次:

  • startingDeadlineSeconds 被设置为一个较大的值,或者不设置该值(默认值将被采纳)
  • concurrencyPolicy 被设置为 Allow

2.1 一个简单的CronJob

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "*/1 * * * *"    #分、时、日、月、周。他是基于 master 所在时区来进行计算的。
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date; echo Hello from the Kubernetes cluster
          restartPolicy: OnFailure