Dev&Ops/DevOps

[Infra] Karpenter와 Empty Pod을 활용한 스케일링(1)

zeroneCoder 2023. 7. 13. 15:55

요즘에 Microservices Architecture(이하 MSA)와 쿠버네티스(Kubernetes)에 대한 관심이 많아지고 AWS를 사용하는 많은 회사에서 온프레미스 혹은 EC2, ECS 환경에서 Elastic Kubernetes Services(이하 EKS)로 많이 옮기고 있는 추세입니다.

EKS 환경에서 보다 안정적인 서비스를 제공하기 위해서는 빠르게 파드 프로비저닝(pod provisioning)이 필요하고, 파드(Pod)가 많이 생기게 되면 노드 프로비저닝(node provisioning)이 필요하게 됩니다. 그러나 필요한 만큼 빠르게 파드 및 노드 프로비저닝으로 서비스의 안정성을 확보하고, 동시에 비용 효율적으로 조정하는 것은 쉽지 않습니다.

 
 

오늘 공유해볼 내용은 저희 팀에서 빠른 프로비저닝을 위해 사용하고 있는 카펜터(Karpenter)와 빈(empty) 파드를 활용한 스케일링 전략에 대해 공유해보도록 하겠습니다.

1부에서는 카펜터에 대해서 설명드리고, 2부에서는 빈 파드를 활용한 스케일링 전략에 대해 설명할 예정이니 다음 글까지 꼭!!읽어보시면 좋을 거 같습니다.

EKS 환경에서 왜 카펜터를 써야하지?

 

AWS EKS 클러스터를 자동으로 조정하기 위한 프로비저닝 도구로, 많이 알려져있는 쿠버네티스 클러스터 오토스케일러(Cluster Autoscaler, 이하 CA)가 있고, 카펜터도 있습니다. 이 중에서 왜 카펜터를 사용해야 하는지에 대해 이야기 해보도록 하겠습니다.

먼저 Auto Scaling Group(이하 ASG)을 기반으로 동작하는 CA의 동작 방식에 대해 알아야 합니다.


파드 하나 이상의 EC2 노드에 배포되며, 노드는 Amazon EC2 ASG와 연결된 노드 그룹을 통해 배포됩니다. CA는 스케줄링 되지 못한 파드가 있으면 ASG를 통해 노드를 EKS 클러스터에 추가하여 프로비저닝을 합니다. 이 때 두 가지 동작이 있습니다. 한 가지는 추가적으로 늘어나는 경우인 프로비저닝과 반대로 줄어드는 경우인 디프로비저닝(deprovisioning)입니다.

Provisioning

그림 1. Cluster Autoscaler의 동작 과정

  1. 자원 부족으로 Pending 상태인 파드가 존재한다.
  2. CA는 ASG의 Desired 수를 증가시킨다.
  3. AWS ASG은 새로운 노드를 프로비저닝 시킨다.
  4. kube-scheduler는 Pending된 파드를 새로운 노드에 할당한다.

CA는 노드를 추가로 프로비저닝할 때, 노드의 리소스 사용률이 아니라 할당되지 않는 파드의 여부를 기준으로 판단합니다. 이 때, CA는 ASG에 desired 수를 변경하고, 새롭게 노드를 프로비저닝 하는 구조입니다. 그리고 새롭게 뜬 노드에 파드가 할당되어 동작합니다.

Deprovisioning

디프로비저닝의 경우에는 노드의 할당 가능한 리소스를 기준으로 사용률이 50% 이하인 경우 디프로비저닝 대상으로 간주하며, 해당 노드에 실행 중이던 파트를 다른 곳으로 옮길 수 있는지 계산한 뒤 노드의 삭제를 진행합니다. 그리고 ASG의 desired 수를 변경하게 됩니다.

 

위 두 경우에서 공통적으로 CA는 노드 그룹에 연결된 ASG에 따라 노드의 타입이 한정된다는 제약이 있습니다. 즉 필요 이상으로 더 많은 노드 타입이 생길 수도 있습니다. 그렇게 되면 비용 최적화가 되기가 어렵겠죠?

