Що маємо: є у нас Kubernetes cluster, на якому скейлінгом WorkerNodes займається Karpenter, який для NodePool має параметр disruption.consolidationPolicy=WhenUnderutilized
, тобто він буде намагитись “ущільніти” розміщеня подів на нодах так, щоб максимально ефективно використати ресурси CPU та Memory.
В цілому все працює, але це призводить до того, що досить часто перестворюються WorkerNodes, а це викликає “переселення” наших Pods на інші ноди.
Тож задача зараз зробити так, щоб скейлінг і процес consolidation не викликав перебоїв в роботі наших сервісів.
Загалом це тема не стільки про сам Karpenter, скільки про забезпечення стабільності роботи подів в Kubernetes загалом, але зараз я детально зайнявся цим питанням саме через Karpenter, тому будемо трохи говорити і про нього.
Зміст
Karpenter Disruption Flow
Щоб краще розуміти, що відбувається з нашими подами, давайте коротко глянемо як Karpenter виводить з пулу WorkerNode. Див. Termination Controller.
Після того, як Karpenter виявив, що є ноди, які треба термінейтити, він:
- ставить на ноду Kubernetes finalizer
- ставить на такую ноду taint
karpenter.sh/disruption:NoSchedule
щоб Kubernetes не створював нових подів на цій ноді - при необхідності створює нову ноду, на яку буде переносити поди з ноди, яка буде виведена з роботи (або викорстає ноду яка вже є, якщо вона може прийняти додаткові поди відповідно до їх
requests
) - виконує Pod Eviction подів з ноди (див. Safely Drain a Node та API-initiated Eviction)
- після того, як з ноди всі поди окрім DaemonSets видалені, Karpenter видаляє відповідний NodeClaim
- видаляє finalizer ноди, що дозволяє Kubernetes виконати видалення цієї ноди
Kubernetes Pod Eviction Flow
І коротко процес того, як сам Kubernetes виконує “виселення” поду:
- API Server отримує Eviction request і виконує перевірку – чи можна цей под виселити (наприклад – чи не порушить його видалення обмежень якогось PodDisruptionBudget)
- відмічає ресурс цього поду на видалення
kubelet
починає процес gracefully shut down – тобто відправляє сигналSIGTERM
- Kubernetes видаляє IP цього поду зі списку ендпоінтів
- якщо под не закінчив роботи на протязі заданого – то
kubelet
відправляє сигналSIGKILL
, щоб вбити процес негайно kubelet
відправляє сигнал API Server, що под можна видаляти зі списку об’єктів- API Server видаляє под з бази
Див. How API-initiated eviction works та Pod Lifecycle – Termination of Pods.
Kubernetes Pod High Availability Options
Тож що ми можемо зробити з подами, щоб наш сервіс працював незалежно від роботи Karpenter і взагалі стабільно і “бєз єдіного разрива” (с) ?
- мати мінімум по 2 поди на критичних сервісах
- мати Pod Topology Spread Constraints, щоб Pods розміщались на різних WorkerNodes – тоді якщо вбивається одна нода з одним подом – інший под на іншій ноді залишиться живим
- мати PodDisruptionBudget, щоб мінімум 1 под був завжди живий – це не дасть Karpenter виконати evict всіх подів відразу, бо він слідкує за додтриманням PDB
- і щоб гарантовано не дати виконати Pod Eviction – можемо задати поду анотацію
karpenter.sh/do-not-disrupt
– тоді Karpenter буде ігнорувати таки поди (і, відповідно, ноди, на яких буде запущено такий под)
Kubernetes Deployment replicas
Саме просте і очевидне рішення – це мати як мінімум 2 одночасно працюючих поди.
Хоча це не гарантує, що Kubernetes не виконає їхній eviction одночасно, але це мінімальна умова для подальших дій.
Тож або виконуємо руками kubectl scale deployment --replicas=2
, або оновлюємо поле replicas
в Deployment/StatefulSets/ReplicaSet (див. Workload Resources):
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-demo-deployment spec: replicas: 2 selector: matchLabels: app: nginx-demo template: metadata: labels: app: nginx-demo spec: containers: - name: nginx-demo-container image: nginx:latest ports: - containerPort: 80
Pod Topology Spread Constraints
Більш детально описував у Pod Topology Spread Constraints, але якщо коротко, то ми можемо задати правила розміщення Kubernetes Pod так, щоб вони знаходились на різних WorkerNodes. Таким чином коли Karpenter захоче вивести одну ноду з роботи – то у нас залишиться под на іншій ноді.
Проте ніхто не завадить Karpenter-у виконати drain обох нод відразу, тож і це не є 100% гарантією, але це друга умова для забезпечення стабільності роботи нашого сервісу.
Крім того, з Pod Topology Spread Constraints, ми можемо задати розміщення подів у різних Availabilty Zones, що є фактичного must have опцією при побудові High-Availabiltiy архітектури.
Тож додаємо до нашого деплойменту topologySpreadConstraints
:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-demo-deployment spec: replicas: 2 selector: matchLabels: app: nginx-demo template: metadata: labels: app: nginx-demo spec: containers: - name: nginx-demo-container image: nginx:latest ports: - containerPort: 80 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: nginx-demo
І тепер обидва поди мають бути розміщені на різних WorkerNodes:
$ kk get pod -l app=nginx-demo -o json | jq '.items[].spec.nodeName' "ip-10-1-54-144.ec2.internal" "ip-10-1-45-7.ec2.internal"
Див. також Scaling Kubernetes with Karpenter: Advanced Scheduling with Pod Affinity and Volume Topology Awareness.
Kubernetes PodDisruptionBudget
За допомогою PodDisruptionBudget ми можемо задати правило на мінімальну кількість доступних або максимальну кількість недоступних подів. Значення може бути як у виді числа, так і у виді відсотка від загальної кількості подів в replicas
для Deployment/StatefulSets/ReplicaSet.
У випадку з Deployment в якому маємо два поди і який має topologySpreadConstraints
по різним WorkerNodes це дасть гарантію того, що Karpenter не виконає Node Drain двох WorkerNdoes одночасно. Натомість він “переселить” спочатку один под, вб’є його ноду, а потім повторить процес для іншої ноди.
Див. Specifying a Disruption Budget for your Application.
Створимо PDB для нашого деплойменту:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-demo-deployment spec: replicas: 2 selector: matchLabels: app: nginx-demo template: metadata: labels: app: nginx-demo spec: containers: - name: nginx-demo-container image: nginx:latest ports: - containerPort: 80 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: nginx-demo --- apiVersion: policy/v1 kind: PodDisruptionBudget metadata: name: nginx-demo-pdb spec: minAvailable: 50% selector: matchLabels: app: nginx-demo
Деплоїмо і перевіряємо:
$ kk get pdb NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE nginx-demo-pdb 50% N/A 1 21s
Анотація karpenter.sh/do-not-disrupt
Окрім налаштувань на стороні Kubernetes, ми можемо явно задати заборону на видаленя поду самому Karpenter через додавання анотації karpenter.sh/do-not-disrupt
(раніше, до Beta, це були анотації karpenter.sh/do-not-evict
та karpenter.sh/do-not-consolidate
).
Це може знадобитись наприклад для подів, які мають бути запущені в одному екземплярі (як VictoriaMetircs VMSingle instance), і які небажано зупиняти.
Для цього в template
цього поду додаємо annotation
:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx-demo-deployment spec: replicas: 1 selector: matchLabels: app: nginx-demo template: metadata: labels: app: nginx-demo annotations: karpenter.sh/do-not-disrupt: "true" spec: containers: - name: nginx-demo-container image: nginx:latest ports: - containerPort: 80 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: nginx-demo
Див. Pod-Level Controls.
В цілому, це начебто всі основні рішення, які допоможуть забезпечити безперервну роботу подів.