1 - 使用 Deployment 运行一个无状态应用

本文介绍如何通过 Kubernetes Deployment 对象去运行一个应用。

教程目标

  • 创建一个 nginx Deployment。
  • 使用 kubectl 列举该 Deployment 的相关信息。
  • 更新该 Deployment。

准备开始

你必须拥有一个 Kubernetes 的集群,同时你必须配置 kubectl 命令行工具与你的集群通信。 建议在至少有两个不作为控制平面主机的节点的集群上运行本教程。 如果你还没有集群,你可以通过 Minikube 构建一个你自己的集群,或者你可以使用下面的 Kubernetes 练习环境之一:

你的 Kubernetes 服务器版本必须不低于版本 v1.9. 要获知版本信息,请输入 kubectl version.

创建并了解一个 nginx Deployment

你可以通过创建一个 Kubernetes Deployment 对象来运行一个应用, 且你可以在一个 YAML 文件中描述 Deployment。例如,下面这个 YAML 文件描述了一个运行 nginx:1.14.2 Docker 镜像的 Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2 # 告知 Deployment 运行 2 个与该模板匹配的 Pod
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
  1. 通过 YAML 文件创建一个 Deployment:

    kubectl apply -f https://k8s.io/examples/application/deployment.yaml
    
  1. 显示该 Deployment 的相关信息:

    kubectl describe deployment nginx-deployment
    

    输出类似于这样:

    Name:     nginx-deployment
    Namespace:    default
    CreationTimestamp:  Tue, 30 Aug 2016 18:11:37 -0700
    Labels:     app=nginx
    Annotations:    deployment.kubernetes.io/revision=1
    Selector:   app=nginx
    Replicas:   2 desired | 2 updated | 2 total | 2 available | 0 unavailable
    StrategyType:   RollingUpdate
    MinReadySeconds:  0
    RollingUpdateStrategy:  1 max unavailable, 1 max surge
    Pod Template:
      Labels:       app=nginx
      Containers:
        nginx:
        Image:              nginx:1.14.2
        Port:               80/TCP
        Environment:        <none>
        Mounts:             <none>
      Volumes:              <none>
    Conditions:
      Type          Status  Reason
      ----          ------  ------
      Available     True    MinimumReplicasAvailable
      Progressing   True    NewReplicaSetAvailable
    OldReplicaSets:   <none>
    NewReplicaSet:    nginx-deployment-1771418926 (2/2 replicas created)
    No events.
    
  1. 列出该 Deployment 创建的 Pod:

    kubectl get pods -l app=nginx
    

    输出类似于这样:

    NAME                                READY     STATUS    RESTARTS   AGE
    nginx-deployment-1771418926-7o5ns   1/1       Running   0          16h
    nginx-deployment-1771418926-r18az   1/1       Running   0          16h
    
  1. 展示某一个 Pod 信息:

    kubectl describe pod <pod-name>
    

    这里的 <pod-name> 是某一 Pod 的名称。

更新 Deployment

你可以通过应用一个新的 YAML 文件来更新 Deployment。下面的 YAML 文件指定该 Deployment 镜像更新为 nginx 1.16.1。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.16.1 # 将 nginx 版本从 1.14.2 更新为 1.16.1
        ports:
        - containerPort: 80
  1. 应用新的 YAML:

    kubectl apply -f https://k8s.io/examples/application/deployment-update.yaml
    
  1. 查看该 Deployment 以新的名称创建 Pod 同时删除旧的 Pod:

    kubectl get pods -l app=nginx
    

通过增加副本数来扩缩应用

你可以通过应用新的 YAML 文件来增加 Deployment 中 Pod 的数量。 下面的 YAML 文件将 replicas 设置为 4,指定该 Deployment 应有 4 个 Pod:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 4 # 将副本数从 2 更新为 4
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.16.1
        ports:
        - containerPort: 80
  1. 应用新的 YAML 文件:

    kubectl apply -f https://k8s.io/examples/application/deployment-scale.yaml
    
  1. 验证该 Deployment 有 4 个 Pod:

    kubectl get pods -l app=nginx
    

    输出的结果类似于:

    NAME                               READY     STATUS    RESTARTS   AGE
    nginx-deployment-148880595-4zdqq   1/1       Running   0          25s
    nginx-deployment-148880595-6zgi1   1/1       Running   0          25s
    nginx-deployment-148880595-fxcez   1/1       Running   0          2m
    nginx-deployment-148880595-rwovn   1/1       Running   0          2m
    

删除 Deployment

基于名称删除 Deployment:

kubectl delete deployment nginx-deployment

ReplicationController —— 旧的方式

创建一个多副本应用首选方法是使用 Deployment,该 Deployment 内部将轮流使用 ReplicaSet。 在 Deployment 和 ReplicaSet 被引入到 Kubernetes 之前,多副本应用通过 ReplicationController 来配置。

接下来

2 - 运行一个单实例有状态应用

本文介绍在 Kubernetes 中如何使用 PersistentVolume 和 Deployment 运行一个单实例有状态应用。 该示例应用是 MySQL。

教程目标

  • 在你的环境中创建一个引用磁盘的 PersistentVolume。
  • 创建一个 MySQL Deployment。
  • 在集群内以一个已知的 DNS 名称将 MySQL 暴露给其他 Pod。

准备开始

部署 MySQL

你可以通过创建一个 Kubernetes Deployment 并使用 PersistentVolumeClaim 将其连接到 某已有的 PersistentVolume 来运行一个有状态的应用。 例如,这里的 YAML 描述的是一个运行 MySQL 的 Deployment,其中引用了 PersistentVolumeClaim。 文件为 /var/lib/mysql 定义了卷挂载,并创建了一个 PersistentVolumeClaim,寻找一个 20G 大小的卷。 该申领可以通过现有的满足需求的卷来满足,也可以通过动态供应卷的机制来满足。

注意:在配置的 YAML 文件中定义密码的做法是不安全的。具体安全解决方案请参考 Kubernetes Secrets

apiVersion: v1
kind: Service
metadata:
  name: mysql
spec:
  ports:
  - port: 3306
  selector:
    app: mysql
  clusterIP: None
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - image: mysql:5.6
        name: mysql
        env:
          # 在实际中使用 secret
        - name: MYSQL_ROOT_PASSWORD
          value: password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-persistent-storage
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-persistent-storage
        persistentVolumeClaim:
          claimName: mysql-pv-claim
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mysql-pv-volume
  labels:
    type: local
spec:
  storageClassName: manual
  capacity:
    storage: 20Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/data"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pv-claim
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
  1. 部署 YAML 文件中定义的 PV 和 PVC:

    kubectl apply -f https://k8s.io/examples/application/mysql/mysql-pv.yaml
    
  1. 部署 YAML 文件中定义的 Deployment:

    kubectl apply -f https://k8s.io/examples/application/mysql/mysql-deployment.yaml
    
  1. 展示 Deployment 相关信息:

    kubectl describe deployment mysql
    

    输出类似于:

    Name:                 mysql
    Namespace:            default
    CreationTimestamp:    Tue, 01 Nov 2016 11:18:45 -0700
    Labels:               app=mysql
    Annotations:          deployment.kubernetes.io/revision=1
    Selector:             app=mysql
    Replicas:             1 desired | 1 updated | 1 total | 0 available | 1 unavailable
    StrategyType:         Recreate
    MinReadySeconds:      0
    Pod Template:
      Labels:       app=mysql
      Containers:
       mysql:
        Image:      mysql:5.6
        Port:       3306/TCP
        Environment:
          MYSQL_ROOT_PASSWORD:      password
        Mounts:
          /var/lib/mysql from mysql-persistent-storage (rw)
      Volumes:
       mysql-persistent-storage:
        Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
        ClaimName:  mysql-pv-claim
        ReadOnly:   false
    Conditions:
      Type          Status  Reason
      ----          ------  ------
      Available     False   MinimumReplicasUnavailable
      Progressing   True    ReplicaSetUpdated
    OldReplicaSets:       <none>
    NewReplicaSet:        mysql-63082529 (1/1 replicas created)
    Events:
      FirstSeen    LastSeen    Count    From                SubobjectPath    Type        Reason            Message
      ---------    --------    -----    ----                -------------    --------    ------            -------
      33s          33s         1        {deployment-controller }             Normal      ScalingReplicaSet Scaled up replica set mysql-63082529 to 1
    
  1. 列举出 Deployment 创建的 Pod:

    kubectl get pods -l app=mysql
    

    输出类似于:

    NAME                   READY     STATUS    RESTARTS   AGE
    mysql-63082529-2z3ki   1/1       Running   0          3m
    
  1. 查看 PersistentVolumeClaim:

    kubectl describe pvc mysql-pv-claim
    

    输出类似于:

    Name:         mysql-pv-claim
    Namespace:    default
    StorageClass:
    Status:       Bound
    Volume:       mysql-pv-volume
    Labels:       <none>
    Annotations:    pv.kubernetes.io/bind-completed=yes
                    pv.kubernetes.io/bound-by-controller=yes
    Capacity:     20Gi
    Access Modes: RWO
    Events:       <none>
    

访问 MySQL 实例

前面 YAML 文件中创建了一个允许集群内其他 Pod 访问的数据库 Service。该 Service 中选项 clusterIP: None 让 Service 的 DNS 名称直接解析为 Pod 的 IP 地址。 当在一个 Service 下只有一个 Pod 并且不打算增加 Pod 的数量这是最好的。

运行 MySQL 客户端以连接到服务器:

kubectl run -it --rm --image=mysql:5.6 --restart=Never mysql-client -- mysql -h mysql -ppassword

此命令在集群内创建一个新的 Pod 并运行 MySQL 客户端,并通过 Service 连接到服务器。 如果连接成功,你就知道有状态的 MySQL 数据库正处于运行状态。

Waiting for pod default/mysql-client-274442439-zyp6i to be running, status is Pending, pod ready: false
If you don't see a command prompt, try pressing enter.

mysql>

更新

Deployment 中镜像或其他部分同往常一样可以通过 kubectl apply 命令更新。 以下是特定于有状态应用的一些注意事项:

  • 不要对应用进行规模扩缩。这里的设置仅适用于单实例应用。下层的 PersistentVolume 仅只能挂载到一个 Pod 上。对于集群级有状态应用,请参考 StatefulSet 文档
  • 在 Deployment 的 YAML 文件中使用 strategy: type: Recreate。 该选项指示 Kubernetes 使用滚动升级。滚动升级无法工作,因为这里一次不能运行多个 Pod。在使用更新的配置文件创建新的 Pod 前,Recreate 策略将保证先停止第一个 Pod。

删除 Deployment

通过名称删除部署的对象:

kubectl delete deployment,svc mysql
kubectl delete pvc mysql-pv-claim
kubectl delete pv mysql-pv-volume

如果通过手动的方式供应 PersistentVolume,那么也需要手动删除它以释放下层资源。 如果是用动态供应方式创建的 PersistentVolume,在删除 PersistentVolumeClaim 后 PersistentVolume 将被自动删除。 一些存储服务(比如 EBS 和 PD)也会在 PersistentVolume 被删除时自动回收下层资源。

接下来

3 - 运行一个有状态的应用程序

本页展示如何使用 StatefulSet 控制器运行一个有状态的应用程序。此例是多副本的 MySQL 数据库。 示例应用的拓扑结构有一个主服务器和多个副本,使用异步的基于行(Row-Based) 的数据复制。

准备开始

  • 本教程假定你熟悉 PersistentVolumesStatefulSet, 以及其他核心概念,例如 PodServiceConfigMap
  • 熟悉 MySQL 会有所帮助,但是本教程旨在介绍对其他系统应该有用的常规模式。
  • 你正在使用默认命名空间或不包含任何冲突对象的另一个命名空间。

教程目标

  • 使用 StatefulSet 部署多副本 MySQL 拓扑架构。
  • 发送 MySQL 客户端请求。
  • 观察对宕机的抵抗力。
  • 扩缩 StatefulSet 的规模。