CA에서 비용 최적화적인 부분과 ASG과 별개로 행동할 수 있도록 카펜터는 더 고도화되었습니다. 카펜터와 CA의 차이점을 비교해보면 크게 세 가지로 볼 수 있습니다.

  1. 그룹 불필요
    1. CA는 ASG에 요청을 보내는 구조이기 때문에 여러 타입의 인스턴스를 사용하기 위해서는 노드 그룹을 여러 개를 구성해야 합니다.
    2. 카펜터는 다양한 타입의 인스턴스 목록을 지정하고 프로비저닝 시점에 지정된 조건을 만족하고 리전의 가용 영역에서 사용할 수 있는 인스턴스 중 최적 비용의 인스턴스를 할당해서 사용합니다.
  2. kube-scheduler 우회
    1. CA는 kube-scheduler에 의해 할당되지 않은 파드를 감지해서 ASG에 알리는 구조이기 때문에 바로 노드가 생기지 않습니다.
    2. 카펜터는 kube-scheduler에 의해 동작하지 않고, 현재 kube-scheduler에서 표시된 pending 상태인 파드가 있으면 kube-scheduler를 거치지 않고 바로 노드를 생성해서 파드를 할당하기 때문에 비교적 빠르게 동작합니다.
  3. 비용 최적화
    1. 카펜터는 온디멘드 타입으로 프로비저닝된 노드에 대해 현재의 인스턴스 가격과 리소스를 비교해서 더 적절한 형태의 노드 타입으로 합치는 작업을 해줍니다. (엄청 싸게 할당된 스팟 타입은 해주지 않네요.) 따라서 비용을 최적화 할 수 있습니다.

이렇게 3가지 관점에서 CA를 대체할 수 있게 되었기 때문에 저희 팀에서는 노드 프로비저닝 도구로 카펜터를 도입했습니다.

자, 지금까지는 CA의 동작 방식과 카펜터를 도입해서 사용하는 이유에 대해 알아봤습니다. CA의 동작을 알아봤으니까 카펜터의 동작도 함께 봐야지 확실히 비교가 되겠죠?

이번에는 카펜터가 어떻게 동작하는지에 대해 알아보도록 하겠습니다.

 

카펜터, 넌 누구냐?

카펜터 구성은 어떻게 되지?

카펜터의 동작을 이해하기 전에 구성 요소를 살펴볼 필요가 있습니다.

 

카펜터는 Provisioner와 NodeTemplate을 통해 조건에 알맞은 노드 타입을 빠르게 프로비저닝 해줍니다.

먼저 Provisioner는 프로비저닝이 될 때 사용할 수 있는 인스턴스 패밀리나 가용영역, 가중치 등으로 구성해서 노드가 생성될 때 필요한 프로바이더(providerref)의 역할을 하게 됩니다.

apiVersion: karpenter.sh/v1alpha5
kind: Provisioner
metadata:
  name: on-demand
spec:
  providerRef:
    name: on-demand
  ttlSecondsAfterEmpty: 30
  limits:
    resources:
      cpu: 1k
      memory: 1000Gi
  requirements:
    # Include general purpose instance families
    - key: karpenter.sh/capacity-type
      operator: In
      values: ["on-demand"]
    - key: karpenter.k8s.aws/instance-family
      operator: In
      values: ["t3"]
    - key: karpenter.k8s.aws/instance-size
      operator: NotIn
      values: ["nano", "micro", "small"]
    - key: kubernetes.io/arch
      operator: In
      values: ["amd64"]
    - key: topology.kubernetes.io/zone
      operator: In
      values: ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]

 

다음으로 NodeTemplate는 Provisioner의 spec.providerRef부분에 들어가고, 프로비저닝될 노드가 어떤 AMI로 실행하고, 혹은 어떤 보안 그룹을 사용할 것인지 등 노드에 대한 정의를 하는 템플릿이라고 볼 수 있습니다.

apiVersion: karpenter.k8s.aws/v1alpha1
kind: AWSNodeTemplate
metadata:
  name: spot
spec:
  subnetSelector:
    karpenter.sh/discovery: cluster-name
  securityGroupSelector:
    karpenter.sh/discovery: cluster-name
  instanceProfile: MyInstanceProfile
  amiFamily: Bottlerocket
 

이렇게 카펜터의 두 가지 구성요소에 대해 알아봤습니다. 이제 본격적으로 카펜터의 동작을 알아보겠습니다.

카펜터는 어떻게 동작할까?

