自建Kubernetes集群实现节点自动伸缩

重要

本文中含有需要您注意的重要提示信息,忽略该信息可能对您的业务造成影响,请务必仔细阅读。

如果您是自建的Kubernetes集群,且期望根据实际工作负载动态调整工作节点数量,确保资源的有效利用并维持服务的稳定性,可通过Cluster Autoscaler和阿里云弹性伸缩实现。

重要

本文采用手动部署Cluster Autocaler的方式实现K8s节点弹性伸缩。除了此方式之外,您可以将您的自建Kubernetes集群接入到ACK One注册集群中,通过ACK One的自动弹性伸缩能力实现节点的自动伸缩。您可以参考以下步骤实现此功能。

  1. 将自建Kubernetes接入到ACK One

  2. 为ACK One创建自动伸缩节点池

更多ACK One的信息,请参见ACK One概述

工作原理

Cluster AutoScaler(简称CA)是一个自动扩展和收缩Kubernetes集群节点的组件。CA会定期检测是否有因资源不足而处于Pending状态的Pod,如果有,会驱动伸缩组进行扩容,其工作原理如下图所示:

image

CA在监测到某些节点资源使用率持续低于预设的阈值,且这些节点上的Pod能够迁移到其他节点时,会先将Pod驱逐到其他节点,之后驱动伸缩组进行缩容,其工作原理如下图所示:

image

更多Cluster AutoScaler的信息,请参见Cluster Autoscaling官方介绍

准备工作

在操作前,请确保您已经完成以下工作。

  • 已自建Kubernetes集群,且集群版本在v1.9.3及以上。

    重要

    本文档基于在阿里云ECS上搭建的K8s集群进行测试,如果涉及云下IDC机器、跨云供应商等混合云场景,建议参考VPN网关或者智能接入网关等产品解决网络连通性问题。

  • 创建RAM用户。

    当CA需要访问阿里云ESS时,必须先通过访问凭证来验证身份信息和访问权限。您需要为CA创建RAM用户并授予访问ESS的权限。

    1. 创建一个RAM用户,并开启OpenAPI访问控制。具体操作,请参见创建RAM用户

    2. 为RAM用户授权以下自定义权限策略。如何为RAM用户授权,请参见为RAM用户授权

      {
        "Version": "1",
        "Statement": [
          {
            "Action": [
              "ess:Describe*",
              "ess:CreateScalingRule",
              "ess:ModifyScalingGroup",
              "ess:RemoveInstances",
              "ess:ExecuteScalingRule",
              "ess:ModifyScalingRule",
              "ess:DeleteScalingRule",
              "ess:DetachInstances",
              "ecs:DescribeInstanceTypes"
            ],
            "Resource": [
              "*"
            ],
            "Effect": "Allow"
          }
        ]
      }
    3. 创建AccessKey并保存AccessKey ID和AccessKey Secret,在后续步骤中会使用。如何创建AccessKey,请参见创建AccessKey

操作步骤

(可选)步骤一:构建Cluster AutoScaler镜像

通过源码构建自己的Cluster AutoScaler镜像,该镜像用于在您的K8s集群部署Cluster AutoScaler。

重要

您可以直接跳过此步骤,直接使用阿里云已构建好的cluster-autoscaler镜像:ess-cluster-autoscaler-registry.cn-hangzhou.cr.aliyuncs.com/ess-cluster-autoscaler/cluster-autoscaler:v1.7

  1. 从Github下载源码。

    mkdir -p $GOPATH/src/github.com/kubernetes
    cd $GOPATH/src/github.com/kubernetes
    git clone https://github.com/kubernetes/autoscaler.git
    cd autoscaler
  2. 构建镜像。

    # 编译
    cd cluster-autoscaler && make build-arch-amd64
    # 构建镜像
    docker build -t cluster-autoscaler:v1.0 -f Dockerfile.amd64 .
    # 打Tag
    docker tag cluster-autoscaler:v1.0 您的镜像仓库域名/cluster-autoscaler:v1.0
    # 上传镜像
    docker push 您的镜像仓库域名/cluster-autoscaler:v1.0

