AWS: VPC Prefix та максимальна кількість подів на Kubernetes WorkerNodes

Автор |  28/02/2024
 

Кожна WorkerNode в Kubernetes може мати обмежену кількість подів, і цей ліміт визначається трьома параметрами:

  • CPU: загальна кількість requests.cpu не може бути більше, ніж є CPU на Node
  • Memory: загальна кількість requests.memory не може бути більше, ніж є Memory на Node
  • IP: загальна кількість подів не може бути більшою, ніж є IP-адрес у ноди

І якщо перші два ліміти такі собі “soft” – бо ми можемо просто не задавати requests взагалі – то ліміт по кількості IP-адрес на ноді це вже “hard” ліміт, бо кожному поду, який запускається на ноді, потрібно видати власну адресу з пула Secondary IP його ноди.

А проблема полягає в тому, що ці адреси дуже часто використовуються ще до того, як на ноді закінчаться CPU або Memory – і в такому випадку ми опиняємось в ситуації, коли наша нода underutilized, тобто – ми могли б ще запустити на ній поди, але не можемо, бо для них нема вільних IP.

Наприклад, одна з наших нод t3.medium виглядає так:

В неї є вільні CPU, не вся пам’ять requested, але Pods Allocation вже 100% – бо до ноди t3.medium може бути додано 17 Secondary IP для подів, і вони всі вже зайняті.

Максимум Secondary IP на AWS EC2

Кількість же додаткових (secondary) IP на ЕС2 залежить від кількості ENI (Elastic network Interface) та кількості IP на кожен інтерфейс, і ці параметри залежать від типу EC2.

Наприклад, t3.medium може мати 3 інтерфейси, і на кожному може бути до 6 IP (див. IP addresses per network interface per instance type):

Тобто всього 18 адрес, але мінус по 1 Private IP роботу ENI самого інстансу – і для подів на такій ноді буде доступно 17 адрес.

Amazon VPC Prefix Assignment Mode

Щоб вирішити проблему з кількістью Secondary IP на EC2 можна використати VPC Prefix Assignment Mode – коли на інтерфейс підключається не окремий IP, а цілий блок /28, див. Assign prefixes to Amazon EC2 network interfaces.

Наприклад, ми можемо створити новий ENI і йому присвоїти CIDR (Classless Inter-Domain Routing) префікс:

$ aws --profile work ec2 create-network-interface --subnet-id subnet-01de26778bea10395 --ipv4-prefix-count 1

Перевіряємо цей ENI:

Єдиний момент, який варто мати на увазі це те, що VPC Prefix Assignment Mode доступний тільки для інстансів на AWS Nitro System – останнє покоління гіпервізорів AWS, на якому працюють інстанси T3, M5, C5, R5 і т.д. – див. Instances built on the Nitro System.

What is: CIDR /28

Кожна IPv4-адреса складається з 32 бітів, поділених на 4 октети (групи по 8 біт). Ці біти можуть бути представлені у двійковій системі (0 або 1) або у десятковій формі (значення між 0 та 255 для кожного октету). Ми будемо оперувати саме 0 та 1.

Маска підмережі /28 вказує на те, що перші 28 бітів IP-адреси зарезервовані для ідентифікації мережі – тоді 4 біти (32 всього мінус 28 зарезервованих) залишаються для визначення індивідуальних хостів в мережі:

Знаючи, що у нас є 4 вільні біти, а кожен біт може мати значення 0 або 1, ми можемо порахувати загальну кількість комбінацій: 2 в ступені 4, або 2×2×2×2=16 – тобто в мережі /28 може бути загалом 16 адрес, включаючи як адресу мережі (перший IP), так і broadcast адресу (останній IP), отже саме для хостів буде доступно 14 адрес.

Тож замість того, щоб на ENI підключати один Secondary IP – ми підключаємо відразу 16.

При цьому варто враховувати скільки ваша VPC Subnet зможе мати таких блоків, бо це буде визначати кільікість WorkerNodes, які ви зможете запустити.

Тут вже простіше використати утіліти накшалт ipcalc.

Наприклад, в мене Private Subnets мають префікс /20, і якщо всю цю мережу розбити на блоки по /28, то будемо мати 256 підмереж і 3584 адрес:

$ ipcalc 10.0.16.0/20 /28
...
Subnets:   256
Hosts:     3584

Або можна використати калькулятор онлайн – ipcalc.

VPC Prefix та AWS EKS VPC CNI

Добре – ми побачили, як ми можемо виділити блок адрес на інтерфейс, який підключений до EC2.

Що далі? Як з цього пулу адрес видається окрема адреса поду, який ми запускаємо в Kubernetes?

Цим займається VPC Container Networking Interface (CNI) Plugin, який складається з двох основних компонентів:

  • L-IPAM daemon (IPAMD): відповідає за створення та підключення ENI до EC2-інстансів, призначення блоків адрес до цих інтерфейсів та “прогрів” IP-префіксів для пришвидшення запуску подів (поговоримо далі)
  • CNI plugin: відповідає за налаштування мережевих інтерфейсів на ноді – як ethernet, та і віртуальних, і комунікує з IPAMD через RPC (Remote Procedure Call)

Як саме це реалізовано чудово описано в пості Amazon VPC CNI plugin increases pods per node limits > How it works:

Тож процес виглядає таким чином:

  1. Kubernetes Pod при запуску виконує запит до IPAMD на виділення IP
  2. IPAMD перевіряє доступні адреси, якщо вільні адреси є – виділяє один для поду
  3. якщо вільних адрес в підключених до ЕС2 префіксах нема – то IPAMD робить запит на підключення до ENI нового префіксу
    1. якщо до існуючого ENI вже не можна додавати нові префікси – робиться запит на підключення нового ENI
      1. якщо нода вже має максимальну кількість ENI – то запит поду на новий IP фейлиться
    2. якщо новий префікс додано (на існуючий або новий ENI) – то з нього вибирається IP для поду

WARM_PREFIX_TARGET, WARM_IP_TARGET та MINIMUM_IP_TARGET

Див. WARM_PREFIX_TARGET, WARM_IP_TARGET and MINIMUM_IP_TARGET.

Для конфігурації виділення префіксів нодам та IP подам VPC CNI має три додаткові опції – WARM_PREFIX_TARGET, WARM_IP_TARGET та MINIMUM_IP_TARGET:

  • WARM_PREFIX_TARGET: скільки підключених /28 префіксів тримати “в запасі”, тобто вони будуть підключені до ENI, але адреси з них ще не використовуються
  • WARM_IP_TARGET: скільки мінімально IP адрес підключати при створенні ноди
  • MINIMUM_IP_TARGET: скільки мінімально IP адрес тримати “в запасі”

При використанні VPC Prefix Assignment Mode ви не можете задати всі три параметри в нуль – як мінімум або WARM_PREFIX_TARGET або WARM_IP_TARGET мають бути задані хоча б в 1.

Якщо заданий WARM_IP_TARGET та/або MINIMUM_IP_TARGET – вони будуть мати перевагу над WARM_PREFIX_TARGET, тобто значення з WARM_PREFIX_TARGET буде ігноруватись.

Subnet CIDR reservations

Документація – Subnet CIDR reservations.

При використанні Prefix IP, адреси в префіксу мають бути суміжні, тобто в одному префіксу не можуть бути адреси “10.0.31.162” (блок 10.0.31.160/28) та “10.0.31.178” (блок 10.0.31.176/28).

Якщо сабнет активно використовується, і в ньому немає безперервного блоку адрес для виділння цілого префіксу, то ви отримаєте помилку:

failed to allocate a private IP/Prefix address: InsufficientCidrBlocks: The specified subnet does not have enough free cidr blocks to satisfy the request

Щоб запобігти цьому, можна використати функцію резервації блоків – VPC Subnet CIDR reservations для створення єдиного блоку, з якого потім будуть “нарізатись” блоки по /28. Такий блок не буде використовуватись для виділення Private IP для EC2, натомість VPC CNI буде створювати префікси саме з цієї “резервації”.

При цьому ви можете створити таку резервацію навіть якщо окремі IP в цьому блоку вже використовуються на EC2 – як тільки такі адреси звільняться, вони більше не будуть виділятись окремим інстансам EC2, а будуть зберігатись для формування префіксів /28.

Отже, якщо в мене є VPC Subnet з блоком /20 – я можу розбити її на два CIDR Reservation блоки по /21, і в кожному /21 блоці мати:

$ ipcalc 10.0.24.0/21 /28
...
Subnets:   128
Hosts:     1792

128 блоків /28 по 14 IP для хостів – разом 1792 IP для подів.

Активація VPC CNI Prefix Assignment Mode в AWS EKS

Документація – Increase the amount of available IP addresses for your Amazon EC2 nodes.

Все, що треба зробити – це змінити значення змінної ENABLE_PREFIX_DELEGATION в aws-node DaemonSet:

$ kubectl set env daemonset aws-node -n kube-system ENABLE_PREFIX_DELEGATION=true

При використанні Terraform модулю terraform-aws-modules/eks/aws це можна зробити через configuration_values для vpc-cni AddOn:

...
module "eks" {
  ...
  cluster_addons = {
    coredns = {
      most_recent = true
    }
    kube-proxy = {
      most_recent = true
    }
    vpc-cni = {
      most_recent    = true
      before_compute = true
      configuration_values = jsonencode({
        env = {
          ENABLE_PREFIX_DELEGATION = "true"
          WARM_PREFIX_TARGET       = "1"
        }
      })
    }
  }
...

Див. examples.

Перевіряємо:

$ kubectl describe daemonset aws-node -n kube-system
...
    Environment:
      ...
      VPC_ID:                                 vpc-0fbaffe234c0d81ea
      WARM_ENI_TARGET:                        1
      WARM_PREFIX_TARGET:                     1
...

При використанні AWS Managed NodeGroups новий ліміт буде заданий автоматично.

В цілому, максимальна кількість подів буде залежати від типу інстансу і кількості vCPU на ньому – 110 подів на кожні 10 ядер (див. Kubernetes scalability thresholds). Але є ще ї ліміти, які задані самим AWS.

Наприклад для t3.nano з 2 vCPU це буде 34 поди – перевіримо скриптом max-pod-calculator.sh:

$ ./max-pods-calculator.sh --instance-type t3.nano --cni-version 1.9.0 --cni-prefix-delegation-enabled
34

На c5.4xlarge з 16 vCPU – 110 подів:

$ ./max-pods-calculator.sh --instance-type c5.4xlarge --cni-version 1.9.0 --cni-prefix-delegation-enabled 
110

А на c5.24xlarge з 96 ядрами – 250 подів, бо це вже обмеження від AWS:

$ ./max-pods-calculator.sh --instance-type c5.24xlarge --cni-version 1.9.0 --cni-prefix-delegation-enabled
250

Налаштування Karpenter

Для того, щоб задати максимальну кількість подів на WorkerNodes, які створює Karpenter – використовуємо опцію maxPods для NodePool:

...
        - key: karpenter.sh/capacity-type
          operator: In 
          values: ["spot", "on-demand"]
      kubelet:
        maxPods: 110
...

Тестую на тестовому кластері, де зараз є тільки одна нода для CiritcalAddons, тобто звичайні поди на ній не запускаються:

$ kk get node
NAME                          STATUS   ROLES    AGE   VERSION
ip-10-0-61-176.ec2.internal   Ready    <none>   2d    v1.28.5-eks-5e0fdde

Для перевірки створимо Deployment з 3 подами:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80

Деплоїмо, і Karpenter створює один NodeClaim з t3.small:

$ kk get nodeclaim
NAME            TYPE       ZONE         NODE   READY   AGE
default-w7g9l   t3.small   us-east-1a          False   3s

Пара хвилин – і поди на ньому запустились:

Тепер скейлимо Deployment до, наприклад, 50 подів:

$ kk scale deployment nginx-deployment --replicas=50
deployment.apps/nginx-deployment scaled

І все ще маємо один NodeClaim з тим же t3.small, але тепер на ньому запущено 50 подів:

Звісно, при такому підході треба завжди задавати Pod requests, щоб кожен под мав доступ до CPU та Memory – саме requests для нас тепер будуть лімітами на кількість подів на нодах.