Корректное завершение работы контейнеров в Kubernetes
Это вторая часть нашего пути (прим. пер. — ссылка на первую статью) к достижению нулевого времени простоя при обновлении Kubernetes-кластера. В первой части мы изложили проблемы и задачи, возникающие при выполнении операции drain для нод в кластере. В этом посте мы расскажем, как решить одну из таких проблем: корректно завершить работу pod’ов.
Жизненный цикл переселения pod’а
По умолчанию kubectl drain
при выселении pod’ов учитывает схему их жизненного цикла. На практике это означает, что он будет действовать по следующей схеме:
drain
создаст запрос на удаление pod’ов на выбранной ноде в control plane. Это впоследствии уведомитkubelet
на выбранной ноде, чтобы он начал завершать работу pod’ов.kubelet
на ноде запустит в pod’е хукpreStop
- как только хук
preStop
завершится,kubelet
на ноде подаст сигналTERM
работающему приложению в контейнере pod’а. kubelet
на ноде будет ждать определенный период (определенный в pod’е, или заданный из командной строки; по умолчанию это 30 секунд) пока контейнер выключится, прежде чем принудительно убить процесс (черезSIGKILL
). Запись о времени ожидания включает в себя время для выполненияpreStop
хука.
Согласно данной схеме, можно задействовать preStop
хук и подать сигнал для завершения работы, чтобы приложение смогло “сделать очистку” и завершиться корректно. Например, если у вас есть работающий процесс, отслеживающий задачи в очереди, ваше приложение может перехватить сигнал TERM
, и воспринять его как указание для того, чтобы перестать принимать для выполнения новые задачи и завершиться, после того, как все задачи будут выполнены. Или, если ваше приложение нельзя модифицировать, чтобы оно перехватывало TERM
сигнал (например, какое-либо стороннее приложение), тогда вы можете использовать preStop
хук для реализации API, который бы предоставил возможность корректного завершения работы приложения.
В нашем примере Nginx по умолчанию не завершится корректно при подаче сигнала TERM
, потому что существующие обращения к нему завершатся с ошибкой. Поэтому мы будем использовать для корректного завершения preStop
хук. Мы модифицируем наш resource и добавим этап lifecycle
в спецификацию контейнера. Этап lifecycle
выглядит следующим образом:
lifecycle:
preStop:
exec:
command: [
# Gracefully shutdown nginx
"/usr/sbin/nginx", "-s", "quit"
]
Согласно этой настройке, при выключении, перед тем как отправить сигнал SIGTERM
в контейнер с Nginx будет выполнена команда /usr/sbin/nginx -s quit
. Обратите внимание, что, поскольку команда корректно завершит процесс Nginx и сам pod, сигнал TERM
по факту становится noop (ничего не делает — прим.переводчика).
Данная настройка прописывается в спецификации контейнера Nginx. Когда мы добавим её, полный конфиг для Deployment
будет выглядеть следующим образом:
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.15
ports:
- containerPort: 80
lifecycle:
preStop:
exec:
command: [
# Gracefully shutdown nginx
"/usr/sbin/nginx", "-s", "quit"
]
Обслуживание трафика после выключения pod’а
При корректном выключении pod’а должна обеспечиваться остановка Nginx таким образом, чтобы обслужить текущий трафик перед выключением. В любом случае, как вы могли заметить, несмотря на благие намерения, контейнеру с Nginx продолжает поступать трафик после его отключения, создавая время простоя в работе вашего сервиса.
Чтобы понять всю сложность ситуации, давайте взглянем на пример с нашим deployment. Для примера, рассмотрим, что нода приняла клиентский трафик. При этом внутри нее запустится процесс обработки запроса. Обозначим этот процесс кружком в контейнере pod’а.
Предположим, что в этот момент оператор кластера решает провести работы на Node 1. В рамках этой задачи оператор запускает команду kubectl drain node-1
, в результате чего процесс kubelet
на узле выполняет хук preStop
, запуская команду для корректного завершения работы процесса Nginx:
Так как nginx все ещё обрабатывает исходный запрос, его процесс не завершается сразу. Однако, когда nginx начинает последовательное завершение работы, он начинает выдавать ошибки и отклоняет дополнительный трафик, который к нему приходит.
Предположим, в этот момент на наш сервис пришел новый запрос. Так как сервис все еще видит этот pod, он все ещё может принимать трафик. Если он его примет, то вернет ошибку, потому что nginx завершает работу:
Чтобы завершить процедуру выключения, в конечном итоге nginx завершит обработку исходного запроса, который завершит работу pod’а и на ноде будет выполнена операция drain
:
В этом примере, в случае, если приложение в pod’е получает запрос после запуска его отключения, первый клиент получит ответ от сервера. Однако, второй клиент получит ошибку, которая будет восприниматься как время простоя.
Так почему это происходит? И как можно уменьшить потенциальное время простоя для клиентов, которые выполняют запрос к серверу в момент, когда он начинает выключаться? В следующей части нашего цикла, мы будем подробно рассказывать о жизненном цикле выселении pod’а и описывать, как можно ввести задержку в preStop
хуке так, чтобы смягчить последствия подачи непрерывного трафика от Service
.
Чтобы получить полностью внедренную и протестированную версию обновлений кластера Kubernetes для нулевого временем простоя на AWS и других ресурсах, посетите Gruntwork.io.
Также читайте другие статьи в нашем блоге:
- Blue-Green Deployment приложений на Spring c веб-сервером Nginx
- Как запустить несколько пайплайнов с помощью GitLab CI/CD
- /etc/resolv.conf для Kubernetes pods, опция ndots:5, как это может негативно сказаться на производительности приложения
- Разбираемся с пакетом Context в Golang
- Три простых приема для уменьшения Docker-образов
- Traefik как Ingress-контроллер для K8S