步骤二:创建并配置伸缩组

  1. 创建伸缩组。

    1. 登录阿里云弹性伸缩控制台

    2. 在顶部菜单栏选择可用区,在左侧点击伸缩组管理,点击创建伸缩组

    3. 通过表单创建页签下,完成伸缩组配置,然后点击创建按钮。本示例采用以下配置,更多关于伸缩组的配置说明,请参见创建伸缩组

      配置项

      说明

      示例

      伸缩组名称

      输入伸缩组名称,格式参照界面提示。

      K8s-Node-Scaling-Group

      伸缩组类型

      选择ECS,表示伸缩组内的实例类型为ECS实例。

      ECS

      组内实例配置信息来源

      先不指定自动创建实例的模板。伸缩组创建完成后,您需要继续创建伸缩配置。

      从零开始创建

      组内最小实例数

      代表伸缩组最少有0台ECS实例。

      0

      组内最大实例数

      代表伸缩组最大有5台ECS实例。

      5

      专有网络

      该伸缩组下创建的ECS实例会在此专有网络下。

      vpc-test****-001

      选择交换机

      您可以配置多个可用区的交换机以提高扩容成功率。

      vsw-test****

      重要

      在伸缩组创建完成后,请记录您的可用区伸缩组ID以供后续步骤使用。

  2. 为伸缩组创建伸缩配置。

    1. 找到刚刚创建的伸缩组,点击查看详情进入伸缩组详情页。

    2. 实例配置来源页签下,点击伸缩配置,点击创建伸缩配置按钮进入创建伸缩配置页。

    3. 本实例采用以下配置,更多关于创建伸缩配置的说明,请参见创建伸缩配置(ECS实例)

      配置项

      说明

      示例

      伸缩配置名称

      输入伸缩配置名称,格式参考界面提示。

      K8s-Scaling-Node-Config

      付费模式

      可以根据您的需求选择。

      按量付费

      实例配置方式

      可以根据您的需求选择。

      指定实例规格

      选择实例规格

      可以根据您的需求选择。

      警告

      该功能支持的实例规格如下:

      • 企业级x86计算规格族群。

      • 企业级异构计算规格族群。

      • 高性能计算实例规格族群。

      • 弹性裸金属服务器规格族群。

      暂不支持企业级ARM计算规格族群。关于实例规格族的说明,请参见:实例规格族

      ecs.g6a.large

      选择镜像

      根据您的需求选择合适的镜像。

      Alibaba Cloud Linux

    4. 配置网络和安全组

      • 安全组:选择安全组请确保该安全组可以连接到您Kubernetes集群所在网络。

      • 分配公网IPv4地址:如果您的Kubernetes集群的API Server地址为公网IP,则需要勾选,为实例配置公网访问能力。

        警告

        如果您的Kubernetes集群的API Server地址为公网IP,请确保您的Kubernetes集群的API Server已放开6443端口。

    5. 配置高级设置 > 实例自定义数据,请在实例自定义数据中填入以下脚本,用于初始化Kubernetes Worker节点环境并将Worker节点加入Kubernetes集群。

      重要

      将<<YOUR_MASTER_NODE_IP>>替换为您的Kubernetes的主节点IP。

      #!/bin/bash
      
      #关闭防火墙
      systemctl stop firewalld
      
      systemctl disable firewalld
      
      #关闭selinux
      sed -i 's/enforcing/disabled/' /etc/selinux/config  # 永久
      setenforce 0  # 临时
      
      #关闭swap
      swapoff -a  # 临时
      sed -ri 's/.swap./#&/' /etc/fstab    # 永久
      
      #将桥接的IPv4流量传递到iptables的链
      cat > /etc/sysctl.d/k8s.conf << EOF
      net.bridge.bridge-nf-call-ip6tables = 1
      net.bridge.bridge-nf-call-iptables = 1
      EOF
      sysctl --system  # 生效
      
      
      #增加Kubernetes 源
      cat <<EOF > /etc/yum.repos.d/kubernetes.repo
      [kubernetes]
      name=Kubernetes
      baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
      enabled=1
      gpgcheck=1
      repo_gpgcheck=1
      gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
      EOF
      
      
      #通用安装包
      yum install vim bash-completion net-tools gcc -y
      
      #安装Docker
      wget https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo -O /etc/yum.repos.d/docker-ce.repo
      yum -y install docker-ce
      
      systemctl enable docker && systemctl start docker
      
      cat > /etc/docker/daemon.json << EOF
      {
        "exec-opts": ["native.cgroupdriver=systemd"]
      }
      EOF
      
      systemctl restart docker
      
      
      # 安装kubeadm、kubectl、kubelet
      yum install -y kubelet-1.23.0 kubeadm-1.23.0 kubectl-1.23.0
      
      # 启动kubelet服务
      systemctl enable kubelet && systemctl start kubelet
      
      #如果kubelet起不来,通过这个命名排查:journalctl -xeu kubelet
      
      #Worker节点加入集群
      regionId=$(sed -n 's/.*"region-id": "\(.*\)".*/\1/p' /run/cloud-init/instance-data.json)
      instanceId=$(sed -n 's/.*"instance_id": "\(.*\)".*/\1/p' /run/cloud-init/instance-data.json)
      privateIpv4=$(sed -n 's/.*"private-ipv4": "\(.*\)".*/\1/p' /run/cloud-init/instance-data.json)
      
      cat > kubeadm-config.yaml << EOF
      apiVersion: kubeadm.k8s.io/v1beta2
      kind: JoinConfiguration
      discovery:
        bootstrapToken:
          token: "your-bootstrap-token"
          apiServerEndpoint: "<<YOUR_MASTER_NODE_IP>>:6443"
          caCertHashes:
          - "sha256:your-discovery-token-ca-cert-hash"
      nodeRegistration:
        name: "$regionId-$privateIpv4"
        kubeletExtraArgs:
          provider-id: "$regionId.$instanceId"
      
      EOF
      
      kubeadm join --config=kubeadm-config.yaml
      说明

      需要在扩容时为worker节点指定--provider-id,文中的脚本已实现此功能。

    6. 点击创建,并确保伸缩配置已生效。

  3. (可选)验证伸缩组扩容实例是否可以正常加入K8s集群。

    您可以通过手动修改伸缩组最小实例数为1来扩容一台ECS实例,并观察扩容出来的ECS实例是否已经初始化并正常加入您的K8s集群。

步骤三:在K8s集群部署Cluster AutoScaler组件

  1. 将准备工作的RAM用户的AccessKey ID和AccessKey Secret作Base64转换。

    echo $AccessKey-ID | tr -d '\n' | base64
    echo $AccessKey-Secret | tr -d '\n' | base64 
    echo $RegionId | tr -d '\n' | base64
  2. 新建deploy-ca.yaml,内容如下,修改其中的相关字段信息后,部署到您K8s集群的kube-system命名空间。

    重要

    更新Secret的access-key-id、access-key-secret、region-id,以及在Deployment的容器启动命令中,更新您的ESS伸缩组ID,具体操作如下:

    • 将<<YOUR_ACCESS_KEY_ID>>替换为Base64转换后的AccessKey ID。

    • 将<<YOUR_ACCESS_KEY_SECRET>>替换为Base64转换后的AccessKey Secret。

    • 将<<YOUR_REGION_ID>>替换为Base64转换后的RegionID,RegionID获取请参见地域

    • 将<<YOUR_ESS_SCALING_GROUP_ID>>替换为您刚刚创建的伸缩组ID

    • 讲<<KUBERNETES_SERVICE_HOST>>替换为您的K8s集群ApiServer地址。

    ---
    apiVersion: v1
    kind: Secret
    metadata:
      name: cloud-config
    type: Opaque
    data:
      access-key-id: <<YOUR_ACCESS_KEY_ID>>
      access-key-secret: <<YOUR_ACCESS_KEY_SECRET>>
      region-id: <<YOUR_REGION_ID>>
    
    ---
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      labels:
        k8s-addon: cluster-autoscaler.addons.k8s.io
        k8s-app: cluster-autoscaler
      name: cluster-autoscaler
      namespace: kube-system
    
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      name: cluster-autoscaler
      labels:
        k8s-addon: cluster-autoscaler.addons.k8s.io
        k8s-app: cluster-autoscaler
    rules:
    - apiGroups: [""]
      resources: ["events","endpoints"]
      verbs: ["create", "patch"]
    - apiGroups: [""]
      resources: ["pods/eviction"]
      verbs: ["create"]
    - apiGroups: [""]
      resources: ["pods/status"]
      verbs: ["update"]
    - apiGroups: [""]
      resources: ["endpoints"]
      resourceNames: ["cluster-autoscaler"]
      verbs: ["get","update"]
    - apiGroups: [""]
      resources: ["nodes"]
      verbs: ["watch","list","get","update"]
    - apiGroups: [""]
      resources: ["namespaces","pods","services","replicationcontrollers","persistentvolumeclaims","persistentvolumes"]
      verbs: ["watch","list","get"]
    - apiGroups: ["extensions"]
      resources: ["replicasets","daemonsets"]
      verbs: ["watch","list","get"]
    - apiGroups: ["policy"]
      resources: ["poddisruptionbudgets"]
      verbs: ["watch","list"]
    - apiGroups: ["apps"]
      resources: ["statefulsets", "replicasets", "daemonsets"]
      verbs: ["watch","list","get"]
    - apiGroups: ["batch"]
      resources: ["jobs"]
      verbs: ["watch","list","get"]
    - apiGroups: ["storage.k8s.io"]
      resources: ["storageclasses", "csinodes", "csidrivers", "csistoragecapacities"]
      verbs: ["watch","list","get"]
    
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: cluster-autoscaler
      namespace: kube-system
      labels:
        k8s-addon: cluster-autoscaler.addons.k8s.io
        k8s-app: cluster-autoscaler
    rules:
    - apiGroups: [""]
      resources: ["configmaps"]
      verbs: ["create","list","watch"]
    - apiGroups: [""]
      resources: ["configmaps"]
      resourceNames: ["cluster-autoscaler-status", "cluster-autoscaler-priority-expander"]
      verbs: ["delete","get","update","watch"]
    
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: cluster-autoscaler
      labels:
        k8s-addon: cluster-autoscaler.addons.k8s.io
        k8s-app: cluster-autoscaler
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: cluster-autoscaler
    subjects:
      - kind: ServiceAccount
        name: cluster-autoscaler
        namespace: kube-system
    
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: cluster-autoscaler
      namespace: kube-system
      labels:
        k8s-addon: cluster-autoscaler.addons.k8s.io
        k8s-app: cluster-autoscaler
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: Role
      name: cluster-autoscaler
    subjects:
      - kind: ServiceAccount
        name: cluster-autoscaler
        namespace: kube-system
    
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      labels:
        app: cluster-autoscaler
      name: cluster-autoscaler
      namespace: kube-system
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: cluster-autoscaler
      template:
        metadata:
          labels:
            app: cluster-autoscaler
        spec:
          dnsPolicy: "None"
          dnsConfig:
            nameservers:
              - 100.100.2.136
              - 100.100.2.138
            options:
              - name: timeout
                value: "1"
              - name: attempts
                value: "3"
          priorityClassName: system-cluster-critical
          serviceAccountName: cluster-autoscaler
          containers:
            - command:
                - ./cluster-autoscaler
                - '--v=2'
                - '--logtostderr=true'
                - '--stderrthreshold=info'
                - '--cloud-provider=alicloud'
                - '--expander=least-waste'
                - '--scan-interval=60s'
                - '--scale-down-enabled=true'
                - '--scale-down-delay-after-add=10m'
                - '--scale-down-delay-after-failure=1m'
                - '--scale-down-unready-time=2m'
                - '--ok-total-unready-count=1000'
                - '--max-empty-bulk-delete=50'
                - '--leader-elect=false'
                - '--max-node-provision-time=5m'
                - '--scale-up-from-zero=true'
                - '--daemonset-eviction-for-empty-nodes=false'
                - '--daemonset-eviction-for-occupied-nodes=false'
                - '--max-graceful-termination-sec=14400'
                - '--skip-nodes-with-system-pods=true'
                - '--skip-nodes-with-local-storage=false'
                - '--min-replica-count=0'
                - '--scale-down-unneeded-time=10m'
                - '--scale-down-utilization-threshold=0.3'
                - '--scale-down-gpu-utilization-threshold=0.3'
                - '--nodes=0:100:<<YOUR_ESS_SCALING_GROUP_ID>>'
              image: >-
                ess-cluster-autoscaler-registry.cn-hangzhou.cr.aliyuncs.com/ess-cluster-autoscaler/cluster-autoscaler:v1.7
              imagePullPolicy: Always
              name: cluster-autoscaler
              resources:
                requests:
                  cpu: 100m
                  memory: 300Mi
              securityContext:
                allowPrivilegeEscalation: true
                capabilities:
                  add:
                    - SYS_ADMIN
                  drop:
                    - ALL
              env:
              - name: ACCESS_KEY_ID
                valueFrom:
                  secretKeyRef:
                    name: cloud-config
                    key: access-key-id
              - name: ACCESS_KEY_SECRET
                valueFrom:
                  secretKeyRef:
                    name: cloud-config
                    key: access-key-secret
              - name: REGION_ID
                valueFrom:
                  secretKeyRef:
                    name: cloud-config
                    key: region-id
              - name: KUBERNETES_SERVICE_HOST
                value: "<<KUBERNETES_SERVICE_HOST>>"
              - name: KUBERNETES_SERVICE_PORT
                value: "6443"
              - name: KUBERNETES_SERVICE_PORT_HTTPS
                value: "6443"
    说明
    • 通过参数--scale-down-enabled可以控制是否开启缩容。如果开启缩容,CA定期会检测集群状态,判断当前集群状态下,哪些节点资源利用率小于50%(通过参数--scale-down-utilization-threshold控制)。

    • CA默认不会终止kube-system命名空间的Pods,可以通过指定--skip-nodes-with-system-pods=false来覆盖此默认设置。

    • CA的缩容操作默认会等待10分钟,可以通过指定--scale-down-delay来修改等待时长,例如--scale-down-delay=5m

    • 如果运行在多个伸缩组上,--expander参数支持3种选项:randommost-podsleast-waste

      • random:扩容时随机选择一个伸缩组。

      • most-pods:在拥有最多Pod的伸缩组上扩容。

      • least-waste:在浪费最少CPU/内存的伸缩组上扩容。如果多个伸缩组判定一致,会回退到随机模式。

    通过以下命令部署CA到K8s集群。

    kubectl apply -f deploy-ca.yaml -n kube-system

功能验证(可选)

当集群中有因为资源不足而产生Pending状态的Pod时,CA会驱动伸缩组扩容节点,当一个节点资源使用率持续低于预设的阈值时,CA会驱动伸缩组缩容节点。

  1. 部署一个简单的nginx-demo.yaml,来验证自动扩容功能,yaml文件内容如下:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx-demo
    spec:
      selector:
        matchLabels:
          app: nginx-demo
      replicas: 2
      template:
        metadata:
          labels:
            app: nginx-demo
        spec:
          containers:
            - name: nginx
              image: ess-cluster-autoscaler-registry.cn-hangzhou.cr.aliyuncs.com/ess-cluster-autoscaler/nginx-demo:v1.0
              ports:
                - containerPort: 80
                  name: http
                - containerPort: 443
                  name: https
              resources:
                requests:
                  memory: 1Gi
                  cpu: 1
                limits:
                  memory: 1Gi
                  cpu: '1'

    使用以下命令部署nginx-demo.yaml:

    kubectl apply -f nginx-demo.yaml
  2. 根据集群当前的Node资源空闲情况,通过增加replicas数量来产生因资源不足而Pending的Pod。使用以下命令增加replicas数量:

    kubectl scale deployment nginx-demo --replicas=5
  3. 等待1分钟左右,观察伸缩组是否发生扩容。

  4. 伸缩组实例扩容完成后,等待3分钟,观察新节点是否加入集群中,使用以下命令查看集群所有Node,观察是否有新的Node节点加入K8s集群:

    kubectl get nodes
说明

验证缩容时,您可以通过减少nginx-demo的副本数量来降低节点的使用率使其低于阈值,并观察伸缩组是否发缩容活动来判断缩容是否成功。