部署 MySQL

MySQL 示例部署包含一个 ConfigMap、两个 Service 与一个 StatefulSet。

创建一个 ConfigMap

使用以下的 YAML 配置文件创建 ConfigMap :

apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql
  labels:
    app: mysql
    app.kubernetes.io/name: mysql
data:
  primary.cnf: |
    # 仅在主服务器上应用此配置
    [mysqld]
    log-bin    
  replica.cnf: |
    # 仅在副本服务器上应用此配置
    [mysqld]
    super-read-only    

kubectl apply -f https://k8s.io/examples/application/mysql/mysql-configmap.yaml

这个 ConfigMap 提供 my.cnf 覆盖设置,使你可以独立控制 MySQL 主服务器和副本服务器的配置。 在这里,你希望主服务器能够将复制日志提供给副本服务器, 并且希望副本服务器拒绝任何不是通过复制进行的写操作。

ConfigMap 本身没有什么特别之处,因而也不会出现不同部分应用于不同的 Pod 的情况。 每个 Pod 都会在初始化时基于 StatefulSet 控制器提供的信息决定要查看的部分。

创建 Service

使用以下 YAML 配置文件创建服务:

# 为 StatefulSet 成员提供稳定的 DNS 表项的无头服务(Headless Service)
apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: mysql
    app.kubernetes.io/name: mysql
spec:
  ports:
  - name: mysql
    port: 3306
  clusterIP: None
  selector:
    app: mysql
---
# 用于连接到任一 MySQL 实例执行读操作的客户端服务
# 对于写操作,你必须连接到主服务器:mysql-0.mysql
apiVersion: v1
kind: Service
metadata:
  name: mysql-read
  labels:
    app: mysql
    app.kubernetes.io/name: mysql
    readonly: "true"
spec:
  ports:
  - name: mysql
    port: 3306
  selector:
    app: mysql
kubectl apply -f https://k8s.io/examples/application/mysql/mysql-services.yaml

这个无头 Service 给 StatefulSet 控制器 为集合中每个 Pod 创建的 DNS 条目提供了一个宿主。 因为无头服务名为 mysql,所以可以通过在同一 Kubernetes 集群和命名空间中的任何其他 Pod 内解析 <Pod 名称>.mysql 来访问 Pod。

客户端 Service 称为 mysql-read,是一种常规 Service,具有其自己的集群 IP。 该集群 IP 在报告就绪的所有 MySQL Pod 之间分配连接。 可能的端点集合包括 MySQL 主节点和所有副本节点。

请注意,只有读查询才能使用负载平衡的客户端 Service。 因为只有一个 MySQL 主服务器,所以客户端应直接连接到 MySQL 主服务器 Pod (通过其在无头 Service 中的 DNS 条目)以执行写入操作。

创建 StatefulSet

