Karpenter: використання Disruption budgets

Автор |  17/09/2024

Disruption budgets з’явились в версії 0.36, і виглядає як дуже цікавий інструмент для того, аби обмежити Karpenter в перестворенні WorkerNodes.

Наприклад в моєму випадку ми не хочемо, аби EC2 вбивались в робочі часи по США, бо там у нас клієнти, а тому зараз маємо consolidationPolicy=whenEmpty, аби запобігти “зайвому” видаленню серверів та Pods на них.

Натомість з Disruption budgets ми можемо налаштувати політики таким чином, що в один період часу будуть дозволені операції з WhenEmpty, а в інший – WhenEmptyOrUnderutilized.

Див. також Kubernetes: забезпечення High Availability для Pods – бо при використанні Karpenter навіть при налаштованих Disruption budgets необхідно мати відповідно налаштовані поди з Topology Spread та PodDisruptionBudget.

Типи Karpenter Disruption

Документація – Automated Graceful Methods.

Спочатку глянемо, в яких випадках Disruption взагалі відбувається:

  • Drift: виникає, коли є різниця між створеними конфігураціями NodePools або EC2NodeClass та існуючими WorkerNodes – тоді Karpenter почне перестворювати EC2 аби привести їх у відповідність до заданих параметрів
  • Interruption: якщо Karpenter отримує AWS Event, що інстанс буде виключено, наприклад – якщо це Spot
  • Consolidation: якщо маємо налаштування Consolidation на WhenEmptyOrUnderutilized або WhenEmpty, і Karpenter переносить наші Pods на інші WorkerNodes
    • у нас Karpenter 1.0, тому полісі WhenEmptyOrUnderutilized, для 0.37 це WhenUnderutilized

Karpenter Disruption Budgets

За допомогою Disruption budgets ми можемо дуже гнучко налаштувати в який час і які операції Karpenter може проводити, і задати ліміт на те, скільки WorkerNodes одночасно будуть видалятись.

Документація – NodePool Disruption Budgets.

Формат конфігурації доволі простий:

budgets:
- nodes: "20%"
  reasons: 
  - "Empty"
  schedule: "@daily"
  duration: 10m

Тут ми задаємо:

  • дозволити видалення WorkerNodes для 20% від загальної кількості
  • для операції, коли Disruption викликаний умовою WhenEmpty
  • виконуємо це кожен день
  • на протязі 10 хвилин

Параметри тут можуть мати значення:

  • nodes: в процентах або просто кількості нод
  • reasons: Drifted, Underutilized або Empty
  • schedule: розклад, за яким правило застосовується, в UTC (інші таймзони поки не підтримуються), див. Kubernetes Schedule syntax
  • duration: і скільки часу правило діє, наприклад – 1h15m

При цьому не обов’язково задавати всі параметри.

Наприклад, ми можемо описати два таких бюджети:

- nodes: "25%"
- nodes: "10"

Тоді у нас постійно будуть працювати обидва правила, і перше обмежує кількість нод в 25% від загальної кількості, а друге – не більше як 10 інстансів – якщо у нас більш ніж 40 серверів.

Також, Budgets можна комбінувати, і якщо їх задано кілька – то ліміти будуть братись по найбільш суворому.

В першому прикладі ми застосовуємо правило на 20% нод і умові WhenEmpty, а решту часу будуть працювати дефолтні правила disruption – тобто, 10% від загальної кількості серверів із заданою consolidationPolicy.

Тому можемо записати правило так:

budgets:
- nodes: "20%"
  reasons: 
  - "Empty"
  schedule: "@daily"
  duration: 10m
- nodes: 0

Тут останнє правило працює постійно, і буде таким собі запобіжником: ми забороняємо все, але дозоляємо виконувати disruption за політикою WhenEmpty на протязі 10 хвилин раз на добу починаючи з 00:00 UTC.

Приклад Disruption Budgets

Повертаючись до моєї задачі:

  • маємо Backend API в Kubernetes на окремому NodePool, а наші клієнти в основному з США, тому ми хочемо мінімізувати down-скейлінг WorkerNodes в робочий час по США
  • для цього ми хочемо заблокувати всі операції по WhenUnderutilized в період робочого часу по Central Time USA
    • в schedule Karpenter використовує зону UTC, тому початок робочого дня по Central Time USA 9:00 – це 15:00 UTC
  • операції з WhenEmpty дозволимо в будь-який час, але тільки по 1 WorkerNode одночасно
  • Drift – аналогічно, бо коли я деплою зміни – то хочу побачити результат відразу

Фактично, нам потрібно задати два бюджети:

  • по Underutilized – забороняємо все з понеділка по п’ятницю на протязі 9 годин починаючи з 15:00 по UTC
  • по Empty та Drifted – дозволяємо в будь-який час, але тільки по 1 ноді, а не дефолтні 10%

Тоді наш NodePool буде виглядати так:

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: backend1a
spec:
  template:
    metadata:
      labels:
        created-by: karpenter
        component: devops
    spec:
      taints:
        - key: BackendOnly
          operator: Exists
          effect: NoSchedule
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass      
        name: defaultv1a
      requirements:
        - key: karpenter.k8s.aws/instance-family
          operator: In
          values: ["c5"]
        - key: karpenter.k8s.aws/instance-size
          operator: In
          values: ["large", "xlarge"]
        - key: topology.kubernetes.io/zone
          operator: In
          values: ["us-east-1a"]
        - key: karpenter.sh/capacity-type
          operator: In 
          values: ["spot", "on-demand"]
  # total cluster limits 
  limits:
    cpu: 1000
    memory: 1000Gi
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 600s
    budgets:
      - nodes: "0"                   # block all
        reasons:
        - "Underutilized"            # if reason == underutilized
        schedule: "0 15 * * mon-fri" # starting at 15:00 UTC during weekdays
        duration: 9h                 # during 9 hours
      - nodes: "1"                   # allow by 1 WorkerNode at a time
        reasons:
        - "Empty"
        - "Drifted"

Деплоїмо, перевіряємо NodePool:

$ kk describe nodepool backend1a   
Name:         backend1a
...
API Version:  karpenter.sh/v1
Kind:         NodePool
...
Spec:
  Disruption:
    Budgets:
      Duration:  9h
      Nodes:     0
      Reasons:
        Underutilized
      Schedule:  0 15 * * mon-fri
      Nodes:     1
      Reasons:
        Empty
        Drifted
    Consolidate After:     600s
    Consolidation Policy:  WhenEmptyOrUnderutilized
...

І в логах бачимо, що спрацював Disruption по WhenUnderutilized:

karpenter-55b845dd4c-tlrdr:controller {"level":"INFO","time":"2024-09-16T10:48:26.777Z","logger":"controller","message":"disrupting nodeclaim(s) via delete, terminating 1 nodes (2 pods) ip-10-0-42-250.ec2.internal/t3.small/spot","commit":"62a726c","controller":"disruption","namespace":"","name":"","reconcileID":"db2233c3-c64b-41f2-a656-d6a5addeda8a","command-id":"1cd3a8d8-57e9-4107-a701-bd167ed23686","reason":"underutilized"}
karpenter-55b845dd4c-tlrdr:controller {"level":"INFO","time":"2024-09-16T10:48:27.016Z","logger":"controller","message":"tainted node","commit":"62a726c","controller":"node.termination","controllerGroup":"","controllerKind":"Node","Node":{"name":"ip-10-0-42-250.ec2.internal"},"namespace":"","name":"ip-10-0-42-250.ec2.internal","reconcileID":"f0815e43-94fb-4546-9663-377441677028","taint.Key":"karpenter.sh/disrupted","taint.Value":"","taint.Effect":"NoSchedule"}
karpenter-55b845dd4c-tlrdr:controller {"level":"INFO","time":"2024-09-16T10:50:35.212Z","logger":"controller","message":"deleted node","commit":"62a726c","controller":"node.termination","controllerGroup":"","controllerKind":"Node","Node":{"name":"ip-10-0-42-250.ec2.internal"},"namespace":"","name":"ip-10-0-42-250.ec2.internal","reconcileID":"208e5ff7-8371-442a-9c02-919e3525001b"}

Готово.