먼저 아래 카펜터의 동작 과정 그림을 보겠습니다. 그림을 보시면 CA와는 다르게 ASG와 관계없이 동작하는 것을 확인할 수 있습니다. 이렇기 때문에 파드를 할당할 수 있는 노드가 부족할 때, JIT(Just-In-Time)으로 노드 프로비저닝이 일어나기 때문에 보다 빠르게 파드를 할당 할 수 있게 됩니다.

 

Karpenter 동작 원리

카펜터도 CA와 마찬가지로 프로비저닝과 디프로비저닝이 있겠죠? 각 동작에 대해서 딥 다이브해보도록 하겠습니다.

Provisioning

 
Karpenter Pod의 동작 Log

카펜터 파드의 로그를 보면 Pending상태인 파드가 3개 가 있고 이를 위해 3개의 온디맨드 타입의 노드가 새롭게 뜨는 것을 확인해 볼 수 있습니다. 이 동작이 어떻게 이뤄질까요? 프로비저닝도 Provisioner의 특정 조건에 만족해야 하는데, 이 조건들이 무엇인지에 대해 하나씩 짚어보도록 하겠습니다.

조건 1 - 리소스 요청 : 띄우려는 파드의 리소스가 현재 있는 노드보다 많이 필요한 경우, Provisioner가 동작하도록 지정합니다.

  • 아래 예시에서는 vCpu 2core와 메모리 2.5Gi가 필요하기 때문에 xlarge타입의 노드가 프로비저닝됩니다.
spec:
  containers:
    - name: reserve-resources
      image: k8s.gcr.io/pause
      resources:
        requests:
          cpu: 2000m
          memory: 2.5Gi
        limits:
          cpu: 2000m
          memory: 2.5Gi

조건 2 - 노드 선택: NodeSelector 에 레이블을 지정해서 원하는 Provisioner가 동작하도록 지정합니다.

  • 아래 예시에서는 NodeSelector에 지정된 레이블과 맞는 온디맨스 혹은 스팟 provisioner가 동작해서 새로운 노드가 뜨거나 기존의 노드 중 레이블에 매칭된 노드에 파드가 할당됩니다.
spec:
  template:
    metadata:
      labels:
        app: nginx
    spec:
      NodeSelector:
        topology.kubernetes.io/zone: ap-northeast-2a
        karpenter.sh/capacity-type: spot

조건 3 - NodeAffinity를 만족하는 경우에 Provisioner가 동작하도록 지정합니다.

Node affinity에는 두 가지 조건으로 동작을 결정됩니다. requiredDuringSchedulingIgnoredDuringExecution(반드시 조건을 만족해야 한다.)와 preferredDuringSchedulingIgnoredDuringExecution(되도록 조건을 만족해야 한다.) 입니다. 두 가지의 Node affinity에 NodeSelectTerms로 Provisioner에 지정된 레이블이나 requirements의 키/값을 지정해서 원하는 Provisioner로 동작할 수 있도록 지정할 수 있습니다.

  • 아래 예시에서는 requiredDuringSchedulingIgnoredDuringExecution 가 걸려있기 때문에 아래 조건과 반드시 만족한 노드가 아니면 새로운 노드가 프로비저닝됩니다.
      affinity:
        NodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            NodeSelectorTerms:
              - matchExpressions:
                  - key: "Nodetype"
                    operator: "In"
                    values: ["on-demand"]

조건 4 - 토폴로지 분배: topologySpreadConstraints의 조건으로 지정해서 만족하는 경우 Provisioner가 동작하도록 지정합니다.

하나의 노드에 같은 파드가 뜨지 않도록 제약사항을 걸어 여러 노드가 프로비저닝 될 수 있도록 조건을 걸 수 있습니다. 현재 지원하는 topologyKey는 topology.kubernetes.io/zone kubernetes.io/hostname karpenter.sh/capacity-type 3가지 입니다.

  • 아래 예시에서는 같은 kubernetes.io/hostname 레이블이 있는 노드에 1개의 파드만 있을 수 있도록 설정했습니다. 설정대로라면 새로운 파드가 뜰 때마다 노드가 프로비저닝 됩니다.
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              run: empty-Pods
 

조건 5 - Pod affinity/anti-affinity: affinity 조건에 맞는 경우, 파드를 할당하기 위해 필요한 노드가 없으면 Provisioner가 동작하도록 지정합니다.

PodAffinity와 PodAntiAffinity조건에 부합하는 노드에 파드를 할당할 수 있습니다. 만약 파드를 할당했는데 필요한 노드가 없다면 Provisioner가 동작해서 노드를 새롭게 프로비저닝합니다.

  • 아래 예시에서는 system: backend 레이블이 있는 노드에는 뜨고, app: inflate label이 있는 노드에는 파드가 뜨지 않도록 지정했습니다.
spec:
  affinity:
    PodAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: system
            operator: In
            values:
            - backend
        topologyKey: topology.kubernetes.io/zone
    PodAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app: inflate
        topologyKey: kubernetes.io/hostname

Deprovisioning

프로비저닝 됐다가 더이상 사용하지 않는 노드를 비용 최적화를 하기 위해 스케일을 줄이게 됩니다. 이 때 디프로비저닝을 하기 위한 조건은 4가지로 구성되는데 각각에 대해 알아보겠습니다.

  • Provisioner의 삭제
    • Provisioner에 의해 생성된 노드는 Provisioner가 소유한 것으로 간주가 됩니다. 따라서 Provisioner가 삭제되면 Provisioner에 의해 생성된 노드가 중지되서 디프로비저닝 됩니다.
  • Empty
    • Deamonset이 아닌 파드가 사라진 다음 Provisioner에 지정된 ttlSecondsAfterEmpty 이 지난 이후 디프로비저닝이 동작해서 노드를 중지합니다.
  • Interrupt
    • Spot stop interrupt나 Node termination등의 노드와 관련된 중단 이벤트가 Event Bridge를 통해 SQS의 대기열로 들어오면 이를 받아 디프로비저닝 동작을 하게 됩니다.
  • Expire
    • 노드가 프로비저닝 된 이후 Provisioner에 지정된 ttlSecondsUntilExpired 시간이 지나면 디프로비저닝이 동작해서 노드를 중지하게 됩니다.
  • Integration
    • 비용 최적화를 하기 위해서 현재 생성된 단일 혹은 여러 노드의 비용를 비교해서 더 적절한 하나의 노드로 합치는 작업을 하게 됩니다. 이 때 스팟 타입의 경우에는 동작하지 않으며 온디맨드 타입의 노드의 경우에만 합치게 됩니다.

 

지금까지 카펜터 구성요소와 어떻게 동작하는지에 대해 자세히 알아봤습니다. CA와 카펜터의 동작이 좀 다르다는게 느껴지시나요? 와닿지 않으시다면 직접 구축해보시는건 어떨까요?

이번 글에서는 Empty Pod에 대한 내용은 언급하지 않았습니다. 여러분의 궁금증을 풀려면…다음 글을 읽어주셔야 합니다~

다음 글에서는 PriorityClass와 빈 파드, 그리고 오늘 설명드린 카펜터를 활용한 스케일링 전략을 집중적으로 다뤄보도록 하겠습니다.

 

Reference

https://karpenter.sh/

 

Karpenter

Just-in-time Nodes for Any Kubernetes Cluster

karpenter.sh

https://catalog.us-east-1.prod.workshops.aws/workshops/fd6ccd33-980d-422f-b6a6-cb3c3424a78c/en-US/scaling/scaling-clusters

 

Workshop Studio

 

catalog.us-east-1.prod.workshops.aws

https://aws.amazon.com/ko/blogs/tech/amazon-eks-cluster-auto-scaling-karpenter-bp/

 

Amazon EKS 클러스터를 비용 효율적으로 오토스케일링하기 | Amazon Web Services

애플리케이션 현대화를 위해 많은 고객이 컨테이너를 선택하며, 컨테이너 운영 부담을 완화하기 위해 Amazon Elastic Container Service (Amazon ECS) 또는 Amazon Elastic Kubernetes Service (Amazon EKS)와 같은 관리형

aws.amazon.com

https://github.com/kubernetes/autoscaler

 

GitHub - kubernetes/autoscaler: Autoscaling components for Kubernetes

Autoscaling components for Kubernetes. Contribute to kubernetes/autoscaler development by creating an account on GitHub.

github.com

https://www.wisen.co.kr/pages/blog/blog-detail.html?idx=12079 

 

[Re2021] 새로운 K8s Cluster Autoscaler, Karpenter 소개

GS네오텍의 IT서비스 브랜드 WiseN으로 진정한 프리미엄을 경험하세요

www.wisen.co.kr:443