Архив автора: admin

Проекты искусственного интеллекта 2023: бесплатно, бесплатно и открыто

Проекты искусственного интеллекта 2023: бесплатно, бесплатно и открыто

Проекты искусственного интеллекта 2023: бесплатно, бесплатно и открыто

До и во время 2021 год, когда Бум искусственного интеллекта (ИИ) Это еще не произошло в сфере обычных пользователей, мы уже выложили много технического и информативного контента о текущем состоянии этих технологий и их потенциале.

И с тех пор, В течение 2022 года эта веха, без сомнения, была достигнута.Ну а сегодня мы прокомментируем некоторые известные «Проекты искусственного интеллекта», бесплатно, бесплатно и открыто, стоит знать в 2023 год.



Читать

Kubernetes: Ingress, ошибка 502, readinessProbe и livenessProbe

Имеется приложение на Go, API-бекенд.




Периодически начинает возвращать 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)

    }   

}




Тут мы запускаем http.ListenAndServe() на порту 8080, и определяем два роута:




  • /ping – при обращении сюда всегда возвращаем 200



  • /err – при обращении сюда прерываем выполнение функции с panic, что сэмулировать некорретный ответ приложения




Проверяем локально.




Запускаем:




go run http.go

Starting server at port 8080




Проверяем роут /ping:




curl -I localhost:8080/ping

HTTP/1.1 200 OK




И URI /err, который вызовет panic:




curl -I localhost:8080/err

curl: (52) Empty reply from server




Лог приложения:




go run http.go

Starting server at port 8080

2020/11/11 14:34:53 http: panic serving [::1]:43008: Error

goroutine 6 [running]:

...




Docker образ




Пишем Dockefile:




FROM golang:latest

WORKDIR /app

COPY . .

RUN go build -o main .

EXPOSE 8080

CMD ["./main"]




Собираем образ и пушим в Docker Hub:




docker build -t setevoy/go-http .

docker push setevoy/go-http




Kubernetes




Deployment




Описываем запуск пода с этим образом – создаём 1 под, Service для него, и Ingress, который создаст AWS Application Load Balancer.




Начнём с Deployment:




apiVersion: apps/v1

kind: Deployment

metadata:

  name: go-http

  labels:

    app: go-http

spec:

  replicas: 1

  strategy:

    type: Recreate

  selector: 

    matchLabels:

      app: go-http

  template:

    metadata:

      labels:

        app: go-http

    spec:

      containers:

      - name: go-http

        image: setevoy/go-http

        ports:

        - containerPort: 8080

        imagePullPolicy: Always

        livenessProbe:

          httpGet:

            path: /ping

            port: 8080

          initialDelaySeconds: 1

          periodSeconds: 1

        readinessProbe:

          httpGet:

            path: /ping

            port: 8080

          initialDelaySeconds: 1

          periodSeconds: 1

      restartPolicy: Always




Тут создаём один под, который слушает порт 8080.




В strategy деплоймента указываем Recreate, что бы при тестах не оставались старые поды.




Для него описываем проверки – livenessProbe и readinessProbe, обе проверки ходят на URI /ping, где получают ответ 200.




Позже мы поменяем путь в проверках, и посмотрим, к чему это приведёт.




Service




Создаём Kubernetes Service, который откроет на WorkerNode порт для ALB, и будет роутить трафик к нашему поду на порт 8080 (см. Kubernetes: Service, балансировка нагрузки, kube-proxy и iptables):




---

apiVersion: v1

kind: Service

metadata:

  name: go-http-svc

spec:

  type: NodePort

  ports:

  - port: 80

    targetPort: 8080

    protocol: TCP

  selector:

    app: go-http




Ingress




Добавляем Ingress, который создаст AWS Application Load Balancer, который будет направлять трафик к go-http-svc Service:




--- 

apiVersion: extensions/v1beta1

kind: Ingress

metadata:

  name: go-http-ingress

  annotations:    

    kubernetes.io/ingress.class: alb

    alb.ingress.kubernetes.io/scheme: internet-facing

    alb.ingress.kubernetes.io/healthcheck-path: /ping

    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'

  labels:   

    app: go-http

