• Kubernetes 控制器


    在实际使用的时候并不会直接使用 Pod,而是会使用各种控制器来满足我们的需求,Kubernetes 中运行了一系列控制器来确保集群的当前状态与期望状态保持一致,它们就是 Kubernetes 的大脑。例如,ReplicaSet 控制器负责维护集群中运行的 Pod 数量;Node 控制器负责监控节点的状态,并在节点出现故障时及时做出响应。总而言之,在 Kubernetes 中,每个控制器只负责某种类型的特定资源。

    控制器

    Kubernetes 控制器会监听资源的 创建/更新/删除 事件,并触发 Reconcile 函数作为响应。整个调整过程被称作 “Reconcile Loop”(调谐循环) 或者 “Sync Loop”(同步循环)。Reconcile 是一个使用资源对象的命名空间和资源对象名称来调用的函数,使得资源对象的实际状态与 资源清单中定义的状态保持一致。调用完成后,Reconcile 会将资源对象的状态更新为当前实际状态。我们可以用下面的一段伪代码来表示这个过程:

    for {
      desired := getDesiredState()  // 期望的状态
      current := getCurrentState()  // 当前实际状态
      if current == desired {  // 如果状态一致则什么都不做
        // nothing to do
      } else {  // 如果状态不一致则调整编排,到一致为止
        // change current to desired status
      }
    }
    

    这个编排模型就是 Kubernetes 项目中的一个通用编排模式,即:控制循环(control loop)。

    ReplicaSet 控制器

    假如我们现在有一个 Pod 正在提供线上的服务,我们来想想一下我们可能会遇到的一些场景:

    • 某次运营活动非常成功,网站访问量突然暴增
    • 运行当前 Pod 的节点发生故障了,Pod 不能正常提供服务了

    第一种情况,可能比较好应对,活动之前我们可以大概计算下会有多大的访问量,提前多启动几个 Pod 副本,活动结束后再把多余的 Pod 杀掉,虽然有点麻烦,但是还是能够应对这种情况的。

    第二种情况,可能某天夜里收到大量报警说服务挂了,然后起来打开电脑在另外的节点上重新启动一个新的 Pod,问题可以解决。

    但是如果我们都人工的去解决遇到的这些问题,似乎又回到了以前刀耕火种的时代了是吧?如果有一种工具能够来帮助我们自动管理 Pod 就好了,Pod 挂了自动帮我在合适的节点上重新启动一个 Pod,这样是不是遇到上面的问题我们都不需要手动去解决了。

    而 ReplicaSet 这种资源对象就可以来帮助我们实现这个功能,ReplicaSet(RS) 的主要作用就是维持一组 Pod 副本的运行,保证一定数量的 Pod 在集群中正常运行,ReplicaSet 控制器会持续监听它所控制的这些 Pod 的运行状态,在 Pod 发送故障数量减少或者增加时会触发调谐过程,始终保持副本数量一定。

    和 Pod 一样我们仍然还是通过 YAML 文件来描述我们的 ReplicaSet 资源对象,如下 YAML 文件是一个常见的 ReplicaSet 定义:(nginx-rs.yaml)

    apiVersion: apps/v1
    kind: ReplicaSet  
    metadata:
      name:  nginx-rs
      namespace: default
    spec:
      replicas: 3  # 期望的 Pod 副本数量,默认值为1
      selector:  # Label Selector,必须匹配 Pod 模板中的标签
        matchLabels:
          app: nginx
      template:  # Pod 模板
        metadata:
          labels:
            app: nginx
        spec:
          containers:
          - name: nginx
            image: nginx
            ports:
            - containerPort: 80
    

    上面的 YAML 文件结构和我们之前定义的 Pod 看上去没太大两样,有常见的 apiVersion、kind、metadata,在 spec 下面描述 ReplicaSet 的基本信息,其中包含3个重要内容:

    • replias:表示期望的 Pod 的副本数量
    • selector:Label Selector,用来匹配要控制的 Pod 标签,需要和下面的 Pod 模板中的标签一致
    • template:Pod 模板,实际上就是以前我们定义的 Pod 内容,相当于把一个 Pod 的描述以模板的形式嵌入到了 ReplicaSet 中来。

    上面就是我们定义的一个普通的 ReplicaSet 资源清单文件,ReplicaSet 控制器会通过定义的 Label Selector 标签去查找集群中的 Pod 对象:

    我们直接来创建上面的资源对象:

    $ kubectl apply -f nginx-rs.yaml
    replicaset.apps/nginx-rs created
    $ kubectl get rs nginx-rs
    NAME       DESIRED   CURRENT   READY   AGE
    nginx-rs   3         3         3       17m
    

    通过查看 RS 可以看到当前资源对象的描述信息,包括DESIRED、CURRENT、READY的状态值,创建完成后,可以利用如下命令查看下 Pod 列表:

    $ kubectl get pods -l app=nginx
    NAME             READY   STATUS              RESTARTS   AGE
    nginx-rs-nxklf   1/1     Running   0          52s
    nginx-rs-t46qc   1/1     Running   0          52s
    nginx-rs-xfqrn   1/1     Running   0          52s
    

    可以看到现在有 3 个 Pod,这 3 个 Pod 就是我们在 RS 中声明的 3 个副本,比如我们删除其中一个 Pod:

    $ kubectl delete pod nginx-rs-xfqrn
    pod "nginx-rs-xfqrn" deleted
    

    然后再查看 Pod 列表:

    $ kubectl get pods -l app=nginx    
    NAME             READY   STATUS    RESTARTS   AGE
    nginx-rs-nxklf   1/1     Running   0          3m19s
    nginx-rs-t46qc   1/1     Running   0          3m19s
    nginx-rs-xsb59   1/1     Running   0          10s
    

    可以看到又重新出现了一个 Pod,这个就是上面我们所说的 ReplicaSet 控制器为我们做的工作,我们在 YAML 文件中声明了 3 个副本,然后现在我们删除了一个副本,就变成了两个,这个时候 ReplicaSet 控制器监控到控制的 Pod 数量和期望的 3 不一致,所以就需要启动一个新的 Pod 来保持 3 个副本,这个过程上面我们说了就是调谐的过程。同样可以查看 RS 的描述信息来查看到相关的事件信息:

    $ kubectl describe rs nginx-rs
    Name:         nginx-rs
    Namespace:    default
    Selector:     app=nginx
    Labels:       <none>
    Annotations:  kubectl.kubernetes.io/last-applied-configuration:
                    {"apiVersion":"apps/v1","kind":"ReplicaSet","metadata":{"annotations":{},"name":"nginx-rs","namespace":"default"},"spec":{"replicas":3,"se...
    Replicas:     3 current / 3 desired
    Pods Status:  3 Running / 0 Waiting / 0 Succeeded / 0 Failed
    Pod Template:
      Labels:  app=nginx
      Containers:
       nginx:
        Image:        nginx
        Port:         80/TCP
        Host Port:    0/TCP
        Environment:  <none>
        Mounts:       <none>
      Volumes:        <none>
    Events:
      Type    Reason            Age   From                   Message
      ----    ------            ----  ----                   -------
      Normal  SuccessfulCreate  17m   replicaset-controller  Created pod: nginx-rs-xfqrn
      Normal  SuccessfulCreate  17m   replicaset-controller  Created pod: nginx-rs-nxklf
      Normal  SuccessfulCreate  17m   replicaset-controller  Created pod: nginx-rs-t46qc
      Normal  SuccessfulCreate  14m   replicaset-controller  Created pod: nginx-rs-xsb59
    

    可以发现最开始通过 ReplicaSet 控制器创建了 3 个 Pod,后面我们删除了 Pod 后, ReplicaSet 控制器又为我们创建了一个 Pod,和上面我们的描述是一致的。如果这个时候我们把 RS 资源对象的 Pod 副本更改为 2 spec.replicas=2,这个时候我们来更新下资源对象:

    $ kubectl apply -f rs.yaml 
    replicaset.apps/nginx-rs configured
    $ kubectl get rs nginx-rs 
    NAME       DESIRED   CURRENT   READY   AGE
    nginx-rs   2         2         2       27m
    $ kubectl describe rs nginx-rs
    Name:         nginx-rs
    Namespace:    default
    Selector:     app=nginx
    Labels:       <none>
    Annotations:  kubectl.kubernetes.io/last-applied-configuration:
                    {"apiVersion":"apps/v1","kind":"ReplicaSet","metadata":{"annotations":{},"name":"nginx-rs","namespace":"default"},"spec":{"replicas":2,"se...
    Replicas:     2 current / 2 desired
    Pods Status:  2 Running / 1 Waiting / 0 Succeeded / 0 Failed
    Pod Template:
      Labels:  app=nginx
      Containers:
       nginx:
        Image:        nginx
        Port:         80/TCP
        Host Port:    0/TCP
        Environment:  <none>
        Mounts:       <none>
      Volumes:        <none>
    Events:
      Type    Reason            Age   From                   Message
      ----    ------            ----  ----                   -------
      Normal  SuccessfulCreate  27m   replicaset-controller  Created pod: nginx-rs-xfqrn
      Normal  SuccessfulCreate  27m   replicaset-controller  Created pod: nginx-rs-nxklf
      Normal  SuccessfulCreate  27m   replicaset-controller  Created pod: nginx-rs-t46qc
      Normal  SuccessfulCreate  24m   replicaset-controller  Created pod: nginx-rs-xsb59
      Normal  SuccessfulDelete  7s    replicaset-controller  Deleted pod: nginx-rs-xsb59
    

    可以看到 Replicaset 控制器在发现我们的资源声明中副本数变更为 2 后,就主动去删除了一个 Pod,这样副本数就和期望的始终保持一致了:

    $ kubectl get pods -l app=nginx
    NAME             READY   STATUS    RESTARTS   AGE
    nginx-rs-nxklf   1/1     Running   0          30m
    nginx-rs-t46qc   1/1     Running   0          30m
    

    我们可以随便查看一个 Pod 的描述信息可以看到这个 Pod 的所属控制器信息:

    $ kubectl describe pod nginx-rs-xsb59
    Name:               nginx-rs-xsb59
    Namespace:          default
    Priority:           0
    PriorityClassName:  <none>
    Node:               ydzs-node1/10.151.30.22
    Start Time:         Fri, 15 Nov 2019 14:18:10 +0800
    Labels:             app=nginx
    Annotations:        <none>
    Status:             Running
    IP:                 10.244.1.148
    Controlled By:      ReplicaSet/nginx-rs
    .......
    

    另外被 ReplicaSet 持有的 Pod 有一个 metadata.ownerReferences 指针指向当前的 ReplicaSet,表示当前 Pod 的所有者,这个引用主要会被集群中的垃圾收集器使用以清理失去所有者的 Pod 对象。这个 ownerReferences 和数据库中的外键是不是非常类似。可以通过将 Pod 资源描述信息导出查看:

    $ kubectl get pod nginx-rs-xsb59 -o yaml
    apiVersion: v1
    kind: Pod
    metadata:
      creationTimestamp: "2019-11-15T06:18:10Z"
      generateName: nginx-rs-
      labels:
        app: nginx
      name: nginx-rs-xsb59
      namespace: default
      ownerReferences:  
      - apiVersion: apps/v1
        blockOwnerDeletion: true
        controller: true
        kind: ReplicaSet
        name: nginx-rs
        uid: 4a3121fa-b5ae-4def-b2d2-bf17bc06b7b7
      resourceVersion: "1781596"
      selfLink: /api/v1/namespaces/default/pods/nginx-rs-xsb59
      uid: 0a4cae9a-105b-4024-ae96-ee516bfb2d23
    ......
    

    我们可以看到 Pod 中有一个 metadata.ownerReferences 的字段指向了 ReplicaSet 资源对象。如果要彻底删除 Pod,我们就只能删除 RS 对象:

    $ kubectl delete rs nginx-rs
    # 或者执行 kubectl delete -f nginx-rs.yaml
    

    这就是 ReplicaSet 对象的基本使用。

    Replication Controller

    Replication Controller 简称 RC,实际上 RC 和 RS 的功能几乎一致,RS 算是对 RC 的改进,目前唯一的一个区别就是 RC 只支持基于等式的 selector(env=dev或environment!=qa),但 RS 还支持基于集合的 selector(version in (v1.0, v2.0)),这对复杂的运维管理就非常方便了。

    比如上面资源对象如果我们要使用 RC 的话,对应的 selector 是这样的:

    selector: 
      app: nginx
    

    RC 只支持单个 Label 的等式,而 RS 中的 Label Selector 支持 matchLabels 和 matchExpressions 两种形式:

    selector:  
      matchLabels:
        app: nginx
    
    ---
    selector:
      matchExpressions:  # 该选择器要求 Pod 包含名为 app 的标签
      - key: app
        operator: In
        values:  # 并且标签的值必须是 nginx
        - nginx 
    

    总的来说 RS 是新一代的 RC,所以以后我们不使用 RC,直接使用 RS 即可,他们的功能都是一致的,但是实际上在实际使用中我们也不会直接使用 RS,而是使用更上层的类似于 Deployment 这样的资源对象。

    Deployment 控制器

    前面我们学习了 ReplicaSet 控制器,了解到该控制器是用来维护集群中运行的 Pod 数量的,但是往往在实际操作的时候,我们反而不会去直接使用 RS,而是会使用更上层的控制器,比如我们今天要学习的主角 Deployment,Deployment 一个非常重要的功能就是实现了 Pod 的“水平扩展/收缩”,比如我们应用更新了,我们只需要更新我们的容器镜像,然后修改 Deployment 里面的 Pod 模板镜像,那么 Deployment 就会用滚动更新(Rolling Update)的方式来升级现在的 Pod,这个能力是非常重要的,因为对于线上的服务我们需要做到不中断服务,所以滚动更新就成了必须的一个功能。而 Deployment 这个能力的实现,依赖的就是上节课我们学习的 ReplicaSet 这个资源对象,实际上我们可以通俗的理解就是每个 Deployment 就对应集群中的一次部署,这样就更好理解了。

    Deployment

    Deployment 资源对象的格式和 ReplicaSet 几乎一致,如下资源对象就是一个常见的 Deployment 资源类型:(nginx-deploy.yaml)

    apiVersion: apps/v1
    kind: Deployment  
    metadata:
      name:  nginx-deploy
      namespace: default
    spec:
      replicas: 3  # 期望的 Pod 副本数量,默认值为1
      selector:  # Label Selector,必须匹配 Pod 模板中的标签
        matchLabels:
          app: nginx
      template:  # Pod 模板
        metadata:
          labels:
            app: nginx
        spec:
          containers:
          - name: nginx
            image: nginx
            ports:
            - containerPort: 80
    

    我们这里只是将类型替换成了 Deployment,我们可以先来创建下这个资源对象:

    $ kubectl apply -f nginx-deploy.yaml
    deployment.apps/nginx-deploy created
    $ kubectl get deployment
    NAME           READY   UP-TO-DATE   AVAILABLE   AGE
    nginx-deploy   3/3     3            3           58s
    

    创建完成后,查看 Pod 状态:

    $ kubectl get pods -l app=nginx
    NAME                            READY   STATUS    RESTARTS   AGE
    nginx-deploy-85ff79dd56-7r76h   1/1     Running   0          41s
    nginx-deploy-85ff79dd56-d5gjs   1/1     Running   0          41s
    nginx-deploy-85ff79dd56-txc4h   1/1     Running   0          41s
    

    到这里我们发现和之前的 RS 对象是否没有什么两样,都是根据spec.replicas来维持的副本数量,我们随意查看一个 Pod 的描述信息:

    $ kubectl describe pod nginx-deploy-85ff79dd56-txc4h
    Name:               nginx-deploy-85ff79dd56-txc4h
    Namespace:          default
    Priority:           0
    PriorityClassName:  <none>
    Node:               ydzs-node1/10.151.30.22
    Start Time:         Sat, 16 Nov 2019 16:01:25 +0800
    Labels:             app=nginx
                        pod-template-hash=85ff79dd56
    Annotations:        podpreset.admission.kubernetes.io/podpreset-time-preset: 2062768
    Status:             Running
    IP:                 10.244.1.166
    Controlled By:      ReplicaSet/nginx-deploy-85ff79dd56
    ......
    Events:
      Type    Reason     Age        From                 Message
      ----    ------     ----       ----                 -------
      Normal  Scheduled  <unknown>  default-scheduler    Successfully assigned default/nginx-deploy-85ff79dd56-txc4h to ydzs-node1
      Normal  Pulling    2m         kubelet, ydzs-node1  Pulling image "nginx"
      Normal  Pulled     117s       kubelet, ydzs-node1  Successfully pulled image "nginx"
      Normal  Created    117s       kubelet, ydzs-node1  Created container nginx
      Normal  Started    116s       kubelet, ydzs-node1  Started container nginx
    

    我们仔细查看其中有这样一个信息Controlled By: ReplicaSet/nginx-deploy-85ff79dd56,什么意思?是不是表示当前我们这个 Pod 的控制器是一个 ReplicaSet 对象啊,我们不是创建的一个 Deployment 吗?为什么 Pod 会被 RS 所控制呢?那我们再去看下这个对应的 RS 对象的详细信息如何呢:

    $ kubectl describe rs nginx-deploy-85ff79dd56
    Name:           nginx-deploy-85ff79dd56
    Namespace:      default
    Selector:       app=nginx,pod-template-hash=85ff79dd56
    Labels:         app=nginx
                    pod-template-hash=85ff79dd56
    Annotations:    deployment.kubernetes.io/desired-replicas: 3
                    deployment.kubernetes.io/max-replicas: 4
                    deployment.kubernetes.io/revision: 1
    Controlled By:  Deployment/nginx-deploy
    Replicas:       3 current / 3 desired
    Pods Status:    3 Running / 0 Waiting / 0 Succeeded / 0 Failed
    ......
    Events:
      Type    Reason            Age    From                   Message
      ----    ------            ----   ----                   -------
      Normal  SuccessfulCreate  4m52s  replicaset-controller  Created pod: nginx-deploy-85ff79dd56-7r76h
      Normal  SuccessfulCreate  4m52s  replicaset-controller  Created pod: nginx-deploy-85ff79dd56-d5gjs
      Normal  SuccessfulCreate  4m52s  replicaset-controller  Created pod: nginx-deploy-85ff79dd56-txc4h
    

    其中有这样的一个信息:Controlled By: Deployment/nginx-deploy,明白了吧?意思就是我们的 Pod 依赖的控制器 RS 实际上被我们的 Deployment 控制着呢,我们可以用下图来说明 Pod、ReplicaSet、Deployment 三者之间的关系:

    通过上图我们可以很清楚的看到,定义了3个副本的 Deployment 与 ReplicaSet 和 Pod 的关系,就是一层一层进行控制的。ReplicaSet 作用和之前一样还是来保证 Pod 的个数始终保存指定的数量,所以 Deployment 中的容器 restartPolicy=Always 是唯一的就是这个原因,因为容器必须始终保证自己处于 Running 状态,ReplicaSet 才可以去明确调整 Pod 的个数。而 Deployment 是通过管理 ReplicaSet 的数量和属性来实现水平扩展/收缩以及滚动更新两个功能的。

    水平伸缩

    水平扩展/收缩的功能比较简单,因为 ReplicaSet 就可以实现,所以 Deployment 控制器只需要去修改它缩控制的 ReplicaSet 的 Pod 副本数量就可以了。比如现在我们把 Pod 的副本调整到 4 个,那么 Deployment 所对应的 ReplicaSet 就会自动创建一个新的 Pod 出来,这样就水平扩展了,我们可以使用一个新的命令 kubectl scale 命令来完成这个操作:

    $ kubectl scale deployment nginx-deploy --replicas=4
    deployment.apps/nginx-deployment scaled
    

    扩展完成后可以查看当前的 RS 对象:

    $ kubectl get rs
    NAME                      DESIRED   CURRENT   READY   AGE
    nginx-deploy-85ff79dd56   4         4         3       40m
    

    可以看到期望的 Pod 数量已经变成 4 了,只是 Pod 还没准备完成,所以 READY 状态数量还是 3,同样查看 RS 的详细信息:

    $ kubectl describe rs nginx-deploy-85ff79dd56
    Name:           nginx-deploy-85ff79dd56
    Namespace:      default
    Selector:       app=nginx,pod-template-hash=85ff79dd56
    ......
    Events:
      Type    Reason            Age   From                   Message
      ----    ------            ----  ----                   -------
      Normal  SuccessfulCreate  40m   replicaset-controller  Created pod: nginx-deploy-85ff79dd56-7r76h
      Normal  SuccessfulCreate  40m   replicaset-controller  Created pod: nginx-deploy-85ff79dd56-d5gjs
      Normal  SuccessfulCreate  40m   replicaset-controller  Created pod: nginx-deploy-85ff79dd56-txc4h
      Normal  SuccessfulCreate  17s   replicaset-controller  Created pod: nginx-deploy-85ff79dd56-tph9g
    

    可以看到 ReplicaSet 控制器增加了一个新的 Pod,同样的 Deployment 资源对象的事件中也可以看到完成了扩容的操作:

    $ kubectl describe deploy nginx-deploy
    Name:                   nginx-deploy
    Namespace:              default
    ......
    OldReplicaSets:  <none>
    NewReplicaSet:   nginx-deploy-85ff79dd56 (4/4 replicas created)
    Events:
      Type    Reason             Age    From                   Message
      ----    ------             ----   ----                   -------
      Normal  ScalingReplicaSet  43m    deployment-controller  Scaled up replica set nginx-deploy-85ff79dd56 to 3
      Normal  ScalingReplicaSet  3m16s  deployment-controller  Scaled up replica set nginx-deploy-85ff79dd56 to 4
    

    滚动更新

    如果只是水平扩展/收缩这两个功能,就完全没必要设计 Deployment 这个资源对象了,Deployment 最突出的一个功能是支持滚动更新,比如现在我们需要把应用容器更改为 nginx:1.7.9 版本,修改后的资源清单文件如下所示:

    apiVersion: apps/v1
    kind: Deployment  
    metadata:
      name:  nginx-deploy
      namespace: default
    spec:
      replicas: 3  
      selector:  
        matchLabels:
          app: nginx
      minReadySeconds: 5
      strategy:  
        type: RollingUpdate  # 指定更新策略:RollingUpdate和Recreate
        rollingUpdate:
          maxSurge: 1
          maxUnavailable: 1
      template:  
        metadata:
          labels:
            app: nginx
        spec:
          containers:
          - name: nginx
            image: nginx:1.7.9
            ports:
            - containerPort: 80
    

    跟前面相比较,除了更改了镜像之外,我们还指定了更新策略:

    minReadySeconds: 5
    strategy:
      type: RollingUpdate
      rollingUpdate:
        maxSurge: 1
        maxUnavailable: 1
    
    • minReadySeconds:表示 Kubernetes 在等待设置的时间后才进行升级,如果没有设置该值,Kubernetes 会假设该容器启动起来后就提供服务了,如果没有设置该值,在某些极端情况下可能会造成服务不正常运行,默认值就是0。
    • type=RollingUpdate:表示设置更新策略为滚动更新,可以设置为Recreate和RollingUpdate两个值,Recreate表示全部重新创建,默认值就是RollingUpdate。
    • maxSurge:表示升级过程中最多可以比原先设置多出的 Pod 数量,例如:maxSurage=1,replicas=5,就表示Kubernetes 会先启动一个新的 Pod,然后才删掉一个旧的 Pod,整个升级过程中最多会有5+1个 Pod。
    • maxUnavaible:表示升级过程中最多有多少个 Pod 处于无法提供服务的状态,当maxSurge不为0时,该值也不能为0,例如:maxUnavaible=1,则表示 Kubernetes 整个升级过程中最多会有1个 Pod 处于无法服务的状态。

    现在我们来直接更新上面的 Deployment 资源对象:

    $ kubectl apply -f nginx-deploy.yaml 
    

    可以添加了一个额外的 --record 参数来记录下我们的每次操作所执行的命令,以方便后面查看。

    更新后,我们可以执行下面的 kubectl rollout status 命令来查看我们此次滚动更新的状态:

    $ kubectl rollout status deployment/nginx-deploy
    Waiting for deployment "nginx-deploy" rollout to finish: 2 out of 3 new replicas have been updated...
    

    从上面的信息可以看出我们的滚动更新已经有两个 Pod 已经更新完成了,在滚动更新过程中,我们还可以执行如下的命令来暂停更新:

    $ kubectl rollout pause deployment/nginx-deploy
    deployment.apps/nginx-deploy paused
    

    这个时候我们的滚动更新就暂停了,此时我们可以查看下 Deployment 的详细信息:

    $ kubectl describe deploy nginx-deploy
    Name:                   nginx-deploy
    Namespace:              default
    CreationTimestamp:      Sat, 16 Nov 2019 16:01:24 +0800
    Labels:                 <none>
    Annotations:            deployment.kubernetes.io/revision: 2
                            kubectl.kubernetes.io/last-applied-configuration:
                              {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"name":"nginx-deploy","namespace":"default"},"spec":{"minReadySec...
    Selector:               app=nginx
    Replicas:               3 desired | 2 updated | 4 total | 4 available | 0 unavailable
    StrategyType:           RollingUpdate
    MinReadySeconds:        5
    RollingUpdateStrategy:  1 max unavailable, 1 max surge
    ......
    OldReplicaSets:  nginx-deploy-85ff79dd56 (2/2 replicas created)
    NewReplicaSet:   nginx-deploy-5b7b9ccb95 (2/2 replicas created)
    Events:
      Type    Reason             Age    From                   Message
      ----    ------             ----   ----                   -------
      Normal  ScalingReplicaSet  26m    deployment-controller  Scaled up replica set nginx-deploy-85ff79dd56 to 4
      Normal  ScalingReplicaSet  3m44s  deployment-controller  Scaled down replica set nginx-deploy-85ff79dd56 to 3
      Normal  ScalingReplicaSet  3m44s  deployment-controller  Scaled up replica set nginx-deploy-5b7b9ccb95 to 1
      Normal  ScalingReplicaSet  3m44s  deployment-controller  Scaled down replica set nginx-deploy-85ff79dd56 to 2
      Normal  ScalingReplicaSet  3m44s  deployment-controller  Scaled up replica set nginx-deploy-5b7b9ccb95 to 2
    

    仔细观察 Events 事件区域的变化,上面我们用 kubectl scale 命令将 Pod 副本调整到了 4,现在我们更新的时候是不是声明又变成 3 了,所以 Deployment 控制器首先是将之前控制的 nginx-deploy-85ff79dd56 这个 RS 资源对象进行缩容操作,然后滚动更新开始了,可以发现 Deployment 为一个新的 nginx-deploy-5b7b9ccb95 RS 资源对象首先新建了一个新的 Pod,然后将之前的 RS 对象缩容到 2 了,再然后新的 RS 对象扩容到 2,后面由于我们暂停滚动升级了,所以没有后续的事件了,大家有看明白这个过程吧?这个过程就是滚动更新的过程,启动一个新的 Pod,杀掉一个旧的 Pod,然后再启动一个新的 Pod,这样滚动更新下去,直到全都变成新的 Pod,这个时候系统中应该存在 4 个 Pod,因为我们设置的策略maxSurge=1,所以在升级过程中是允许的,而且是两个新的 Pod,两个旧的 Pod:

    $ kubectl get pods -l app=nginx
    NAME                            READY   STATUS    RESTARTS   AGE
    nginx-deploy-5b7b9ccb95-k6pkh   1/1     Running   0          11m
    nginx-deploy-5b7b9ccb95-l6lmx   1/1     Running   0          11m
    nginx-deploy-85ff79dd56-7r76h   1/1     Running   0          75m
    nginx-deploy-85ff79dd56-txc4h   1/1     Running   0          75m
    

    查看 Deployment 的状态也可以看到当前的 Pod 状态:

    $ kubectl get deployment  
    NAME           READY   UP-TO-DATE   AVAILABLE   AGE
    nginx-deploy   4/3     2            4           75m
    

    这个时候我们可以使用kubectl rollout resume来恢复我们的滚动更新:

    $ kubectl rollout resume deployment/nginx-deploy
    deployment.apps/nginx-deploy resumed
    $ kubectl rollout status deployment/nginx-deploy
    Waiting for deployment "nginx-deploy" rollout to finish: 2 of 3 updated replicas are available...
    deployment "nginx-deploy" successfully rolled out
    

    看到上面的信息证明我们的滚动更新已经成功了,同样可以查看下资源状态:

    $ kubectl get pod -l app=nginx
    NAME                            READY   STATUS    RESTARTS   AGE
    nginx-deploy-5b7b9ccb95-gmq7v   1/1     Running   0          115s
    nginx-deploy-5b7b9ccb95-k6pkh   1/1     Running   0          15m
    nginx-deploy-5b7b9ccb95-l6lmx   1/1     Running   0          15m
    $ kubectl get deployment                        
    NAME           READY   UP-TO-DATE   AVAILABLE   AGE
    nginx-deploy   3/3     3            3           79m
    

    这个时候我们查看 ReplicaSet 对象,可以发现会出现两个:

    $ kubectl get rs -l app=nginx
    NAME                      DESIRED   CURRENT   READY   AGE
    nginx-deploy-5b7b9ccb95   3         3         3       18m
    nginx-deploy-85ff79dd56   0         0         0       81m
    

    从上面可以看出滚动更新之前我们使用的 RS 资源对象的 Pod 副本数已经变成 0 了,而滚动更新后的 RS 资源对象变成了 3 个副本,我们可以导出之前的 RS 对象查看:

    $ kubectl get rs nginx-deploy-85ff79dd56 -o yaml
    apiVersion: apps/v1
    kind: ReplicaSet
    metadata:
      annotations:
        deployment.kubernetes.io/desired-replicas: "3"
        deployment.kubernetes.io/max-replicas: "4"
        deployment.kubernetes.io/revision: "1"
      creationTimestamp: "2019-11-16T08:01:24Z"
      generation: 5
      labels:
        app: nginx
        pod-template-hash: 85ff79dd56
      name: nginx-deploy-85ff79dd56
      namespace: default
      ownerReferences:
      - apiVersion: apps/v1
        blockOwnerDeletion: true
        controller: true
        kind: Deployment
        name: nginx-deploy
        uid: b0fc5614-ef58-496c-9111-740353bd90d4
      resourceVersion: "2140545"
      selfLink: /apis/apps/v1/namespaces/default/replicasets/nginx-deploy-85ff79dd56
      uid: 8eca2998-3610-4f80-9c21-5482ba579892
    spec:
      replicas: 0
      selector:
        matchLabels:
          app: nginx
          pod-template-hash: 85ff79dd56
      template:
        metadata:
          creationTimestamp: null
          labels:
            app: nginx
            pod-template-hash: 85ff79dd56
        spec:
          containers:
          - image: nginx
            imagePullPolicy: Always
            name: nginx
            ports:
            - containerPort: 80
              protocol: TCP
            resources: {}
            terminationMessagePath: /dev/termination-log
            terminationMessagePolicy: File
          dnsPolicy: ClusterFirst
          restartPolicy: Always
          schedulerName: default-scheduler
          securityContext: {}
          terminationGracePeriodSeconds: 30
    status:
      observedGeneration: 5
      replicas: 0
    

    仔细观察这个资源对象里面的描述信息除了副本数变成了replicas=0之外,和更新之前没有什么区别吧?大家看到这里想到了什么?有了这个 RS 的记录存在,是不是我们就可以回滚了啊?而且还可以回滚到前面的任意一个版本,这个版本是如何定义的呢?我们可以通过命令 rollout history 来获取:

    $ kubectl rollout history deployment nginx-deploy
    deployment.apps/nginx-deploy 
    REVISION  CHANGE-CAUSE
    1         <none>
    2         <none>
    

    其实 rollout history 中记录的 revision 是和 ReplicaSets 一 一对应。如果我们手动删除某个 ReplicaSet,对应的rollout history就会被删除,也就是说你无法回滚到这个revison了,同样我们还可以查看一个revison的详细信息:

    $ kubectl rollout history deployment nginx-deploy --revision=1 
    deployment.apps/nginx-deploy with revision #1
    Pod Template:
      Labels:       app=nginx
            pod-template-hash=85ff79dd56
      Containers:
       nginx:
        Image:      nginx
        Port:       80/TCP
        Host Port:  0/TCP
        Environment:        <none>
        Mounts:     <none>
      Volumes:      <none>
    

    假如现在要直接回退到当前版本的前一个版本,我们可以直接使用如下命令进行操作:

    $ kubectl rollout undo deployment nginx-deploy
    

    当然也可以回退到指定的revision版本:

    $ kubectl rollout undo deployment nginx-deploy --to-revision=1
    deployment "nginx-deploy" rolled back
    

    回滚的过程中我们同样可以查看回滚状态:

    $ kubectl rollout status deployment/nginx-deploy
    Waiting for deployment "nginx-deploy" rollout to finish: 1 old replicas are pending termination...
    Waiting for deployment "nginx-deploy" rollout to finish: 1 old replicas are pending termination...
    Waiting for deployment "nginx-deploy" rollout to finish: 1 old replicas are pending termination...
    Waiting for deployment "nginx-deploy" rollout to finish: 2 of 3 updated replicas are available...
    Waiting for deployment "nginx-deploy" rollout to finish: 2 of 3 updated replicas are available...
    deployment "nginx-deploy" successfully rolled out
    

    这个时候查看对应的 RS 资源对象可以看到 Pod 副本已经回到之前的 RS 里面去了。

    $ kubectl get rs -l app=nginx
    NAME                      DESIRED   CURRENT   READY   AGE
    nginx-deploy-5b7b9ccb95   0         0         0       31m
    nginx-deploy-85ff79dd56   3         3         3       95m
    

    不过需要注意的是回滚的操作滚动的revision始终是递增的:

    $ kubectl rollout history deployment nginx-deploy
    deployment.apps/nginx-deploy 
    REVISION  CHANGE-CAUSE
    2         <none>
    3         <none>
    

    保留旧版本:在很早之前的 Kubernetes 版本中,默认情况下会为我们暴露下所有滚动升级的历史记录,也就是 ReplicaSet 对象,但一般情况下没必要保留所有的版本,毕竟会存在 etcd 中,我们可以通过配置 spec.revisionHistoryLimit 属性来设置保留的历史记录数量,不过新版本中该值默认为 10,如果希望多保存几个版本可以设置该字段。

    StatefulSet 控制器

    在实际使用的过程中,Deployment 并不能编排所有类型的应用,对无状态服务编排是非常容易的,但是对于有状态服务就无能为力了。我们需要先明白一个概念:什么是有状态服务,什么是无状态服务。

    • 无状态服务(Stateless Service):该服务运行的实例不会在本地存储需要持久化的数据,并且多个实例对于同一个请求响应的结果是完全一致的,比如前面我们讲解的 WordPress 实例,我们是不是可以同时启动多个实例,但是我们访问任意一个实例得到的结果都是一样的吧?因为他唯一需要持久化的数据是存储在MySQL数据库中的,所以我们可以说 WordPress 这个应用是无状态服务,但是 MySQL 数据库就不是了,因为他需要把数据持久化到本地。
    • 有状态服务(Stateful Service):就和上面的概念是对立的了,该服务运行的实例需要在本地存储持久化数据,比如上面的 MySQL 数据库,你现在运行在节点 A,那么他的数据就存储在节点 A 上面的,如果这个时候你把该服务迁移到节点 B 去的话,那么就没有之前的数据了,因为他需要去对应的数据目录里面恢复数据,而此时没有任何数据。

    现在对有状态和无状态有一定的认识了吧,比如我们常见的 WEB 应用,是通过 Session 来保持用户的登录状态的,如果我们将 Session 持久化到节点上,那么该应用就是一个有状态的服务了,因为我现在登录进来你把我的 Session 持久化到节点 A 上了,下次我登录的时候可能会将请求路由到节点 B 上去了,但是节点 B 上根本就没有我当前的 Session 数据,就会被认为是未登录状态了,这样就导致我前后两次请求得到的结果不一致了。所以一般为了横向扩展,我们都会把这类 WEB 应用改成无状态的服务,怎么改?将 Session 数据存入一个公共的地方,比如 Redis 里面,是不是就可以了,对于一些客户端请求 API 的情况,我们就不使用 Session 来保持用户状态,改成用 Token 也是可以的。

    无状态服务利用我们前面的 Deployment 可以很好的进行编排,对应有状态服务,需要考虑的细节就要多很多了,容器化应用程序最困难的任务之一,就是设计有状态分布式组件的部署体系结构。由于无状态组件没有预定义的启动顺序、集群要求、点对点 TCP 连接、唯一的网络标识符、正常的启动和终止要求等,因此可以很容易地进行容器化。诸如数据库,大数据分析系统,分布式 key/value 存储、消息中间件需要有复杂的分布式体系结构,都可能会用到上述功能。为此,Kubernetes 引入了 StatefulSet 这种资源对象来支持这种复杂的需求。StatefulSet 类似于 ReplicaSet,但是它可以处理 Pod 的启动顺序,为保留每个 Pod 的状态设置唯一标识,具有以下几个功能特性:

    • 稳定的、唯一的网络标识符
    • 稳定的、持久化的存储
    • 有序的、优雅的部署和缩放
    • 有序的、优雅的删除和终止
    • 有序的、自动滚动更新

    Headless Service

    在我们学习 StatefulSet 对象之前,我们还必须了解一个新的概念:Headless Service。Service 是应用服务的抽象,通过 Labels 为应用提供负载均衡和服务发现,每个 Service 都会自动分配一个 cluster IP 和 DNS 名,在集群内部我们可以通过该地址或者通过 FDQN 的形式来访问服务。比如,一个 Deployment 有 3 个 Pod,那么我就可以定义一个 Service,有如下两种方式来访问这个 Service:

    • cluster IP 的方式,比如:当我访问 10.109.169.155 这个 Service 的 IP 地址时,10.109.169.155 其实就是一个 VIP,它会把请求转发到该 Service 所代理的 Endpoints 列表中的某一个 Pod 上。具体原理我们会在后面的 Service 章节中和大家深入了解。
    • Service 的 DNS 方式,比如我们访问“mysvc.mynamespace.svc.cluster.local”这条 DNS 记录,就可以访问到 mynamespace 这个命名空间下面名为 mysvc 的 Service 所代理的某一个 Pod。

    对于 DNS 这种方式实际上也有两种情况:

    • 第一种就是普通的 Service,我们访问“mysvc.mynamespace.svc.cluster.local”的时候是通过集群中的 DNS 服务解析到的 mysvc 这个 Service 的 cluster IP 的
    • 第二种情况就是Headless Service,对于这种情况,我们访问“mysvc.mynamespace.svc.cluster.local”的时候是直接解析到的 mysvc 代理的某一个具体的 Pod 的 IP 地址,中间少了 cluster IP 的转发,这就是二者的最大区别,Headless Service 不需要分配一个 VIP,而是可以直接以 DNS 的记录方式解析到后面的 Pod 的 IP 地址。

    比如我们定义一个如下的 Headless Service:(headless-svc.yaml)

    apiVersion: v1
    kind: Service
    metadata:
      name: nginx
      namespace: default
      labels:
        app: nginx
    spec:
      ports:
      - name: http
        port: 80
      clusterIP: None
      selector:
        app: nginx
    

    实际上 Headless Service 在定义上和普通的 Service 几乎一致, 只是他的 clusterIP=None,所以,这个 Service 被创建后并不会被分配一个 cluster IP,而是会以 DNS 记录的方式暴露出它所代理的 Pod,而且还有一个非常重要的特性,对于 Headless Service 所代理的所有 Pod 的 IP 地址都会绑定一个如下所示的 DNS 记录:

    <pod-name>.<svc-name>.<namespace>.svc.cluster.local
    

    这个 DNS 记录正是 Kubernetes 集群为 Pod 分配的一个唯一标识,只要我们知道 Pod 的名字,以及它对应的 Service 名字,就可以组装出这样一条 DNS 记录访问到 Pod 的 IP 地址,这个能力是非常重要的,接下来我们就来看下 StatefulSet 资源对象是如何结合 Headless Service 提供服务的。

    StatefulSet

    在开始之前,我们先准备两个 1G 的存储卷(PV),在后面的课程中我们也会和大家详细讲解 PV 和 PVC 的使用方法的,这里我们先不深究:(pv.yaml)

    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: pv001
    spec:
      capacity:
        storage: 1Gi
      accessModes:
      - ReadWriteOnce
      hostPath:
        path: /tmp/pv001
    
    ---
    
    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: pv002
    spec:
      capacity:
        storage: 1Gi
      accessModes:
      - ReadWriteOnce
      hostPath:
        path: /tmp/pv002
    

    然后直接创建 PV 即可:

    $ kubectl apply -f pv.yaml
    persistentvolume "pv001" created
    persistentvolume "pv002" created
    $ kubectl get pv
    kubectl get pv
    NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM     STORAGECLASS   REASON    AGE
    pv001     1Gi        RWO            Recycle          Available                                      12s
    pv002     1Gi        RWO            Recycle          Available                                      11s
    

    可以看到成功创建了两个 PV 对象,状态是:Available。

    特性

    然后接下来声明一个如下所示的 StatefulSet 资源清单:(nginx-sts.yaml)

    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: web
      namespace: default
    spec:
      serviceName: "nginx"
      replicas: 2
      selector:
        matchLabels:
          app: nginx
      template:
        metadata:
          labels:
            app: nginx
        spec:
          containers:
          - name: nginx
            image: nginx:1.7.9
            ports:
            - name: web
              containerPort: 80
            volumeMounts:
            - name: www
              mountPath: /usr/share/nginx/html
      volumeClaimTemplates:
      - metadata:
          name: www
        spec:
          accessModes: [ "ReadWriteOnce" ]
          resources:
            requests:
              storage: 1Gi
    

    从上面的资源清单中可以看出和我们前面的 Deployment 基本上也是一致的,也是通过声明的 Pod 模板来创建 Pod 的,另外上面资源清单中和 volumeMounts 进行关联的不是 volumes 而是一个新的属性:volumeClaimTemplates,该属性会自动创建一个 PVC 对象,其实这里就是一个 PVC 的模板,和 Pod 模板类似,PVC 被创建后会自动去关联当前系统中和他合适的 PV 进行绑定。除此之外,还多了一个 serviceName: "nginx" 的字段,serviceName 就是管理当前 StatefulSet 的服务名称,该服务必须在 StatefulSet 之前存在,并且负责该集合的网络标识,Pod 会遵循以下格式获取 DNS/主机名:pod-specific-string.serviceName.default.svc.cluster.local,其中 pod-specific-string 由 StatefulSet 控制器管理。

    StatefulSet 的拓扑结构和其他用于部署的资源对象其实比较类似,比较大的区别在于 StatefulSet 引入了 PV 和 PVC 对象来持久存储服务产生的状态,这样所有的服务虽然可以被杀掉或者重启,但是其中的数据由于 PV 的原因不会丢失。

    由于我们这里用volumeClaimTemplates声明的模板是挂载点的方式,并不是 volume,所有实际上相当于把 PV 的存储挂载到容器中,所以会覆盖掉容器中的数据,在容器启动完成后我们可以手动在 PV 的存储里面新建 index.html 文件来保证容器的正常访问,当然也可以进入到容器中去创建,这样更加方便:

    $ for i in 0 1; do kubectl exec web-$i -- sh -c 'echo hello $(hostname) > /usr/share/nginx/html/index.html'; done
    

    现在我们优先创建上面定义的 Headless Service:

    $ kubectl apply -f headless-svc.yaml
    service/nginx created
    $ kubectl get service nginx
    NAME    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
    nginx   ClusterIP   None         <none>        80/TCP    9s
    

    Headless Service 创建完成后就可以来创建对应的 StatefulSet 对象了:

    $ kubectl apply -f nginx-sts.yaml 
    statefulset.apps/web created
    $ kubectl get pvc
    NAME        STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
    www-web-0   Bound    pv001    1Gi        RWO                           10m
    www-web-1   Bound    pv002    1Gi        RWO                           6m26s
    

    可以看到这里通过 Volume 模板自动生成了两个 PVC 对象,也自动和 PV 进行了绑定。这个时候我们可以快速通过一个 --watch 参数来查看 Pod 的创建过程:

    $ kubectl get pods -l app=nginx --watch 
    NAME                      READY   STATUS              RESTARTS   AGE
    web-0                     0/1     ContainerCreating   0          1s
    web-0                     1/1     Running             0          2s
    web-1                     0/1     Pending             0          0s
    web-1                     0/1     Pending             0          0s
    web-1                     0/1     ContainerCreating   0          0s
    web-1                     1/1     Running             0          6s
    

    我们仔细观察整个过程出现了两个 Pod:web-0 和 web-1,而且这两个 Pod 是按照顺序进行创建的,web-0 启动起来后 web-1 才开始创建。如同上面 StatefulSet 概念中所提到的,StatefulSet 中的 Pod 拥有一个具有稳定的、独一无二的身份标志。这个标志基于 StatefulSet 控制器分配给每个 Pod 的唯一顺序索引。Pod 的名称的形式为-。我们这里的对象拥有两个副本,所以它创建了两个 Pod 名称分别为:web-0 和 web-1,我们可以使用 kubectl exec 命令进入到容器中查看它们的 hostname:

    $ kubectl exec web-0 hostname
    web-0
    $ kubectl exec web-1 hostname
    web-1
    

    StatefulSet 中 Pod 副本的创建会按照序列号升序处理,副本的更新和删除会按照序列号降序处理。

    可以看到,这两个 Pod 的 hostname 与 Pod 名字是一致的,都被分配了对应的编号。我们随意查看一个 Pod 的描述信息:

    $ kubectl describe pod web-0
    Name:               web-0
    Namespace:          default
    Priority:           0
    PriorityClassName:  <none>
    Node:               ydzs-node3/10.151.30.57
    Start Time:         Sun, 17 Nov 2019 12:32:50 +0800
    Labels:             app=nginx
                        controller-revision-hash=web-6c5c7fd59b
                        statefulset.kubernetes.io/pod-name=web-0
    Annotations:        podpreset.admission.kubernetes.io/podpreset-time-preset: 2062768
    Status:             Running
    IP:                 10.244.3.98
    Controlled By:      StatefulSet/web
    ......
    

    我们可以看到Controlled By: StatefulSet/web,证明我们的 Pod 是直接受到 StatefulSet 控制器管理的。

    现在我们创建一个 busybox(该镜像中有一系列的工具)的容器,在容器中用 DNS 的方式来访问一下这个 Headless Service,由于我们这里只是单纯的为了测试,所以没必要写资源清单文件来声明,用kubectl run命令启动一个测试的容器即可:

    $ kubectl run -it --image busybox:1.28.3 test --restart=Never --rm /bin/sh
    If you don't see a command prompt, try pressing enter.
    / #
    
    

    busybox 最新版本的镜像有 BUG,会出现nslookup提示无法解析的问题,我们这里使用老一点的镜像版本1.28.3即可。

    如果对 kubectl run 命令的使用参数不清楚,我们可以使用 kubectl run --help 命令查看可使用的参数。我们这里使用 kubectl run 命令启动了一个以 busybox 为镜像的 Pod,--rm 参数意味着我们退出 Pod 后就会被删除,和之前的 docker run 命令用法基本一致,现在我们在这个 Pod 容器里面可以使用 nslookup 命令来尝试解析下上面我们创建的 Headless Service:

    / # nslookup nginx
    Server:    10.96.0.10
    Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
    
    Name:      nginx
    Address 1: 10.244.1.175 web-1.nginx.default.svc.cluster.local
    Address 2: 10.244.4.83 web-0.nginx.default.svc.cluster.local
    / # ping nginx
    PING nginx (10.244.1.175): 56 data bytes
    64 bytes from 10.244.1.175: seq=0 ttl=62 time=1.076 ms
    64 bytes from 10.244.1.175: seq=1 ttl=62 time=1.029 ms
    64 bytes from 10.244.1.175: seq=2 ttl=62 time=1.075 ms
    

    我们直接解析 Headless Service 的名称,可以看到得到的是两个 Pod 的解析记录,但实际上如果我们通过nginx这个 DNS 去访问我们的服务的话,并不会随机或者轮询背后的两个 Pod,而是访问到一个固定的 Pod,所以不能代替普通的 Service。如果分别解析对应的 Pod 呢?

    $ / # nslookup web-0.nginx
    Server:    10.96.0.10
    Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
    
    Name:      web-0.nginx
    Address 1: 10.244.4.83 web-0.nginx.default.svc.cluster.local
    / # nslookup web-1.nginx
    Server:    10.96.0.10
    Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
    
    Name:      web-1.nginx
    Address 1: 10.244.1.175 web-1.nginx.default.svc.cluster.local
    

    可以看到解析 web-0.nginx 的时候解析到了 web-0 这个 Pod 的 IP,web-1.nginx 解析到了 web-1 这个 Pod 的 IP,而且这个 DNS 地址还是稳定的,因为 Pod 名称就是固定的,比如我们这个时候去删掉 web-0 和 web-1 这两个 Pod:

    $ kubectl delete pod -l app=nginx
    pod "web-0" deleted
    pod "web-1" deleted
    

    删除完成后才看 Pod 状态:

    $ kubectl get pods -l app=nginx  
    NAME    READY   STATUS    RESTARTS   AGE
    web-0   1/1     Running   0          42s
    web-1   1/1     Running   0          39s
    

    可以看到 StatefulSet 控制器仍然会安装顺序创建出两个 Pod 副本出来,而且 Pod 的唯一标识依然没变,所以这两个 Pod 的网络标识还是固定的,我们依然可以通过web-0.nginx去访问到web-0这个 Pod,虽然 Pod 已经重建了,对应 Pod IP 已经变化了,但是访问这个 Pod 的地址依然没变,并且他们依然还是关联的之前的 PVC,数据并不会丢失:

    / # nslookup web-0.nginx
    Server:    10.96.0.10
    Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
    
    Name:      web-0.nginx
    Address 1: 10.244.3.98 web-0.nginx.default.svc.cluster.local
    / # nslookup web-1.nginx
    Server:    10.96.0.10
    Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
    
    Name:      web-1.nginx
    Address 1: 10.244.1.176 web-1.nginx.default.svc.cluster.local
    

    通过 Headless Service,StatefulSet 就保证了 Pod 网络标识的唯一稳定性,由于 Pod IP 并不是固定的,所以我们访问有状态应用实例的时候,就必须使用 DNS 记录的方式来访问了,所以很多同学偶尔有固定的 Pod IP 的需求,或许可以用这种方式来代替。

    最后我们可以通过删除 StatefulSet 对象来删除所有的 Pod,仔细观察也会发现是按照倒序的方式进行删除的:

    $ kubectl delete statefulsets  web
    statefulset.apps "web" deleted
    $ kubectl get pods --watch
    NAME    READY   STATUS    RESTARTS   AGE
    web-1   1/1   Terminating   0     3h/31m
    web-0   1/1   Terminating   0     3h/31m
    

    管理策略

    对于某些分布式系统来说,StatefulSet 的顺序性保证是不必要和/或者不应该的,这些系统仅仅要求唯一性和身份标志。为了解决这个问题,我们只需要在声明 StatefulSet 的时候重新设置 spec.podManagementPolicy 的策略即可。

    默认的管理策略是 OrderedReady,表示让 StatefulSet 控制器遵循上文演示的顺序性保证。除此之外,还可以设置为 Parallel 管理模式,表示让 StatefulSet 控制器并行的终止所有 Pod,在启动或终止另一个 Pod 前,不必等待这些 Pod 变成 Running 和 Ready 或者完全终止状态。

    更新策略

    学习了 Deployment 的升级策略,在 StatefulSet 中同样也支持两种升级策略:onDelete 和 RollingUpdate,同样可以通过设置 .spec.updateStrategy.type 进行指定。

    • OnDelete: 该策略表示当更新了 StatefulSet 的模板后,只有手动删除旧的 Pod 才会创建新的 Pod。
    • RollingUpdate:该策略表示当更新 StatefulSet 模板后会自动删除旧的 Pod 并创建新的Pod,如果更新发生了错误,这次“滚动更新”就会停止。不过需要注意 StatefulSet 的 Pod 在部署时是顺序从 0~n 的,而在滚动更新时,这些 Pod 则是按逆序的方式即 n~0 一次删除并创建。

    另外SatefulSet 的滚动升级还支持 Partitions的特性,可以通过.spec.updateStrategy.rollingUpdate.partition 进行设置,在设置 partition 后,SatefulSet 的 Pod 中序号大于或等于 partition 的 Pod 会在 StatefulSet 的模板更新后进行滚动升级,而其余的 Pod 保持不变

    在实际的项目中,其实我们还是很少会去直接通过 StatefulSet 来部署我们的有状态服务的,除非你自己能够完全能够 hold 住,对于一些特定的服务,我们可能会使用更加高级的 Operator 来部署,比如 etcd-operator、prometheus-operator 等等,这些应用都能够很好的来管理有状态的服务,而不是单纯的使用一个 StatefulSet 来部署一个 Pod 就行,因为对于有状态的应用最重要的还是数据恢复、故障转移等等。

    DaemonSet 控制器

    通过该控制器的名称我们可以看出它的用法:Daemon,就是用来部署守护进程的,DaemonSet用于在每个 Kubernetes 节点中将守护进程的副本作为后台进程运行,说白了就是在每个节点部署一个 Pod副本,当节点加入到 Kubernetes 集群中,Pod 会被调度到该节点上运行,当节点从集群只能够被移除后,该节点上的这个 Pod 也会被移除,当然,如果我们删除 DaemonSet,所有和这个对象相关的 Pods都会被删除。那么在哪种情况下我们会需要用到这种业务场景呢?其实这种场景还是比较普通的,比如:

    • 集群存储守护程序,如 glusterd、ceph 要部署在每个节点上以提供持久性存储;
    • 节点监控守护进程,如 Prometheus 监控集群,可以在每个节点上运行一个 node-exporter 进程来收集监控节点的信息;
    • 日志收集守护程序,如 fluentd 或 logstash,在每个节点上运行以收集容器的日志
    • 节点网络插件,比如 flannel、calico,在每个节点上运行为 Pod 提供网络服务。

    这里需要特别说明的一个就是关于 DaemonSet 运行的 Pod 的调度问题,正常情况下,Pod 运行在哪个节点上是由 Kubernetes 的调度器策略来决定的,然而,由 DaemonSet 控制器创建的 Pod 实际上提前已经确定了在哪个节点上了(Pod创建时指定了.spec.nodeName),所以:

    • DaemonSet 并不关心一个节点的 unshedulable 字段。
    • DaemonSet 可以创建 Pod,即使调度器还没有启动。

    下面我们直接使用一个示例来演示下,在每个节点上部署一个 Nginx Pod:(nginx-ds.yaml)

    apiVersion: apps/v1
    kind: DaemonSet
    metadata:
      name: nginx-ds
      namespace: default
    spec:
      selector:
        matchLabels:
          k8s-app: nginx
      template:
        metadata:
          labels:
            k8s-app: nginx
        spec:
          containers:
          - image: nginx:1.7.9
            name: nginx
            ports:
            - name: http
              containerPort: 80
    

    然后直接创建即可:

    $ kubectl apply -f nginx-ds.yaml
    daemonset.apps/nginx-ds created
    

    创建完成后,我们查看 Pod 的状态:

    $ kubectl get nodes
    NAME          STATUS   ROLES    AGE     VERSION
    ydzs-master   Ready    master   8d      v1.16.2
    ydzs-node1    Ready    <none>   8d      v1.16.2
    ydzs-node2    Ready    <none>   8d      v1.16.2
    ydzs-node3    Ready    <none>   6d23h   v1.16.2
    ydzs-node4    Ready    <none>   6d23h   v1.16.2
    
    $ kubectl get pods -l k8s-app=nginx -o wide
    NAME             READY   STATUS    RESTARTS   AGE   IP             NODE         NOMINATED NODE   READINESS GATES
    nginx-ds-bpsb7   1/1     Running   0          75s   10.244.3.102   ydzs-node3   <none>           <none>
    nginx-ds-f4s2w   1/1     Running   0          75s   10.244.2.74    ydzs-node2   <none>           <none>
    nginx-ds-j9789   1/1     Running   0          75s   10.244.4.85    ydzs-node4   <none>           <none>
    nginx-ds-ngkt2   1/1     Running   0          75s   10.244.1.178   ydzs-node1   <none>           <none>
    

    我们观察可以发现除了 master 节点之外的4个节点上都有一个相应的 Pod 运行,因为 master 节点上默认被打上了污点,所以默认情况下不能调度普通的 Pod 上去。

    基本上我们可以用下图来描述 DaemonSet 的拓扑图:

    集群中的 Pod 和 Node 是一 一对应d的,而 DaemonSet 会管理全部机器上的 Pod 副本,负责对它们进行更新和删除。

    那么,DaemonSet 控制器是如何保证每个 Node 上有且只有一个被管理的 Pod 呢?

    • 首先控制器从 Etcd 获取到所有的 Node 列表,然后遍历所有的 Node。
    • 根据资源对象定义是否有调度相关的配置,然后分别检查 Node 是否符合要求。
    • 在可运行 Pod 的节点上检查是否已有对应的 Pod,如果没有,则在这个 Node 上创建该 Pod;如果有,并且数量大于 1,那就把多余的 Pod 从这个节点上删除;如果有且只有一个 Pod,那就说明是正常情况。

    实际上当我们学习了资源调度后,我们也可以自己用 Deployment 来实现 DaemonSet 的效果,这里我们明白 DaemonSet 如何使用的即可,当然该资源对象也有对应的更新策略,有OnDelete和RollingUpdate两种方式,默认是滚动更新。

    Job 与 CronJob

    我们在日常的工作中经常都会遇到一些需要进行批量数据处理和分析的需求,当然也会有按时间来进行调度的工作,在我们的 Kubernetes 集群中为我们提供了 Job 和 CronJob 两种资源对象来应对我们的这种需求。

    Job 负责处理任务,即仅执行一次的任务,它保证批处理任务的一个或多个 Pod 成功结束。而CronJob 则就是在 Job 上加上了时间调度。

    Job

    我们用 Job 这个资源对象来创建一个任务,我们定义一个 Job 来执行一个倒计时的任务,对应的资源清单如下所示:(job-demo.yaml)

    apiVersion: batch/v1
    kind: Job
    metadata:
      name: job-demo
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
          - name: counter
            image: busybox
            command:
            - "bin/sh"
            - "-c"
            - "for i in 9 8 7 6 5 4 3 2 1; do echo $i; done"
    

    我们可以看到 Job 中也是一个 Pod 模板,和之前的 Deployment、StatefulSet 之类的是一致的,只是 Pod 中的容器要求是一个任务,而不是一个常驻前台的进程了,因为需要退出,另外值得注意的是 Job 的 RestartPolicy 仅支持 Never 和 OnFailure 两种,不支持 Always,我们知道 Job 就相当于来执行一个批处理任务,执行完就结束了,如果支持 Always 的话是不是就陷入了死循环了?

    直接创建这个 Job 对象:

    $ kubectl apply -f job-demo.yaml
    job.batch/job-demo created
    $ kubectl get job 
    NAME       COMPLETIONS   DURATION   AGE
    job-demo   1/1           26s        3m4s
    $ kubectl get pods         
    NAME                      READY   STATUS      RESTARTS   AGE
    job-demo-frs2r            0/1     Completed   0          2m41s
    

    Job 对象创建成功后,我们可以查看下对象的详细描述信息:

    $ kubectl describe job job-demo
    Name:           job-demo
    Namespace:      default
    Selector:       controller-uid=b1191631-c71c-45e7-86c9-ebca69cb6125
    Labels:         controller-uid=b1191631-c71c-45e7-86c9-ebca69cb6125
                    job-name=job-demo
    .......
    Pod Template:
      Labels:  controller-uid=b1191631-c71c-45e7-86c9-ebca69cb6125
               job-name=job-demo
      Containers:
       ......
    Events:
      Type    Reason            Age   From            Message
      ----    ------            ----  ----            -------
      Normal  SuccessfulCreate  49s   job-controller  Created pod: job-demo-z48h2
    

    可以看到,Job 对象在创建后,它的 Pod 模板,被自动加上了一个 controller-uid=< 一个随机字符串 > 这样的 Label 标签,而这个 Job 对象本身,则被自动加上了这个 Label 对应的 Selector,从而 保证了 Job 与它所管理的 Pod 之间的匹配关系。而 Job 控制器之所以要使用这种携带了 UID 的 Label,就是为了避免不同 Job 对象所管理的 Pod 发生重合。

    我们可以看到很快 Pod 变成了 Completed 状态,这是因为容器的任务执行完成正常退出了,我们可以查看对应的日志:

    $ kubectl logs job-demo-frs2r
    9
    8
    7
    6
    5
    4
    3
    2
    1
    

    如果任务执行失败了,我们这里定义了 restartPolicy=Never,那么任务在执行失败后 Job 控制器就会不断地尝试创建一个新 Pod,当然,这个尝试肯定不能无限进行下去。我们可以通过 Job 对象的 spec.backoffLimit 字段来定义重试次数,另外需要注意的是 Job 控制器重新创建 Pod 的间隔是呈指数增加的,即下一次重新创建 Pod 的动作会分别发生在 10s、20s、40s… 后。

    如果我们定义的 restartPolicy=OnFailure,那么任务执行失败后,Job 控制器就不会去尝试创建新的 Pod了,它会不断地尝试重启 Pod 里的容器。

    上面我们这里的 Job 任务对应的 Pod 在运行结束后,会变成 Completed 状态,但是如果执行任务的 Pod 因为某种原因一直没有结束怎么办呢?同样我们可以在 Job 对象中通过设置字段 spec.activeDeadlineSeconds 来限制任务运行的最长时间,比如:

    spec:
     activeDeadlineSeconds: 100
    

    那么当我们的任务 Pod 运行超过了 100s 后,这个 Job 的所有 Pod 都会被终止,并且, Pod 的终止原因会变成 DeadlineExceeded。

    除此之外,我们还可以通过设置 spec.parallelism 参数来进行并行控制,该参数定义了一个 Job 在任意时间最多可以有多少个 Pod 同时运行。spec.completions 参数可以定义 Job 至少要完成的 Pod 数目。

    CronJob

    CronJob 其实就是在 Job 的基础上加上了时间调度,我们可以在给定的时间点运行一个任务,也可以周期性地在给定时间点运行。这个实际上和我们 Linux 中的 crontab 就非常类似了。

    一个 CronJob 对象其实就对应中 crontab 文件中的一行,它根据配置的时间格式周期性地运行一个Job,格式和 crontab 也是一样的。

    crontab 的格式为:分 时 日 月 星期 要运行的命令 。

    • 第1列分钟0~59
    • 第2列小时0~23)
    • 第3列日1~31
    • 第4列月1~12
    • 第5列星期0~7(0和7表示星期天)
    • 第6列要运行的命令

    现在,我们用 CronJob 来管理我们上面的 Job 任务,定义如下所示的资源清单:(cronjob-demo.yaml)

    apiVersion: batch/v1beta1
    kind: CronJob
    metadata:
      name: cronjob-demo
    spec:
      schedule: "*/1 * * * *"
      jobTemplate:
        spec:
          template:
            spec:
              restartPolicy: OnFailure
              containers:
              - name: hello
                image: busybox
                args:
                - "bin/sh"
                - "-c"
                - "for i in 9 8 7 6 5 4 3 2 1; do echo $i; done"
    

    这里的 Kind 变成了 CronJob 了,要注意的是.spec.schedule字段是必须填写的,用来指定任务运行的周期,格式就和 crontab 一样,另外一个字段是.spec.jobTemplate, 用来指定需要运行的任务,格式当然和 Job 是一致的。还有一些值得我们关注的字段 .spec.successfulJobsHistoryLimit.spec.failedJobsHistoryLimit,表示历史限制,是可选的字段,指定可以保留多少完成和失败的 Job,默认没有限制,所有成功和失败的 Job 都会被保留。然而,当运行一个 CronJob 时,Job 可以很快就堆积很多,所以一般推荐设置这两个字段的值。如果设置限制的值为 0,那么相关类型的 Job 完成后将不会被保留。

    我们直接新建上面的资源对象:

    $ kubectl create -f cronjob-demo.yaml
    cronjob "cronjob-demo" created
    

    然后可以查看对应的 Cronjob 资源对象:

    $ kubectl get cronjob
    NAME           SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
    cronjob-demo   */1 * * * *   False     0        <none>          7s
    

    稍微等一会儿查看可以发现多了几个 Job 资源对象,这个就是因为上面我们设置的 CronJob 资源对象,每1分钟执行一个新的 Job:

    $ kubectl get job 
    NAME                      COMPLETIONS   DURATION   AGE
    cronjob-demo-1574147280   1/1           6s         2m45s
    cronjob-demo-1574147340   1/1           11s        105s
    cronjob-demo-1574147400   1/1           5s         45s
    $ kubectl get pods
    NAME                      READY   STATUS      RESTARTS   AGE
    cronjob-demo-1574147340-ksd5x   0/1     Completed           0          3m7s
    cronjob-demo-1574147400-pts94   0/1     Completed           0          2m7s
    cronjob-demo-1574147460-t5hcd   0/1     Completed           0          67s
    cronjob-demo-1574147520-vmjfr   0/1     ContainerCreating   0          7s
    

    这个就是 CronJob 的基本用法,一旦不再需要 CronJob,我们可以使用 kubectl 命令删除它:

    $ kubectl delete cronjob cronjob-demo
    cronjob "cronjob-demo" deleted
    

    不过需要注意的是这将会终止正在创建的 Job,但是运行中的 Job 将不会被终止,不会删除 Job 或 它们的 Pod。

    HPA 控制器

    kubectl scale 命令可以来实现 Pod 的扩缩容功能,但是这个毕竟是完全手动操作的,要应对线上的各种复杂情况,我们需要能够做到自动化去感知业务,来自动进行扩缩容。为此,Kubernetes 也为我们提供了这样的一个资源对象:Horizontal Pod Autoscaling(Pod 水平自动伸缩),简称HPA,HPA 通过监控分析一些控制器控制的所有 Pod 的负载变化情况来确定是否需要调整 Pod 的副本数量,这是 HPA 最基本的原理:

    我们可以简单的通过 kubectl autoscale 命令来创建一个 HPA 资源对象,HPA Controller默认30s轮询一次(可通过 kube-controller-manager 的--horizontal-pod-autoscaler-sync-period 参数进行设置),查询指定的资源中的 Pod 资源使用率,并且与创建时设定的值和指标做对比,从而实现自动伸缩的功能。

    Metrics Server

    在 HPA 的第一个版本中,我们需要 Heapster 提供 CPU 和内存指标,在 HPA v2 过后就需要安装 Metrcis Server 了,Metrics Server 可以通过标准的 Kubernetes API 把监控数据暴露出来,有了 Metrics Server 之后,我们就完全可以通过标准的 Kubernetes API 来访问我们想要获取的监控数据了:

    https://10.96.0.1/apis/metrics.k8s.io/v1beta1/namespaces/<namespace-name>/pods/<pod-name>
    

    比如当我们访问上面的 API 的时候,我们就可以获取到该 Pod 的资源数据,这些数据其实是来自于 kubelet 的 Summary API 采集而来的。不过需要说明的是我们这里可以通过标准的 API 来获取资源监控数据,并不是因为 Metrics Server 就是 APIServer 的一部分,而是通过 Kubernetes 提供的 Aggregator 汇聚插件来实现的,是独立于 APIServer 之外运行的。

    聚合 API

    Aggregator 允许开发人员编写一个自己的服务,把这个服务注册到 Kubernetes 的 APIServer 里面去,这样我们就可以像原生的 APIServer 提供的 API 使用自己的 API 了,我们把自己的服务运行在 Kubernetes 集群里面,然后 Kubernetes 的 Aggregator 通过 Service 名称就可以转发到我们自己写的 Service 里面去了。这样这个聚合层就带来了很多好处:

    • 增加了 API 的扩展性,开发人员可以编写自己的 API 服务来暴露他们想要的 API。
    • 丰富了 API,核心 kubernetes 团队阻止了很多新的 API 提案,通过允许开发人员将他们的 API 作为单独的服务公开,这样就无须社区繁杂的审查了。
    • 开发分阶段实验性 API,新的 API 可以在单独的聚合服务中开发,当它稳定之后,在合并会 APIServer 就很容易了。
    • 确保新 API 遵循 Kubernetes 约定,如果没有这里提出的机制,社区成员可能会被迫推出自己的东西,这样很可能造成社区成员和社区约定不一致。

    安装

    要使用 HPA,就需要在集群中安装 Metrics Server 服务,要安装 Metrics Server 就需要开启 Aggregator,因为 Metrics Server 就是通过该代理进行扩展的,不过我们集群是通过 Kubeadm 搭建的,默认已经开启了,如果是二进制方式安装的集群,需要单独配置 kube-apsierver 添加如下所示的参数:

    --requestheader-client-ca-file=<path to aggregator CA cert>
    --requestheader-allowed-names=aggregator
    --requestheader-extra-headers-prefix=X-Remote-Extra-
    --requestheader-group-headers=X-Remote-Group
    --requestheader-username-headers=X-Remote-User
    --proxy-client-cert-file=<path to aggregator proxy cert>
    --proxy-client-key-file=<path to aggregator proxy key>
    

    如果 kube-proxy 没有和 APIServer 运行在同一台主机上,那么需要确保启用了如下 kube-apsierver 的参数:

    --enable-aggregator-routing=true
    

    对于这些证书的生成方式,我们可以查看官方文档:https://github.com/kubernetes-sigs/apiserver-builder-alpha/blob/master/docs/concepts/auth.md。

    Aggregator 聚合层启动完成后,就可以来安装 Metrics Server 了,我们可以获取该仓库的官方安装资源清单:

    # 官方仓库地址:https://github.com/kubernetes-sigs/metrics-server
    $ wget https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.3.6/components.yaml
    

    等部署完成后,可以查看 Pod 日志是否正常:

    $ kubectl apply -f components.yaml
    $ kubectl get pods -n kube-system -l k8s-app=metrics-server
    NAME                              READY   STATUS    RESTARTS   AGE
    metrics-server-6886856d7c-g5k6q   1/1     Running   0          2m39s
    $ kubectl logs -f metrics-server-6886856d7c-g5k6q -n kube-system
    ......
    E1119 09:05:57.234312       1 manager.go:111] unable to fully collect metrics: [unable to fully scrape metrics from source kubelet_summary:ydzs-node1: unable to fetch metrics from Kubelet ydzs-node1 (ydzs-node1): Get https://ydzs-node1:10250/stats/summary?only_cpu_and_memory=true: dial tcp: lookup ydzs-node1 on 10.96.0.10:53: no such host, unable to fully scrape metrics from source kubelet_summary:ydzs-node4: unable to fetch metrics from Kubelet ydzs-node4 (ydzs-node4): Get https://ydzs-node4:10250/stats/summary?only_cpu_and_memory=true: dial tcp: lookup ydzs-node4 on 10.96.0.10:53: no such host, unable to fully scrape metrics from source kubelet_summary:ydzs-node3: unable to fetch metrics from Kubelet ydzs-node3 (ydzs-node3): Get https://ydzs-node3:10250/stats/summary?only_cpu_and_memory=true: dial tcp: lookup ydzs-node3 on 10.96.0.10:53: no such host, unable to fully scrape metrics from source kubelet_summary:ydzs-master: unable to fetch metrics from Kubelet ydzs-master (ydzs-master): Get https://ydzs-master:10250/stats/summary?only_cpu_and_memory=true: dial tcp: lookup ydzs-master on 10.96.0.10:53: no such host, unable to fully scrape metrics from source kubelet_summary:ydzs-node2: unable to fetch metrics from Kubelet ydzs-node2 (ydzs-node2): Get https://ydzs-node2:10250/stats/summary?only_cpu_and_memory=true: dial tcp: lookup ydzs-node2 on 10.96.0.10:53: no such host]
    

    我们可以发现 Pod 中出现了一些错误信息:xxx: no such host,我们看到这个错误信息一般就可以确定是 DNS 解析不了造成的,我们可以看到 Metrics Server 会通过 kubelet 的 10250 端口获取信息,使用的是 hostname,我们部署集群的时候在节点的 /etc/hosts 里面添加了节点的 hostname 和 ip 的映射,但是是我们的 Metrics Server 的 Pod 内部并没有这个 hosts 信息,当然也就不识别 hostname 了,要解决这个问题,有两种方法:

    第一种方法就是在集群内部的 DNS 服务里面添加上 hostname 的解析,比如我们这里集群中使用的是 CoreDNS,我们就可以去修改下 CoreDNS 的 Configmap 信息,添加上 hosts 信息:

    $ kubectl edit configmap coredns -n kube-system
    apiVersion: v1
    data:
      Corefile: |
        .:53 {
            errors
            health
            hosts {  # 添加集群节点hosts隐射信息
              10.151.30.11 ydzs-master
              10.151.30.57 ydzs-node3
              10.151.30.59 ydzs-node4
              10.151.30.22 ydzs-node1
              10.151.30.23 ydzs-node2
              fallthrough
            }
            kubernetes cluster.local in-addr.arpa ip6.arpa {
               pods insecure
               upstream
               fallthrough in-addr.arpa ip6.arpa
            }
            prometheus :9153
            proxy . /etc/resolv.conf
            cache 30
            reload
        }
    kind: ConfigMap
    metadata:
      creationTimestamp: 2019-05-18T11:07:46Z
      name: coredns
      namespace: kube-system
    

    这样当在集群内部访问集群的 hostname 的时候就可以解析到对应的 ip 了,另外一种方法就是在 metrics-server 的启动参数中修改 kubelet-preferred-address-types 参数,如下:

    args:
    - --cert-dir=/tmp
    - --secure-port=4443
    - --kubelet-preferred-address-types=InternalIP
    

    我们这里使用第二种方式,然后重新安装:

    $ kubectl get pods -n kube-system -l k8s-app=metrics-server
    NAME                              READY   STATUS        RESTARTS   AGE
    metrics-server-6dcfdf89b5-tvdcp   1/1     Running       0          33s
    $ kubectl logs -f metric-metrics-server-58fc94d9f-jlxcb -n kube-system
    ......
    E1119 09:08:49.805959       1 manager.go:111] unable to fully collect metrics: [unable to fully scrape metrics from source kubelet_summary:ydzs-node3: unable to fetch metrics from Kubelet ydzs-node3 (10.151.30.57): Get https://10.151.30.57:10250/stats/summary?only_cpu_and_memory=true: x509: cannot validate certificate for 10.151.30.57 because it doesn't contain any IP SANs, unable to fully scrape metrics from source kubelet_summary:ydzs-node4: unable to fetch metrics from Kubelet ydzs-node4 (10.151.30.59): Get https://10.151.30.59:10250/stats/summary?only_cpu_and_memory=true: x509: cannot validate certificate for 10.151.30.59 because it doesn't contain any IP SANs, unable to fully scrape metrics from source kubelet_summary:ydzs-node2: unable to fetch metrics from Kubelet ydzs-node2 (10.151.30.23): Get https://10.151.30.23:10250/stats/summary?only_cpu_and_memory=true: x509: cannot validate certificate for 10.151.30.23 because it doesn't contain any IP SANs, unable to fully scrape metrics from source kubelet_summary:ydzs-master: unable to fetch metrics from Kubelet ydzs-master (10.151.30.11): Get https://10.151.30.11:10250/stats/summary?only_cpu_and_memory=true: x509: cannot validate certificate for 10.151.30.11 because it doesn't contain any IP SANs, unable to fully scrape metrics from source kubelet_summary:ydzs-node1: unable to fetch metrics from Kubelet ydzs-node1 (10.151.30.22): Get https://10.151.30.22:10250/stats/summary?only_cpu_and_memory=true: x509: cannot validate certificate for 10.151.30.22 because it doesn't contain any IP SANs]
    

    因为部署集群的时候,CA 证书并没有把各个节点的 IP 签上去,所以这里 Metrics Server 通过 IP 去请求时,提示签的证书没有对应的 IP(错误:x509: cannot validate certificate for 10.151.30.22 because it doesn’t contain any IP SANs),我们可以添加一个--kubelet-insecure-tls参数跳过证书校验:

    args:
    - --cert-dir=/tmp
    - --secure-port=4443
    - --kubelet-insecure-tls
    - --kubelet-preferred-address-types=InternalIP
    

    然后再重新安装即可成功!可以通过如下命令来验证:

    $ kubectl apply -f components.yaml
    $ kubectl get pods -n kube-system -l k8s-app=metrics-server
    NAME                              READY   STATUS    RESTARTS   AGE
    metrics-server-5d4dbb78bb-6klw6   1/1     Running   0          14s
    $ kubectl logs -f metrics-server-5d4dbb78bb-6klw6 -n kube-system
    I1119 09:10:44.249092       1 serving.go:312] Generated self-signed cert (/tmp/apiserver.crt, /tmp/apiserver.key)
    I1119 09:10:45.264076       1 secure_serving.go:116] Serving securely on [::]:4443
    $ kubectl get apiservice | grep metrics
    v1beta1.metrics.k8s.io                 kube-system/metrics-server   True        9m
    $ kubectl get --raw "/apis/metrics.k8s.io/v1beta1/nodes"
    {"kind":"NodeMetricsList","apiVersion":"metrics.k8s.io/v1beta1","metadata":{"selfLink":"/apis/metrics.k8s.io/v1beta1/nodes"},"items":[{"metadata":{"name":"ydzs-node3","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/ydzs-node3","creationTimestamp":"2019-11-19T09:11:53Z"},"timestamp":"2019-11-19T09:11:38Z","window":"30s","usage":{"cpu":"240965441n","memory":"3004360Ki"}},{"metadata":{"name":"ydzs-node4","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/ydzs-node4","creationTimestamp":"2019-11-19T09:11:53Z"},"timestamp":"2019-11-19T09:11:37Z","window":"30s","usage":{"cpu":"167036681n","memory":"2574664Ki"}},{"metadata":{"name":"ydzs-master","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/ydzs-master","creationTimestamp":"2019-11-19T09:11:53Z"},"timestamp":"2019-11-19T09:11:38Z","window":"30s","usage":{"cpu":"350907350n","memory":"2986716Ki"}},{"metadata":{"name":"ydzs-node1","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/ydzs-node1","creationTimestamp":"2019-11-19T09:11:53Z"},"timestamp":"2019-11-19T09:11:39Z","window":"30s","usage":{"cpu":"1319638039n","memory":"2094376Ki"}},{"metadata":{"name":"ydzs-node2","selfLink":"/apis/metrics.k8s.io/v1beta1/nodes/ydzs-node2","creationTimestamp":"2019-11-19T09:11:53Z"},"timestamp":"2019-11-19T09:11:36Z","window":"30s","usage":{"cpu":"320381888n","memory":"3270368Ki"}}]}
    $ kubectl top nodes
    NAME          CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%   
    ydzs-master   351m         17%    2916Mi          79%       
    ydzs-node1    1320m        33%    2045Mi          26%       
    ydzs-node2    321m         8%     3193Mi          41%       
    ydzs-node3    241m         6%     2933Mi          37%       
    ydzs-node4    168m         4%     2514Mi          32% 
    

    现在我们可以通过 kubectl top 命令来获取到资源数据了,证明 Metrics Server 已经安装成功了。

    HPA

    现在我们用 Deployment 来创建一个 Nginx Pod,然后利用 HPA 来进行自动扩缩容。资源清单如下所示:(hpa-demo.yaml)

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: hpa-demo
    spec:
      selector:
        matchLabels:
          app: nginx
      template:
        metadata:
          labels:
            app: nginx
        spec:
          containers:
          - name: nginx
            image: nginx
            ports:
            - containerPort: 80
    

    然后直接创建 Deployment:

    $ kubectl apply -f hpa-demo.yaml
    deployment.apps/hpa-demo created
    $ kubectl get pods -l app=nginx
    NAME                        READY   STATUS    RESTARTS   AGE
    hpa-demo-85ff79dd56-pz8th   1/1     Running   0          21s
    

    现在我们来创建一个 HPA 资源对象,可以使用kubectl autoscale命令来创建:

    $ kubectl autoscale deployment hpa-demo --cpu-percent=10 --min=1 --max=10
    horizontalpodautoscaler.autoscaling/hpa-demo autoscaled
    $ kubectl get hpa
    NAME       REFERENCE             TARGETS         MINPODS   MAXPODS   REPLICAS   AGE
    hpa-demo   Deployment/hpa-demo   <unknown>/10%   1         10        1          16s
    

    此命令创建了一个关联资源 hpa-demo 的 HPA,最小的 Pod 副本数为1,最大为10。HPA 会根据设定的 cpu 使用率(10%)动态的增加或者减少 Pod 数量。

    当然我们依然还是可以通过创建 YAML 文件的形式来创建 HPA 资源对象。如果我们不知道怎么编写的话,可以查看上面命令行创建的HPA的YAML文件:

    $ kubectl get hpa hpa-demo -o yaml
    apiVersion: autoscaling/v1
    kind: HorizontalPodAutoscaler
    metadata:
      annotations:
        autoscaling.alpha.kubernetes.io/conditions: '[{"type":"AbleToScale","status":"True","lastTransitionTime":"2019-11-19T09:15:12Z","reason":"SucceededGetScale","message":"the
          HPA controller was able to get the target''s current scale"},{"type":"ScalingActive","status":"False","lastTransitionTime":"2019-11-19T09:15:12Z","reason":"FailedGetResourceMetric","message":"the
          HPA was unable to compute the replica count: missing request for cpu"}]'
      creationTimestamp: "2019-11-19T09:14:56Z"
      name: hpa-demo
      namespace: default
      resourceVersion: "3094084"
      selfLink: /apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers/hpa-demo
      uid: b84d79f1-75b0-46e0-95b5-4cbe3509233b
    spec:
      maxReplicas: 10
      minReplicas: 1
      scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: hpa-demo
      targetCPUUtilizationPercentage: 10
    status:
      currentReplicas: 1
      desiredReplicas: 0
    

    然后我们可以根据上面的 YAML 文件就可以自己来创建一个基于 YAML 的 HPA 描述文件了。但是我们发现上面信息里面出现了一些 Fail 信息,我们来查看下这个 HPA 对象的信息:

    $ kubectl describe hpa hpa-demo
    Name:                                                  hpa-demo
    Namespace:                                             default
    Labels:                                                <none>
    Annotations:                                           <none>
    CreationTimestamp:                                     Tue, 19 Nov 2019 17:14:56 +0800
    Reference:                                             Deployment/hpa-demo
    Metrics:                                               ( current / target )
      resource cpu on pods  (as a percentage of request):  <unknown> / 10%
    Min replicas:                                          1
    Max replicas:                                          10
    Deployment pods:                                       1 current / 0 desired
    Conditions:
      Type           Status  Reason                   Message
      ----           ------  ------                   -------
      AbleToScale    True    SucceededGetScale        the HPA controller was able to get the target's current scale
      ScalingActive  False   FailedGetResourceMetric  the HPA was unable to compute the replica count: missing request for cpu
    Events:
      Type     Reason                        Age                From                       Message
      ----     ------                        ----               ----                       -------
      Warning  FailedGetResourceMetric       14s (x4 over 60s)  horizontal-pod-autoscaler  missing request for cpu
      Warning  FailedComputeMetricsReplicas  14s (x4 over 60s)  horizontal-pod-autoscaler  invalid metrics (1 invalid out of 1), first error is: failed to get cpu utilization: missing request for cpu
    

    我们可以看到上面的事件信息里面出现了 failed to get cpu utilization: missing request for cpu 这样的错误信息。这是因为我们上面创建的 Pod 对象没有添加 request 资源声明,这样导致 HPA 读取不到 CPU 指标信息,所以如果要想让 HPA 生效,对应的 Pod 资源必须添加 requests 资源声明,更新我们的资源清单文件:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: hpa-demo
    spec:
      selector:
        matchLabels:
          app: nginx
      template:
        metadata:
          labels:
            app: nginx
        spec:
          containers:
          - name: nginx
            image: nginx
            ports:
            - containerPort: 80
            resources:
              requests:
                memory: 50Mi
                cpu: 50m
    

    然后重新更新 Deployment,重新创建 HPA 对象:

    $ kubectl apply -f hpa.yaml 
    deployment.apps/hpa-demo configured
    $ kubectl get pods -o wide -l app=nginx
    NAME                        READY   STATUS    RESTARTS   AGE     IP            NODE         NOMINATED NODE   READINESS GATES
    hpa-demo-69968bb59f-twtdp   1/1     Running   0          4m11s   10.244.4.97   ydzs-node4   <none>           <none>
    $ kubectl delete hpa hpa-demo
    horizontalpodautoscaler.autoscaling "hpa-demo" deleted
    $ kubectl autoscale deployment hpa-demo --cpu-percent=10 --min=1 --max=10
    horizontalpodautoscaler.autoscaling/hpa-demo autoscaled
    $ kubectl describe hpa hpa-demo                                          
    Name:                                                  hpa-demo
    Namespace:                                             default
    Labels:                                                <none>
    Annotations:                                           <none>
    CreationTimestamp:                                     Tue, 19 Nov 2019 17:23:49 +0800
    Reference:                                             Deployment/hpa-demo
    Metrics:                                               ( current / target )
      resource cpu on pods  (as a percentage of request):  0% (0) / 10%
    Min replicas:                                          1
    Max replicas:                                          10
    Deployment pods:                                       1 current / 1 desired
    Conditions:
      Type            Status  Reason               Message
      ----            ------  ------               -------
      AbleToScale     True    ScaleDownStabilized  recent recommendations were higher than current one, applying the highest recent recommendation
      ScalingActive   True    ValidMetricFound     the HPA was able to successfully calculate a replica count from cpu resource utilization (percentage of request)
      ScalingLimited  False   DesiredWithinRange   the desired count is within the acceptable range
    Events:           <none>
    $ kubectl get hpa                                                        
    NAME       REFERENCE             TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
    hpa-demo   Deployment/hpa-demo   0%/10%    1         10        1          52s
    

    现在可以看到 HPA 资源对象已经正常了,现在我们来增大负载进行测试,我们来创建一个 busybox 的 Pod,并且循环访问上面创建的 Pod:

    $ kubectl run -it --image busybox test-hpa --restart=Never --rm /bin/sh
    If you don't see a command prompt, try pressing enter.
    / # while true; do wget -q -O- http://10.244.4.97; done
    

    可以看到,HPA 已经开始工作:

    $  kubectl get hpa
    NAME       REFERENCE             TARGETS    MINPODS   MAXPODS   REPLICAS   AGE
    hpa-demo   Deployment/hpa-demo   338%/10%   1         10        1          5m15s
    $ kubectl get pods -l app=nginx --watch 
    NAME                        READY   STATUS              RESTARTS   AGE
    hpa-demo-69968bb59f-8hjnn   1/1     Running             0          22s
    hpa-demo-69968bb59f-9ss9f   1/1     Running             0          22s
    hpa-demo-69968bb59f-bllsd   1/1     Running             0          22s
    hpa-demo-69968bb59f-lnh8k   1/1     Running             0          37s
    hpa-demo-69968bb59f-r8zfh   1/1     Running             0          22s
    hpa-demo-69968bb59f-twtdp   1/1     Running             0          6m43s
    hpa-demo-69968bb59f-w792g   1/1     Running             0          37s
    hpa-demo-69968bb59f-zlxkp   1/1     Running             0          37s
    hpa-demo-69968bb59f-znp6q   0/1     ContainerCreating   0          6s
    hpa-demo-69968bb59f-ztnvx   1/1     Running             0          6s
    

    我们可以看到已经自动拉起了很多新的 Pod,最后定格在了我们上面设置的 10 个 Pod,同时查看资源 hpa-demo 的副本数量,副本数量已经从原来的1变成了10个:

    $ kubectl get deployment hpa-demo
    NAME       READY   UP-TO-DATE   AVAILABLE   AGE
    hpa-demo   10/10   10           10          17m
    

    查看 HPA 资源的对象了解工作过程:

    $ kubectl describe hpa hpa-demo
    Name:                                                  hpa-demo
    Namespace:                                             default
    Labels:                                                <none>
    Annotations:                                           <none>
    CreationTimestamp:                                     Tue, 19 Nov 2019 17:23:49 +0800
    Reference:                                             Deployment/hpa-demo
    Metrics:                                               ( current / target )
      resource cpu on pods  (as a percentage of request):  0% (0) / 10%
    Min replicas:                                          1
    Max replicas:                                          10
    Deployment pods:                                       10 current / 10 desired
    Conditions:
      Type            Status  Reason               Message
      ----            ------  ------               -------
      AbleToScale     True    ScaleDownStabilized  recent recommendations were higher than current one, applying the highest recent recommendation
      ScalingActive   True    ValidMetricFound     the HPA was able to successfully calculate a replica count from cpu resource utilization (percentage of request)
      ScalingLimited  True    TooManyReplicas      the desired replica count is more than the maximum replica count
    Events:
      Type    Reason             Age    From                       Message
      ----    ------             ----   ----                       -------
      Normal  SuccessfulRescale  5m45s  horizontal-pod-autoscaler  New size: 4; reason: cpu resource utilization (percentage of request) above target
      Normal  SuccessfulRescale  5m30s  horizontal-pod-autoscaler  New size: 8; reason: cpu resource utilization (percentage of request) above target
      Normal  SuccessfulRescale  5m14s  horizontal-pod-autoscaler  New size: 10; reason: cpu resource utilization (percentage of request) above target
    

    同样的这个时候我们来关掉 busybox 来减少负载,然后等待一段时间观察下 HPA 和 Deployment 对象:

    $ kubectl get hpa
    NAME       REFERENCE             TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
    hpa-demo   Deployment/hpa-demo   0%/10%    1         10        1          14m
    $ kubectl get deployment hpa-demo
    NAME       READY   UP-TO-DATE   AVAILABLE   AGE
    hpa-demo   1/1     1            1           24m
    

    可以看到副本数量已经由 10 变为 1.

    缩放间隙: 从 Kubernetes v1.12 版本开始我们可以通过设置 kube-controller-manager 组件的--horizontal-pod-autoscaler-downscale-stabilization 参数来设置一个持续时间,用于指定在当前操作完成后,HPA 必须等待多长时间才能执行另一次缩放操作。默认为5分钟,也就是默认需要等待5分钟后才会开始自动缩放。

  • 相关阅读:
    Webpack笔记(三)——一款破产版脚手架的开发
    Google C++命名规范
    视觉词袋模型(BOVW)
    机器学习之四:决策树
    Zernike不变矩
    互联网产品各阶段的标准流程文档
    机器学习之三:logistic回归(最优化)
    Python
    机器学习之二:K-近邻(KNN)算法
    Python学习之二:Python 与 C 区别
  • 原文地址:https://www.cnblogs.com/sanduzxcvbnm/p/16193921.html
Copyright © 2020-2023  润新知