亚马逊AWS官方博客

Placement Group 在 Amazon EKS 集群中的应用 – 在可用区内实现无状态服务的高可用

背景

Amazon EKS(Elastic Kubernetes Service)是 AWS 提供的一种托管的 Kubernetes 服务。它可以让您在 AWS 云上轻松部署、管理和扩展容器化应用程序。越来越多的用户将容器化作为公司的技术方向,容器化为软件开发人员和开发团队带来了敏捷性、可移动性以及低成本等众多优势。

当使用分布式架构部署容器集群时,通常由一个节点(主机)运行多个 Pod 服务对内对外提供服务。当物理层出现机架物理设备出现故障时,通常会影响一个机架上的所有主机节点,进而会影响该硬件设备内的所有 Pod 服务,因此防止硬件设备故障带来的影响范围至关重要。本文将以在 AWS EKS 中部署的 Nginx 集群为例,介绍在可用区内如何利用 Placement Group 在 AZ 内搭建高可用的集群架构,在发生物理故障的时候缩小物理故障的爆炸半径,同时保障主副节点的高可用。

常见的集群架构设计和可用性风险

以 Nginx 集群架构设计为例,为了实现 Nginx 集群的高可用,一般我们会部署多个 Nginx 节点组成集群,并会避免相同的 Nginx 服务在相同的节点中。但是在云服务中,不同的节点可能在底层是相同物理设施,为了避免底层物理设施的故障导致大面积相同应应用服务的不可用的现象,我们期望将相同的 Nginx 应用服务尽可能分散到不同的物理设备上来,这样即使单个物理主机、机架出现故障,依然可以保持整体集群的可用性,减少物理设备故障的影响面。

除了 Placement Group 之外,我们也可以考虑使用 AZ 打散应用服务。但本文主要关注在单个 AZ 中如何建设高可用的应用。

什么是置放群组(Placement Group)

置放群组是实例在底层硬件上分布放置的策略,有三种置放群组类型可供选择:

分类 介绍 适用场景
1 集群 将一个可用区内靠近的实例打包在一起。通过使用该策略,工作负载可以实现所需的低延迟网络性能,以满足高性能计算(HPC)应用程序通常使用的紧密耦合的节点到节点通信的要求 实例之间靠的更近,享受更高的吞吐和低延迟网络性能。
适合高性能计算(HPC)应用程序等
2 分区 将实例分布在不同的逻辑分区上,以便一个分区中的实例组不会与不同分区中的实例组使用相同的基础硬件 该策略通常为大型分布式和重复的工作负载所使用,例如,Hadoop、Cassandra 和 Kafka
3 分布 将一小组实例严格放置在不同的基础硬件上,以减少相关的故障 适合较少数量实例严格放置在不同的基础硬件上的场景

通过分区置放群组和分布置放群组的特征我们发现,可以利用这两种置放群组尽可能地将集群的实例打散,那么什么时候选择分区置放群组,什么时候选择分布式置放群组呢?

我们建议:

  • 当集群的规模稳定并小于等于 7 个实例,并且服务需要严格放置在不同的机架上时,建议使用分布置放群组;
  • 当集群规模较大(大于 7 个节点),并且可以根据一组机架进行隔离的时候,推荐使用分区置放群组。

如何在 EKS 集群中应用置放群组

通过分区置放群组,我们可以建立最多 7 个分区,每个分区中都有一组机架。利用 7 个分区,可以尽可能地将 EKS 集群中的 Node 打散。同时,为了实现 Nginx 服务的打散到不同分区从而实现高可用,避免单个 EC2 或机架故障导致 Nginx 服务大规模不可用,我们需要实现主节点和副本节点的反亲和,将 Nginx 服务尽可能调度到不同的置放群组分区中。

架构设计

方案实施

整个方案的实现共分为 4 个步骤:

  1. 创建分区置放群组(Partition Placement Group),划分 7 个分区;
  2. 创建 EKS 集群和节点组并关联节点组和分区置放群组的关系;
  3. 初始化节点的 Label 信息,标记节点所在的分区信息;
  4. 利用 Node 反亲和策略,将相同分片的 Pod 主分片和 Pod 副本分片分布在不同的分区中。

接下来,我们将依次介绍每个步骤的详细操作。

Step 1. 创建分区置放群组

我们可以在控制台创建分区置放群组或者使用 CLI 命令来创建分区置放群组,详情可以参考 Deploying applications using partition placement groups

这里,我们使用AWS CLI 命令的方式创建分区置放群组。

创建分区置放群组(Partition Placement Group)并指定 7 个分区(Partition)数量:

aws ec2 create-placement-group  --group-name nginx-partition --strategy partition --partition-count 7 

你会立刻得到一个输出结果,显示创建的分区置放群组的资源信息:

{
    "PlacementGroup": {
        "GroupName": "nginx-partition",
        "State": "available",
        "Strategy": "partition",
        "PartitionCount": 7,
        "GroupId": "pg-029a71c8704d7dc75",
        "GroupArn": "arn:aws:ec2:us-west-2:xxx:placement-group/nginx-partition"
    }
}

Step 2. 关联 Node 和 Placement Group 的 Partition 信息

接下来,我们创建 EKS 集群(Cluster)和节点组(Node Group)并关联 PG(Placement Group)。

当我们创建完所需要的资源后,新启动的节点(Node)会自动的加入分区置放群组中,并且会尽可能均衡地放置在不同的分区中。然后,我们还需要让 Nginx 的节点尽可能隔离在不同的分区中,因此需要在 Node 上标记所处的分片编号信息。

所以接下来,我们需要让 Node 在启动后获取自己所属的分区的编号信息并作为 Label 标记在 Node 上。因为 Node 本身不知道所启动的节点在哪个分区,我们需要利用启动模版(Launch Template)的启动命令功能,在启动时读取 Node 所在的置放群组的分区(Partition)信息,并创建 Node Label 以供后续反亲和使用。

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: nginx-cluster
  region: us-west-2

nodeGroups:
- name: partition-node-group
  minSize: 1
  desiredCapacity: 2
  maxSize: 10
  instanceType: r6i.xlarge
  placement:
    groupName: nginx-partition
  overrideBootstrapCommand: |
    #!/bin/bash

    source /var/lib/cloud/scripts/eksctl/bootstrap.helper.sh
    
    GROUP_NAME="$(get_metadata placement/group-name)"
    PARTITION_NUMBER="$(get_metadata placement/partition-number)"

    # Note "--node-labels=${NODE_LABELS}" needs the above helper sourced to work, otherwise will have to be defined manually.
    /etc/eks/bootstrap.sh ${CLUSTER_NAME} --container-runtime containerd --kubelet-extra-args "--node-labels=${NODE_LABELS},placement/group-name=${GROUP_NAME},placement/partition=${GROUP_NAME}/${PARTITION_NUMBER}"

Step 3. 通过反亲和分散 Pod 所在分区

在 EKS 集群中使用 Pod 反亲和方式,保证 Master 节点和 Slave 节点不在相同的 Partition(分区)中:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 6
  selector:
    matchLabels:
      app: nginx-user
  template:
    metadata:
      labels:
        app: nginx-partition
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - nginx-use
          topologyKey: "placement/partition" 

Step 4. 确认主节点和副本节点的分散

查询 PG 中每个 Partition 中对应的主机数量,确认服务已经尽可能分散:

#!/bin/bash

PLACEMENT_GROUP=test-2 #指定Placement Group的Name
for id in $(seq  $(($(aws ec2 describe-placement-groups --group-names $PLACEMENT_GROUP --query "PlacementGroups[0].PartitionCount" --output text) )))
do
  INSTANCES=$(aws ec2 describe-instances --filters "Name=placement-group-name,Values=$PLACEMENT_GROUP" "Name=placement-partition-number,Values=$id" --query "Reservations[*].Instances[*].InstanceId" --output text)
  echo "Partition $id has $(echo $INSTANCES | wc -w) instances"
done

检查 POD 主分片和副本分片是否在相同 Partition:

kubectl get pods -o=custom-columns='NAME:.metadata.name,NODE:.spec.nodeName,NODE_HOSTNAME:.spec.nodeName' --all-namespaces --no-headers | awk '{print $1, $2, $(NF)}' | while read pod node hostname; do
  node_labels=$(kubectl get node "$node" --label-columns=topology.kubernetes.io/zone  --no-headers|awk '{print $(NF)}')
  printf "%-40s %-20s %-20s\n" "$pod" "$node" "$node_labels"
done

总结

本文介绍了如何在 EKS 集群中利用 Partition Placement Group(分区置放群组)防止相同业务的节点同时不可用,实现应用服务的高可用。针对无状态的应用,无需实现主从节点的情况可以参考本文来实现,下一篇中,我们将针对有状态的应用,介绍如何利用 Partition Placement Group 实现主从节点高可用。

本篇作者

张凯

亚马逊云科技解决方案架构师,主要负责基于 AWS 云计算的解决方案架构设计和的方案咨询。具有多年的架构设计、项目管理经验。

叶鹏勇

亚马逊云科技解决方案架构师,负责容器以及 DevOps 相关领域的方案和架构设计工作。在容器、微服务、SRE 相关领域有多年的实战经验。

韩攀

亚马逊云科技技术客户经理,负责支持全球头部电商客户的架构优化、成本管理、技术咨询等工作,并专注在容器、数据库、系统集成和交付方向的技术研究和实践。