最后,使用以下 YAML 配置文件创建 StatefulSet:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  selector:
    matchLabels:
      app: mysql
      app.kubernetes.io/name: mysql
  serviceName: mysql
  replicas: 3
  template:
    metadata:
      labels:
        app: mysql
        app.kubernetes.io/name: mysql
    spec:
      initContainers:
      - name: init-mysql
        image: mysql:5.7
        command:
        - bash
        - "-c"
        - |
          set -ex
          # 基于 Pod 序号生成 MySQL 服务器的 ID。
          [[ $HOSTNAME =~ -([0-9]+)$ ]] || exit 1
          ordinal=${BASH_REMATCH[1]}
          echo [mysqld] > /mnt/conf.d/server-id.cnf
          # 添加偏移量以避免使用 server-id=0 这一保留值。
          echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf
          # 将合适的 conf.d 文件从 config-map 复制到 emptyDir。
          if [[ $ordinal -eq 0 ]]; then
            cp /mnt/config-map/primary.cnf /mnt/conf.d/
          else
            cp /mnt/config-map/replica.cnf /mnt/conf.d/
          fi          
        volumeMounts:
        - name: conf
          mountPath: /mnt/conf.d
        - name: config-map
          mountPath: /mnt/config-map
      - name: clone-mysql
        image: gcr.io/google-samples/xtrabackup:1.0
        command:
        - bash
        - "-c"
        - |
          set -ex
          # 如果已有数据,则跳过克隆。
          [[ -d /var/lib/mysql/mysql ]] && exit 0
          # 跳过主实例(序号索引 0)的克隆。
          [[ `hostname` =~ -([0-9]+)$ ]] || exit 1
          ordinal=${BASH_REMATCH[1]}
          [[ $ordinal -eq 0 ]] && exit 0
          # 从原来的对等节点克隆数据。
          ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql
          # 准备备份。
          xtrabackup --prepare --target-dir=/var/lib/mysql          
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
      containers:
      - name: mysql
        image: mysql:5.7
        env:
        - name: MYSQL_ALLOW_EMPTY_PASSWORD
          value: "1"
        ports:
        - name: mysql
          containerPort: 3306
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
        resources:
          requests:
            cpu: 500m
            memory: 1Gi
        livenessProbe:
          exec:
            command: ["mysqladmin", "ping"]
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
        readinessProbe:
          exec:
            # 检查我们是否可以通过 TCP 执行查询(skip-networking 是关闭的)。
            command: ["mysql", "-h", "127.0.0.1", "-e", "SELECT 1"]
          initialDelaySeconds: 5
          periodSeconds: 2
          timeoutSeconds: 1
      - name: xtrabackup
        image: gcr.io/google-samples/xtrabackup:1.0
        ports:
        - name: xtrabackup
          containerPort: 3307
        command:
        - bash
        - "-c"
        - |
          set -ex
          cd /var/lib/mysql

          # 确定克隆数据的 binlog 位置(如果有的话)。
          if [[ -f xtrabackup_slave_info && "x$(<xtrabackup_slave_info)" != "x" ]]; then
            # XtraBackup 已经生成了部分的 “CHANGE MASTER TO” 查询
            # 因为我们从一个现有副本进行克隆。(需要删除末尾的分号!)
            cat xtrabackup_slave_info | sed -E 's/;$//g' > change_master_to.sql.in
            # 在这里要忽略 xtrabackup_binlog_info (它是没用的)。
            rm -f xtrabackup_slave_info xtrabackup_binlog_info
          elif [[ -f xtrabackup_binlog_info ]]; then
            # 我们直接从主实例进行克隆。解析 binlog 位置。
            [[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1
            rm -f xtrabackup_binlog_info xtrabackup_slave_info
            echo "CHANGE MASTER TO MASTER_LOG_FILE='${BASH_REMATCH[1]}',\
                  MASTER_LOG_POS=${BASH_REMATCH[2]}" > change_master_to.sql.in
          fi

          # 检查我们是否需要通过启动复制来完成克隆。
          if [[ -f change_master_to.sql.in ]]; then
            echo "Waiting for mysqld to be ready (accepting connections)"
            until mysql -h 127.0.0.1 -e "SELECT 1"; do sleep 1; done

            echo "Initializing replication from clone position"
            mysql -h 127.0.0.1 \
                  -e "$(<change_master_to.sql.in), \
                          MASTER_HOST='mysql-0.mysql', \
                          MASTER_USER='root', \
                          MASTER_PASSWORD='', \
                          MASTER_CONNECT_RETRY=10; \
                        START SLAVE;" || exit 1
            # 如果容器重新启动,最多尝试一次。
            mv change_master_to.sql.in change_master_to.sql.orig
          fi

          # 当对等点请求时,启动服务器发送备份。
          exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c \
            "xtrabackup --backup --slave-info --stream=xbstream --host=127.0.0.1 --user=root"          
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
          subPath: mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
        resources:
          requests:
            cpu: 100m
            memory: 100Mi
      volumes:
      - name: conf
        emptyDir: {}
      - name: config-map
        configMap:
          name: mysql
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi
kubectl apply -f https://k8s.io/examples/application/mysql/mysql-statefulset.yaml

你可以通过运行以下命令查看启动进度:

kubectl get pods -l app=mysql --watch

一段时间后,你应该看到所有 3 个 Pod 进入 Running 状态:

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

输入 Ctrl+C 结束监视操作。

此清单使用多种技术来管理作为 StatefulSet 的一部分的有状态 Pod。 下一节重点介绍其中的一些技巧,以解释 StatefulSet 创建 Pod 时发生的状况。

了解有状态的 Pod 初始化

StatefulSet 控制器按序数索引顺序地每次启动一个 Pod。 它一直等到每个 Pod 报告就绪才再启动下一个 Pod。

此外,控制器为每个 Pod 分配一个唯一、稳定的名称,形如 <statefulset 名称>-<序数索引>, 其结果是 Pod 名为 mysql-0mysql-1mysql-2

上述 StatefulSet 清单中的 Pod 模板利用这些属性来执行 MySQL 副本的有序启动。

生成配置

在启动 Pod 规约中的任何容器之前,Pod 首先按顺序运行所有的 Init 容器

第一个名为 init-mysql 的 Init 容器根据序号索引生成特殊的 MySQL 配置文件。

该脚本通过从 Pod 名称的末尾提取索引来确定自己的序号索引,而 Pod 名称由 hostname 命令返回。 然后将序数(带有数字偏移量以避免保留值)保存到 MySQL conf.d 目录中的文件 server-id.cnf。 这一操作将 StatefulSet 所提供的唯一、稳定的标识转换为 MySQL 服务器 ID, 而这些 ID 也是需要唯一性、稳定性保证的。

通过将内容复制到 conf.d 中,init-mysql 容器中的脚本也可以应用 ConfigMap 中的 primary.cnfreplica.cnf。 由于示例部署结构由单个 MySQL 主节点和任意数量的副本节点组成, 因此脚本仅将序数 0 指定为主节点,而将其他所有节点指定为副本节点。

与 StatefulSet 控制器的部署顺序保证相结合, 可以确保 MySQL 主服务器在创建副本服务器之前已准备就绪,以便它们可以开始复制。

克隆现有数据

通常,当新 Pod 作为副本节点加入集合时,必须假定 MySQL 主节点可能已经有数据。 还必须假设复制日志可能不会一直追溯到时间的开始。

这些保守的假设是允许正在运行的 StatefulSet 随时间扩大和缩小而不是固定在其初始大小的关键。

第二个名为 clone-mysql 的 Init 容器,第一次在带有空 PersistentVolume 的副本 Pod 上启动时,会在从属 Pod 上执行克隆操作。 这意味着它将从另一个运行中的 Pod 复制所有现有数据,使此其本地状态足够一致, 从而可以开始从主服务器复制。

MySQL 本身不提供执行此操作的机制,因此本示例使用了一种流行的开源工具 Percona XtraBackup。 在克隆期间,源 MySQL 服务器性能可能会受到影响。 为了最大程度地减少对 MySQL 主服务器的影响,该脚本指示每个 Pod 从序号较低的 Pod 中克隆。 可以这样做的原因是 StatefulSet 控制器始终确保在启动 Pod N+1 之前 Pod N 已准备就绪。

开始复制

Init 容器成功完成后,应用容器将运行。MySQL Pod 由运行实际 mysqld 服务的 mysql 容器和充当辅助工具的 xtrabackup 容器组成。

xtrabackup sidecar 容器查看克隆的数据文件,并确定是否有必要在副本服务器上初始化 MySQL 复制。 如果是这样,它将等待 mysqld 准备就绪,然后使用从 XtraBackup 克隆文件中提取的复制参数执行 CHANGE MASTER TOSTART SLAVE 命令。

一旦副本服务器开始复制后,它会记住其 MySQL 主服务器,并且如果服务器重新启动或连接中断也会自动重新连接。 另外,因为副本服务器会以其稳定的 DNS 名称查找主服务器(mysql-0.mysql), 即使由于重新调度而获得新的 Pod IP,它们也会自动找到主服务器。

最后,开始复制后,xtrabackup 容器监听来自其他 Pod 的连接,处理其数据克隆请求。 如果 StatefulSet 扩大规模,或者下一个 Pod 失去其 PersistentVolumeClaim 并需要重新克隆, 则此服务器将无限期保持运行。

发送客户端请求

你可以通过运行带有 mysql:5.7 镜像的临时容器并运行 mysql 客户端二进制文件, 将测试查询发送到 MySQL 主服务器(主机名 mysql-0.mysql)。

kubectl run mysql-client --image=mysql:5.7 -i --rm --restart=Never --\
  mysql -h mysql-0.mysql <<EOF
CREATE DATABASE test;
CREATE TABLE test.messages (message VARCHAR(250));
INSERT INTO test.messages VALUES ('hello');
EOF

使用主机名 mysql-read 将测试查询发送到任何报告为就绪的服务器:

kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never --\
  mysql -h mysql-read -e "SELECT * FROM test.messages"

你应该获得如下输出:

Waiting for pod default/mysql-client to be running, status is Pending, pod ready: false
+---------+
| message |
+---------+
| hello   |
+---------+
pod "mysql-client" deleted

为了演示 mysql-read 服务在服务器之间分配连接,你可以在循环中运行 SELECT @@server_id

kubectl run mysql-client-loop --image=mysql:5.7 -i -t --rm --restart=Never --\
  bash -ic "while sleep 1; do mysql -h mysql-read -e 'SELECT @@server_id,NOW()'; done"

你应该看到报告的 @@server_id 发生随机变化,因为每次尝试连接时都可能选择了不同的端点:

+-------------+---------------------+
| @@server_id | NOW()               |
+-------------+---------------------+
|         100 | 2006-01-02 15:04:05 |
+-------------+---------------------+
+-------------+---------------------+
| @@server_id | NOW()               |
+-------------+---------------------+
|         102 | 2006-01-02 15:04:06 |
+-------------+---------------------+
+-------------+---------------------+
| @@server_id | NOW()               |
+-------------+---------------------+
|         101 | 2006-01-02 15:04:07 |
+-------------+---------------------+

要停止循环时可以按 Ctrl+C ,但是让它在另一个窗口中运行非常有用, 这样你就可以看到以下步骤的效果。

模拟 Pod 和 Node 失效

为了证明从副本节点缓存而不是单个服务器读取数据的可用性提高,请在使 Pod 退出 Ready 状态时,保持上述 SELECT @@server_id 循环一直运行。

破坏就绪态探测

mysql 容器的就绪态探测 运行命令 mysql -h 127.0.0.1 -e 'SELECT 1',以确保服务器已启动并能够执行查询。

迫使就绪态探测失败的一种方法就是中止该命令:

kubectl exec mysql-2 -c mysql -- mv /usr/bin/mysql /usr/bin/mysql.off

此命令会进入 Pod mysql-2 的实际容器文件系统,重命名 mysql 命令,导致就绪态探测无法找到它。 几秒钟后, Pod 会报告其中一个容器未就绪。你可以通过运行以下命令进行检查:

kubectl get pod mysql-2

READY 列中查找 1/2

NAME      READY     STATUS    RESTARTS   AGE
mysql-2   1/2       Running   0          3m

此时,你应该会看到 SELECT @@server_id 循环继续运行,尽管它不再报告 102。 回想一下,init-mysql 脚本将 server-id 定义为 100 + $ordinal, 因此服务器 ID 102 对应于 Pod mysql-2

现在修复 Pod,几秒钟后它应该重新出现在循环输出中:

kubectl exec mysql-2 -c mysql -- mv /usr/bin/mysql.off /usr/bin/mysql

删除 Pod

如果删除了 Pod,则 StatefulSet 还会重新创建 Pod,类似于 ReplicaSet 对无状态 Pod 所做的操作。

kubectl delete pod mysql-2

StatefulSet 控制器注意到不再存在 mysql-2 Pod,于是创建一个具有相同名称并链接到相同 PersistentVolumeClaim 的新 Pod。 你应该看到服务器 ID 102 从循环输出中消失了一段时间,然后又自行出现。

腾空节点

如果你的 Kubernetes 集群具有多个节点,则可以通过发出以下 drain 命令来模拟节点停机(就好像节点在被升级)。

首先确定 MySQL Pod 之一在哪个节点上:

kubectl get pod mysql-2 -o wide

节点名称应显示在最后一列中:

NAME      READY     STATUS    RESTARTS   AGE       IP            NODE
mysql-2   2/2       Running   0          15m       10.244.5.27   kubernetes-node-9l2t

接下来,通过运行以下命令腾空节点,该命令将其保护起来,以使新的 Pod 不能调度到该节点, 然后逐出所有现有的 Pod。将 <节点名称> 替换为在上一步中找到的节点名称。

# 关于对其他负载的影响,参见前文建议
kubectl drain <节点名称> --force --delete-local-data --ignore-daemonsets

现在,你可以监视 Pod 被重新调度到其他节点上:

kubectl get pod mysql-2 -o wide --watch

它看起来应该像这样:

NAME      READY   STATUS          RESTARTS   AGE       IP            NODE
mysql-2   2/2     Terminating     0          15m       10.244.1.56   kubernetes-node-9l2t
[...]
mysql-2   0/2     Pending         0          0s        <none>        kubernetes-node-fjlm
mysql-2   0/2     Init:0/2        0          0s        <none>        kubernetes-node-fjlm
mysql-2   0/2     Init:1/2        0          20s       10.244.5.32   kubernetes-node-fjlm
mysql-2   0/2     PodInitializing 0          21s       10.244.5.32   kubernetes-node-fjlm
mysql-2   1/2     Running         0          22s       10.244.5.32   kubernetes-node-fjlm
mysql-2   2/2     Running         0          30s       10.244.5.32   kubernetes-node-fjlm

再次,你应该看到服务器 ID 102SELECT @@server_id 循环输出中消失一段时间,然后再次出现。

现在去掉节点保护(Uncordon),使其恢复为正常模式:

kubectl uncordon <节点名称>

扩展副本节点数量

使用 MySQL 复制时,你可以通过添加副本节点来扩展读取查询的能力。 对于 StatefulSet,你可以使用单个命令实现此目的:

kubectl scale statefulset mysql --replicas=5

运行下面的命令,监视新的 Pod 启动:

kubectl get pods -l app=mysql --watch

一旦 Pod 启动,你应该看到服务器 ID 103104 开始出现在 SELECT @@server_id 循环输出中。

你还可以验证这些新服务器在存在之前已添加了数据:

kubectl run mysql-client --image=mysql:5.7 -i -t --rm --restart=Never --\
  mysql -h mysql-3.mysql -e "SELECT * FROM test.messages"
Waiting for pod default/mysql-client to be running, status is Pending, pod ready: false
+---------+
| message |
+---------+
| hello   |
+---------+
pod "mysql-client" deleted

向下缩容操作也是很平滑的:

kubectl scale statefulset mysql --replicas=3

你可以通过运行以下命令查看此效果:

kubectl get pvc -l app=mysql

这表明,尽管将 StatefulSet 缩小为 3,所有 5 个 PVC 仍然存在:

NAME           STATUS    VOLUME                                     CAPACITY   ACCESSMODES   AGE
data-mysql-0   Bound     pvc-8acbf5dc-b103-11e6-93fa-42010a800002   10Gi       RWO           20m
data-mysql-1   Bound     pvc-8ad39820-b103-11e6-93fa-42010a800002   10Gi       RWO           20m
data-mysql-2   Bound     pvc-8ad69a6d-b103-11e6-93fa-42010a800002   10Gi       RWO           20m
data-mysql-3   Bound     pvc-50043c45-b1c5-11e6-93fa-42010a800002   10Gi       RWO           2m
data-mysql-4   Bound     pvc-500a9957-b1c5-11e6-93fa-42010a800002   10Gi       RWO           2m

如果你不打算重复使用多余的 PVC,则可以删除它们:

kubectl delete pvc data-mysql-3
kubectl delete pvc data-mysql-4

清理现场

  1. 通过在终端上按 Ctrl+C 取消 SELECT @@server_id 循环,或从另一个终端运行以下命令:

    kubectl delete pod mysql-client-loop --now
    
  1. 删除 StatefulSet。这也会开始终止 Pod。

    kubectl delete statefulset mysql
    
  1. 验证 Pod 消失。它们可能需要一些时间才能完成终止。

    kubectl get pods -l app=mysql
    

    当上述命令返回如下内容时,你就知道 Pod 已终止:

    No resources found.
    
  1. 删除 ConfigMap、Service 和 PersistentVolumeClaim。

    kubectl delete configmap,service,pvc -l app=mysql
    
  1. 如果你手动制备 PersistentVolume,则还需要手动删除它们,并释放下层资源。 如果你使用了动态制备器,当得知你删除 PersistentVolumeClaim 时,它将自动删除 PersistentVolume。 一些动态制备器(例如用于 EBS 和 PD 的制备器)也会在删除 PersistentVolume 时释放下层资源。

接下来

4 - 扩缩 StatefulSet

本文介绍如何扩缩 StatefulSet。StatefulSet 的扩缩指的是增加或者减少副本个数。

准备开始

  • StatefulSets 仅适用于 Kubernetes 1.5 及以上版本。 要查看你的 Kubernetes 版本,运行 kubectl version

  • 不是所有 Stateful 应用都能很好地执行扩缩操作。 如果你不是很确定是否要扩缩你的 StatefulSet,可先参阅 StatefulSet 概念 或者 StatefulSet 教程

  • 仅当你确定你的有状态应用的集群是完全健康的,才可执行扩缩操作.

扩缩 StatefulSet

使用 kubectl 扩缩 StatefulSet

首先,找到你要扩缩的 StatefulSet。

kubectl get statefulsets <statefulset 名称>

更改 StatefulSet 中副本个数:

kubectl scale statefulsets <statefulset 名称> --replicas=<新的副本数>

对 StatefulSet 执行就地更新

另外, 你可以就地更新 StatefulSet。

如果你的 StatefulSet 最初通过 kubectl applykubectl create --save-config 创建, 你可以更新 StatefulSet 清单中的 .spec.replicas,然后执行命令 kubectl apply

kubectl apply -f <更新后的 statefulset 文件>

否则,可以使用 kubectl edit 编辑副本字段:

kubectl edit statefulsets <statefulset 名称>

或者使用 kubectl patch

kubectl patch statefulsets <statefulset 名称> -p '{"spec":{"replicas":<new-replicas>}}'

故障排查

缩容操作无法正常工作

当 Stateful 所管理的任何 Pod 不健康时,你不能对该 StatefulSet 执行缩容操作。 仅当 StatefulSet 的所有 Pod 都处于运行状态和 Ready 状况后才可缩容。

如果 spec.replicas 大于 1,Kubernetes 无法判定 Pod 不健康的原因。 Pod 不健康可能是由于永久性故障造成也可能是瞬态故障。 瞬态故障可能是节点升级或维护而引起的节点重启造成的。

如果该 Pod 不健康是由于永久性故障导致,则在不纠正该故障的情况下进行缩容可能会导致 StatefulSet 进入一种状态,其成员 Pod 数量低于应正常运行的副本数。 这种状态也许会导致 StatefulSet 不可用。

如果由于瞬态故障而导致 Pod 不健康并且 Pod 可能再次变为可用,那么瞬态错误可能会干扰你对 StatefulSet 的扩容/缩容操作。一些分布式数据库在同时有节点加入和离开时会遇到问题。 在这些情况下,最好是在应用级别进行分析扩缩操作的状态,并且只有在确保 Stateful 应用的集群是完全健康时才执行扩缩操作。

接下来

5 - 删除 StatefulSet

本任务展示如何删除 StatefulSet

准备开始

  • 本任务假设在你的集群上已经运行了由 StatefulSet 创建的应用。

删除 StatefulSet

你可以像删除 Kubernetes 中的其他资源一样删除 StatefulSet: 使用 kubectl delete 命令,并按文件或者名字指定 StatefulSet。

kubectl delete -f <file.yaml>
kubectl delete statefulsets <statefulset 名称>

删除 StatefulSet 之后,你可能需要单独删除关联的无头服务(Headless Service)。

kubectl delete service <Service 名称>

当通过 kubectl 删除 StatefulSet 时,StatefulSet 会被缩容为 0。 属于该 StatefulSet 的所有 Pod 也被删除。 如果你只想删除 StatefulSet 而不删除 Pod,使用 --cascade=orphan

kubectl delete -f <file.yaml> --cascade=orphan

通过将 --cascade=orphan 传递给 kubectl delete,在删除 StatefulSet 对象之后, StatefulSet 管理的 Pod 会被保留下来。如果 Pod 具有标签 app.kubernetes.io/name=MyApp, 则可以按照如下方式删除它们:

kubectl delete pods -l app.kubernetes.io/name=MyApp

持久卷

删除 StatefulSet 管理的 Pod 并不会删除关联的卷。这是为了确保你有机会在删除卷之前从卷中复制数据。 在 Pod 已经终止后删除 PVC 可能会触发删除背后的 PV 持久卷,具体取决于存储类和回收策略。 永远不要假定在 PVC 删除后仍然能够访问卷。

完全删除 StatefulSet

要删除 StatefulSet 中的所有内容,包括关联的 Pod, 你可以运行如下所示的一系列命令:

grace=$(kubectl get pods <stateful-set-pod> --template '{{.spec.terminationGracePeriodSeconds}}')
kubectl delete statefulset -l app.kubernetes.io/name=MyApp
sleep $grace
kubectl delete pvc -l app.kubernetes.io/name=MyApp

在上面的例子中,Pod 的标签为 app.kubernetes.io/name=MyApp;适当地替换你自己的标签。

强制删除 StatefulSet 的 Pod

如果你发现 StatefulSet 的某些 Pod 长时间处于 'Terminating' 或者 'Unknown' 状态, 则可能需要手动干预以强制从 API 服务器中删除这些 Pod。这是一项有点危险的任务。 详细信息请阅读强制删除 StatefulSet 的 Pod

接下来

进一步了解强制删除 StatefulSet 的 Pod

6 - 强制删除 StatefulSet 中的 Pod

本文介绍如何删除 StatefulSet 管理的 Pod,并解释这样操作时需要记住的一些注意事项。

准备开始

  • 这是一项相当高级的任务,并且可能会违反 StatefulSet 固有的某些属性。
  • 请先熟悉下面列举的注意事项再开始操作。

StatefulSet 注意事项

在正常操作 StatefulSet 时,永远不需要强制删除 StatefulSet 管理的 Pod。 StatefulSet 控制器负责创建、 扩缩和删除 StatefulSet 管理的 Pod。此控制器尽力确保指定数量的从序数 0 到 N-1 的 Pod 处于活跃状态并准备就绪。StatefulSet 确保在任何时候,集群中最多只有一个具有给定标识的 Pod。 这就是所谓的由 StatefulSet 提供的最多一个(At Most One) Pod 的语义。

应谨慎进行手动强制删除操作,因为它可能会违反 StatefulSet 固有的至多一个的语义。 StatefulSet 可用于运行分布式和集群级的应用,这些应用需要稳定的网络标识和可靠的存储。 这些应用通常配置为具有固定标识固定数量的成员集合。 具有相同身份的多个成员可能是灾难性的,并且可能导致数据丢失 (例如票选系统中的脑裂场景)。

删除 Pod

你可以使用下面的命令执行体面地删除 Pod:

kubectl delete pods <pod>

为了让上面操作能够体面地终止 Pod,Pod 一定不能设置 pod.Spec.TerminationGracePeriodSeconds 为 0。 将 pod.Spec.TerminationGracePeriodSeconds 设置为 0 秒的做法是不安全的,强烈建议 StatefulSet 类型的 Pod 不要使用。体面删除是安全的,并且会在 kubelet 从 API 服务器中删除资源名称之前确保 体面地结束 Pod

当某个节点不可达时,不会引发自动删除 Pod。在无法访问的节点上运行的 Pod 在超时后会进入 “Terminating” 或者 “Unknown” 状态。 当用户尝试体面地删除无法访问的节点上的 Pod 时 Pod 也可能会进入这些状态。 从 API 服务器上删除处于这些状态 Pod 的仅有可行方法如下:

  • 删除 Node 对象(要么你来删除, 要么节点控制器 来删除)
  • 无响应节点上的 kubelet 开始响应,杀死 Pod 并从 API 服务器上移除 Pod 对象
  • 用户强制删除 Pod

推荐使用第一种或者第二种方法。 如果确认节点已经不可用了(比如,永久断开网络、断电等), 则应删除 Node 对象。 如果节点遇到网裂问题,请尝试解决该问题或者等待其解决。 当网裂愈合时,kubelet 将完成 Pod 的删除并从 API 服务器上释放其名字。

通常,Pod 一旦不在节点上运行,或者管理员删除了节点,系统就会完成其删除动作。 你也可以通过强制删除 Pod 来绕过这一机制。

强制删除

强制删除不会等待来自 kubelet 对 Pod 已终止的确认消息。 无论强制删除是否成功杀死了 Pod,它都会立即从 API 服务器中释放该名字。 这将让 StatefulSet 控制器创建一个具有相同标识的替身 Pod;因而可能导致正在运行 Pod 的重复, 并且如果所述 Pod 仍然可以与 StatefulSet 的成员通信,则将违反 StatefulSet 所要保证的最多一个的语义。

当你强制删除 StatefulSet 类型的 Pod 时,你要确保有问题的 Pod 不会再和 StatefulSet 管理的其他 Pod 通信并且可以安全地释放其名字以便创建替代 Pod。

如果要使用 kubectl 1.5 以上版本强制删除 Pod,请执行下面命令:

kubectl delete pods <pod> --grace-period=0 --force

如果你使用 kubectl 的 1.4 以下版本,则应省略 --force 选项:

kubectl delete pods <pod> --grace-period=0

如果在执行这些命令后 Pod 仍处于 Unknown 状态,请使用以下命令从集群中删除 Pod:

kubectl patch pod <pod> -p '{"metadata":{"finalizers":null}}'

请始终谨慎地执行强制删除 StatefulSet 类型的 Pod,并充分了解强制删除操作所涉及的风险。

接下来

进一步了解调试 StatefulSet

7 - Pod 水平自动扩缩

在 Kubernetes 中,HorizontalPodAutoscaler 自动更新工作负载资源 (例如 Deployment 或者 StatefulSet), 目的是自动扩缩工作负载以满足需求。

水平扩缩意味着对增加的负载的响应是部署更多的 Pod。 这与“垂直(Vertical)”扩缩不同,对于 Kubernetes, 垂直扩缩意味着将更多资源(例如:内存或 CPU)分配给已经为工作负载运行的 Pod。

如果负载减少,并且 Pod 的数量高于配置的最小值, HorizontalPodAutoscaler 会指示工作负载资源(Deployment、StatefulSet 或其他类似资源)缩减。

水平 Pod 自动扩缩不适用于无法扩缩的对象(例如:DaemonSet。)

HorizontalPodAutoscaler 被实现为 Kubernetes API 资源和控制器

资源决定了控制器的行为。 在 Kubernetes 控制平面内运行的水平 Pod 自动扩缩控制器会定期调整其目标(例如:Deployment)的所需规模,以匹配观察到的指标, 例如,平均 CPU 利用率、平均内存利用率或你指定的任何其他自定义指标。

使用水平 Pod 自动扩缩演练示例

HorizontalPodAutoscaler 是如何工作的?

graph BT hpa[Pod 水平自动扩缩] --> scale[规模] subgraph rc[RC / Deployment] scale end scale -.-> pod1[Pod 1] scale -.-> pod2[Pod 2] scale -.-> pod3[Pod N] classDef hpa fill:#D5A6BD,stroke:#1E1E1D,stroke-width:1px,color:#1E1E1D; classDef rc fill:#F9CB9C,stroke:#1E1E1D,stroke-width:1px,color:#1E1E1D; classDef scale fill:#B6D7A8,stroke:#1E1E1D,stroke-width:1px,color:#1E1E1D; classDef pod fill:#9FC5E8,stroke:#1E1E1D,stroke-width:1px,color:#1E1E1D; class hpa hpa; class rc rc; class scale scale; class pod1,pod2,pod3 pod

图 1. HorizontalPodAutoscaler 控制 Deployment 及其 ReplicaSet 的规模

Kubernetes 将水平 Pod 自动扩缩实现为一个间歇运行的控制回路(它不是一个连续的过程)。间隔由 kube-controller-manager--horizontal-pod-autoscaler-sync-period 参数设置(默认间隔为 15 秒)。

在每个时间段内,控制器管理器都会根据每个 HorizontalPodAutoscaler 定义中指定的指标查询资源利用率。 控制器管理器找到由 scaleTargetRef 定义的目标资源,然后根据目标资源的 .spec.selector 标签选择 Pod, 并从资源指标 API(针对每个 Pod 的资源指标)或自定义指标获取指标 API(适用于所有其他指标)。

  • 对于按 Pod 统计的资源指标(如 CPU),控制器从资源指标 API 中获取每一个 HorizontalPodAutoscaler 指定的 Pod 的度量值,如果设置了目标使用率,控制器获取每个 Pod 中的容器资源使用情况, 并计算资源使用率。如果设置了 target 值,将直接使用原始数据(不再计算百分比)。 接下来,控制器根据平均的资源使用率或原始值计算出扩缩的比例,进而计算出目标副本数。

    需要注意的是,如果 Pod 某些容器不支持资源采集,那么控制器将不会使用该 Pod 的 CPU 使用率。 下面的算法细节章节将会介绍详细的算法。

  • 如果 Pod 使用自定义指示,控制器机制与资源指标类似,区别在于自定义指标只使用原始值,而不是使用率。
  • 如果 Pod 使用对象指标和外部指标(每个指标描述一个对象信息)。 这个指标将直接根据目标设定值相比较,并生成一个上面提到的扩缩比例。 在 autoscaling/v2 版本 API 中,这个指标也可以根据 Pod 数量平分后再计算。

HorizontalPodAutoscaler 的常见用途是将其配置为从聚合 APImetrics.k8s.iocustom.metrics.k8s.ioexternal.metrics.k8s.io)获取指标。 metrics.k8s.io API 通常由名为 Metrics Server 的插件提供,需要单独启动。有关资源指标的更多信息, 请参阅 Metrics Server

Metrics API 的支持解释了这些不同 API 的稳定性保证和支持状态。

HorizontalPodAutoscaler 控制器访问支持扩缩的相应工作负载资源(例如:Deployment 和 StatefulSet)。 这些资源每个都有一个名为 scale 的子资源,该接口允许你动态设置副本的数量并检查它们的每个当前状态。 有关 Kubernetes API 子资源的一般信息, 请参阅 Kubernetes API 概念

算法细节

从最基本的角度来看,Pod 水平自动扩缩控制器根据当前指标和期望指标来计算扩缩比例。

期望副本数 = ceil[当前副本数 * (当前指标 / 期望指标)]

例如,如果当前指标值为 200m,而期望值为 100m,则副本数将加倍, 因为 200.0 / 100.0 == 2.0 如果当前值为 50m,则副本数将减半, 因为 50.0 / 100.0 == 0.5。如果比率足够接近 1.0(在全局可配置的容差范围内,默认为 0.1), 则控制平面会跳过扩缩操作。

如果 HorizontalPodAutoscaler 指定的是 targetAverageValuetargetAverageUtilization, 那么将会把指定 Pod 度量值的平均值做为 currentMetricValue

在检查容差并决定最终值之前,控制平面还会考虑是否缺少任何指标, 以及有多少 Pod Ready

所有设置了删除时间戳的 Pod(带有删除时间戳的对象正在关闭/移除的过程中)都会被忽略, 所有失败的 Pod 都会被丢弃。

如果某个 Pod 缺失度量值,它将会被搁置,只在最终确定扩缩数量时再考虑。

当使用 CPU 指标来扩缩时,任何还未就绪(还在初始化,或者可能是不健康的)状态的 Pod 最近的指标度量值采集于就绪状态前的 Pod,该 Pod 也会被搁置。

由于技术限制,HorizontalPodAutoscaler 控制器在确定是否保留某些 CPU 指标时无法准确确定 Pod 首次就绪的时间。 相反,如果 Pod 未准备好并在其启动后的一个可配置的短时间窗口内转换为准备好,它会认为 Pod “尚未准备好”。 该值使用 --horizontal-pod-autoscaler-initial-readiness-delay 标志配置,默认值为 30 秒。 一旦 Pod 准备就绪,如果它发生在自启动后较长的、可配置的时间内,它就会认为任何向准备就绪的转换都是第一个。 该值由 -horizontal-pod-autoscaler-cpu-initialization-period 标志配置,默认为 5 分钟。

在排除掉被搁置的 Pod 后,扩缩比例就会根据 currentMetricValue/desiredMetricValue 计算出来。

如果缺失某些度量值,控制平面会更保守地重新计算平均值,在需要缩小时假设这些 Pod 消耗了目标值的 100%, 在需要放大时假设这些 Pod 消耗了 0% 目标值。这可以在一定程度上抑制扩缩的幅度。

此外,如果存在任何尚未就绪的 Pod,工作负载会在不考虑遗漏指标或尚未就绪的 Pod 的情况下进行扩缩, 控制器保守地假设尚未就绪的 Pod 消耗了期望指标的 0%,从而进一步降低了扩缩的幅度。

考虑到尚未准备好的 Pod 和缺失的指标后,控制器会重新计算使用率。 如果新的比率与扩缩方向相反,或者在容差范围内,则控制器不会执行任何扩缩操作。 在其他情况下,新比率用于决定对 Pod 数量的任何更改。

注意,平均利用率的原始值是通过 HorizontalPodAutoscaler 状态体现的, 而不考虑尚未准备好的 Pod 或缺少的指标,即使使用新的使用率也是如此。

如果创建 HorizontalPodAutoscaler 时指定了多个指标, 那么会按照每个指标分别计算扩缩副本数,取最大值进行扩缩。 如果任何一个指标无法顺利地计算出扩缩副本数(比如,通过 API 获取指标时出错), 并且可获取的指标建议缩容,那么本次扩缩会被跳过。 这表示,如果一个或多个指标给出的 desiredReplicas 值大于当前值,HPA 仍然能实现扩容。

最后,在 HPA 控制器执行扩缩操作之前,会记录扩缩建议信息。 控制器会在操作时间窗口中考虑所有的建议信息,并从中选择得分最高的建议。 这个值可通过 kube-controller-manager 服务的启动参数 --horizontal-pod-autoscaler-downscale-stabilization 进行配置, 默认值为 5 分钟。 这个配置可以让系统更为平滑地进行缩容操作,从而消除短时间内指标值快速波动产生的影响。

API 对象

HorizontalPodAutoscaler 是 Kubernetes autoscaling API 组中的 API 资源。 当前的稳定版本可以在 autoscaling/v2 API 版本中找到,其中包括对基于内存和自定义指标执行扩缩的支持。 在使用 autoscaling/v1 时,autoscaling/v2 中引入的新字段作为注释保留。

创建 HorizontalPodAutoscaler 对象时,需要确保所给的名称是一个合法的 DNS 子域名。 有关 API 对象的更多信息,请查阅 HorizontalPodAutoscaler 对象文档

工作量规模的稳定性

在使用 HorizontalPodAutoscaler 管理一组副本的规模时,由于评估的指标的动态特性, 副本的数量可能会经常波动。这有时被称为 抖动(thrashing)波动(flapping)。 它类似于控制论中的 滞后(hysteresis) 概念。

滚动升级时扩缩

Kubernetes 允许你在 Deployment 上执行滚动更新。在这种情况下,Deployment 为你管理下层的 ReplicaSet。 当你为一个 Deployment 配置自动扩缩时,你要为每个 Deployment 绑定一个 HorizontalPodAutoscaler。 HorizontalPodAutoscaler 管理 Deployment 的 replicas 字段。 Deployment Controller 负责设置下层 ReplicaSet 的 replicas 字段, 以便确保在上线及后续过程副本个数合适。

如果你对一个副本个数被自动扩缩的 StatefulSet 执行滚动更新,该 StatefulSet 会直接管理它的 Pod 集合(不存在类似 ReplicaSet 这样的中间资源)。

对资源指标的支持

HPA 的任何目标资源都可以基于其中的 Pod 的资源用量来实现扩缩。 在定义 Pod 规约时,类似 cpumemory 这类资源请求必须被设定。 这些设定值被用来确定资源利用量并被 HPA 控制器用来对目标资源完成扩缩操作。 要使用基于资源利用率的扩缩,可以像下面这样指定一个指标源:

type: Resource
resource:
  name: cpu
  target:
    type: Utilization
    averageUtilization: 60

基于这一指标设定,HPA 控制器会维持扩缩目标中的 Pods 的平均资源利用率在 60%。 利用率是 Pod 的当前资源用量与其请求值之间的比值。 关于如何计算利用率以及如何计算平均值的细节可参考算法小节。

容器资源指标

特性状态: Kubernetes v1.27 [beta]

HorizontalPodAutoscaler API 也支持容器指标源,这时 HPA 可以跟踪记录一组 Pod 中各个容器的资源用量,进而触发扩缩目标对象的操作。 容器资源指标的支持使得你可以为特定 Pod 中最重要的容器配置规模扩缩阈值。 例如,如果你有一个 Web 应用和一个执行日志操作的边车容器,你可以基于 Web 应用的资源用量来执行扩缩,忽略边车容器的存在及其资源用量。

如果你更改扩缩目标对象,令其使用新的、包含一组不同的容器的 Pod 规约,你就需要修改 HPA 的规约才能基于新添加的容器来执行规模扩缩操作。 如果指标源中指定的容器不存在或者仅存在于部分 Pod 中,那么这些 Pod 会被忽略, HPA 会重新计算资源用量值。参阅算法小节进一步了解计算细节。 要使用容器资源用量来完成自动扩缩,可以像下面这样定义指标源:

type: ContainerResource
containerResource:
  name: cpu
  container: application
  target:
    type: Utilization
    averageUtilization: 60

在上面的例子中,HPA 控制器会对目标对象执行扩缩操作以确保所有 Pod 中 application 容器的平均 CPU 用量为 60%。

扩展自定义指标

特性状态: Kubernetes v1.23 [stable]

(之前的 autoscaling/v2beta2 API 版本将此功能作为 beta 功能提供)

如果你使用 autoscaling/v2 API 版本,则可以将 HorizontalPodAutoscaler 配置为基于自定义指标(未内置于 Kubernetes 或任何 Kubernetes 组件)进行扩缩。 HorizontalPodAutoscaler 控制器能够从 Kubernetes API 查询这些自定义指标。

有关要求,请参阅对 Metrics APIs 的支持

基于多个指标来执行扩缩

特性状态: Kubernetes v1.23 [stable]

(之前的 autoscaling/v2beta2 API 版本将此功能作为 beta 功能提供)

如果你使用 autoscaling/v2 API 版本,你可以为 HorizontalPodAutoscaler 指定多个指标以进行扩缩。 HorizontalPodAutoscaler 控制器评估每个指标,并根据该指标提出一个新的比例。 HorizontalPodAutoscaler 采用为每个指标推荐的最大比例, 并将工作负载设置为该大小(前提是这不大于你配置的总体最大值)。

对 Metrics API 的支持

默认情况下,HorizontalPodAutoscaler 控制器会从一系列的 API 中检索度量值。 集群管理员需要确保下述条件,以保证 HPA 控制器能够访问这些 API:

  • 启用了 API 聚合层

  • 相应的 API 已注册:

    • 对于资源指标,将使用 metrics.k8s.io API, 一般由 metrics-server 提供。 它可以作为集群插件启动。

    • 对于自定义指标,将使用 custom.metrics.k8s.io API。 它由其他度量指标方案厂商的“适配器(Adapter)” API 服务器提供。 检查你的指标管道以查看是否有可用的 Kubernetes 指标适配器。

    • 对于外部指标,将使用 external.metrics.k8s.io API。 可能由上面的自定义指标适配器提供。

关于指标来源以及其区别的更多信息,请参阅相关的设计文档, HPA V2custom.metrics.k8s.ioexternal.metrics.k8s.io

关于如何使用它们的示例, 请参考使用自定义指标的教程使用外部指标的教程

可配置的扩缩行为

特性状态: Kubernetes v1.23 [stable]

(之前的 autoscaling/v2beta2 API 版本将此功能作为 beta 功能提供)

如果你使用 v2 HorizontalPodAutoscaler API,你可以使用 behavior 字段 (请参阅 API 参考) 来配置单独的放大和缩小行为。你可以通过在行为字段下设置 scaleUp 和/或 scaleDown 来指定这些行为。

你可以指定一个“稳定窗口”,以防止扩缩目标的副本计数发生波动。 扩缩策略还允许你在扩缩时控制副本的变化率。

扩缩策略

可以在规约的 behavior 部分中指定一个或多个扩缩策略。当指定多个策略时, 允许最大更改量的策略是默认选择的策略。以下示例显示了缩小时的这种行为:

behavior:
  scaleDown:
    policies:
    - type: Pods
      value: 4
      periodSeconds: 60
    - type: Percent
      value: 10
      periodSeconds: 60

periodSeconds 表示在过去的多长时间内要求策略值为真。 你可以设置 periodSeconds 的最大值为 1800(半小时)。 第一个策略(Pods)允许在一分钟内最多缩容 4 个副本。第二个策略(Percent) 允许在一分钟内最多缩容当前副本个数的百分之十。

由于默认情况下会选择容许更大程度作出变更的策略,只有 Pod 副本数大于 40 时, 第二个策略才会被采用。如果副本数为 40 或者更少,则应用第一个策略。 例如,如果有 80 个副本,并且目标必须缩小到 10 个副本,那么在第一步中将减少 8 个副本。 在下一轮迭代中,当副本的数量为 72 时,10% 的 Pod 数为 7.2,但是这个数字向上取整为 8。 在 autoscaler 控制器的每个循环中,将根据当前副本的数量重新计算要更改的 Pod 数量。 当副本数量低于 40 时,应用第一个策略(Pods),一次减少 4 个副本。

可以指定扩缩方向的 selectPolicy 字段来更改策略选择。 通过设置 Min 的值,它将选择副本数变化最小的策略。 将该值设置为 Disabled 将完全禁用该方向的扩缩。

稳定窗口

当用于扩缩的指标不断波动时,稳定窗口用于限制副本计数的波动。 自动扩缩算法使用此窗口来推断先前的期望状态并避免对工作负载规模进行不必要的更改。

例如,在以下示例代码段中,为 scaleDown 指定了稳定窗口。

behavior:
  scaleDown:
    stabilizationWindowSeconds: 300

当指标显示目标应该缩容时,自动扩缩算法查看之前计算的期望状态,并使用指定时间间隔内的最大值。 在上面的例子中,过去 5 分钟的所有期望状态都会被考虑。

这近似于滚动最大值,并避免了扩缩算法频繁删除 Pod 而又触发重新创建等效 Pod。

默认行为

要使用自定义扩缩,不必指定所有字段。 只有需要自定义的字段才需要指定。 这些自定义值与默认值合并。 默认值与 HPA 算法中的现有行为匹配。

behavior:
  scaleDown:
    stabilizationWindowSeconds: 300
    policies:
    - type: Percent
      value: 100
      periodSeconds: 15
  scaleUp:
    stabilizationWindowSeconds: 0
    policies:
    - type: Percent
      value: 100
      periodSeconds: 15
    - type: Pods
      value: 4
      periodSeconds: 15
    selectPolicy: Max

用于缩小稳定窗口的时间为 300 秒(或是 --horizontal-pod-autoscaler-downscale-stabilization 参数设定值)。 只有一种缩容的策略,允许 100% 删除当前运行的副本,这意味着扩缩目标可以缩小到允许的最小副本数。 对于扩容,没有稳定窗口。当指标显示目标应该扩容时,目标会立即扩容。 这里有两种策略,每 15 秒最多添加 4 个 Pod 或 100% 当前运行的副本数,直到 HPA 达到稳定状态。

示例:更改缩容稳定窗口

将下面的 behavior 配置添加到 HPA 中,可提供一个 1 分钟的自定义缩容稳定窗口:

behavior:
  scaleDown:
    stabilizationWindowSeconds: 60

示例:限制缩容速率

将下面的 behavior 配置添加到 HPA 中,可限制 Pod 被 HPA 删除速率为每分钟 10%:

behavior:
  scaleDown:
    policies:
    - type: Percent
      value: 10
      periodSeconds: 60

为了确保每分钟删除的 Pod 数不超过 5 个,可以添加第二个缩容策略,大小固定为 5,并将 selectPolicy 设置为最小值。 将 selectPolicy 设置为 Min 意味着 autoscaler 会选择影响 Pod 数量最小的策略:

behavior:
  scaleDown:
    policies:
    - type: Percent
      value: 10
      periodSeconds: 60
    - type: Pods
      value: 5
      periodSeconds: 60
    selectPolicy: Min

示例:禁用缩容

selectPolicy 的值 Disabled 会关闭对给定方向的缩容。 因此使用以下策略,将会阻止缩容:

behavior:
  scaleDown:
    selectPolicy: Disabled

kubectl 对 HorizontalPodAutoscaler 的支持

与每个 API 资源一样,HorizontalPodAutoscaler 都被 kubectl 以标准方式支持。 你可以使用 kubectl create 命令创建一个新的自动扩缩器。 你可以通过 kubectl get hpa 列出自动扩缩器或通过 kubectl describe hpa 获取详细描述。 最后,你可以使用 kubectl delete hpa 删除自动扩缩器。

此外,还有一个特殊的 kubectl autoscale 命令用于创建 HorizontalPodAutoscaler 对象。 例如,执行 kubectl autoscale rs foo --min=2 --max=5 --cpu-percent=80 将为 ReplicaSet foo 创建一个自动扩缩器,目标 CPU 利用率设置为 80%,副本数在 2 到 5 之间。

隐式维护状态禁用

你可以在不必更改 HPA 配置的情况下隐式地为某个目标禁用 HPA。 如果此目标的期望副本个数被设置为 0,而 HPA 的最小副本个数大于 0, 则 HPA 会停止调整目标(并将其自身的 ScalingActive 状况设置为 false), 直到你通过手动调整目标的期望副本个数或 HPA 的最小副本个数来重新激活。

将 Deployment 和 StatefulSet 迁移到水平自动扩缩

当启用 HPA 时,建议从它们的清单中删除 Deployment 和/或 StatefulSet 的 spec.replicas 的值。 如果不这样做,则只要应用对该对象的更改,例如通过 kubectl apply -f deployment.yaml, 这将指示 Kubernetes 将当前 Pod 数量扩缩到 spec.replicas 键的值。这可能不是所希望的, 并且当 HPA 处于活动状态时可能会很麻烦。

请记住,删除 spec.replicas 可能会导致 Pod 计数一次性降级,因为此键的默认值为 1 (参考 Deployment Replicas)。 更新后,除 1 之外的所有 Pod 都将开始其终止程序。之后的任何部署应用程序都将正常运行, 并根据需要遵守滚动更新配置。你可以根据修改部署的方式选择以下两种方法之一来避免这种降级:

  1. kubectl apply edit-last-applied deployment/<Deployment 名称>
  2. 在编辑器中,删除 spec.replicas。当你保存并退出编辑器时,kubectl 会应用更新。 在此步骤中不会更改 Pod 计数。
  3. 你现在可以从清单中删除 spec.replicas。如果你使用源代码管理, 还应提交你的更改或采取任何其他步骤来修改源代码,以适应你如何跟踪更新。
  4. 从这里开始,你可以运行 kubectl apply -f deployment.yaml

使用服务器端 Apply 机制, 你可以遵循交出所有权说明, 该指南涵盖了这个确切的用例。

接下来

如果你在集群中配置自动扩缩,你可能还需要考虑运行集群级别的自动扩缩器, 例如 Cluster Autoscaler

有关 HorizontalPodAutoscaler 的更多信息:

8 - HorizontalPodAutoscaler 演练

HorizontalPodAutoscaler(简称 HPA ) 自动更新工作负载资源(例如 Deployment 或者 StatefulSet), 目的是自动扩缩工作负载以满足需求。

水平扩缩意味着对增加的负载的响应是部署更多的 Pod。 这与“垂直(Vertical)”扩缩不同,对于 Kubernetes, 垂直扩缩意味着将更多资源(例如:内存或 CPU)分配给已经为工作负载运行的 Pod。

如果负载减少,并且 Pod 的数量高于配置的最小值, HorizontalPodAutoscaler 会指示工作负载资源(Deployment、StatefulSet 或其他类似资源)缩减。

本文档将引导你完成启用 HorizontalPodAutoscaler 以自动管理示例 Web 应用程序的扩缩的示例。 此示例工作负载是运行一些 PHP 代码的 Apache httpd。

准备开始

你必须拥有一个 Kubernetes 的集群,同时你必须配置 kubectl 命令行工具与你的集群通信。 建议在至少有两个不作为控制平面主机的节点的集群上运行本教程。 如果你还没有集群,你可以通过 Minikube 构建一个你自己的集群,或者你可以使用下面的 Kubernetes 练习环境之一:

你的 Kubernetes 服务器版本必须不低于版本 1.23. 要获知版本信息,请输入 kubectl version.

如果你运行的是旧版本的 Kubernetes,请参阅该版本的文档版本 (可用的文档版本)。

按照本演练进行操作,你需要一个部署并配置了 Metrics Server 的集群。 Kubernetes Metrics Server 从集群中的 kubelets 收集资源指标, 并通过 Kubernetes API 公开这些指标, 使用 APIService 添加代表指标读数的新资源。

要了解如何部署 Metrics Server,请参阅 metrics-server 文档

如果你正在运行 Minikube,运行以下命令以启用 metrics-server:

minikube addons enable metrics-server

运行 php-apache 服务器并暴露服务

为了演示 HorizontalPodAutoscaler,你将首先启动一个 Deployment 用 hpa-example 镜像运行一个容器, 然后使用以下清单文件将其暴露为一个 服务(Service)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-apache
spec:
  selector:
    matchLabels:
      run: php-apache
  template:
    metadata:
      labels:
        run: php-apache
    spec:
      containers:
      - name: php-apache
        image: registry.k8s.io/hpa-example
        ports:
        - containerPort: 80
        resources:
          limits:
            cpu: 500m
          requests:
            cpu: 200m
---
apiVersion: v1
kind: Service
metadata:
  name: php-apache
  labels:
    run: php-apache
spec:
  ports:
  - port: 80
  selector:
    run: php-apache

为此,运行下面的命令:

kubectl apply -f https://k8s.io/examples/application/php-apache.yaml
deployment.apps/php-apache created
service/php-apache created

创建 HorizontalPodAutoscaler

现在服务器正在运行,使用 kubectl 创建自动扩缩器。 kubectl autoscale 子命令是 kubectl 的一部分, 可以帮助你执行此操作。

你将很快运行一个创建 HorizontalPodAutoscaler 的命令, 该 HorizontalPodAutoscaler 维护由你在这些说明的第一步中创建的 php-apache Deployment 控制的 Pod 存在 1 到 10 个副本。

粗略地说,HPA 控制器将增加和减少副本的数量 (通过更新 Deployment)以保持所有 Pod 的平均 CPU 利用率为 50%。 Deployment 然后更新 ReplicaSet —— 这是所有 Deployment 在 Kubernetes 中工作方式的一部分 —— 然后 ReplicaSet 根据其 .spec 的更改添加或删除 Pod。

由于每个 Pod 通过 kubectl run 请求 200 milli-cores,这意味着平均 CPU 使用率为 100 milli-cores。 有关算法的更多详细信息, 请参阅算法详细信息

创建 HorizontalPodAutoscaler:

kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10
horizontalpodautoscaler.autoscaling/php-apache autoscaled

你可以通过运行以下命令检查新制作的 HorizontalPodAutoscaler 的当前状态:

# 你可以使用 “hpa” 或 “horizontalpodautoscaler”;任何一个名字都可以。
kubectl get hpa

输出类似于:

NAME         REFERENCE                     TARGET    MINPODS   MAXPODS   REPLICAS   AGE
php-apache   Deployment/php-apache/scale   0% / 50%  1         10        1          18s

(如果你看到其他具有不同名称的 HorizontalPodAutoscalers,这意味着它们已经存在,这通常不是问题)。

请注意当前的 CPU 利用率是 0%,这是由于我们尚未发送任何请求到服务器 (TARGET 列显示了相应 Deployment 所控制的所有 Pod 的平均 CPU 利用率)。

增加负载

接下来,看看自动扩缩器如何对增加的负载做出反应。 为此,你将启动一个不同的 Pod 作为客户端。 客户端 Pod 中的容器在无限循环中运行,向 php-apache 服务发送查询。

# 在单独的终端中运行它
# 以便负载生成继续,你可以继续执行其余步骤
kubectl run -i --tty load-generator --rm --image=busybox:1.28 --restart=Never -- /bin/sh -c "while sleep 0.01; do wget -q -O- http://php-apache; done"

现在执行:

# 准备好后按 Ctrl+C 结束观察
kubectl get hpa php-apache --watch

一分钟时间左右之后,通过以下命令,我们可以看到 CPU 负载升高了;例如:

NAME         REFERENCE                     TARGET      MINPODS   MAXPODS   REPLICAS   AGE
php-apache   Deployment/php-apache/scale   305% / 50%  1         10        1          3m

然后,更多的副本被创建。例如:

NAME         REFERENCE                     TARGET      MINPODS   MAXPODS   REPLICAS   AGE
php-apache   Deployment/php-apache/scale   305% / 50%  1         10        7          3m

这时,由于请求增多,CPU 利用率已经升至请求值的 305%。 可以看到,Deployment 的副本数量已经增长到了 7:

kubectl get deployment php-apache

你应该会看到与 HorizontalPodAutoscaler 中的数字与副本数匹配

NAME         READY   UP-TO-DATE   AVAILABLE   AGE
php-apache   7/7      7           7           19m

停止产生负载

要完成该示例,请停止发送负载。

在我们创建 busybox 容器的终端中,输入 <Ctrl> + C 来终止负载的产生。

然后验证结果状态(大约一分钟后):

# 准备好后按 Ctrl+C 结束观察
kubectl get hpa php-apache --watch

输出类似于:

NAME         REFERENCE                     TARGET       MINPODS   MAXPODS   REPLICAS   AGE
php-apache   Deployment/php-apache/scale   0% / 50%     1         10        1          11m

Deployment 也显示它已经缩小了:

kubectl get deployment php-apache
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
php-apache   1/1     1            1           27m

一旦 CPU 利用率降至 0,HPA 会自动将副本数缩减为 1。

自动扩缩完成副本数量的改变可能需要几分钟的时间。

基于多项度量指标和自定义度量指标自动扩缩

利用 autoscaling/v2 API 版本,你可以在自动扩缩 php-apache 这个 Deployment 时使用其他度量指标。

首先,将 HorizontalPodAutoscaler 的 YAML 文件改为 autoscaling/v2 格式:

kubectl get hpa php-apache -o yaml > /tmp/hpa-v2.yaml

在编辑器中打开 /tmp/hpa-v2.yaml,你应看到如下所示的 YAML 文件:

apiVersion: autoscaling/v2
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
status:
  observedGeneration: 1
  lastScaleTime: <some-time>
  currentReplicas: 1
  desiredReplicas: 1
  currentMetrics:
  - type: Resource
    resource:
      name: cpu
      current:
        averageUtilization: 0
        averageValue: 0

需要注意的是,targetCPUUtilizationPercentage 字段已经被名为 metrics 的数组所取代。 CPU 利用率这个度量指标是一个 resource metric(资源度量指标),因为它表示容器上指定资源的百分比。 除 CPU 外,你还可以指定其他资源度量指标。默认情况下,目前唯一支持的其他资源度量指标为内存。 只要 metrics.k8s.io API 存在,这些资源度量指标就是可用的,并且他们不会在不同的 Kubernetes 集群中改变名称。

你还可以指定资源度量指标使用绝对数值,而不是百分比,你需要将 target.typeUtilization 替换成 AverageValue,同时设置 target.averageValue 而非 target.averageUtilization 的值。

还有两种其他类型的度量指标,他们被认为是 custom metrics(自定义度量指标): 即 Pod 度量指标和 Object 度量指标。 这些度量指标可能具有特定于集群的名称,并且需要更高级的集群监控设置。

第一种可选的度量指标类型是 Pod 度量指标。这些指标从某一方面描述了 Pod, 在不同 Pod 之间进行平均,并通过与一个目标值比对来确定副本的数量。 它们的工作方式与资源度量指标非常相像,只是它们支持 target 类型为 AverageValue

Pod 度量指标通过如下代码块定义:

type: Pods
pods:
  metric:
    name: packets-per-second
  target:
    type: AverageValue
    averageValue: 1k

第二种可选的度量指标类型是对象 (Object)度量指标。 这些度量指标用于描述在相同名字空间中的别的对象,而非 Pod。 请注意这些度量指标不一定来自某对象,它们仅用于描述这些对象。 对象度量指标支持的 target 类型包括 ValueAverageValue。 如果是 Value 类型,target 值将直接与 API 返回的度量指标比较, 而对于 AverageValue 类型,API 返回的度量值将按照 Pod 数量拆分, 然后再与 target 值比较。 下面的 YAML 文件展示了一个表示 requests-per-second 的度量指标。

type: Object
object:
  metric:
    name: requests-per-second
  describedObject:
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    name: main-route
  target:
    type: Value
    value: 2k

如果你指定了多个上述类型的度量指标,HorizontalPodAutoscaler 将会依次考量各个指标。 HorizontalPodAutoscaler 将会计算每一个指标所提议的副本数量,然后最终选择一个最高值。

比如,如果你的监控系统能够提供网络流量数据,你可以通过 kubectl edit 命令将上述 Horizontal Pod Autoscaler 的定义更改为:

apiVersion: autoscaling/v2
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
  - type: Pods
    pods:
      metric:
        name: packets-per-second
      target:
        type: AverageValue
        averageValue: 1k
  - type: Object
    object:
      metric:
        name: requests-per-second
      describedObject:
        apiVersion: networking.k8s.io/v1
        kind: Ingress
        name: main-route
      target:
        type: Value
        value: 10k
status:
  observedGeneration: 1
  lastScaleTime: <some-time>
  currentReplicas: 1
  desiredReplicas: 1
  currentMetrics:
  - type: Resource
    resource:
      name: cpu
    current:
      averageUtilization: 0
      averageValue: 0
  - type: Object
    object:
      metric:
        name: requests-per-second
      describedObject:
        apiVersion: networking.k8s.io/v1
        kind: Ingress
        name: main-route
      current:
        value: 10k

这样,你的 HorizontalPodAutoscaler 将会尝试确保每个 Pod 的 CPU 利用率在 50% 以内, 每秒能够服务 1000 个数据包请求, 并确保所有在 Ingress 后的 Pod 每秒能够服务的请求总数达到 10000 个。

基于更特别的度量值来扩缩

许多度量流水线允许你通过名称或附加的标签来描述度量指标。 对于所有非资源类型度量指标(Pod、Object 和后面将介绍的 External), 可以额外指定一个标签选择算符。例如,如果你希望收集包含 verb 标签的 http_requests 度量指标,可以按如下所示设置度量指标块,使得扩缩操作仅针对 GET 请求执行:

type: Object
object:
  metric:
    name: http_requests
    selector: {matchLabels: {verb: GET}}

这个选择算符使用与 Kubernetes 标签选择算符相同的语法。 如果名称和标签选择算符匹配到多个系列,监测管道会决定如何将多个系列合并成单个值。 选择算符是可以累加的,它不会选择目标以外的对象(类型为 Pods 的目标 Pod 或者类型为 Object 的目标对象)。

运行在 Kubernetes 上的应用程序可能需要基于与 Kubernetes 集群中的任何对象没有明显关系的度量指标进行自动扩缩, 例如那些描述与任何 Kubernetes 名字空间中的服务都无直接关联的度量指标。 在 Kubernetes 1.10 及之后版本中,你可以使用外部度量指标(external metrics)。

使用外部度量指标时,需要了解你所使用的监控系统,相关的设置与使用自定义指标时类似。 外部度量指标使得你可以使用你的监控系统的任何指标来自动扩缩你的集群。 你需要在 metric 块中提供 nameselector,同时将类型由 Object 改为 External。 如果 metricSelector 匹配到多个度量指标,HorizontalPodAutoscaler 将会把它们加和。 外部度量指标同时支持 ValueAverageValue 类型,这与 Object 类型的度量指标相同。

例如,如果你的应用程序处理来自主机上消息队列的任务, 为了让每 30 个任务有 1 个工作者实例,你可以将下面的内容添加到 HorizontalPodAutoscaler 的配置中。

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

如果可能,还是推荐定制度量指标而不是外部度量指标,因为这便于让系统管理员加固定制度量指标 API。 而外部度量指标 API 可以允许访问所有的度量指标。 当暴露这些服务时,系统管理员需要仔细考虑这个问题。

附录:Horizontal Pod Autoscaler 状态条件

使用 autoscaling/v2 格式的 HorizontalPodAutoscaler 时,你将可以看到 Kubernetes 为 HorizongtalPodAutoscaler 设置的状态条件(Status Conditions)。 这些状态条件可以显示当前 HorizontalPodAutoscaler 是否能够执行扩缩以及是否受到一定的限制。

status.conditions 字段展示了这些状态条件。 可以通过 kubectl describe hpa 命令查看当前影响 HorizontalPodAutoscaler 的各种状态条件信息:

kubectl describe hpa cm-test
Name:                           cm-test
Namespace:                      prom
Labels:                         <none>
Annotations:                    <none>
CreationTimestamp:              Fri, 16 Jun 2017 18:09:22 +0000
Reference:                      ReplicationController/cm-test
Metrics:                        ( current / target )
  "http_requests" on pods:      66m / 500m
Min replicas:                   1
Max replicas:                   4
ReplicationController pods:     1 current / 1 desired
Conditions:
  Type                  Status  Reason                  Message
  ----                  ------  ------                  -------
  AbleToScale           True    ReadyForNewScale        the last scale time was sufficiently old as to warrant a new scale
  ScalingActive         True    ValidMetricFound        the HPA was able to successfully calculate a replica count from pods metric http_requests
  ScalingLimited        False   DesiredWithinRange      the desired replica count is within the acceptable range
Events:

对于上面展示的这个 HorizontalPodAutoscaler,我们可以看出有若干状态条件处于健康状态。 首先,AbleToScale 表明 HPA 是否可以获取和更新扩缩信息,以及是否存在阻止扩缩的各种回退条件。 其次,ScalingActive 表明 HPA 是否被启用(即目标的副本数量不为零)以及是否能够完成扩缩计算。 当这一状态为 False 时,通常表明获取度量指标存在问题。 最后一个条件 ScalingLimited 表明所需扩缩的值被 HorizontalPodAutoscaler 所定义的最大或者最小值所限制(即已经达到最大或者最小扩缩值)。 这通常表明你可能需要调整 HorizontalPodAutoscaler 所定义的最大或者最小副本数量的限制了。

量纲

HorizontalPodAutoscaler 和 度量指标 API 中的所有的度量指标使用 Kubernetes 中称为量纲(Quantity)的特殊整数表示。 例如,数量 10500m 用十进制表示为 10.5。 如果可能的话,度量指标 API 将返回没有后缀的整数,否则返回以千分单位的数量。 这意味着你可能会看到你的度量指标在 11500m(也就是在十进制记数法中的 11.5)之间波动。

其他可能的情况

以声明式方式创建 Autoscaler

除了使用 kubectl autoscale 命令,也可以使用以下清单以声明方式创建 HorizontalPodAutoscaler:

apiVersion: autoscaling/v2
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

使用如下命令创建 Autoscaler:

kubectl create -f https://k8s.io/examples/application/hpa/php-apache.yaml
horizontalpodautoscaler.autoscaling/php-apache created

9 - 为应用程序设置干扰预算(Disruption Budget)

特性状态: Kubernetes v1.21 [stable]

本文展示如何限制应用程序的并发干扰数量,在允许集群管理员管理集群节点的同时保证高可用。

准备开始

你的 Kubernetes 服务器版本必须不低于版本 v1.21. 要获知版本信息,请输入 kubectl version.
  • 你是 Kubernetes 集群中某应用的所有者,该应用有高可用要求。
  • 你应了解如何部署无状态应用 和/或有状态应用
  • 你应当已经阅读过关于 Pod 干扰的文档。
  • 用户应当与集群所有者或服务提供者确认其遵从 Pod 干扰预算(Pod Disruption Budgets)的规则。

用 PodDisruptionBudget 来保护应用

  1. 确定想要使用 PodDisruptionBudget(PDB)来保护的应用。
  2. 考虑应用对干扰的反应。
  3. 以 YAML 文件形式定义 PDB。
  4. 通过 YAML 文件创建 PDB 对象。

确定要保护的应用

用户想要保护通过内置的 Kubernetes 控制器指定的应用,这是最常见的使用场景:

  • Deployment
  • ReplicationController
  • ReplicaSet
  • StatefulSet

在这种情况下,在控制器的 .spec.selector 字段中做记录,并在 PDB 的 .spec.selector 字段中加入同样的选择算符。

从 1.15 版本开始,PDB 支持启用 Scale 子资源 的自定义控制器。

用户也可以用 PDB 来保护不受上述控制器控制的 Pod,或任意的 Pod 集合,但是正如 任意工作负载和任意选择算符中描述的,这里存在一些限制。

考虑应用对干扰的反应

确定在自发干扰时,多少实例可以在短时间内同时关闭。

  • 无状态的前端:
    • 关注:不能降低服务能力 10% 以上。
      • 解决方案:例如,使用 PDB,指定其 minAvailable 值为 90%。
  • 单实例有状态应用:
    • 关注:不要在不通知的情况下终止该应用。
      • 可能的解决方案 1:不使用 PDB,并忍受偶尔的停机。
      • 可能的解决方案 2:设置 maxUnavailable=0 的 PDB。 意为(Kubernetes 范畴之外的)集群操作人员需要在终止应用前与用户协商, 协商后准备停机,然后删除 PDB 表示准备接受干扰,后续再重新创建。
  • 多实例有状态应用,如 Consul、ZooKeeper 或 etcd:
    • 关注:不要将实例数量减少至低于仲裁规模,否则将出现写入失败。
      • 可能的解决方案 1:设置 maxUnavailable 值为 1 (适用于不同规模的应用)。
      • 可能的解决方案 2:设置 minAvailable 值为仲裁规模(例如规模为 5 时设置为 3)。 (允许同时出现更多的干扰)。
  • 可重新启动的批处理任务:
    • 关注:自发干扰的情况下,需要确保任务完成。
      • 可能的解决方案:不创建 PDB。任务控制器会创建一个替换 Pod。

指定百分比时的舍入逻辑

minAvailablemaxUnavailable 的值可以表示为整数或百分比。

  • 指定整数值时,它表示 Pod 个数。例如,如果将 minAvailable 设置为 10, 那么即使在干扰期间,也必须始终有 10 个 Pod 可用。
  • 通过将值设置为百分比的字符串表示形式(例如 "50%")来指定百分比时,它表示占总 Pod 数的百分比。 例如,如果将 minAvailable 设置为 "50%",则干扰期间至少 50% 的 Pod 保持可用。

如果将值指定为百分比,则可能无法映射到确切数量的 Pod。例如,如果你有 7 个 Pod, 并且你将 minAvailable 设置为 "50%",具体是 3 个 Pod 或 4 个 Pod 必须可用并非显而易见。 Kubernetes 采用向上取整到最接近的整数的办法,因此在这种情况下,必须有 4 个 Pod。 当你将 maxUnavailable 值指定为一个百分比时,Kubernetes 将可以干扰的 Pod 个数向上取整。 因此干扰可以超过你定义的 maxUnavailable 百分比。 你可以检查控制此行为的代码

指定 PodDisruptionBudget

一个 PodDisruptionBudget 有 3 个字段:

  • 标签选择算符 .spec.selector 用于指定其所作用的 Pod 集合,该字段为必需字段。
  • .spec.minAvailable 表示驱逐后仍须保证可用的 Pod 数量。即使因此影响到 Pod 驱逐 (即该条件在和 Pod 驱逐发生冲突时优先保证)。 minAvailable 值可以是绝对值,也可以是百分比。
  • .spec.maxUnavailable (Kubernetes 1.7 及更高的版本中可用)表示驱逐后允许不可用的 Pod 的最大数量。其值可以是绝对值或是百分比。

用户在同一个 PodDisruptionBudget 中只能够指定 maxUnavailableminAvailable 中的一个。 maxUnavailable 只能够用于控制存在相应控制器的 Pod 的驱逐(即不受控制器控制的 Pod 不在 maxUnavailable 控制范围内)。在下面的示例中, “所需副本”指的是相应控制器的 scale,控制器对 PodDisruptionBudget 所选择的 Pod 进行管理。

示例 1:设置 minAvailable 值为 5 的情况下,驱逐时需保证 PodDisruptionBudget 的 selector 选中的 Pod 中 5 个或 5 个以上处于健康状态。

示例 2:设置 minAvailable 值为 30% 的情况下,驱逐时需保证 Pod 所需副本的至少 30% 处于健康状态。

示例 3:设置 maxUnavailable 值为 5 的情况下,驱逐时需保证所需副本中最多 5 个处于不可用状态。

示例 4:设置 maxUnavailable 值为 30% 的情况下,只要不健康的副本数量不超过所需副本总数的 30% (取整到最接近的整数),就允许驱逐。如果所需副本的总数仅为一个,则仍允许该单个副本中断, 从而导致不可用性实际达到 100%。

在典型用法中,干扰预算会被用于一个控制器管理的一组 Pod 中 —— 例如:一个 ReplicaSet 或 StatefulSet 中的 Pod。

如果你将 maxUnavailable 的值设置为 0%(或 0)或设置 minAvailable 值为 100%(或等于副本数) 则会阻止所有的自愿驱逐。 当你为 ReplicaSet 等工作负载对象设置阻止自愿驱逐时,你将无法成功地腾空运行其中一个 Pod 的节点。 如果你尝试腾空正在运行着被阻止驱逐的 Pod 的节点,则腾空永远不会完成。 按照 PodDisruptionBudget 的语义,这是允许的。

用户可以在下面看到 Pod 干扰预算定义的示例,它们与带有 app: zookeeper 标签的 Pod 相匹配:

使用 minAvailable 的 PDB 示例:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: zk-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: zookeeper

使用 maxUnavailable 的 PDB 示例:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: zk-pdb
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: zookeeper

例如,如果上述 zk-pdb 选择的是一个规格为 3 的 StatefulSet 对应的 Pod, 那么上面两种规范的含义完全相同。 推荐使用 maxUnavailable,因为它自动响应控制器副本数量的变化。

创建 PDB 对象

你可以使用 kubectl 创建或更新 PDB 对象。

kubectl apply -f mypdb.yaml

检查 PDB 的状态

使用 kubectl 来确认 PDB 被创建。

假设用户的名字空间下没有匹配 app: zookeeper 的 Pod,用户会看到类似下面的信息:

kubectl get poddisruptionbudgets
NAME     MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
zk-pdb   2               N/A               0                     7s

假设有匹配的 Pod(比如说 3 个),那么用户会看到类似下面的信息:

kubectl get poddisruptionbudgets
NAME     MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
zk-pdb   2               N/A               1                     7s

ALLOWED DISRUPTIONS 值非 0 意味着干扰控制器已经感知到相应的 Pod,对匹配的 Pod 进行统计, 并更新了 PDB 的状态。

用户可以通过以下命令获取更多 PDB 状态相关信息:

kubectl get poddisruptionbudgets zk-pdb -o yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  annotations:

  creationTimestamp: "2020-03-04T04:22:56Z"
  generation: 1
  name: zk-pdb

status:
  currentHealthy: 3
  desiredHealthy: 2
  disruptionsAllowed: 1
  expectedPods: 3
  observedGeneration: 1

Pod 的健康

如果 Pod 的 .status.conditions 中包含 type="Ready"status="True" 的项, 则当前实现将其视为健康的 Pod。这些 Pod 通过 PDB 状态中的 .status.currentHealthy 字段被跟踪。

不健康的 Pod 驱逐策略

特性状态: Kubernetes v1.26 [beta]

守护应用程序的 PodDisruptionBudget 通过不允许驱逐健康的 Pod 来确保 .status.currentHealthy 的 Pod 数量不低于 .status.desiredHealthy 中指定的数量。通过使用 .spec.unhealthyPodEvictionPolicy, 你还可以定义条件来判定何时应考虑驱逐不健康的 Pod。未指定策略时的默认行为对应于 IfHealthyBudget 策略。

策略包含:

IfHealthyBudget
对于运行中但还不健康的 Pod(.status.phase="Running"),只有所守护的应用程序不受干扰 (.status.currentHealthy 至少等于 .status.desiredHealthy)时才能被驱逐。

此策略确保已受干扰的应用程序所运行的 Pod 会尽可能成为健康。 这对腾空节点有负面影响,可能会因 PDB 守护的应用程序行为错误而阻止腾空。 更具体地说,这些应用程序的 Pod 处于 CrashLoopBackOff 状态 (由于漏洞或错误配置)或其 Pod 只是未能报告 Ready 状况。

AlwaysAllow
运行中但还不健康的 Pod(.status.phase="Running")将被视为已受干扰且可以被驱逐, 与是否满足 PDB 中的判决条件无关。

这意味着受干扰的应用程序所运行的 Pod 可能没有机会恢复健康。 通过使用此策略,集群管理器可以轻松驱逐由 PDB 所守护的行为错误的应用程序。 更具体地说,这些应用程序的 Pod 处于 CrashLoopBackOff 状态 (由于漏洞或错误配置)或其 Pod 只是未能报告 Ready 状况。

任意工作负载和任意选择算符

如果你只针对内置的工作负载资源(Deployment、ReplicaSet、StatefulSet 和 ReplicationController) 或在实现了 scale 子资源自定义资源使用 PDB, 并且 PDB 选择算符与 Pod 所属资源的选择算符完全匹配,那么可以跳过这一节。

你可以针对由其他资源、某个 "operator" 控制的或者“裸的(不受控制器控制)” Pod 使用 PDB,但存在以下限制:

  • 只能够使用 .spec.minAvailable,而不能够使用 .spec.maxUnavailable
  • 只能够使用整数作为 .spec.minAvailable 的值,而不能使用百分比。

你无法使用其他的可用性配置,因为如果没有被支持的属主资源,Kubernetes 无法推导出 Pod 的总数。

你可以使用能够选择属于工作负载资源的 Pod 的子集或超集的选择算符。 驱逐 API 将不允许驱逐被多个 PDB 覆盖的任何 Pod,因此大多数用户都希望避免重叠的选择算符。 重叠 PDB 的一种合理用途是将 Pod 从一个 PDB 转交到另一个 PDB 的场合。

10 - 从 Pod 中访问 Kubernetes API

本指南演示了如何从 Pod 中访问 Kubernetes API。

准备开始

你必须拥有一个 Kubernetes 的集群,同时你必须配置 kubectl 命令行工具与你的集群通信。 建议在至少有两个不作为控制平面主机的节点的集群上运行本教程。 如果你还没有集群,你可以通过 Minikube 构建一个你自己的集群,或者你可以使用下面的 Kubernetes 练习环境之一:

从 Pod 中访问 API

从 Pod 内部访问 API 时,定位 API 服务器和向服务器认证身份的操作与外部客户端场景不同。

从 Pod 使用 Kubernetes API 的最简单的方法就是使用官方的 客户端库。 这些库可以自动发现 API 服务器并进行身份验证。

使用官方客户端库

从一个 Pod 内部连接到 Kubernetes API 的推荐方式为:

在以上场景中,客户端库都使用 Pod 的服务账号凭据来与 API 服务器安全地通信。

直接访问 REST API

在运行在 Pod 中时,你的容器可以通过获取 KUBERNETES_SERVICE_HOSTKUBERNETES_SERVICE_PORT_HTTPS 环境变量为 Kubernetes API 服务器生成一个 HTTPS URL。 API 服务器的集群内地址也发布到 default 命名空间中名为 kubernetes 的 Service 中, 从而 Pod 可以引用 kubernetes.default.svc 作为本地 API 服务器的 DNS 名称。

向 API 服务器进行身份认证的推荐做法是使用 服务账号凭据。 默认情况下,每个 Pod 与一个服务账号关联,该服务账号的凭据(令牌)放置在此 Pod 中每个容器的文件系统树中的 /var/run/secrets/kubernetes.io/serviceaccount/token 处。

如果证书包可用,则凭据包被放入每个容器的文件系统树中的 /var/run/secrets/kubernetes.io/serviceaccount/ca.crt 处, 且将被用于验证 API 服务器的服务证书。

最后,用于命名空间域 API 操作的默认命名空间放置在每个容器中的 /var/run/secrets/kubernetes.io/serviceaccount/namespace 文件中。

使用 kubectl proxy

如果你希望不使用官方客户端库就完成 API 查询,可以将 kubectl proxy 作为 command 在 Pod 中启动一个边车(Sidecar)容器。这样,kubectl proxy 自动完成对 API 的身份认证,并将其暴露到 Pod 的 localhost 接口,从而 Pod 中的其他容器可以直接使用 API。

不使用代理

通过将认证令牌直接发送到 API 服务器,也可以避免运行 kubectl proxy 命令。 内部的证书机制能够为连接提供保护。

# 指向内部 API 服务器的主机名
APISERVER=https://kubernetes.default.svc

# 服务账号令牌的路径
SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount

# 读取 Pod 的名字空间
NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace)

# 读取服务账号的持有者令牌
TOKEN=$(cat ${SERVICEACCOUNT}/token)

# 引用内部证书机构(CA)
CACERT=${SERVICEACCOUNT}/ca.crt

# 使用令牌访问 API
curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api

输出类似于:

{
  "kind": "APIVersions",
  "versions": ["v1"],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "10.0.1.149:443"
    }
  ]
}