Примечание переводчика: это не первый наш перевод материалов Learnk8s и её управляющего директора Daniele Polencic. В благодарность за интересную статью мы оставляем здесь ссылку на страницу компании. Learnk8s специализируется на обучении Kubernetes и предлагает очные и онлайн-курсы под руководством опытных инструкторов, оказывает консультационные услуги и проводит публичные воркшопы. Также на их сайте вы найдёте интересные исследования. На этом передаём слово автору.
TL;DR: из этой статьи вы узнаете, как предотвратить разрыв соединений при запуске или остановке пода. А ещё мы поговорим о том, как корректно завершать длительные (long-running) задачи и соединения.
В Kubernetes создание и удаление подов — одна из самых распространённых задач. Поды создаются при выполнении скользящего обновления, масштабировании Deployment’ов, для каждого нового релиза, для каждой задачи (Job), CronJob и т. д. Кроме того, поды удаляются и воссоздаются после вытеснений — например, когда вы помечаете узел как не подлежащий планированию (kubectl cordon $NODENAME
).
Если природа этих подов настолько эфемерна, то что случится, если сигнал о завершении работы поступит в под, когда тот будет обрабатывать запрос? Будет ли запрос выполнен до отключения? А как насчёт последующих запросов? Будут ли они перенаправлены куда-либо ещё?
Что происходит, когда вы создаёте под в Kubernetes
Прежде чем говорить о том, что происходит при удалении пода, необходимо обсудить, что происходит при его создании. Предположим, вы хотите создать следующий под в кластере:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: web
image: nginx
ports:
- name: web
containerPort: 80
Применить YAML-манифест в кластере можно с помощью следующей команды:
$ kubectl apply -f pod.yaml
Когда вы нажмёте на Enter, kubectl отправит манифест пода в Kubernetes API. Именно здесь и начинается наше путешествие. API получает и проверяет манифест пода, которое затем сохраняется в базе данных — etcd. Под также добавляется в очередь планировщика.
Планировщик:
Изучает манифест.
Собирает подробности о рабочей нагрузке (запросы на ресурсы CPU и памяти).
Решает, какой узел лучше всего подходит для его запуска (с помощью процесса, известного как Filters and Predicates).
Примечание переводчика о фильтрах и предикатах
Фильтры (Filters) в Kubernetes используются для сокращения списка узлов, подходящих для запуска конкретного пода. Это первый этап в процессе планирования. Здесь проверяются различные атрибуты узлов и подов, чтобы оценить их совместимость.
Несколько примеров:
Node Affinity/Anti-Affinity — проверка, удовлетворяет ли узел требования affinity или anti-affinity, указанные в спецификации пода.
Taints and Tolerations проверяет, могут ли поды размещаться на узлах, которые имеют назначенные taints. Под должен иметь соответствующую toleration, чтобы быть запланированным на таком узле.
Node Resources — оценка, достаточно ли на узле ресурсов (CPU, памяти и других) для запуска конкретного пода.
Предикаты (Predicates) — это правила или условия, которым должен удовлетворять узел для запуска на нём пода. Многие из них являются частью процесса фильтрации, но могут быть более конкретными и сложными.
Например:
PodFitsHostPorts — проверка, соответствуют ли порты, которые требуют поды, доступным портам на узле.
PodFitsResources — оценка, имеется ли на узле достаточно ресурсов (CPU, памяти и других) для каждого пода.
NoDiskConflict — проверка, не вызывает ли диск, требуемый подом, конфликтов на узле.
В конце процесса:
Под помечается как запланированный в etcd.
Под приписывается к конкретному узлу.
Состояние пода хранится в etcd.
Резюме блока в картинках
Предыдущие действия производились в управляющем слое (control plane), а состояние сохранялось в etcd. Так кто же создаёт поды на узлах?
kubelet создаёт поды и следит за ними
Работа kubelet — опрашивать управляющий слой на предмет обновлений. Представьте, что он неустанно спрашивает у control plane: «Я присматриваю за рабочим узлом 1; есть ли для меня новый под?» Когда под появляется, kubelet создаёт его. Ну, вроде того.
kubelet не создаёт под самостоятельно. Вместо этого он делегирует работу трём другим компонентам:
Container Runtime Interface (CRI) создаёт контейнеры для пода.
Container Network Interface (CNI) подключает контейнеры к сети кластера и назначает им IP-адреса.
Container Storage Interface (CSI) монтирует тома в контейнерах.
В большинстве случаев Container Runtime Interface делает примерно то же, что и команда ниже:
$ docker run -d <my-container-image>
Container Networking Interface чуточку интереснее, поскольку отвечает за:
генерирование валидного IP-адреса для пода;
подключение контейнера к остальной сети.
Как вы понимаете, существует несколько способов подключить контейнер к сети и назначить ему валидный IP-адрес (IPv4 или IPv6, несколько IP-адресов).
Чтобы больше узнать о сетевых пространствах имён Linux и CNI, ознакомьтесь с этой статьёй о трассировке сетевого трафика в Kubernetes.
Когда Container Network Interface завершает свою работу, под подключается к остальной сети и получает корректный IP-адрес. Есть только одна проблема. kubelet знает об IP-адресе (потому что он, собственно, и запустил CNI), а вот управляющий слой о нём не в курсе. Никто не сообщил control plane, что под получил IP-адрес и готов принимать трафик. Так что управляющий слой продолжает считать, что под всё ещё создается.
Задача kubelet'а — собрать всю информацию о поде, такую как IP-адрес, и сообщить её управляющему слою. etcd подскажет, где запущен под и каков его IP-адрес.
Резюме блока в картинках
Если под не входит ни в один из сервисов, на этом путешествие заканчивается. Под создан и готов к работе. Если же под — часть сервиса, нужно выполнить ещё несколько шагов.
Поды, сервисы и эндпоинты
При создании сервиса обычно есть два момента, на которые следует обратить внимание:
Селектор для поиска подов, которые будут получать трафик.
TargetPort
— порт, через который поды будут получать трафик.
Типичное YAML-описание сервиса выглядит так:
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
ports:
- port: 80
targetPort: 3000
selector:
name: app
Когда вы применяете конфиг сервиса с помощью kubectl apply
, Kubernetes находит все поды с тем же лейблом, что прописан в селекторе name: app
, и собирает их IP-адреса — но только если они прошли проверку на готовность (то есть Readiness-пробу). Затем для каждого IP-адреса Kubernetes объединяет IP-адрес и порт.
Предположим, что IP-адрес равен 10.0.0.3
, а targetPort — 3000
. Тогда Kubernetes объединит их и получит то, что называется эндпоинтом или конечной точкой.
IP address + port = endpoint
---------------------------------
10.0.0.3 + 3000 = 10.0.0.3:3000
Эндпоинты хранятся в etcd в объекте под названием Endpoint.
В общем:
эндпоинт — это пара IP-адрес + порт (
10.0.0.3:3000
);Endpoint — это объект с набором эндпоинтов.
Endpoint — реальный объект. Kubernetes автоматически создаёт его для каждого сервиса. Убедитесь в этом сами:
$ kubectl get services,endpoints
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
service/my-service-1 ClusterIP 10.105.17.65 <none> 80/TCP
service/my-service-2 ClusterIP 10.96.0.1 <none> 443/TCP
NAME ENDPOINTS
endpoints/my-service-1 172.17.0.6:80,172.17.0.7:80
endpoints/my-service-2 192.168.99.100:443
$
Endpoint собирает все IP-адреса и порты из подов. Но это не разовое событие. Он обновляется всякий раз, когда:
под создаётся;
под удаляется;
меняется лейбл пода.
То есть всякий раз, когда под создаётся и kubelet отправляет его IP-адрес в управляющий слой, Kubernetes обновляет объект Endpoint, чтобы отразить это изменение:
$ kubectl get services,endpoints
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
service/my-service-1 ClusterIP 10.105.17.65 <none> 80/TCP
service/my-service-2 ClusterIP 10.96.0.1 <none> 443/TCP
NAME ENDPOINTS
endpoints/my-service-1 172.17.0.6:80,172.17.0.7:80,172.17.0.8:80
endpoints/my-service-2 192.168.99.100:443
$
Эндпоинт доставлен в управляющий слой, и объект Endpoint обновлён.
Резюме блока в картинках
Готовы начать использовать под? Но это ещё не всё.
Эндпоинты — «валюта» Kubernetes
Их используют различные компоненты K8s. Kube-proxy с помощью эндпоинтов настраивает правила iptables на узлах. Каждый раз, когда объект Endpoint меняется, kube-proxy получает новый список IP-адресов и портов и пишет новые правила iptables.
Резюме блока в картинках
Ingress-контроллер использует тот же список эндпоинтов. Ingress-контроллер — это компонент кластера, который направляет в него внешний трафик. При настройке манифеста Ingress в качестве получателя обычно указывается сервис:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
rules:
- http:
paths:
- backend:
service:
name: my-service
port:
number: 80
path: /
pathType: Prefix
Но на самом деле трафик не направляется в сервис. Вместо этого контроллер Ingress подписывается на уведомления об изменении эндпоинтов этого сервиса.
Ingress направляет трафик непосредственно к подам, минуя сервис. То есть всякий раз, когда происходит изменение объекта Endpoint, Ingress получает новый список IP-адресов и портов и переконфигурирует контроллер, подключая новые поды.
Резюме блока в картинках
Есть и другие компоненты Kubernetes, которые подписываются на изменения эндпоинтов. Например, CoreDNS, отвечающий за DNS в кластере. Если вы используете сервисы типа Headless, CoreDNS придётся подписываться на изменения эндпоинтов и перенастраиваться каждый раз, когда эндпоинт добавляется или удаляется. Те же самые эндпоинты используются сервисными сетями вроде Istio или Linkerd, облачными провайдерами для создания сервисов типа LoadBalancer и бесчисленными операторами.
Надо помнить, что на изменения эндпоинтов подписано несколько компонентов, и они могут получать уведомления об их обновлениях в разное время.
На этом всё, больше после создания пода ничего не происходит.
Вот краткое резюме того, что происходит при создании пода:
Под хранится в etcd.
Планировщик приписывает его к узлу. Узел записывается в etcd.
kubelet получает уведомление о новом поде, запланированном на узел.
kubelet делегирует создание контейнера интерфейсу Container Runtime Interface (CRI).
kubelet делегирует присоединение контейнера к сетевому интерфейсу Container Network Interface (CNI).
kubelet делегирует монтирование томов в контейнере интерфейсу Container Storage Interface (CSI).
Container Network Interface присваивает поду IP-адрес.
kubelet сообщает IP-адрес управляющему слою.
IP-адрес записывается в etcd.
Если под принадлежит к какому-либо сервису:
kubelet дожидается окончания успешной Readiness-пробы.
Все связанные эндпоинты (объекты Endpoint) получают уведомление об изменении.
Новый эндпоинт (пара IP-адрес + порт) добавляется в список эндпоинтов.
Kube-proxy получает уведомление об изменении эндпоинтов и обновляет правила iptables на всех узлах.
Контроллер Ingress получает уведомление об изменении эндпоинтов и направляет трафик на новые IP-адреса.
CoreDNS получает уведомление об изменении эндпоинтов. Если у сервиса (Service) тип Headless, обновляется запись DNS.
Облачный провайдер получает уведомление об изменении эндпоинтов. Если у сервиса тип LoadBalancer, новые эндпоинты добавляются в пул балансировщиков нагрузки.
Об изменении эндпоинтов узнаёт service mesh (если он установлен в кластере).
Любой другой оператор, подписанный на изменения эндпоинтов, также получает уведомление.
Согласитесь, длинный список для, казалось бы, обычной задачи — создания пода.
Итак, наш под запущен и работает. Пора поговорить о том, что произойдёт при его удалении.
Что происходит, когда вы удаляете под
Вы, наверное, уже догадались, что при удалении пода необходимо выполнить те же действия, но в обратном порядке.
Во-первых, нужно удалить эндпоинт из объекта Endpoint. Readiness-проба пока ещё показывает, что под в норме, но эндпоинт сразу же удаляется из управляющего слоя. Это, в свою очередь, запускает череду уведомлений kube-proxy, Ingress-контроллера, DNS, service mesh и т. д. Эти компоненты обновляют своё внутреннее состояние и прекращают направлять трафик на IP-адрес пода. Поскольку компоненты могут быть заняты чем-то другим, никто не знает, сколько времени потребуется для удаления IP-адреса из их внутреннего состояния. Для одних это занимает менее секунды, для других — больше.
Объяснение в картинках
В то же время статус пода в etcd меняется на Terminating. kubelet получает уведомление об изменении и «просит»:
Container Storage Interface — отмонтировать все тома от контейнера;
Container Network Interface — отключить контейнер от сети и освободить его IP-адрес;
Container Runtime Interface — удалить контейнер.
Объяснение в картинках
Как мы уже выяснили, при удалении пода Kubernetes выполняет те же шаги, что и при его создании, только в обратном порядке. Однако есть тонкое, но существенное отличие. При завершении работы пода удаление эндпоинта и уведомление kubelet'а происходят одновременно. И это может привести к целому ряду неприятных последствий. Что, если под перестанет существовать до того, как будет удалён его эндпоинт?
Объяснение в картинках
Как корректно завершать работу подов
Если под завершит свою работу до того, как его эндпоинт будет удалён из kube-proxy или Ingress-контроллера, может случиться простой. И, если вдуматься, в этом есть смысл.
Kubernetes по-прежнему отправляет трафик на IP-адрес, но пода там больше нет. Контроллер Ingress, kube-proxy, CoreDNS и т. д. не успели удалить IP-адрес из своего внутреннего состояния. В идеале Kubernetes должен дождаться, пока все компоненты кластера получат обновлённый список эндпоинтов, прежде чем удалять под. Только K8s так не работает.
Он предлагает действенные примитивы для распределения эндпоинтов (то есть объект Endpoint и более сложные абстракции вроде EndpointSlices). Однако Kubernetes не следит за тем, чтобы компоненты, подписавшиеся на изменения эндпоинтов, были в курсе актуального состояния кластера. Что же можно сделать, чтобы избежать проблем с преждевременным удалением пода?
Ответ очевиден — нужно подождать. Перед самым удалением под получает сигнал SIGTERM. Приложение может перехватить его и начать завершать работу. Маловероятно, что эндпоинт будет сразу удалён изо всех компонентов Kubernetes, поэтому можно:
Чуть подождать перед выходом.
Обрабатывать входящий трафик, несмотря на SIGTERM.
Закрыть существующие долгоживущие соединения (возможно, соединение с базой данных или WebSocket'ы).
Остановить процесс.
Давайте рассмотрим несколько примеров:
Пример на Go
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
sigs := make(chan os.Signal, 1)
done := make(chan bool, 1)
//registers the channel
signal.Notify(sigs, syscall.SIGTERM)
go func() {
sig := <-sigs
fmt.Println("Caught SIGTERM, shutting down")
// Finish any outstanding requests, then...
done <- true
}()
fmt.Println("Starting application")
// Main logic goes here
<-done
fmt.Println("exiting")
}
Пример на Python
import signal, time, os
def shutdown(signum, frame):
print('Caught SIGTERM, shutting down')
# Finish any outstanding requests, then...
exit(0)
if __name__ == '__main__':
# Register handler
signal.signal(signal.SIGTERM, shutdown)
# Main logic goes here
Пример на Node.js
const express = require('express');
const app = express();
app.listen(3000, () => console.log('Server is up using port 3000'));
process.on('SIGTERM', async () => {
await wait(15 * 1000)
app.close() // terminating the server
db.close() // closing any other connection
process.exit(0)
});
Пример на Java
public class App {
public static void main(String[] args) {
var shutdownListener = new Thread() {
public void run() {
// Main logic goes here
}
};
Runtime.getRuntime().addShutdownHook(shutdownListener);
}
}
Пример на С#
internal class LifetimeEventsHostedService: IHostedService {
private readonly IHostApplicationLifetime _appLifetime;
public LifetimeEventsHostedService(
ILogger < LifetimeEventsHostedService > logger,
IHostApplicationLifetime appLifetime,
TelemetryClient telemtryClient) {
_appLifetime = appLifetime;
}
public Task StartAsync(CancellationToken cancellationToken) {
_appLifetime.ApplicationStarted.Register(OnStarted);
_appLifetime.ApplicationStopping.Register(OnStopping);
_appLifetime.ApplicationStopped.Register(OnStopped);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) {
return Task.CompletedTask;
}
private void OnStarted() {}
private void OnStopping() {
// Main logic here
}
private void OnStopped() {}
}
Как долго нужно ждать?
По умолчанию Kubernetes отправляет сигнал SIGTERM
и ждёт 30 секунд, прежде чем принудительно завершить процесс. Первые 15 секунд можно продолжать работать как ни в чём не бывало. Этого времени должно хватить, чтобы информация об удалении эндпоинта дошла до kube-proxy, Ingress-контроллера, CoreDNS и т. п. Следовательно, всё меньше и меньше трафика будет поступать в под.
Через 15 секунд трафик иссякнет, и можно будет безопасно закрыть соединение с базой данных (или любое другое постоянное соединение) и завершить процесс. Если какой-либо компонент кластера не успел обновить список эндпоинтов за 15 секунд, время ожидания следует увеличить (скажем, до 20 или 25 секунд). Только стоит помнить, что Kubernetes принудительно завершит процесс через 30 секунд (если вы не меняли параметр terminationGracePeriodSeconds
в манифесте пода).
Что, если нельзя изменить код, чтобы он ждал SIGTERM
? Можно вызвать скрипт, который будет ждать определённое время, а затем позволит приложению завершить работу.
Корректное завершение работы с preStop-хуком
Перед тем как послать SIGTERM
, Kubernetes запускает хук preStop
. Его можно настроить на задержку в 15 секунд.
Давайте рассмотрим пример:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: web
image: nginx
ports:
- name: web
containerPort: 80
lifecycle:
preStop:
exec:
command: ["sleep", "15"]
Хук preStop
— один из хуков жизненного цикла пода. Важно отметить, что preStop-хук и ожидание средствами самого приложения — два разных подхода. Хук preStop запускается перед отправкой сигнала SIGTERM
в приложение. То есть во время работы хука приложение не имеет понятия, что оно вот-вот завершит работу.
Но это ещё не всё. Задержка в preStop-хуке входит в 30 секунд ожидания, прописанные в terminationGracePeriodSeconds
. Предположим, что хук ждёт 25 секунд. Тогда сразу после его завершения приложение получит сигнал SIGTERM
, и у него останется всего 5 секунд на завершение своей работы. Когда они истекут, kubelet пошлёт SIGKILL
.
Что произойдёт, если хук preStop будет ждать дольше 30 секунд? kubelet пошлёт сигнал SIGKILL
и принудительно завершит работу контейнера — сигнала SIGTERM
не будет. Чтобы увеличить время ожидания, поменяйте значение terminationGracePeriodSeconds
.
Резюме блока в картинках
Так какую же задержку поставить? 15, 60, 120 секунд? Однозначного ответа тут нет.
В случае спотовых инстансов максимальное ожидание, скорее всего, будет ограничено 60 секундами или менее. Если приложение перед завершением работы сохраняет логи или метрики, может потребоваться более длительный интервал. Как правило, время ожидания не должно превышать 30 секунд, поскольку более длительные интервалы влияют на использование ресурсов в кластере.
Давайте рассмотрим пример, иллюстрирующий этот сценарий.
Длительное завершение работы и автомасштабирование кластера
Kubernetes постоянно удаляет и запускает различные поды. Например, он делает это, когда выкатывается новая версия приложения или когда вы меняете образ в Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 3
selector:
matchLabels:
name: app
template:
metadata:
labels:
name: app
spec:
containers:
- name: app
# image: nginx:1.18 OLD
image: nginx:1.19
ports:
- containerPort: 3000
lifecycle:
preStop:
exec:
command: ["sleep", "15"]
Предположим, у вас три реплики приложения. После того как вы примените изменённый YAML-манифест Deployment, Kubernetes:
Создаст под с новым образом контейнера.
«Убьёт» старый под.
Дождётся готовности нового пода (то есть успешной Readiness-пробы).
Эти шаги он будет повторять до тех пор, пока все поды не обновятся. При этом он каждый раз будет дожидаться готовности нового пода.
Будет ли K8s ждать удаления старого пода, прежде чем перейти к следующему? Нет. Предположим, что у вас 10 подов и каждому достаточно 2 секунд, чтобы стать Ready, а завершение работы занимает 20 секунд. В этом случае произойдёт следующее:
K8s создаст первый новый под и завершит работу старого.
Под станет Ready через 2 секунды, после чего Kubernetes запустит ещё один под.
В то же время старый под будет продолжать завершать свою работу.
Через 20 секунд все новые поды будут готовы, а все старые окажутся в состоянии Terminating (у первого как раз закончится период ожидания). Другими словами, в кластере на короткое время будет вдвое больше подов, чем необходимо.
Чем длительнее период завершения работы и короче время старта, тем больше будет подов, работающих параллельно. Одни из них — в статусе Ready, другие — в Terminating. Страшно ли это? Нет, если не обрывать соединения.
Но что, если повторить эксперимент с более долгим периодом ожидания (120 секунд) и бóльшим числом реплик (40 штук)?
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 40
selector:
matchLabels:
name: app
template:
metadata:
labels:
name: app
spec:
containers:
- name: app
# image: nginx:1.18 OLD
image: nginx:1.19
ports:
- containerPort: 3000
lifecycle:
preStop:
exec:
command: ["sleep", "120"]
terminationGracePeriodSeconds: 180
В этом случае Kubernetes завершит развёртывание, но на протяжении примерно 120 секунд у вас будет 80 работающих реплик: 40 новых и 40 старых. Такое удвоение реплик может привести к срабатыванию автоскейлера, который создаст новые узлы в кластере. Затем, когда 40 реплик завершат свою работу, узлы придётся удалять. Если период ожидания для корректного завершения работы короче (меньше 30 секунд), старые поды будут удаляться параллельно с созданием новых. То есть число одновременно работающих подов будет меньше.
Но есть и другая причина стремиться к более короткому времени ожидания, и она касается эндпоинтов. Если приложение экспортирует метрики через /metrics
, то данные вряд ли будут собираться во время корректного завершения работы. Почему?
Инструменты вроде Prometheus полагаются на объекты Endpoint при сборе метрик с подов в кластере. Однако при удалении пода сразу удаляется и его эндпоинт. Хотя это и занимает некоторое время, в конечном итоге информация о том, что эндпоинт удалён, доходит и до Prometheus! Другими словами, период ожидания следует рассматривать как возможность корректно завершить работу пода как можно скорее, а не пытаться продлить срок его существования для завершения текущей задачи.
Корректное завершение работы для долгоживущих соединений и задач
Если приложение работает с долгоживущими соединениями вроде WebSocket, закрыть их в течение 30 секунд — не вариант. Такое соединение, скорее всего, имеет смысл держать открытым как можно дольше — в идеале до тех пор, пока клиент не отключится. Аналогично, если вы, например, перекодируете большое видео, вас вряд ли устроит, если скользящее обновление удалит под и кучу часов работы вместе с ним.
Но как избежать принудительного «убийства» пода? Можно увеличить terminationGracePeriodSeconds
до трёх часов в надежде, что к тому моменту работа будет выполнена, а соединение разорвано. Однако тут есть свои тонкости:
Вы не сможете собирать метрики с помощью Prometheus (так как эндпоинт будет удалён).
Отладка станет ещё сложнее, поскольку Running- и Terminating-поды могут быть разных версий.
kubelet не будет проверять Liveness-пробу (если процесс зависнет, никто об этом не узнает).
Вместо увеличения периода ожидания стоит подумать о создании нового Deployment для каждого релиза. При его создании существующий Deployment остаётся нетронутым. Долговременные задачи продолжат обрабатывать видео в обычном режиме, а долгоживущие соединения останутся активными. Потом старый Deployment можно будет удалить вручную.
Чтобы поды удалялись автоматически, можно настроить автоскейлер на масштабирование Deployment до нуля реплик после завершения задач. Пример такого автоскейлера — KEDA, событийно-ориентированный автомасштабировщик Kubernetes. Этот метод иногда называют rainbow-развёртыванием, и он полезен в тех случаях, когда простое увеличение terminationGracePeriodSeconds
не даст желаемого результата.
Создание нового Deployment для каждого релиза — менее очевидный, но более подходящий выбор.
Резюме
При удалении подов из кластера не забывайте о том, что на их IP-адреса может по-прежнему поступать трафик.
Вместо того чтобы завершать работу пода мгновенно, стоит задуматься о том, как сделать процесс более плавным, реализовав соответствующую функциональность в приложении или воспользовавшись preStop-хуком.
Под следует удалять только после того, как обновлённая информация об эндпоинтах распространится по всему кластеру и старый эндпоинт удалится из kube-proxy, Ingress-контроллеров, CoreDNS и т. д.
Если поды выполняют затяжные задачи (например, перекодируют видео или обновляют какие-либо данные в реальном времени через WebSockets), подумайте об использовании rainbow-развёртываний. Суть этого метода в том, что под каждый релиз создаётся новый Deployment, а старый удаляется, когда все задачи завершены, а подключения закрыты (сделать это можно вручную).
Также можно масштабировать старый Deployment до нуля реплик, тем самым автоматизировать процесс.
P. S.
Читайте также в нашем блоге:
Комментарии (5)
jenyabykov
25.10.2024 10:24Начиная с Kubernetes 1.30 больше не нужно вызывать команду `sleep N` в контейнере и хранить бинариник sleep для preStop hook: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/
lifecycle: preStop: sleep: seconds: 15
Кстати, использовать лучше не
command: ["sleep", "15"]
, а что-то вида `/bin/sh -c /bin/sleep 15` т.к. не во всех образах корректно задана переменная $PATHДополнительно: Если используете HPA, уберите секцию replicas из ваших Deployment: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#migrating-deployments-and-statefulsets-to-horizontal-autoscaling (как альтернативное решение: выставьте replicas в максимальный лимит реплик HPA)
olku
25.10.2024 10:24K8s создаст первый новый под и завершит работу старого.
Уточнение - пошлет сигнал SIGTERM.
Про вебсокеты интересно. В общем случае мы не знаем сколько времени клиент их может использовать. Если это потоковый обмен с другой системой, то ждать окончания бессмысленно. Самое простое, не давать клиенту гарантий, пусть переподключается сам. Но если дать гарантию хочется, мы же облако, как реализуется неразрываемый вебсокет?
gozoro
25.10.2024 10:24Пару недель назад тоже наткнулся на эту статью на Learnk8s. Очень удивился, что в таком комбайне нужно делать sleep 15 с надеждой, что через это время запросы перестанут идти на завершающийся Под.
Я правильно понимаю, что другого решения не было. Какое-нибудь отслеживание iptables перед отправкой SIGTERM приводило бы к более глубокому пересечению завершающихся и стартующих процессов и это бы съедало бы больше ресурсов. И разработчики k8s решили отдать это решение пользователям.
slava_k
Спасибо за статью!