spec:         

  rules:

  - http: 

      paths:

      - path: /*

        backend: 

          serviceName: go-http-svc

          servicePort: 80




Создаём ресурсы:




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




Логи пода:




kubectl logs go-http-8dc5b4779-7q4kw

Starting server at port 8080

2020/11/11 12:57:10 http: panic serving 10.3.39.145:8926: Error

goroutine 5169 [running]:

net/http.(*conn).serve.func1(0xc000260e60)

/usr/local/go/src/net/http/server.go:1801 +0x147

panic(0x654840, 0x6f0ba0)

/usr/local/go/src/runtime/panic.go:975 +0x47a

main.main.func2(0x6fa0a0, 0xc00012c540, 0xc000127700)

/app/http.go:16 +0x39

net/http.HandlerFunc.ServeHTTP(0x6bab98, 0x6fa0a0, 0xc00012c540, 0xc000127700)

/usr/local/go/src/net/http/server.go:2042 +0x44

net/http.(*ServeMux).ServeHTTP(0x8615e0, 0x6fa0a0, 0xc00012c540, 0xc000127700)

/usr/local/go/src/net/http/server.go:2417 +0x1ad

net/http.serverHandler.ServeHTTP(0xc0000ea000, 0x6fa0a0, 0xc00012c540, 0xc000127700)

/usr/local/go/src/net/http/server.go:2843 +0xa3

net/http.(*conn).serve(0xc000260e60, 0x6fa4e0, 0xc00011b8c0)

/usr/local/go/src/net/http/server.go:1925 +0x8ad

created by net/http.(*Server).Serve

/usr/local/go/src/net/http/server.go:2969 +0x36c




Тут всё логично – load-balancer отправил наш запрос к поду, под не ответил (вспомним curl: (52) Empty reply from server в наших первых тестах), и мы получили ответ 502 от балансировщика aka Ingress.




readinessProbe и livenessProbe




Теперь посмотрим, как изменение в readinessProbe и livenessProbe повлияют на ответы Ingress и работу самого пода.




readinessProbe




Документация – тут>>>.




readinessProbe используется для проверки того, готов ли под принимать трафик.




Сейчас наш под в статусе Ready:




kubectl get pod -l app=go-http

NAME                      READY   STATUS    RESTARTS   AGE

go-http-8dc5b4779-7q4kw   1/1     Running   0          28m




Или так:




kubectl get pod -l app=go-http -o json | jq '.items[].status.containerStatuses[].ready'

true




И запрос к /ping возвращает нам ответ 200:




curl -I e172ad3e-default-gohttping-ec00-691779486.us-east-2.elb.amazonaws.com/ping

HTTP/1.1 200 OK




Меняем readinessProbe – задаём path=/err, что бы проверка постоянно фейлилась:




...

        readinessProbe:

          httpGet:

            path: /err

...




Передеплоиваем:




kubectl apply -f go-http.yaml

deployment.apps/go-http configured




И проверяем:




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




livenessProbe




Документация – тут>>>.




Вернём readinessProbe в /ping, что бы трафик на под пошёл, но изменим livenessProbe – зададим path в /err, а initialDelaySeconds и periodSeconds установим в 15 секунд, плюс добавим failureThreshold равным одной попытке:




...

        livenessProbe:

          httpGet:

            path: /err

            port: 8080

          initialDelaySeconds: 15

          periodSeconds: 15

          failureThreshold: 1

        readinessProbe:

          httpGet:

            path: /ping

...




Теперь после запуска пода 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.




Источник: https://rtfm.co.ua/ru/kubernetes-ingress-oshibka-502-readinessprobe-i-livenessprobe/



2023-01-03T02:03:12
DevOps

Kubernetes: NGINX/PHP-FPM graceful shutdown – избавляемся от 502 ошибок

Имеется PHP-приложение, работает в Kubernetes в подах с двумя контейнерами – NGINX и PHP-FPM.




Проблема: во время скейлинга приложения начинают проскакивать 502 ошибки. Т.е. при остановке подов – некорректно отрабатывает завершение подключений.




Рассмотрим процесс остановки подов вообще, и особенности NGINX и PHP-FPM в частности.




Тестировать будем приложение в AWS Elastic Kubernetes Service с помощью Yandex.Tank.




Ingress создаёт AWS Application Load Balancer с помощью AWS ALB Ingress Controller.




Для управления контейнерами на 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 в образе, из которого запускался контейнер.




Итак, процесс удаления пода выглядит следующим образом:




  1. мы выполняем kubectl delete pod или kubectl scale deployment – запускается процесс удаления подов и стартует таймер отсчёта grace period с дефолтным значением в 30 секунд



  2. API-сервер обновляет статус пода – из Running он становится Terminating (см. Container states). На WorkerNode, на которой этот под запущен, kubelet получает обновление статус этого пода и начинает процесс его остановки:

    1. если для контейнера(ов) в поде есть preStop hook – kubelet  его выполняет. Если хук продолжает выполнение после истечения grace period – добавляется ещё 2 секунды на его завершение. При необходимости можно изменить дефолтные 30 секунд используя terminationGracePeriodSeconds



    2. после завершения preStop хука – kubelet отправляет указание Docker на остановку контейнеров, и Docker отправляет сигнал SIGTERM процессу с PID 1 в каждом контейнере. При этом контейнеры в поде получают сигнал в случайном порядке.




  3. одновременно с началом процесса graceful shutdown – Kubernetes Control Plane (его kube-controller-manager) удаляет останавливаемый под из списка ендпоинтов (см. Kubernetes – Endpoints), и соответствующий Service перестаёт отправлять новые подключения на останавливаемый под



  4. по завершению grace period, kubelet триггерит force shutdown – Docker отправляет сигнал SIGKILL всем оставшимся процесам во всех контейнерах пода, который они проигнорировать не могут, и мгновенно умирают, аварийно завершая все свои операции



  5. kubelet триггерит удаление объекта пода из API-сервера



  6. API-сервер удаляет запись о поде из базы, и под становится недоступен




Наглядная табличка:





Собственно, тут возникает две проблемы:




  1. сам NGINX и PHP-FPM воспринимают SIGTERM как “жестокое убийство”, и завершают работу немедленно, не заботясь о корректном завершении текущих подключений (см. Controlling nginx и php-fpm(8) – Linux man page)



  2. шаги 2 и 3 – отправка SIGTERM и удаление ендпоинта – выполняются одновременно. Однако, обновление данных в Ingress Service происходит не моментально, и под может начать умирать раньше, чем Ingress перестанет на него слать трафик, соотвественно – получим 502, т.к. процесс в поде уже не может принимать подключения




Т.е. в первом случае, если у нас есть подключение к NGINX с keep-alive – то NGINX при выполнении fast shutdown просто обрубит его, а клиент получит 502 ошибку, см. Avoiding dropped connections in nginx containers with “STOPSIGNAL SIGQUIT”.




NGINX STOPSIGNAL и 502




Попробуем воспроизвести проблему с NGINX.




Возмём пример из поста по ссылке выше, и задеплоим его в Кубер.




Пишем Dockerfile:




FROM nginx

RUN echo 'server {n

    listen 80 default_server;n

    location / {n

      proxy_pass      http://httpbin.org/delay/10;n

    }n

}' > /etc/nginx/conf.d/default.conf

CMD ["nginx", "-g", "daemon off;"]




Тут NGINX выполняет proxy_pass на http://httpbin.org, который отвечает с задержкой в 10 секунд – эмулируем работу PHP-бекенда.




Собираем, пушим в репозиторий:




docker build -t setevoy/nginx-sigterm .

docker push setevoy/nginx-sigterm




  • Re-play



  • Copy to Clipboard



  • Pause



  • Full View




Пишем Deployment с 10 подами из собранного образа.




Тут приведу полный файл, с Namespace, Service и Ingress, далее только те части, которые будут меняться:




---

apiVersion: v1

kind: Namespace

metadata:

  name: test-namespace

---

apiVersion: apps/v1

kind: Deployment

metadata:

  name: test-deployment

  namespace: test-namespace

  labels:

    app: test

spec:

  replicas: 10

  selector:

    matchLabels:

      app: test

  template:

    metadata:

      labels:

        app: test

    spec:

      containers:

      - name: web

        image: setevoy/nginx-sigterm

        ports:

        - containerPort: 80

        resources:

          requests:

            cpu: 100m

            memory: 100Mi

        readinessProbe:

          tcpSocket:

            port: 80

---

apiVersion: v1

kind: Service

metadata:

  name: test-svc

  namespace: test-namespace

spec:

  type: NodePort

  selector:

    app: test

  ports:

    - protocol: TCP

      port: 80

      targetPort: 80

---

apiVersion: extensions/v1beta1

kind: Ingress

metadata:

  name: test-ingress

  namespace: test-namespace

  annotations:

    kubernetes.io/ingress.class: alb

    alb.ingress.kubernetes.io/scheme: internet-facing

    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'

spec:

  rules:

  - http:

      paths:

      - backend:

          serviceName: test-svc

          servicePort: 80




Деплоим:




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




Запущено 10 подов:




kubectl -n test-namespace get pod

NAME                              READY   STATUS    RESTARTS   AGE

test-deployment-ccb7ff8b6-2d6gn   1/1     Running   0          26s

test-deployment-ccb7ff8b6-4scxc   1/1     Running   0          35s

test-deployment-ccb7ff8b6-8b2cj   1/1     Running   0          35s

test-deployment-ccb7ff8b6-bvzgz   1/1     Running   0          35s

test-deployment-ccb7ff8b6-db6jj   1/1     Running   0          35s

test-deployment-ccb7ff8b6-h9zsm   1/1     Running   0          20s

test-deployment-ccb7ff8b6-n5rhz   1/1     Running   0          23s

test-deployment-ccb7ff8b6-smpjd   1/1     Running   0          23s

test-deployment-ccb7ff8b6-x5dc2   1/1     Running   0          35s

test-deployment-ccb7ff8b6-zlqxs   1/1     Running   0          25s




Готовим load.yaml для Yandex.Tank:




phantom:

  address: aadca942-testnamespace-tes-5874-698012771.us-east-2.elb.amazonaws.com

  header_http: "1.1"

  headers:

     - "[Host: aadca942-testnamespace-tes-5874-698012771.us-east-2.elb.amazonaws.com]"

  uris:

    - /    

  load_profile:

    load_type: rps

    schedule: const(100,30m)

  ssl: false

console:

  enabled: true

telegraf:

  enabled: false

  package: yandextank.plugins.Telegraf

  config: monitoring.xml




Тут выполняем 1 запрос в секунду к подам за нашим Ingress (и NGINX в каждом будет ждать 10 секунд ответа от своего “бекенда” перед тем, как ответить 200 нашему Танку).




Запускаем тесты:





Пока всё хорошо.




Теперь скейлим поды с 10 до 1:




kubectl -n test-namespace scale deploy test-deployment --replicas=1

deployment.apps/test-deployment scaled




Поды перешли в Terminating:




kubectl -n test-namespace get pod

NAME                              READY   STATUS        RESTARTS   AGE

test-deployment-647ddf455-67gv8   1/1     Terminating   0          4m15s

test-deployment-647ddf455-6wmcq   1/1     Terminating   0          4m15s

test-deployment-647ddf455-cjvj6   1/1     Terminating   0          4m15s

test-deployment-647ddf455-dh7pc   1/1     Terminating   0          4m15s

test-deployment-647ddf455-dvh7g   1/1     Terminating   0          4m15s

test-deployment-647ddf455-gpwc6   1/1     Terminating   0          4m15s

test-deployment-647ddf455-nbgkn   1/1     Terminating   0          4m15s

test-deployment-647ddf455-tm27p   1/1     Running       0          26m

...




И ловим наши 502 ошибки:





Теперь в Dockerfile добавляем STOPSIGNAL SIGQUIT:




FROM nginx

RUN echo 'server {n

    listen 80 default_server;n

    location / {n

      proxy_pass      http://httpbin.org/delay/10;n

    }n

}' > /etc/nginx/conf.d/default.conf

STOPSIGNAL SIGQUIT

CMD ["nginx", "-g", "daemon off;"]




Билдим, пушим:




docker build -t setevoy/nginx-sigquit .

docker push setevoy/nginx-sigquit




Обновляем деплоймент:




...

    spec:

      containers:

      - name: web

        image: setevoy/nginx-sigquit

        ports:

        - containerPort: 80

...




Передеплоиваем, и проверяем ещё раз.




Запускаем тесты:





Скейлим деплоймент:




kubectl -n test-namespace scale deploy test-deployment --replicas=1

deployment.apps/test-deployment scaled




А ошибок по-прежнему нет:





Прекрасно.




Трафик, preStop и sleep




И всё-таки, если повторить тесты несколько раз, то иногда 502 ошибки всё-таки проскакивают:





Тут мы уже сталкиваемся со второй проблемой – обновление ендпоинтов выполняется параллельно с отправкой SIGTERM.




Добавим preStop хук со sleep, что бы дать время на обновление ендпоинтов – тогда после получения команды на удаление пода, kubelet выждет 5 секунд перед отправкой SIGTERM, за которые Ingress успеет обновить список ендпоинтов:




...

    spec:

      containers:

      - name: web

        image: setevoy/nginx-sigquit

        ports:

        - containerPort: 80

        lifecycle:

          preStop:

            exec:

              command: ["/bin/sleep","5"]

...




Повторяем тесты – всё хорошо.




Выкатили фикс в Production – всё работает замечательно, ошибки пропали, автотесты теперь работают без проблем.




С PHP-FPM проблемы, как оказалось, не было вообще,  т.к. исходный образ изначально был со STOPSIGNAL SIGQUIT.




Другие варианты решения




Конечно, по ходу поиска решения были переброваны самые разные варианты. См. ссылки на интересные материалы в конце, а тут опишу их кратко.




preStop и nginx -s quit




Одним из вариантов было добавить в preStop хук отправку сигнала QUIT в NGINX:




lifecycle:

  preStop:

    exec:

      command:

      - /usr/sbin/nginx

      - -s

      - quit




Но не помогло. Не очень понятно почему, потому как идея вроде бы правильная – вместо того, что бы ожидать TERM от Kubernetes/Docker – мы сами мягко стопаем NGINX, отправляя ему QUIT.




Но можно попробовать.




NGINX + PHP-FPM, supervisord и stopsignal




Наше приложение работает в двух отдельных контейнерах NGINX и PHP-FPM.




Но в процессе поиска решения пробовал использовать образ, в котором оба сервиса собраны в одном образе, например trafex/alpine-nginx-php7.




В нём пробовал добавлять параметр stopsignal и для NGINX, и для PHP-FPM со значением QUIT – но тоже не помогло, хотя идея тоже вроде правильная.




Тем не менее – как вариант на попытку.




PHP-FPM и process_control_timeout




В посте Graceful shutdown in Kubernetes is not always trivial и на Stackoveflow в вопросе Nginx / PHP FPM graceful stop (SIGQUIT): not so graceful есть упоминание о том, что мастер-процесс FPM убивается не дожидаясь своих дочерних процессов, что также может приводить к 502.




Не наш случай, но стоит обратить внимание на параметр process_control_timeout.




NGINX, HTTP и keep-alive session




В принципе, можно было бы указать приложению отправлять заголовок [Connection: close] – тогда клиент по завершении передачи данных закрывал бы соединение, и это уменьшило бы вероятность 502-х. Но они всё равно будут, если NGINX получит SIGTERM в процессе обработки запроса от клиента, а т.к. у нас “сзади” ещё PHP, который ходит в базу – то некоторые запросы могут обрабатываться по 5-10 и более секунд.




См. HTTP persistent connection.




Источник: https://rtfm.co.ua/ru/kubernetes-nginx-php-fpm-graceful-shutdown-izbavlyaemsya-ot-502-oshibok/



2023-01-03T01:50:56
DevOps

Как безопасно завершить работу пода в Kubernetes: разбираемся с graceful shutdown и zero downtime деплоймент

Создание и удаление подов — распространенная задача при работе с Kubernetes. Новые поды создаются, когда вы выполняете плавающее обновление, масштабируете развертывание и релизите новую функциональность, а также при выполнении cron и других задач. Еще поды пересоздаются при каждом удалении и внесении изменений, например, когда узел помечается как непланируемый (unschedulable).




Команда Kubernetes aaS VK Cloud Solutions перевела статью о том, как безопасно завершить работу пода.




Основные термины из статьи:




Graceful shutdown — предсказуемое окончание работы системы, когда все запущенные процессы корректно завершают работу без потери данных или негативного пользовательского опыта.




Zero downtime deploy — нулевой простой во время развертывания новой версии приложения. Пользователь не заметит его недоступности.




Понятная схема, которая показывает, что происходит в кластере при удалении пода (PDF).




Создание пода




Чтобы лучше понимать, что происходит при удалении пода, давайте сначала разберемся, как его создают. Предположим, вы создаете в кластере следующий под:




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. Также под добавляется в очередь заданий планировщика.




Что делает планировщик:




  1. Проверяет описание в YAML-файле.



  2. Собирает сведения о нагрузке на процессор и запрошенную память контейнера.



  3. Решает, какой узел лучше всего подходит для запуска пода (через процесс, называемый фильтрами и предикатами).




В итоге:




  • Под помечается как запланированный (scheduled) в etcd.



  • Для пода определяется узел.



  • Состояние пода сохраняется в etcd.




Но сам под пока не создан!







При использовании команды kubectl apply -f YAML-файл отправляется в Kubernetes API







API сохраняет под в базе данных — etcd








Планировщик выбирает оптимальный узел для создания пода, статус пода изменяется на «Ожидание». Но под пока существует только в etcd




Создание пода в кластере Kubernetes




Предыдущие задачи выполнялись в Control Plane, а состояние сохранилось в базе данных. Кто же создает под? Kubelet — агент Kubernetes. Его задача — опрашивать об обновлениях Control Plane. При этом kubelet не создает контейнеры самостоятельно, а делегирует эту работу трем другим компонентам:




  1. The Container Runtime Interface (CRI) создает контейнер для подов.



  2. The Container Network Interface (CNI) соединяет контейнеры с сетью кластера и назначает IP-адреса.



  3. The Container Storage Interface (CSI) монтирует тома в ваших контейнерах.




В большинстве случаев The Container Runtime Interface (CRI) выполняет работу, аналогичную команде по запуску контейнера в фоновом режиме:




$ docker run -d <my-container-image> 




The Container Network Interface (CNI) устроен немного иначе, он отвечает за:




  1. создание валидного IP-адреса для пода;



  2. подключение контейнера к остальной сети.




Существует несколько способов подключить контейнер к сети и назначить ему валидный 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-файла для описания сервиса:




apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  ports:
  - port: 80
    targetPort: 3000
  selector:
    name: app




После выполнения команды <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-адреса и порты пода и обновляется каждый раз, когда:




  1. вы создаете под;



  2. вы удаляете под;



  3. метка пода изменяется.




Итак, 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 обычно указывают сервис. Вот пример такого файла:




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-контроллер подписывается на изменения 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-контроллер направляет трафик к новому поду




Промежуточный итог




Вот краткий обзор того, что происходит при создании пода, если он не относится к сервису:




  1. Под сохраняется в etcd.



  2. Планировщик определяет узел и записывает этот узел в etcd.



  3. Kubelet получает уведомление о новом и запланированном поде.



  4. Kubelet делегирует создание контейнера The Container Runtime Interface (CRI).



  5. Kubelet делегирует добавление контейнера The Container Network Interface (CNI).



  6. Kubelet делегирует монтирование томов в контейнере The Container Storage Interface (CSI).



  7. CSI назначает IP-адрес.



  8. Kubelet сообщает IP-адрес Control Plane.



  9. IP-адрес сохраняется в etcd.




Если же под относится к сервису:




  1. Kubelet ждет успешного выполнения readiness-пробы.



  2. Все соответствующие Endpoint (объекты) уведомляются об изменении.



  3. В список Endpoint добавляются новые endpoint (пара IP-адрес + порт).



  4. Kube-proxy получает уведомление об изменении endpoint. Kube-proxy обновляет iptables-правила для каждого узла.



  5. Ingress-контроллер уведомляется об изменении endpoint, направляет трафик на новые IP-адреса.



  6. CoreDNS получает уведомление об изменении endpoint. Если сервис headless, то запись DNS обновляется.



  7. Облачный провайдер получает уведомление об изменении endpoint. Если тип Service — LoadBalancer, то новый endpoint становится частью балансировщика нагрузки.



  8. Все служебные сети, установленные в кластере, получают уведомление об изменении endpoint.



  9. Любая другая функциональность, подписанная на изменения 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 уведомляется об изменении и делегирует:




  1. CSI — размонтирование любых томов из контейнера.



  2. CNI — отсоединение контейнера от сети и передачу IP-адреса.



  3. CRI — уничтожение контейнера.




Таким образом, Kubernetes выполняет точно такие же шаги, как и при создании пода, но в обратном порядке.







При удалении пода команда сначала попадает в API Kubernetes







Kubelet опрашивает Control Plane на наличие обновлений и понимает, что под удален







Kubelet делегирует удаление пода Container Runtime Interface, Container Network Interface и Container Storage Interface




Есть небольшое, но существенное различие. Когда вы завершаете работу пода, удаление 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. Так что вы можете:




  1. Немного подождать.



  2. Продолжить обрабатывать входящий трафик, несмотря на SIGTERM.



  3. Закрыть существующие долгоживущие соединения (например, соединение с базой данных или WebSockets).



  4. Завершить процесс.




По умолчанию 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"]




Хук preStop — один из хуков жизненного цикла пода.




15-секундная задержка — рекомендованное время?




Зависит от обстоятельств, но можно оттолкнуться от этого времени и затем подобрать оптимальное в процессе тестирования. Возможные варианты:







Вы уже знаете, что при удалении пода kubelet получает уведомление об изменении







Если у пода есть preStop-хук, он будет вызван первым







Когда preStop завершается, kubelet отправляет в контейнер сигнал SIGTERM. С этого момента контейнер должен закрыть все long-lived-соединения и подготовиться к завершению







По умолчанию у процесса есть 30 секунд для завершения работы, включая хук preStop. Если к этому времени процесс не завершится, то kubelet отправит сигнал SIGKILL и принудительно завершит процесс







Kubelet уведомляет Control Plane об успешном удалении пода




Grace periods и скользящие обновления




При удалении пода применяется плавное завершение работы. Но как быть, если под так и не удален? Даже если вы этого не сделаете, Kubernetes все равно удаляет под.




Kubernetes создает и удаляет под каждый раз, когда вы развертываете новую версию приложения. Когда вы изменяете образ в своем развертывании, Kubernetes разворачивает изменение постепенно. 




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




Допустим, у вас есть три реплики, и как только вы отправляете новый YAML-файл, Kubernetes:




  • создает под с новым образом контейнера;



  • уничтожает существующий контейнер;



  • дожидается готовности пода.




Система повторяет эти шаги до тех пор, пока все поды не будут обновлены. Kubernetes повторяет каждый цикл только после того, как новый под готов к приему трафику, то есть прошел проверку readiness-пробы.




Ждет ли Kubernetes удаления текущего пода перед переходом к следующему?




Нет, если у вас 10 подов и для одного требуется две секунды на подготовку и 20 на завершение работы, то происходит следующее:




  1. Первый под создается, а предыдущий под прекращает работу.



  2. Новый под готов через две секунды, после чего Kubernetes создает новый.



  3. Находясь в процессе завершения, под продолжает жить в течение 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-задачи, а также можете автоматизировать процесс и уменьшать количество реплик до нуля.




Источник: https://habr.com/ru/company/vk/blog/654471/



2023-01-03T01:18:56
DevOps

Kubernetes tips & tricks: особенности выполнения graceful shutdown в NGINX и PHP-FPM

Типовое условие при реализации 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, в блоке контейнера:




       lifecycle:
          preStop:
            exec:
              command:
              - /usr/sbin/nginx
              - -s
              - quit




Теперь в момент завершения работы 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-сигналы:




  1. SIGINTSIGTERM — fast shutdown;



  2. SIGQUIT — graceful shutdown (то, что нам нужно).




Остальные сигналы в данной задаче не требуются, поэтому их разбор опустим. Для корректного завершения процесса понадобится написать следующий preStop-хук:




        lifecycle:
          preStop:
            exec:
              command:
              - /bin/kill
              - -SIGQUIT
              - "1"




На первый взгляд, это всё, что требуется для выполнения 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 для контейнера будет выглядеть следующим образом:




    lifecycle:
      preStop:
        exec:
          command:
          - /bin/sleep
          - "30"




Однако, из-за указания 30-секундного sleep мы сильно увеличим время деплоя, так как каждый pod будет терминироваться минимум 30 секунд, что плохо. Что с этим можно сделать?




Обратимся к стороне, отвечающей за непосредственное исполнение приложения. В нашем случае это PHP-FPM, который по умолчанию не следит за выполнением своих child-процессов: мастер-процесс терминируется сразу же. Изменить это поведение можно с помощью директивы process_control_timeout, которая указывает временные лимиты для ожидания сигналов от мастера дочерними процессами. Если установить значение в 20 секунд, тем самым покроется большинство запросов, выполняемых в контейнере, и после их завершения мастер-процесс будет остановлен.




С этим знанием вернёмся к нашей последней проблеме. Как уже упоминалось, Kubernetes не является монолитной платформой: на взаимодействие между разными её компонентами требуется некоторое время. Это особенно актуально, когда мы рассматриваем работу Ingress’ов и других смежных компонентов, поскольку из-за такой задержки в момент деплоя легко получить всплеск 500-х ошибок. Например, ошибка может возникать на этапе отправки запроса к upstream’у, но сам «временной лаг» взаимодействия между компонентами довольно короткий — меньше секунды.




Поэтому, в совокупности с уже упомянутой директивой process_control_timeout можно использовать следующую конструкцию для lifecycle:




lifecycle:
  preStop:
    exec:
      command: ["/bin/bash","-c","/bin/sleep 1; kill -QUIT 1"]




В таком случае мы компенсируем задержку командой 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 и повторить деплой. Впрочем, в нашем конкретном случае изменений не было видно (ошибок снова нет).




Заключение




Для корректного завершения процесса мы ожидаем от приложения следующего поведения:




  1. Ожидать несколько секунд, после чего перестать принимать новые соединения.



  2. Дождаться завершения всех запросов и закрыть все keepalive-подключения, которые запросы не выполняют.



  3. Завершить свой процесс.




Однако не все приложения умеют так работать. Одним из решений проблемы в реалиях Kubernetes является:




  • добавление хука pre-stop, который будет ожидать несколько секунд;



  • изучение конфигурационного файла нашего бэкенда на предмет соответствующих параметров.




Пример с NGINX позволяет понять, что даже то приложение, которое изначально должно корректно отрабатывает сигналы к завершению, может этого не делать, поэтому критично проверять наличие 500 ошибок во время деплоя приложения. Также это позволяет смотреть на проблему шире и не концентрироваться на отдельном pod’е или контейнере, а смотреть на всю инфраструктуру в целом.




В качестве инструмента для тестирования можно использовать Яндекс.Танк совместно с любой системой мониторинга (в нашем случае для теста брались данные из Grafana с бэкендом в виде Prometheus). Проблемы с graceful shutdown хорошо видны при больших нагрузках, которую может генерировать benchmark, а мониторинг помогает детальнее разобрать ситуацию во время или после теста.




Отвечая на обратную связь по статье: стоит оговориться, что проблемы и пути их решения здесь описываются применительно к NGINX Ingress. Для других случаев есть иные решения, которые, возможно, мы рассмотрим в следующих материалах цикла.




Источник: https://habr.com/ru/company/flant/blog/489994/



2023-01-03T00:41:02
DevOps

Сохраняем кластеры Kubernetes в чистоте и порядке

Одновременно с ростом кластера 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:




apiVersion: v1
kind: LimitRange
metadata:
  name: pvc-limit
spec:
  limits:
  - type: PersistentVolumeClaim
    max:
      storage: 10Gi
    min:
      storage: 1Gi
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: pvc-quota
spec:
  hard:
    persistentvolumeclaims: "10"
    requests.storage: "10Gi"
    # сумма запрошенного хранилища в bronze storage class не может превышать 5 Гб
    bronze.storageclass.storage.k8s.io/requests.storage: "5Gi"




Выше мы имеем два объекта.




LimitRange устанавливает минимальный и максимальный размеры PVC в неймспейсе. Это оградит пользователей от запрашивания слишком больших объемов.




ResourceQuota дополнительно обеспечивает жесткое ограничение на количество PVC и их совокупный размер.




Затем вы можете предотвратить создание кучи объектов и оставление их в качестве мусора после использования. Для этого используйте object count, квоты на количество объектов, которые зададут жесткое ограничение на количество объектов определенного типа в конкретном неймспейсе:




apiVersion: v1
kind: ResourceQuota
metadata:
  name: object-count-quota
spec:
  hard:
    configmaps: "2"
    secrets: "10"
    services: "5"
    count/jobs.batch: "8"




Есть несколько встроенных полей, через которые вы можете задать квоты на количество объектов. Например, configmapssecrets или services, показанные выше. Для всех прочих ресурсов можно использовать формат count/<resource>.<group>, как показано в примере с count/jobs.batch, что может помочь от бесконтрольного создания джоб из-за неправильно настроенного CronJob.




Вероятно, большинству известно о функции установки квот на память и CPU. Но, возможно, для вас станет новостью квота ephemeral storage. Альфа-поддержка квот для эфемерного хранилища была добавлена в v1.18 и дала возможность установить границы ephemeral storage так же, как как для памяти и процессора.




apiVersion: v1
kind: ResourceQuota
metadata:
  name: ephemeral-storage-quota
spec:
  hard:
    requests.ephemeral-storage: 1Gi
    limits.ephemeral-storage: 2Gi




Однако будьте осторожны с этой настройкой. Поды могут быть вытеснены из-за превышения лимита, что может быть вызвано слишком большим размером логов контейнера.




Помимо установки квот и границ ресурсов, можно установить ограничение для истории ревизий 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, которая выглядит примерно так:




apiVersion: v1
kind: ConfigMap
metadata:
  name: kube-janitor
  namespace: default
data:
  rules.yaml: |-
    rules:
      ...




Конечно, для нас самый интересный раздел здесь — правила, 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-метрик, которые могут оказаться полезными для поддержания порядка в кластере:







Это только некоторые метрики из тех, которые вы можете использовать. Какие из них будут доступны, зависит также от ваших инструментов мониторинга, т.е. вы можете использовать какие-то специальные метрики, доступные на вашем сервисе.




Заключение




Мы рассмотрели несколько вариантов очистки кластера Kubernetes — некоторые совсем простые, а некоторые посложнее. Независимо от того, что вы выберете, старайтесь не забывать делать уборку в кластере и сохранять порядок. Это может спасти вас от большой головной боли и, по крайней мере, избавит от ненужного хлама в кластере. В конечном счете такая уборка служит тем же целям, что наведение порядка на рабочем столе.




Также имейте ввиду, что если вы оставляете объекты лежать без использования долгое время, вы просто забудете, зачем они там. Это может сильно осложнить понимание, что должно существовать, а что — нет.




Помимо описанных здесь подходов, вы также можете использовать какое-либо решение GitOps — например, ArgoCD или Flux для создания ресурсов и управления ими, что может значительно упростить их очистку. Обычно потребуется удалить только один кастомный ресурс, что вызовет каскадное удаление всех зависимых ресурсов.




Источник: https://habr.com/ru/company/kts/blog/587236/