Проекты искусственного интеллекта 2023: бесплатно, бесплатно и открыто
До и во время 2021 год, когда Бум искусственного интеллекта (ИИ) Это еще не произошло в сфере обычных пользователей, мы уже выложили много технического и информативного контента о текущем состоянии этих технологий и их потенциале.
И с тех пор, В течение 2022 года эта веха, без сомнения, была достигнута.Ну а сегодня мы прокомментируем некоторые известные «Проекты искусственного интеллекта», бесплатно, бесплатно и открыто, стоит знать в 2023 год.
Периодически начинает возвращать 502 ошибку, хотя сам под работает и в статусе Running.
Что бы рассмотреть, как и почему Ingress и Service могут возвращать 502, и как работают readinessProbe и livenessProbe в Kubernetes Deployment – напишем простой веб-сервер на Go, в котором опишем два ендпоинта – один будет возвращать нормальный ответ, а во втором – выполнение программы будет прерываться.
Затем задеплоим его в AWS Elastic Kubernetes, создадим Kubernetes Ingress, который создаст AWS Application Load balancer, и потрестируем работу приложения.
Golang HTTP server
Пишем приложение на Go, которое потом упакуем в Docker-контейнер, и запустим в Kubernetes:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request){
fmt.Fprintf(w, "pong")
})
http.HandleFunc("/err", func(w http.ResponseWriter, r *http.Request){
panic("Error")
})
fmt.Printf("Starting server at port 8080n")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
kubectl apply -f go-http.yaml
deployment.apps/go-http created
service/go-http-svc created
ingress.extensions/go-http-ingress created
Проверяем под:
kubectl get pod -l app=go-http
NAME READY STATUS RESTARTS AGE
go-http-8dc5b4779-7q4kw 1/1 Running 0 8s
И его Ingress:
kubectl get ingress -l app=go-http
NAME HOSTS ADDRESS PORTS AGE
go-http-ingress * e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com 80 31s
Ждём, пока наш DNS увидит новый URL e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com, и проверяем работу приложения – выполняем запрос к /ping:
curl -I e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/ping
HTTP/1.1 200 OK
Kubernetes Ingress 502
А теперь – обращаемся к ендпоинту /err, который в Go-приложении вызовет panic, и ловим 502 ошибку:
curl -vI e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/err
...
< HTTP/1.1 502 Bad Gateway
HTTP/1.1 502 Bad Gateway
< Server: awselb/2.0
Server: awselb/2.0
Тут всё логично – load-balancer отправил наш запрос к поду, под не ответил (вспомним curl: (52) Empty reply from server в наших первых тестах), и мы получили ответ 502 от балансировщика aka Ingress.
readinessProbe и livenessProbe
Теперь посмотрим, как изменение в readinessProbe и livenessProbe повлияют на ответы Ingress и работу самого пода.
kubectl get pod -l app=go-http
NAME READY STATUS RESTARTS AGE
go-http-5bd557544-2djcw 0/1 Running 0 4s
Если мы теперь отправим запрос даже на ендпоинт /ping – всё-равно получим 502, т.к. бекенд Сервиса, т.е. под, не принимает трафик, потому что не прошёл readinessProbe:
kubectl get pod -l app=go-http -o json | jq '.items[].status.containerStatuses[].ready'
false
Пробуем:
curl -I e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/ping
HTTP/1.1 502 Bad Gateway
Вернём readinessProbe в /ping, что бы трафик на под пошёл, но изменим livenessProbe – зададим path в /err, а initialDelaySeconds и periodSeconds установим в 15 секунд, плюс добавим failureThreshold равным одной попытке:
Теперь после запуска пода Kubernetes выждет 15 секунд, затем выполнит livenessProbe и будет повторять её каждые следующие 15 секунд.
Передеплоиваем, и проверяем:
kubectl get pod -l app=go-http
NAME READY STATUS RESTARTS AGE
go-http-78f6c66c8b-q6fkf 1/1 Running 0 6s
Проверяем запрос к ендпоинту /ping:
curl -I e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/ping
HTTP/1.1 200 OK
Всё хорошо.
И /err нам ожидаемо вернёт 502:
curl -I e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/err
HTTP/1.1 502 Bad Gateway
Через 15 секунд, после выполнения первой live проверки – под будет перезапущен:
kubectl get pod -l app=go-http
NAME READY STATUS RESTARTS AGE
go-http-668c674dcb-4db9x 0/1 Running 1 19s
Events этого пода:
kubectl describe pod -l app=go-http
...
Normal Created 6s (x4 over 81s) kubelet, ip-10-3-55-229.us-east-2.compute.internal Created container go-http
Warning Unhealthy 6s (x3 over 66s) kubelet, ip-10-3-55-229.us-east-2.compute.internal Liveness probe failed: Get http://10.3.53.103:8080/err: EOF
Normal Killing 6s (x3 over 66s) kubelet, ip-10-3-55-229.us-east-2.compute.internal Container go-http failed liveness probe, will be restarted
Normal Started 5s (x4 over 81s) kubelet, ip-10-3-55-229.us-east-2.compute.internal Started container go-http
Контейнер в поде не прошёл проверку livenessProbe, и Kubernetes перезапускает под в попытке “починить” его.
Если получится попасть на сам момент перезапуска контейнера – увидим стаус CrashLoopBackOff, а запрос к /ping снова вернёт нам 502:
kubectl get pod -l app=go-http
NAME READY STATUS RESTARTS AGE
go-http-668c674dcb-4db9x 0/1 CrashLoopBackOff 4 2m21s
Выводы
Используем readinessProbe для проверки того, что приложение запустилось, в данном случае – Go-бинарник начал прослушивать порт 8080, и на него можно направлять трафик, и используем livenessProbe во время работы пода для проверки того, что приложение в нём все ещё живо.
Если приложение начинает отдавать 502 на определённые запросы – то следует поискать проблему именно в запросах, т.к. если бы была проблема в настройках Ingress/Service – получали бы 502 постоянно.
Самое важное – понимать принципиальную разницу между readinessProbe и livenessProbe:
если фейлится readinessProbe – процесс aka контейнер в поде останется в том же состоянии, в котором был на момент сфейленой ready-проверки, но под будет отключен от трафика к нему
если фейлится livenessProbe – трафик на под продолжает идти, но контейнер будет перезапущен
Итак, имеем ввиду, что:
Если readinessProbe не задана вообще – kubelet будет считать, что под готов к работе, и направит к нему трафик сразу после старта пода. При этом если на запуск пода уходит минута – то клиенты, которые к нему были направлены после его запуска, будут ждать эту минуту, пока он ответит.
Если приложение ловит ошибку, которую не может обработать – оно должно завершить свой процесс, и Kubernetes сам перезапустит контейнер.
Используем livenessProbe для проверки состояний, которые нельзя обработать в самом приложении, например – deadlock или бесконечный цикл, при которых контейнер не может ответить на ready-проверку. В таком случае если нет livenessProbe, которая может перезапустить процесс, то под будет отключен от Service – но останется в статусе Running, продолжая потреблять реурсы WorkerNode.
Для управления контейнерами на Kubernetes WorkerNodes испольузется Docker.
Pod Lifecycle – Termination of Pods
Посмотрим, как вообще происходит процесс остановки и удаления подов.
Итак, под – это процесс(ы), запущенные на WorkerNode, для остановки которых используются стандартные сигналы IPC (Inter Process Communication).
Что бы дать возможность поду (процессу/ам) закончить все его текущие операции – система управления контейнерами (container runtime) пытается мягко завершить его работу (graceful shutdown), отправляя сигнал SIGTERM процессу с PID 1 в каждом контейнере этого пода (см. docker stop). При этом, кластер запускает отсчёт grace period перед тем, как жёстко убить под отправкой сигнала SIGKILL.
При этом, можно переопределить сигнал для мягкой остановки используя STOPSIGNAL в образе, из которого запускался контейнер.
Итак, процесс удаления пода выглядит следующим образом:
мы выполняем kubectl delete pod или kubectl scale deployment – запускается процесс удаления подов и стартует таймер отсчёта grace period с дефолтным значением в 30 секунд
API-сервер обновляет статус пода – из Running он становится Terminating (см. Container states). На WorkerNode, на которой этот под запущен, kubelet получает обновление статус этого пода и начинает процесс его остановки:
если для контейнера(ов) в поде есть preStop hook – kubelet его выполняет. Если хук продолжает выполнение после истечения grace period – добавляется ещё 2 секунды на его завершение. При необходимости можно изменить дефолтные 30 секунд используя terminationGracePeriodSeconds
после завершения preStop хука – kubelet отправляет указание Docker на остановку контейнеров, и Docker отправляет сигнал SIGTERM процессу с PID 1 в каждом контейнере. При этом контейнеры в поде получают сигнал в случайном порядке.
одновременно с началом процесса graceful shutdown – Kubernetes Control Plane (его kube-controller-manager) удаляет останавливаемый под из списка ендпоинтов (см. Kubernetes – Endpoints), и соответствующий Service перестаёт отправлять новые подключения на останавливаемый под
по завершению grace period, kubelet триггерит force shutdown – Docker отправляет сигнал SIGKILL всем оставшимся процесам во всех контейнерах пода, который они проигнорировать не могут, и мгновенно умирают, аварийно завершая все свои операции
kubelet триггерит удаление объекта пода из API-сервера
API-сервер удаляет запись о поде из базы, и под становится недоступен
Наглядная табличка:
Собственно, тут возникает две проблемы:
сам NGINX и PHP-FPM воспринимают SIGTERM как “жестокое убийство”, и завершают работу немедленно, не заботясь о корректном завершении текущих подключений (см. Controlling nginx и php-fpm(8) – Linux man page)
шаги 2 и 3 – отправка SIGTERM и удаление ендпоинта – выполняются одновременно. Однако, обновление данных в Ingress Service происходит не моментально, и под может начать умирать раньше, чем Ingress перестанет на него слать трафик, соотвественно – получим 502, т.к. процесс в поде уже не может принимать подключения
kubectl apply -f test-deployment.yaml
namespace/test-namespace created
deployment.apps/test-deployment created
service/test-svc created
ingress.extensions/test-ingress created
Проверяем Ingress:
curl -I aadca942-testnamespace-tes-5874-698012771.us-east-2.elb.amazonaws.com
HTTP/1.1 200 OK
Тут выполняем 1 запрос в секунду к подам за нашим Ingress (и NGINX в каждом будет ждать 10 секунд ответа от своего “бекенда” перед тем, как ответить 200 нашему Танку).
И всё-таки, если повторить тесты несколько раз, то иногда 502 ошибки всё-таки проскакивают:
Тут мы уже сталкиваемся со второй проблемой – обновление ендпоинтов выполняется параллельно с отправкой SIGTERM.
Добавим preStop хук со sleep, что бы дать время на обновление ендпоинтов – тогда после получения команды на удаление пода, kubelet выждет 5 секунд перед отправкой SIGTERM, за которые Ingress успеет обновить список ендпоинтов:
Но не помогло. Не очень понятно почему, потому как идея вроде бы правильная – вместо того, что бы ожидать TERM от Kubernetes/Docker – мы сами мягко стопаем NGINX, отправляя ему QUIT.
Но можно попробовать.
NGINX + PHP-FPM, supervisord и stopsignal
Наше приложение работает в двух отдельных контейнерах NGINX и PHP-FPM.
Но в процессе поиска решения пробовал использовать образ, в котором оба сервиса собраны в одном образе, например trafex/alpine-nginx-php7.
В нём пробовал добавлять параметр stopsignal и для NGINX, и для PHP-FPM со значением QUIT – но тоже не помогло, хотя идея тоже вроде правильная.
В принципе, можно было бы указать приложению отправлять заголовок [Connection: close] – тогда клиент по завершении передачи данных закрывал бы соединение, и это уменьшило бы вероятность 502-х. Но они всё равно будут, если NGINX получит SIGTERM в процессе обработки запроса от клиента, а т.к. у нас “сзади” ещё PHP, который ходит в базу – то некоторые запросы могут обрабатываться по 5-10 и более секунд.
Создание и удаление подов — распространенная задача при работе с Kubernetes. Новые поды создаются, когда вы выполняете плавающее обновление, масштабируете развертывание и релизите новую функциональность, а также при выполнении cron и других задач. Еще поды пересоздаются при каждом удалении и внесении изменений, например, когда узел помечается как непланируемый (unschedulable).
Graceful shutdown — предсказуемое окончание работы системы, когда все запущенные процессы корректно завершают работу без потери данных или негативного пользовательского опыта.
Zero downtime deploy — нулевой простой во время развертывания новой версии приложения. Пользователь не заметит его недоступности.
Чтобы лучше понимать, что происходит при удалении пода, давайте сначала разберемся, как его создают. Предположим, вы создаете в кластере следующий под:
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
После ее выполнения kubectl передает описание пода в Kubernetes API.
Сохранение состояния кластера в базе данных
API Kubernetes получает YAML-описание пода, проверяет его и сохраняет в базе данных — etcd. Также под добавляется в очередь заданий планировщика.
Что делает планировщик:
Проверяет описание в YAML-файле.
Собирает сведения о нагрузке на процессор и запрошенную память контейнера.
Под помечается как запланированный (scheduled) в etcd.
Для пода определяется узел.
Состояние пода сохраняется в etcd.
Но сам под пока не создан!
При использовании команды kubectl apply -f YAML-файл отправляется в Kubernetes API
API сохраняет под в базе данных — etcd
Планировщик выбирает оптимальный узел для создания пода, статус пода изменяется на «Ожидание». Но под пока существует только в etcd
Создание пода в кластере Kubernetes
Предыдущие задачи выполнялись в Control Plane, а состояние сохранилось в базе данных. Кто же создает под? Kubelet — агент Kubernetes. Его задача — опрашивать об обновлениях Control Plane. При этом kubelet не создает контейнеры самостоятельно, а делегирует эту работу трем другим компонентам:
The Container Runtime Interface (CRI) создает контейнер для подов.
The Container Network Interface (CNI) соединяет контейнеры с сетью кластера и назначает IP-адреса.
The Container Storage Interface (CSI) монтирует тома в ваших контейнерах.
В большинстве случаев The Container Runtime Interface (CRI) выполняет работу, аналогичную команде по запуску контейнера в фоновом режиме:
$ docker run -d <my-container-image>
The Container Network Interface (CNI) устроен немного иначе, он отвечает за:
создание валидного IP-адреса для пода;
подключение контейнера к остальной сети.
Существует несколько способов подключить контейнер к сети и назначить ему валидный IP-адрес. Можно выбрать между IPv4 или IPv6, или назначить несколько IP-адресов.
Например, Docker создает пары виртуальных сетей Ethernet и подключает их к сети по типу мостов (bridge). AWS-CNI подключает под напрямую к остальной части виртуального частного облака (Virtual Private Cloud/VPC).
Когда CNI завершает работу, под подключается к остальной сети и получает валидный IP-адрес. Но есть проблема: kubelet знает об IP-адресе, поскольку это он вызвал Container Network Interface, а вот Control Plane не знает. Никто не сообщил главному узлу, что у пода появился IP-адрес и он готов к приему трафика — для Control Plane под все еще создается.
Так что задача kubelet — собрать информацию о поде, например IP-адрес, и передать их на уровень Control Plane. После этого через проверку etcd можно не только посмотреть, где работает под, но и узнать его валидный IP-адрес.
Kubelet выясняет у Control Plane, появились ли обновления
После того как для нового пода назначен узел, kubelet считывает информацию по поду
Kubelet не создает контейнер самостоятельно, а делегирует эту работу трем другим компонентам: Container Runtime Interface, Container Network Interface и Container Storage Interface
Как только все три компонента успешно выполнены, под запускается на узле и ему присваивается IP-адрес
Kubelet сообщает IP-адрес пода в Control Plane
Если под не является частью какого-либо сервиса, то под создан и готов к использованию. Если же он часть cервиса, то нужно выполнить еще несколько шагов, о них рассказываем дальше.
Поды и сервисы
Обычно при создании cервиса нужно обратить внимание на несколько полей:
selector — указывает, на какие поды будет направлен трафик.
targetPort — порт, который поды используют для приема трафика.
Пример стандартного YAML-файла для описания сервиса:
После выполнения команды <kubectl apply.> Kubernetes находит все поды с такой же меткой, что и переданное значение в selector (name: app), и собирает их IP-адреса. При этом поды обязательно должны пройти проверку readiness-пробы.
Каждый IP-адресc конкатенируется с переданным портом для формирования endpoint. Например, если IP-адрес — 10.0.0.3, а targetPort — 3000, то Kubernetes конкатенирует два значения и формирует единый enpdoint.
IP address + port = endpoint
---------------------------------
10.0.0.3 + 3000 = 10.0.0.3:3000
Перечень endpoint хранится в etcd в специальном объекте — Endpoint.
Примечание. В Kubernetes есть два похожих по названию компонента: первый называется endpoint — это пара IP-адрес и порт (10.0.0.3:3000), а второй — Endpoint, то есть перечень endpoint.
Объект Endpoint — реальный объект в Kubernetes, оркестратор создает его автоматически для каждого сервиса 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
endpoints/my-service-2 192.168.99.100:8443
$
Объект Endpoint хранит все IP-адреса и порты пода и обновляется каждый раз, когда:
вы создаете под;
вы удаляете под;
метка пода изменяется.
Итак, Kubernetes обновляет все Endpoint после создания пода и после отправки IP-адреса главному узлу. Проверить это можно командой:
$ 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:8443
$
Отлично, endpoint передан в Control Plane, а объект Endpoint обновлен.
В вашем кластере развернут один под, он относится к сервису. В etcd можно найти подробную информацию как о поде, так и о сервисе
Что происходит при развертывании нового пода?
Kubernetes отслеживает под и его IP-адрес, сервис направляет трафик к новому endpoint. Так что IP-адрес и порт обновятся по всей системе
Что происходит при развертывании еще одного пода?
Процесс такой же: в базе данных появляется новая строка для пода, а новый endpoint распространяется по всей системе
Но что происходит, когда под удаляется?
Сервис немедленно удаляет endpoint, и под удаляется из базы данных
Kubernetes реагирует на любое небольшое изменение в кластере
Готов ли теперь под к использованию? Осталось еще кое-что!
Использование endpoint в Kubernetes
Endpoint используют несколько компонентов Kubernetes.
Kube-proxy использует endpoint, чтобы настраивать iptables-правила на узлах. Каждый раз, когда объект Endpoint меняется, kube-proxy получает новый список IP-адресов и портов и записывает новые iptables-правила.
Рассмотрим трехузловой кластер с двумя подами без сервисов. Состояние подов хранится в etcd
Что происходит, когда вы создаете cервис?
Kubernetes создает объект Endpoint и собирает все endpoint (пары IP-адресов и портов) из подов
Демон Kube-proxy подписан на изменения в Endpoint
Когда Endpoint добавляется, удаляется или обновляется, kube-proxy считывает новый список endpoint
Kube-proxy использует endpoint для создания iptables-правил на каждом узле кластера
Ingress-контроллер — компонент кластера, он направляет внешний трафик в кластер и использует тот же самый список endpoint. При настройке Ingress-манифеста в поле destination обычно указывают сервис. Вот пример такого файла:
На самом деле трафик на сервис не направляется. Вместо этого Ingress-контроллер подписывается на изменения Endpoint этого сервиса.
Ingress направляет трафик непосредственно к поду, не используя сервис. Каждый раз, когда Endpoint объекта меняется, Ingress извлекает новый список IP-адресов и портов и перенастраивает контроллер для подключения новых подов.
На изменения endpoint подписываются и другие компоненты Kubernetes, например CoreDNS — компонент DNS в кластере. Если вы используете сервис типа Headless, CoreDNS подписывается на изменения endpoint и перенастраивает себя после каждого изменения. Эти же endpoint используют Service Mesh, — например Istio или Linkerd, — поставщики облачных услуг для создания сервисов типа LoadBalancer и другие службы Kubernetes.
Несколько компонентов подписываются на изменение endpoint и могут получать уведомления об этом в разное время.
Рассмотрим Ingress-контроллер с развертыванием из двух реплик и сервисом
Если вы хотите направить внешний трафик к подам через Ingress, то отразите это в Ingress-манифесте (YAML-файле)
Как только выполнится команда <kubectl apply -f ingress.yaml>, Ingress-контроллер получит файл от Control Plane
В YAML-описании Ingress есть свойство serviceName, оно описывает, какой сервис должен быть использован
Ingress-контроллер получает список endpoint из сервиса и пропускает его. Трафик направляется непосредственно к endpoint подов
Что происходит при создании нового пода?
Вы уже знаете, как Kubernetes создает под и распространяет его endpoint
Ingress-контроллер подписывается на изменения endpoint. После входящего изменения он получает новый список endpoint
Ingress-контроллер направляет трафик к новому поду
Промежуточный итог
Вот краткий обзор того, что происходит при создании пода, если он не относится к сервису:
Под сохраняется в etcd.
Планировщик определяет узел и записывает этот узел в etcd.
Kubelet получает уведомление о новом и запланированном поде.
Kubelet делегирует создание контейнера The Container Runtime Interface (CRI).
Kubelet делегирует добавление контейнера The Container Network Interface (CNI).
Kubelet делегирует монтирование томов в контейнере The Container Storage Interface (CSI).
CSI назначает IP-адрес.
Kubelet сообщает IP-адрес Control Plane.
IP-адрес сохраняется в etcd.
Если же под относится к сервису:
Kubelet ждет успешного выполнения readiness-пробы.
Все соответствующие Endpoint (объекты) уведомляются об изменении.
В список Endpoint добавляются новые endpoint (пара IP-адрес + порт).
Kube-proxy получает уведомление об изменении endpoint. Kube-proxy обновляет iptables-правила для каждого узла.
Ingress-контроллер уведомляется об изменении endpoint, направляет трафик на новые IP-адреса.
CoreDNS получает уведомление об изменении endpoint. Если сервис headless, то запись DNS обновляется.
Облачный провайдер получает уведомление об изменении endpoint. Если тип Service — LoadBalancer, то новый endpoint становится частью балансировщика нагрузки.
Все служебные сети, установленные в кластере, получают уведомление об изменении endpoint.
Любая другая функциональность, подписанная на изменения endpoint, также получает уведомление.
Удаление пода
При удалении пода те же действия выполняют в обратном порядке:
Сначала необходимо удалить endpoint из Endpoint (объекта).
Readiness-проба игнорируется, и endpoint сразу удаляется из Control Plane. Это, в свою очередь, запускает все события в kube-proxy, Ingress-контроллере, DNS, service mesh и т. д.
Перечисленные компоненты обновляют свое внутреннее состояние и прекращают маршрутизацию трафика на IP-адрес.
Поскольку компоненты могут быть заняты другими задачами, непонятно, сколько времени потребуется для удаления IP-адреса из их внутреннего состояния.
При удалении пода команда сначала попадает в API Kubernetes
Сообщение перехватывает Endpoint-контроллер в Control Plane
Endpoint-контроллер отправляет команду в API для удаления IP-адреса и порта из Endpoint-объекта
Кто следит за изменениями в Endpoint? Об изменениях уведомляют Kube-proxy, Ingress-контроллер, CoreDNS и другие компоненты
Некоторым компонентам, например kube-proxy, может понадобиться дополнительное время для дальнейшего оповещения об изменениях
Kubelet уведомляется об изменении и делегирует:
CSI — размонтирование любых томов из контейнера.
CNI — отсоединение контейнера от сети и передачу IP-адреса.
CRI — уничтожение контейнера.
Таким образом, Kubernetes выполняет точно такие же шаги, как и при создании пода, но в обратном порядке.
При удалении пода команда сначала попадает в API Kubernetes
Kubelet опрашивает Control Plane на наличие обновлений и понимает, что под удален
Есть небольшое, но существенное различие. Когда вы завершаете работу пода, удаление endpoint и сигнал для kubelet отправляется одновременно. Когда вы создаете под в первый раз, Kubernetes ждет, пока kubelet сообщит IP-адрес, а затем оповещает систему об изменении endpoint систему. Но при удалении пода события начинаются параллельно, и это может привести систему в состояние гонки.
Что, если под будет удален до распространения новых endpoint?
Удаление endpoint и пода происходит одновременно
Значит, вы можете удалить endpoint до того, как kube-proxy обновит iptables-правила
Или вам может повезти и под удалится только после того, как endpoint изменится во всей системе
Graceful shutdown
Если работа пода завершается до того, как endpoint удален из kube-proxy или Ingress-контроллера, система может перейти в режим ожидания. И, если подумать, это имеет смысл.
Kubernetes по-прежнему направляет трафик на IP-адрес, но пода больше нет. Компоненты Ingress-контроллер, kube-proxy, CoreDNS и другие компоненты не успели удалить этот IP-адрес из своей памяти.
В идеальном мире Kubernetes проверяет, что все компоненты в кластере получили обновленный список endpoint, и только после этого удаляет под. Но Kubernetes так не работает. Он предлагает надежные примитивы для управления endpoint. Endpoint-объект и более продвинутые абстракции, например, Endpoint Slices. Однако Kubernetes не проверяет, соответствуют ли компоненты, подписывающиеся на изменения endpoint, актуальному состоянию кластера.
Итак, как же убедиться, что под удален после изменения endpoint?
Перед удалением под получает сигнал SIGTERM. Ваше приложение может перехватить его и начать завершение работы. Маловероятно, что endpoint будет немедленно удалена из всех компонентов Kubernetes. Так что вы можете:
Немного подождать.
Продолжить обрабатывать входящий трафик, несмотря на SIGTERM.
Закрыть существующие долгоживущие соединения (например, соединение с базой данных или WebSockets).
Завершить процесс.
По умолчанию Kubernetes отправляет сигнал SIGTERM и ждет 30 секунд перед принудительным завершением процесса.
Таким образом, можно подождать 15 секунд перед завершением работы. Вероятно, этого времени хватит для удаления endpoint из kube-proxy, Ingress-контроллера, CoreDNS и т. д. И меньше трафика будет перенаправлено на под до его отключения. Через 15 секунд можно безопасно закрыть соединение с базой данных (или любыми постоянными соединениями) и завершить процесс.
Примечание. Если вы считаете, что вам нужно больше времени, то можете остановить процесс через 20 или 25 секунд. Но помните: Kubernetes принудительно завершит процесс через 30 секунд, если вы не измените terminationGracePeriodSeconds.
Что делать, если вы не можете изменить код, чтобы управлять процессом завершения?
Вы можете реализовать сценарий, при котором приложение ждет фиксированное время перед завершением работы. Например, перед отправкой команды SIGTERM Kubernetes вызывает хук preStop в поде. Можно настроить 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"]
Зависит от обстоятельств, но можно оттолкнуться от этого времени и затем подобрать оптимальное в процессе тестирования. Возможные варианты:
Вы уже знаете, что при удалении пода kubelet получает уведомление об изменении
Если у пода есть preStop-хук, он будет вызван первым
Когда preStop завершается, kubelet отправляет в контейнер сигнал SIGTERM. С этого момента контейнер должен закрыть все long-lived-соединения и подготовиться к завершению
По умолчанию у процесса есть 30 секунд для завершения работы, включая хук preStop. Если к этому времени процесс не завершится, то kubelet отправит сигнал SIGKILL и принудительно завершит процесс
Kubelet уведомляет Control Plane об успешном удалении пода
Grace periods и скользящие обновления
При удалении пода применяется плавное завершение работы. Но как быть, если под так и не удален? Даже если вы этого не сделаете, Kubernetes все равно удаляет под.
Kubernetes создает и удаляет под каждый раз, когда вы развертываете новую версию приложения. Когда вы изменяете образ в своем развертывании, Kubernetes разворачивает изменение постепенно.
Допустим, у вас есть три реплики, и как только вы отправляете новый YAML-файл, Kubernetes:
создает под с новым образом контейнера;
уничтожает существующий контейнер;
дожидается готовности пода.
Система повторяет эти шаги до тех пор, пока все поды не будут обновлены. Kubernetes повторяет каждый цикл только после того, как новый под готов к приему трафику, то есть прошел проверку readiness-пробы.
Ждет ли Kubernetes удаления текущего пода перед переходом к следующему?
Нет, если у вас 10 подов и для одного требуется две секунды на подготовку и 20 на завершение работы, то происходит следующее:
Первый под создается, а предыдущий под прекращает работу.
Новый под готов через две секунды, после чего Kubernetes создает новый.
Находясь в процессе завершения, под продолжает жить в течение 20 секунд.
Через 20 секунд все новые поды становятся активными, а все 10 предыдущих подов завершают работу. В общей сложности количество подов удваивается на некоторое время: одновременно работают 10 запущенных и 10 завершающихся подов.
Чем дольше graceful-период по сравнению с readiness-пробой, тем больше подов будет запущено (и завершено) одновременно.
Это плохо?
Не обязательно, пока вы не обрываете соединения.
Прерывание long-running-задач
А как насчет длительной работы? Если вы кодируете большое видео, есть ли способ отложить завершение пода?
Представим, что у вас есть развертывание с тремя репликами. Каждой из них поставлена задача кодирования видео, и ее выполнение такой задачи может занимать несколько часов. Когда вы запускаете непрерывное обновление, у подов есть 30 секунд, чтобы выполнить задачу, прежде чем они будут удалены.
Можно ли не откладывать завершение работы пода?
Вы можете увеличить terminationGracePeriodSeconds до нескольких часов. Но endpoint пода в этот момент недоступен.
Инструменты мониторинга не смогут получить доступ к метрикам пода. Почему?
Инструменты вроде Prometheus полагаются на актуальность перечня endpoint в вашем кластере. Как только вы удаляете под, удаление endpoint распространяется на весь кластер и даже на Prometheus!
Поэтому вместо увеличения параметра terminationGracePeriodSeconds рекомендуют создавать новое развертывание для каждого релиза, при этом существующее развертывание остается нетронутым. То есть long-running-задачи могут по-прежнему выполнять обработку видео. По мере выполнения задач можно удалять их вручную. Автоматическое удаление возможно при настройке автомасштабирования, которое позволяет уменьшать количество реплик до нуля по мере выполнения задач.
Такой способ масштабирования называют rainbow deployment. Его используют, когда работу предыдущих версий подов нужно поддерживать дольше, чем значение параметра gracePeriod.
Система поддерживает WebSocket — вам не нужно их прерывать каждый раз. Если вы часто выпускаете релизы в течение дня, это может привести к прерываниям передаче данных в реальном времени. В таком случае создание нового развертывания для каждого релиза — менее очевидный, но более оптимальный выбор.
Существующие пользователи могут продолжать работу с потоковой передачей данных, а новая версия развертывания уже обслуживает новых пользователей. По мере того как пользователь перестает использовать старые поды, вы можете постепенно уменьшать количество реплик и удалять прошлые развертывания.
Заключение
Помните: несмотря на удаление подов из кластера, их IP-адреса могут по-прежнему использоваться для маршрутизации трафика. Так что вместо немедленного отключения пода стоит немного подождать, чтобы приложение могло корректно завершить свою работу, или же вам следует предусмотреть preStop-обработчик.
Под следует удалять только после того, как все endpoint в кластере обновятся в kube-proxy, Ingress-контроллере, CoreDNS и других компонентах.
Если ваши поды выполняют long-running задачи, например кодирование видео или WebSockets, возможно, будет полезно использовать rainbow deployment. В rainbow deployment вы развертываете новую версию для каждого релиза и удаляете предыдущую версию только после завершения всех ее задач.
Вы можете вручную удалить предыдущую версию после завершения long-running-задачи, а также можете автоматизировать процесс и уменьшать количество реплик до нуля.
Типовое условие при реализации CI/CD в Kubernetes: приложение должно уметь перед полной остановкой не принимать новые клиентские запросы, а самое главное — успешно завершать уже существующие.
Соблюдение такого условия позволяет достичь нулевого простоя во время деплоя. Однако, даже при использовании очень популярных связок (вроде NGINX и PHP-FPM) можно столкнуться со сложностями, которые приведут к всплеску ошибок при каждом деплое…
Теория. Как живёт pod
Подробно о жизненном цикле pod’а мы уже публиковали эту статью. В контексте рассматриваемой темы нас интересует следующее: в тот момент, когда pod переходит в состояние Terminating, на него перестают отправляться новые запросы (pod удаляется из списка endpoints для сервиса). Таким образом, для избежания простоя во время деплоя, с нашей стороны достаточно решить проблему корректной остановки приложения.
Также следует помнить, что grace period по умолчанию равен 30 секундам: после этого pod будет терминирован и приложение должно успеть обработать все запросы до этого периода. Примечание: хотя любой запрос, который выполняется более 5-10 секунд, уже является проблемным, и graceful shutdown ему уже не поможет…
Чтобы лучше понять, что происходит, когда pod завершает свою работу, достаточно изучить следующую схему:
А1, B1 — Получение изменений о состоянии пода A2 — Отправление SIGTERM B2 — Удаление pod’а из endpoints B3 — Получение изменений (изменился список endpoints) B4 — Обновление правил iptables
Обратите внимание: удаление endpoint pod’а и посылка SIGTERM происходит не последовательно, а параллельно. А из-за того, что Ingress получает обновленный список Endpoints не сразу, в pod будут отправляться новые запросы от клиентов, что вызовет 500 ошибки во время терминации pod’а (более подробный материал по этому вопросу мы переводили). Решать эту проблему нужно следующими способами:
Отправлять в заголовках ответа Connection: close (если это касается HTTP-приложения).
Если нет возможности вносить изменения в код, то далее в статье описано решение, которое позволит обработать запросы до конца graceful period.
Теория. Как NGINX и PHP-FPM завершают свои процессы
NGINX
Начнем с NGINX, так как с ним все более-менее очевидно. Погрузившись в теорию, мы узнаем, что у NGINX есть один мастер-процесс и несколько «воркеров» — это дочерние процессы, которые и обрабатывают клиентские запросы. Предусмотрена удобная возможность: с помощью команды nginx -s <SIGNAL> завершать процессы либо в режиме fast shutdown, либо в graceful shutdown. Очевидно, что нас интересует именно последний вариант.
Дальше всё просто: требуется добавить в preStop-хук команду, которая будет посылать сигнал о graceful shutdown. Это можно сделать в Deployment, в блоке контейнера:
Теперь в момент завершения работы pod’а в логах контейнера NGINX мы увидим следующее:
2018/01/25 13:58:31 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down
2018/01/25 13:58:31 [notice] 11#11: gracefully shutting down
И это будет означать то, что нам нужно: NGINX ожидает завершения выполнения запросов, после чего убивает процесс. Впрочем, ниже ещё будет рассмотрена распространенная проблема, из-за которой даже при наличии команды nginx -s quit процесс завершается некорректно.
А на данном этапе с NGINX закончили: как минимум по логам можно понять, что всё работает так, как надо.
Как обстоят дела с PHP-FPM? Как он обрабатывает graceful shutdown? Давайте разбираться.
PHP-FPM
В случае с PHP-FPM информации немного меньше. Если ориентироваться на официальный мануал по PHP-FPM, то в нём будет рассказано, что принимаются следующие POSIX-сигналы:
SIGINT, SIGTERM — fast shutdown;
SIGQUIT — graceful shutdown (то, что нам нужно).
Остальные сигналы в данной задаче не требуются, поэтому их разбор опустим. Для корректного завершения процесса понадобится написать следующий preStop-хук:
На первый взгляд, это всё, что требуется для выполнения graceful shutdown в обоих контейнерах. Тем не менее, задача сложнее, чем кажется. Далее разобраны два случая, в которых graceful shutdown не работал и вызывал кратковременную недоступность проекта во время деплоя.
Практика. Возможные проблемы с graceful shutdown
NGINX
В первую очередь полезно помнить: помимо выполнения команды nginx -s quit есть ещё один этап, на который стоит обратить внимание. Мы сталкивались с проблемой, когда NGINX вместо сигнала SIGQUIT всё равно отправлял SIGTERM, из-за чего запросы не завершались корректно. Похожие случаи можно найти, например, здесь. К сожалению, конкретную причину такого поведения нам установить не удалось: было подозрение на версии NGINX, но оно не подтвердилось. Симптоматика же заключалась в том, что в логах контейнера NGINX наблюдались сообщения «open socket #10 left in connection 5», после чего pod останавливался.
Мы можем наблюдать такую проблему, например, по ответам на нужном нам Ingress’e:
Показатели статус-кодов в момент деплоя
В данном случае мы получаем как раз 503 код ошибки от самого Ingress: он не может обратиться к контейнеру NGINX, так как тот уже недоступен. Если посмотреть в логи контейнера с NGINX, в них — следующее:
[alert] 13939#0: *154 open socket #3 left in connection 16
[alert] 13939#0: *168 open socket #6 left in connection 13
После изменения стоп-сигнала контейнер начинает останавливаться корректно: это подтверждается тем, что больше не наблюдается 503 ошибка.
Если вы встретились с похожей проблемой, есть смысл разобраться, какой стоп-сигнал используется в контейнере и как именно выглядит preStop-хук. Вполне возможно, что причина кроется как раз в этом.
PHP-FPM… и не только
Проблема с PHP-FPM описывается тривиально: он не дожидается завершения дочерних процессов, терминирует их, из-за чего возникают 502-е ошибки во время деплоя и других операций. На bugs.php.net с 2005 года есть несколько сообщений об ошибках (например, здесь и здесь), в которых описывается данная проблема. А вот в логах вы, вероятнее всего, ничего не увидите: PHP-FPM объявит о завершении своего процесса без каких-либо ошибок или сторонних уведомлений.
Стоит уточнить, что сама проблема может в меньшей или большей степени зависеть от самого приложения и не проявляться, например, в мониторинге. Если вы все же столкнетесь с ней, то на ум сначала приходит простой workaround: добавить preStop-хук со sleep(30). Он позволит завершить все запросы, которые были до этого (а новые мы не принимаем, так как pod уже в состоянии Terminating), а по истечении 30 секунд сам pod завершится сигналом SIGTERM.
Получается, что lifecycle для контейнера будет выглядеть следующим образом:
Однако, из-за указания 30-секундного sleep мы сильно увеличим время деплоя, так как каждый pod будет терминироваться минимум 30 секунд, что плохо. Что с этим можно сделать?
Обратимся к стороне, отвечающей за непосредственное исполнение приложения. В нашем случае это PHP-FPM, который по умолчанию не следит за выполнением своих child-процессов: мастер-процесс терминируется сразу же. Изменить это поведение можно с помощью директивы process_control_timeout, которая указывает временные лимиты для ожидания сигналов от мастера дочерними процессами. Если установить значение в 20 секунд, тем самым покроется большинство запросов, выполняемых в контейнере, и после их завершения мастер-процесс будет остановлен.
С этим знанием вернёмся к нашей последней проблеме. Как уже упоминалось, Kubernetes не является монолитной платформой: на взаимодействие между разными её компонентами требуется некоторое время. Это особенно актуально, когда мы рассматриваем работу Ingress’ов и других смежных компонентов, поскольку из-за такой задержки в момент деплоя легко получить всплеск 500-х ошибок. Например, ошибка может возникать на этапе отправки запроса к upstream’у, но сам «временной лаг» взаимодействия между компонентами довольно короткий — меньше секунды.
Поэтому, в совокупности с уже упомянутой директивой process_control_timeout можно использовать следующую конструкцию для lifecycle:
В таком случае мы компенсируем задержку командой sleep и сильно не увеличиваем время деплоя: ведь заметна разница между 30 секундами и одной?.. По сути «основную работу» на себя берёт именно process_control_timeout, а lifecycle используется лишь в качестве «подстраховки» на случай лага.
Вообще говоря, описанное поведение и соответствующий workaround касаются не только PHP-FPM. Схожая ситуация может так или иначе возникать при использовании других ЯП/фреймворков. Если не получается другими способами исправить graceful shutdown — например, переписать код так, чтобы приложение корректно обрабатывало сигналы завершения, — можно применить описанный способ. Пусть он не самый красивый, но работает.
Практика. Нагрузочное тестирование для проверки работы pod’а
Нагрузочное тестирование — один из способов проверки, как работает контейнер, поскольку эта процедура приближает к реальным боевым условиям, когда на сайт заходят пользователи. Для тестирования приведённых выше рекомендаций можно воспользоваться Яндекс.Танком: он отлично покрывает все наши потребности. Далее приведены советы и рекомендации по проведению тестирования с наглядным — благодаря графикам Grafana и самого Яндекс.Танка — примером из нашего опыта.
Самое главное здесь — проверять изменения поэтапно. После добавления нового исправления запускайте тестирование и смотрите, изменились ли результаты по сравнению с прошлым запуском. В противном случае будет сложно выявить неэффективные решения, а в перспективе можно и вовсе только навредить (например, увеличить время деплоя).
Другой нюанс — смотрите логи контейнера во время его терминации. Фиксируется ли там информация о graceful shutdown? Есть ли в логах ошибки при обращении к другим ресурсам (например, к соседнему контейнеру PHP-FPM)? Ошибки самого приложения (как в описанном выше случае с NGINX)? Надеюсь, что вводная информация из этой статьи поможет лучше разобраться, что же происходит с контейнером во время его терминирования.
Итак, первый запуск тестирования происходил без lifecycle и без дополнительных директив для сервера приложений (process_control_timeout в PHP-FPM). Целью этого теста было выявление приблизительного количества ошибок (и есть ли они вообще). Также из дополнительной информации следует знать, что среднее время деплоя каждого пода составляло около 5-10 секунд до состояния полной готовности. Результаты таковы:
На информационной панели Яндекс.Танка виден всплеск 502 ошибок, который произошел в момент деплоя и продолжался в среднем до 5 секунд. Предположительно это обрывались существующие запросы к старому pod’у, когда он терминировался. После этого появились 503 ошибки, что стало результатом остановленного контейнера NGINX, который также оборвал соединения из-за бэкенда (из-за чего к нему не смог подключиться Ingress).
Посмотрим, как process_control_timeout в PHP-FPM поможет нам дожидаться завершения child-процессов, т.е. исправить такие ошибки. Повторный деплой уже с использованием этой директивы:
Во время деплоя 500-х ошибок больше нет! Деплой проходит успешно, graceful shutdown работает.
Однако стоит вспомнить момент с Ingress-контейнерами, небольшой процент ошибок в которых мы можем получать из-за временного лага. Чтобы их избежать, остается добавить конструкцию со sleep и повторить деплой. Впрочем, в нашем конкретном случае изменений не было видно (ошибок снова нет).
Заключение
Для корректного завершения процесса мы ожидаем от приложения следующего поведения:
Ожидать несколько секунд, после чего перестать принимать новые соединения.
Дождаться завершения всех запросов и закрыть все keepalive-подключения, которые запросы не выполняют.
Завершить свой процесс.
Однако не все приложения умеют так работать. Одним из решений проблемы в реалиях Kubernetes является:
добавление хука pre-stop, который будет ожидать несколько секунд;
изучение конфигурационного файла нашего бэкенда на предмет соответствующих параметров.
Пример с NGINX позволяет понять, что даже то приложение, которое изначально должно корректно отрабатывает сигналы к завершению, может этого не делать, поэтому критично проверять наличие 500 ошибок во время деплоя приложения. Также это позволяет смотреть на проблему шире и не концентрироваться на отдельном pod’е или контейнере, а смотреть на всю инфраструктуру в целом.
В качестве инструмента для тестирования можно использовать Яндекс.Танк совместно с любой системой мониторинга (в нашем случае для теста брались данные из Grafana с бэкендом в виде Prometheus). Проблемы с graceful shutdown хорошо видны при больших нагрузках, которую может генерировать benchmark, а мониторинг помогает детальнее разобрать ситуацию во время или после теста.
Отвечая на обратную связь по статье: стоит оговориться, что проблемы и пути их решения здесь описываются применительно к NGINX Ingress. Для других случаев есть иные решения, которые, возможно, мы рассмотрим в следующих материалах цикла.
Одновременно с ростом кластера Kubernetes растет количество ресурсов, volume и других API-объектов. Рано или поздно вы упретесь в потолок, будь то etcd, память или процессор. Зачем подвергать себя ненужной боли и проблемам, если можно установить простые — хотя и довольно изощренные — правила? Вы можете настроить автоматизацию и мониторинг, которые будут содержать кластер в аккуратном состоянии. В статье разберемся, как избавиться от лишних нагрузок, через которые утекают ресурсы, и устаревших накопившихся объектов.
Что будет в статье:
В чем проблема?
Основы
Ручная очистка
Kube-janitor
Мониторинг ограничений кластера
Заключение
От переводчиков
В чем проблема?
Несколько забытых подов, неиспользуемые volume, завершенные задачи или, возможно, старый ConfigMap/Secret — насколько все это важно? Все это просто где-то лежит и ждет, пока не понадобится.
Вообще-то этот хлам не сильно вредит до поры до времени. Но когда эти штуки накапливаются, то начинают влиять на производительность и стабильность кластера. Давайте посмотрим, какие проблемы могут вызвать эти забытые ресурсы.
Ограничение количества подов. Каждый кластер Kubernetes имеет несколько основных ограничений. Первое из них — количество подов на узел, которое по документации не рекомендуется делать больше 110. При этом, если у вас достаточно мощные узлы с большим количеством памяти и CPU, вы можете увеличить это количество — возможно, даже до 500, как было протестировано на OpenShift. Но если вы достигнете этих пределов, не удивляйтесь, если что-то пойдет не так, и не только потому, что не хватает памяти или мощности процессора.
Нехватка хранилища ephemeral storage. Все запущенные поды на узле используют по крайней мере часть этого пространства для логов, кэша, рабочего пространства или emptyDir волюмов. Вы можете достичь предела довольно быстро, что приведет к вытеснению уже существующих подов, или невозможности создания новых. Запуск слишком большого количества подов на узле тоже может способствовать этой проблеме: ephemeral storage используется для образов контейнеров и их записываемых слоев. Если узел достигает предела хранилища, он становится нерабочим (на него будет применен taint), о чем вы узнаете довольно быстро. Если вы хотите узнать текущее состояние диска на узле, запустите на нем df -h /var/lib.
Лишние расходы на PVC. Схожим образом источником проблем могут стать persistent volume, особенно если вы запускаете Kubernetes в облаке и платите за каждый предоставленный PVC. Очевидно, что для экономии денег необходимо чистить неиспользуемые PVC. Содержание используемых PVC чистыми тоже важно, потому что позволяет избежать нехватки места для ваших приложений. Особенно, если вы запускаете базы данных в кластере.
Низкая производительность etcd. Еще один источник проблем — чрезмерное количество объектов, поскольку все они находятся в хранилище etcd. По мере роста количества данных в etcd, его производительность может начать снижаться. Этого нужно стараться не допускать всеми силами: etcd — мозг кластера Kubernetes. Учитывая вышесказанное, чтобы упереться в etcd, вам понадобится действительно большой кластер, как продемонстрировано в этом посте OpenAI. В то же время нет какой-то единой метрики для замера производительности etcd, потому что она зависит от количества объектов, их размеров и частоты использования. Так что наилучшим выходом будет профилактика: простое сохранение чистоты и порядка. В противном случае вас могут ждать весьма неприятные сюрпризы.
Нарушение границ безопасности. Наконец, мусор в кластере может стать источником проблем сам по себе. Не забывайте подчищать Role Bindings и учетные записи (Service Account), когда ими никто не пользуется.
Основы
Для решения большинства этих проблем не нужно делать ничего особенно сложного. Лучшим выбором будет совсем не допускать их. Один из вариантов превентивных мер — использование объектных квот, которые вы, как администратор кластера, можете применять в каждом отдельном неймспейсе.
Первое, что можно решить с помощью квот — количество и размер PVC:
LimitRange устанавливает минимальный и максимальный размеры PVC в неймспейсе. Это оградит пользователей от запрашивания слишком больших объемов.
ResourceQuota дополнительно обеспечивает жесткое ограничение на количество PVC и их совокупный размер.
Затем вы можете предотвратить создание кучи объектов и оставление их в качестве мусора после использования. Для этого используйте object count, квоты на количество объектов, которые зададут жесткое ограничение на количество объектов определенного типа в конкретном неймспейсе:
Есть несколько встроенных полей, через которые вы можете задать квоты на количество объектов. Например, configmaps, secrets или services, показанные выше. Для всех прочих ресурсов можно использовать формат count/<resource>.<group>, как показано в примере с count/jobs.batch, что может помочь от бесконтрольного создания джоб из-за неправильно настроенного CronJob.
Вероятно, большинству известно о функции установки квот на память и CPU. Но, возможно, для вас станет новостью квота ephemeral storage. Альфа-поддержка квот для эфемерного хранилища была добавлена в v1.18 и дала возможность установить границы ephemeral storage так же, как как для памяти и процессора.
Однако будьте осторожны с этой настройкой. Поды могут быть вытеснены из-за превышения лимита, что может быть вызвано слишком большим размером логов контейнера.
Помимо установки квот и границ ресурсов, можно установить ограничение для истории ревизий Deployment — для снижения количества репликасетов, хранящихся в кластере. Для этого используйте .spec.revisionHistoryLimit, который по умолчанию равен 10.
Наконец, вы можете установить время жизни (TTL) для очистки кластера от объектов, которые существуют там слишком долго. Эта процедура использует TTL-контроллер, который находится в стадии бета-тестирования с версии v1.21, и в настоящее время работает только для задач, использующих поле .spec.ttlSecondsAfterFinished. В будущем, возможно, он будет расширен на другие ресурсы, например, поды.
Ручная очистка
Если превентивных мер уже недостаточно, потому что у вас накопилось достаточно неиспользуемых ресурсов, вы можете попробовать разовое удаление. Это делается просто с помощью kubectl get иkubectl delete. Пара основных примеров того, что вы можете сделать:
kubectl delete all -l some-label=some-value # Delete based on label
kubectl delete pod $(kubectl get pod -o=jsonpath='{.items[?(@.status.phase=="Succeeded")].metadata.name}') # Delete all "Succeeded" Pods
Первая команда выполняет базовое удаление с использованием лейблов ресурсов. Поэтому этот метод потребует от вас предварительно пометить все подлежащие удалению объекты некоторой парой ключ-значение.
Вторая показывает, как вы можете удалить тип ресурсов, основанный на некотором поле, как правило, каком-то поле статуса. В примере выше это будут все завершенные/успешные поды. Этот вариант можно применить и к другим ресурсам, например, завершенным задачам.
Кроме этих двух команд, довольно сложно найти шаблон, который помог бы разом очистить кластер от хлама. Поэтому вам придется специально поискать конкретные неиспользуемые ресурсы.
Могу посоветовать такой инструмент, как k8spurger. Он ищет неиспользуемые объекты вроде RoleBinding, ServiceAccounts, ConfigMaps и создает список ресурсов-кандидатов на удаление. Это поможет сузить круг поиска.
Kube-janitor
В разделах выше мы рассмотрели несколько вариантов простой очистки для конкретных случаев. Но лучшим решением для наведения порядка в любом кластере будет использование kube-janitor. Этот инструмент работает в вашем кластере так же, как любая другая рабочая нагрузка, и использует JSON-запросы для поиска ресурсов, которые можно удалить на основе TTL или истечения срока действия.
Для развертывания kube-janitor на вашем кластере запустите:
git clone https://codeberg.org/hjacobs/kube-janitor.git
cd kube-janitor
kubectl apply -k deploy/
Это разложит kube-janitor в default namespace и запустит его с правилами по умолчанию в пробном режиме с использованием флага —dry-run.
Перед отключением dry-run нужно установить собственные правила. Они лежат в config map kube-janitor, которая выглядит примерно так:
Конечно, для нас самый интересный раздел здесь — правила, rules. Вот несколько полезных примеров, которые вы можете использовать для очистки своего кластера:
rules:
# Удаление Jobs в development namespaces после 2 дней.
- id: remove-old-jobs
resources:
- jobs
jmespath: "metadata.namespace == 'development'"
ttl: 2d
# Удаление тех подов в development namespaces, которые не в состоянии выполнения (Failed, Completed).
- id: remove-non-running-pods
resources:
- pods
jmespath: "(status.phase == 'Completed' || status.phase == 'Failed') && metadata.namespace == 'development'"
ttl: 2h
# Удаление всех PVC, которые не использует ни один под
- id: remove-unused-pvcs
resources:
- persistentvolumeclaims
jmespath: "_context.pvc_is_not_mounted"
ttl: 1d
# Удаление всех Deployments, чье имя начинается с 'test-'
- id: remove-test-deployments
resources:
- deployments
jmespath: "starts_with(metadata.name, 'test-')"
ttl: 1d
# Удаление всех ресурсов в playground namespace через неделю
- id: remove-test-deployments
resources:
- "*"
jmespath: "metadata.namespace == 'playground'"
ttl: 7d
Этот пример показывает несколько базовых вариантов настройки для очистки от временных, устаревших или неиспользуемых ресурсов. Помимо правил такого рода, можно установить абсолютные дату/время истечения срока действия для конкретных объектов.
Это можно сделать с помощью аннотаций, например:
apiVersion: v1
kind: Namespace
metadata:
annotations:
# будет удалено 18.6.2021 в полночь
janitor/expires: "2021-06-18"
name: temp
spec: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
# будет удалено 20.6.2021 в 17:30
janitor/expires: "2021-06-20T17:30:00Z"
name: nginx
spec:
replicas: 1
...
Когда закончите устанавливать правила и аннотации, вам стоит дать kube-janitor поработать какое-то время в dry-run режиме с включенными логами отладки. Это мера предосторожности, чтобы инструмент не удалил то, что удалять было не нужно.
Другими словами, не обвиняйте меня, если сотрете production волюмы из-за неправильной конфигурации и отсутствия тестирования.
Наконец, при использовании kube-janitor нужно учитывать его потребности в ресурсах. Если в кластере много объектов, ему может потребоваться больше памяти, чем выделенные по умолчанию 100 Мб. Чтобы его под не застревал в CrashLoopBackOff, я выставляю ему лимит 1 Гб.
Мониторинг ограничений кластера
Не все проблемы можно решить ручной или даже автоматической очисткой. В некоторых случаях мониторинг будет лучшим выбором для обеспечения уверенности в том, что вы не упираетесь в лимиты кластера — будь то количество подов, доступное ephemeral storage или количество объектов в etcd.
Мониторинг — это огромная тема, которая требует отдельной статьи, а то и нескольких. Поэтому для сегодняшних целей я просто перечислю несколько Prometheus-метрик, которые могут оказаться полезными для поддержания порядка в кластере:
etcd_db_total_size_in_bytes — размер базы данных etcd
etcd_object_counts — количество объектов в etcd
pod:container_cpu_usage:sum — использование CPU на каждый под в кластере
pod:container_fs_usage_bytes:sum — использование файловой системы на каждый под в кластере
pod:container_memory_usage_bytes:sum — использование памяти на каждый под в кластере
node_memory_MemFree_bytes — свободная память на каждом узле
namespace:container_memory_usage_bytes:sum — использование памяти по неймспейсам
namespace:container_cpu_usage:sum — загрузка CPU на каждый namespace
kubelet_volume_stats_used_bytes — используемое место по каждому волюму
kubelet_running_pods — количество запущенных подов на узле
kubelet_container_log_filesystem_used_bytes — размер логов на каждый контейнер/под
kube_node_status_capacity_pods — рекомендованный максимум подов на узел
kube_node_status_capacity — максимум для всех метрик (CPU, поды, ephemeral storage, память, hugepages)
Это только некоторые метрики из тех, которые вы можете использовать. Какие из них будут доступны, зависит также от ваших инструментов мониторинга, т.е. вы можете использовать какие-то специальные метрики, доступные на вашем сервисе.
Заключение
Мы рассмотрели несколько вариантов очистки кластера Kubernetes — некоторые совсем простые, а некоторые посложнее. Независимо от того, что вы выберете, старайтесь не забывать делать уборку в кластере и сохранять порядок. Это может спасти вас от большой головной боли и, по крайней мере, избавит от ненужного хлама в кластере. В конечном счете такая уборка служит тем же целям, что наведение порядка на рабочем столе.
Также имейте ввиду, что если вы оставляете объекты лежать без использования долгое время, вы просто забудете, зачем они там. Это может сильно осложнить понимание, что должно существовать, а что — нет.
Помимо описанных здесь подходов, вы также можете использовать какое-либо решение GitOps — например, ArgoCD или Flux для создания ресурсов и управления ими, что может значительно упростить их очистку. Обычно потребуется удалить только один кастомный ресурс, что вызовет каскадное удаление всех зависимых ресурсов.