image
Корректное завершение работы контейнеров в 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’а.


image


Предположим, что в этот момент оператор кластера решает провести работы на Node 1. В рамках этой задачи оператор запускает команду kubectl drain node-1, в результате чего процесс kubelet на узле выполняет хук preStop, запуская команду для корректного завершения работы процесса Nginx:


image


Так как nginx все ещё обрабатывает исходный запрос, его процесс не завершается сразу. Однако, когда nginx начинает последовательное завершение работы, он начинает выдавать ошибки и отклоняет дополнительный трафик, который к нему приходит.


Предположим, в этот момент на наш сервис пришел новый запрос. Так как сервис все еще видит этот pod, он все ещё может принимать трафик. Если он его примет, то вернет ошибку, потому что nginx завершает работу:


image


Чтобы завершить процедуру выключения, в конечном итоге nginx завершит обработку исходного запроса, который завершит работу pod’а и на ноде будет выполнена операция drain:


image


image


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


Так почему это происходит? И как можно уменьшить потенциальное время простоя для клиентов, которые выполняют запрос к серверу в момент, когда он начинает выключаться? В следующей части нашего цикла, мы будем подробно рассказывать о жизненном цикле выселении pod’а и описывать, как можно ввести задержку в preStop хуке так, чтобы смягчить последствия подачи непрерывного трафика от Service.


Чтобы получить полностью внедренную и протестированную версию обновлений кластера Kubernetes для нулевого временем простоя на AWS и других ресурсах, посетите Gruntwork.io.


Также читайте другие статьи в нашем блоге: