实现多可用区同时快速弹性扩容

多可用区均衡部署分布式系统实现高可用架构的核心策略之一。业务负载增大时,多可用区均衡调度策略能够自动扩容出多个可用区的实例来满足集群的调度水位。

前提条件

已在待弹出实例的可用区创建至少一个交换机,请参见创建和管理交换机。创建后,您可以在创建和管理节点池时选择对应的vSwitch。

功能介绍

节点自动伸缩组件可通过预调度判断服务能否部署在某伸缩组上,并将扩容请求发送至指定ESS伸缩组完成实例的生成。但当前在单伸缩组配置多可用区vSwitch的机制存在一个问题:当多可用区业务Pod因集群资源不足无法调度时,ACK虽会触发伸缩组扩容,但由于缺乏可用区与实例的关联信息传递机制,ESS弹性伸缩组无法识别具体需要扩容的可用区。这可能导致扩容实例集中在单个可用区,而非多可用区均衡扩展,无法满足业务对跨可用区同时扩容的需求。

image

为此,ACK引入了ack-autoscaling-placeholder组件,通过少量的资源冗余方式,将多可用区的弹性伸缩转变为并发节点池的定向伸缩,详情请参见基于ack-autoscaling-placeholder实现容器秒级伸缩。原理如下。

  1. 为每个可用区创建一个节点池,并分别在各个节点池打上可用区的标签。

  2. 通过配置可用区标签nodeSelector的方式,使用ack-autoscaling-placeholder为每个可用区创建占位Pod。默认的占位Pod具有比较低权重的PriorityClass,优先级低于应用Pod。

  3. 业务应用Pod Pending后会抢占各个可用区占位Pod,带有可用区nodeSelector的多可用区占位Pod处于Pending后,节点自动伸缩组件感知到的调度策略就从多个可用区的antiAffinity变成了可用区的nodeSelector,从而处理发出扩容区域的节点的请求。

以下以两个可用区为例,介绍如何基于现有架构满足多可用区的同时扩容。

image
  1. ack-autoscaling-placeholder作为业务应用和节点自动伸缩组件之间的桥梁,为每个可用区创建占位Pod。占位Pod的调度优先级低于实际业务应用的调度优先级。

  2. 应用Pod Pending后会迅速抢占占位Pod,并部署在各个可用区的已有节点上,同时,被抢占的占位Pod会处于Pending状态。

  3. 占位Pod是带有可用区nodeSelector调度策略的,节点自动伸缩组件可以并发扩容到对应的可用区。

步骤一:为可用区创建节点池并配置自定义节点标签

  1. 登录容器服务管理控制台,在左侧导航栏选择集群列表

  2. 集群列表页面,单击目标集群名称,然后在左侧导航栏,选择节点管理 > 节点池

  3. 单击创建节点池,按照页面提示完成节点池的配置。

    本示例以在可用区I创建开启自动伸缩的节点池auto-zone-I为例。以下仅介绍核心配置项,详情请参见创建和管理节点池

    配置项

    说明

    节点池名称

    auto-zone-I

    扩缩容模式

    选择自动,开启自动弹性伸缩。

    交换机

    选择可用区I的交换机。

    节点标签

    设置节点标签的available_zonei

    节点池列表中,当auto-zone-I节点池状态为已激活时,表示节点池创建成功。

  4. 重复以上步骤,在每个需要扩容的可用区创建开启自动伸缩的节点池。

    节点池多可用区.png

步骤二:部署Placeholder及占位Deployment

  1. 在控制台左侧导航栏,选择市场 > 应用市场

  2. 搜索ack-autoscaling-placeholder,单击组件卡片,然后单击一键部署

  3. 选择集群命名空间,然后单击下一步,选择Chart版本,编辑参数,然后单击确定

    创建成功后,在应用 > Helm页面,可查看到该应用状态为已部署

  4. 在集群管理页左侧导航栏,选择应用 > Helm

  5. Helm页面,单击ack-autoscaling-placeholder-default操作列的更新

  6. 更新发布面板中,参见下方示例更新YAML,然后单击确定。将每个可用区都设置Placeholder,每个区域需要定义一个占位Deployment。

    本示例以在可用区I、K、H创建占位Deployment为例。

    deployments:
    - affinity: {}
      annotations: {}
      containers:
      - image: registry-vpc.cn-beijing.aliyuncs.com/acs/pause:3.1
        imagePullPolicy: IfNotPresent
        name: placeholder
        resources:
          requests:
            cpu: 3500m     # 调度单元CPU。
            memory: 6      # 调度单元内存。
      imagePullSecrets: {}
      labels: {}
      name: ack-place-holder-I             # 占位Deployment名称。
      nodeSelector: {"avaliable_zone":i}   # 可用区标签(需要与步骤一创建节点池中配置的标签一致)。
      replicaCount: 10                     # 每次扩容可快速弹出Pod数量。
      tolerations: []
    - affinity: {}
      annotations: {}
      containers:
      - image: registry-vpc.cn-beijing.aliyuncs.com/acs/pause:3.1
        imagePullPolicy: IfNotPresent
        name: placeholder
        resources:
          requests:
            cpu: 3500m    # 调度单元CPU。
            memory: 6     # 调度单元内存。
      imagePullSecrets: {}
      labels: {}
      name: ack-place-holder-K            # 占位Deployment名称。
      nodeSelector: {"avaliable_zone":k}  # 可用区标签(需要与步骤一创建节点池中配置的标签一致)。
      replicaCount: 10                    # 每次扩容可快速弹出Pod数量。
      tolerations: []
    - affinity: {}
      annotations: {}
      containers:
      - image: registry-vpc.cn-beijing.aliyuncs.com/acs/pause:3.1
        imagePullPolicy: IfNotPresent
        name: placeholder
        resources:
          requests:
            cpu: 3500m   # 调度单元CPU。
            memory: 6    # 调度单元内存。
      imagePullSecrets: {}
      labels: {}
      name: ack-place-holder-H           # 占位Deployment名称。
      nodeSelector: {"avaliable_zone":h} # 可用区标签(需要与步骤一创建节点池中配置的标签一致)。
      replicaCount: 10                   # 每次扩容可快速弹出Pod数量。
      tolerations: []
    fullnameOverride: ""
    nameOverride: ""
    podSecurityContext: {}
    priorityClassDefault:
      enabled: true
      name: default-priority-class
      value: -1

    更新成功后,各个可用区将创建对应的占位Deployment。创建占位Deployment.png

步骤三:创建实际负载的PriorityClass

  1. 使用以下YAML示例,创建名为priorityClass.yaml的文件。

    apiVersion: scheduling.k8s.io/v1
    kind: PriorityClass
    metadata:
      name: high-priority
    value: 1000000              # 配置优先级,比步骤二的工作负载默认优先级高。
    globalDefault: false
    description: "This priority class should be used for XYZ service pods only."

    若无需为Pod单独配置PriorityClass,可通过全局PriorityClass配置全局默认设置。配置生效后,未指定PriorityClassPod将自动采用此优先级值,抢占能力自动生效。

    apiVersion: scheduling.k8s.io/v1
    kind: PriorityClass
    metadata:
      name: global-high-priority
    value: 1                              # 配置优先级,比步骤二的工作负载默认优先级高。
    globalDefault: true
    description: "This priority class should be used for XYZ service pods only."
  2. 部署工作负载的PriorityClass。

    kubectl apply -f priorityClass.yaml

    预期输出:

    priorityclass.scheduling.k8s.io/high-priority created

步骤四:创建实际工作负载

以可用区I为例。

  1. 使用以下YAML示例,创建名为workload.yaml的文件。

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: placeholder-test
      labels:
        app: nginx
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: nginx
      template:
        metadata:
          labels:
            app: nginx
        spec:
          nodeSelector:                        # 节点选择。
            avaliable_zone: "i"
          priorityClassName: high-priority     # 步骤三配置的PriorityClass名称。如果全局配置开启,则为非必填。
          containers:
          - name: nginx
            image: nginx:1.7.9
            ports:
            - containerPort: 80
            resources:
              requests:
                cpu: 3                         # 实际负载的资源需求。
                memory: 5
  2. 部署实际的工作负载。

    kubectl apply -f workload.yaml

    预期输出:

    deployment.apps/placeholder-test created

    部署后,在工作负载 > 容器组页面可以发现,由于实际负载的PriorityClass比占位Pod高,抢占的占位Pod会在弹出节点上运行,被抢占的占位Pod会触发节点自动伸缩组件的并发扩容,为下次实际负载扩容做准备。部署工作负载.png

    节点管理 > 节点页面,可以看到负载Pod运行在之前占位Pod的节点上。负载Pod.png