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

В этой статье объясняется, как настроить контроллер входящего трафика Nginx в Kubernetes с нулевым временем простоя.

Эта проблема

Официальный контроллер Nginx Ingress по умолчанию прерывает работу при перезапуске или повторном развертывании его модулей. Это может привести к разрыву соединений, если входящие модули по какой-либо причине прерываются. Особенно это неприемлемо в производственной среде. Нам нужен контроллер входящего трафика nginx, который всегда доступен для обслуживания трафика.

Kubernetes и SIGTERM

При завершении модуля Kubernetes отправляет сигнал SIGTERM основному процессу и ждет некоторое заранее определенное время для завершения модуля (по умолчанию 30 секунд). По истечении этого времени Kubernetes отправит SIGKILL, что приведет к немедленному завершению процесса.

Документацию по окончанию пода можно найти здесь.

Kubernetes ожидает, что поды будут обрабатывать SIGTERM плавное завершение работы. На самом деле не все капсулы оправдывают это ожидание.

Сигналы Nginx: SIGTERM против SIGQUIT

Nginx обрабатывает сигналы немного иначе, чем ожидает Kubernetes. Согласно документации Nginx по обработке сигналов, он будет обрабатывать TERM и QUIT следующим образом:

 Nginx Signals
+-----------+--------------------+
|   signal  |      response      |
+-----------+--------------------+
| TERM, INT | fast shutdown      |
| QUIT      | graceful shutdown  |
+-----------+--------------------+

Итак, когда Kubernetes отправляет SIGTERM модулю nginx-ingress-controller, Nginx выполняет быстрое завершение работы. Если в это время контроллер обрабатывает какие-либо запросы, они будут прерваны, что приведет к разрыву соединений. Это может вызвать каскад сбоев, что приведет к увеличению количества ответов HTTP 50x во время завершения работы модуля.

Вместо этого мы хотим дать Nginx SIGQUIT сигнал, который будет запрашивать корректное завершение работы. Nginx будет ждать завершения соединений перед выходом, не принимая при этом никаких новых соединений.

Решение: крючок для предварительной остановки жизненного цикла

Kubernetes предлагает preStop ловушку жизненного цикла для спецификации модуля. Во время завершения Kubernetes вызовет preStop после сообщения о том, что модуль Terminating, но перед отправкой модулю сигнала SIGTERM.

Ловушка preStop позволяет модулю вызывать любую команду до сигнала завершения. В этом случае мы хотим упреждающе отправить Nginx сигнал SIGQUIT, чтобы контроллер корректно завершал соединения.

Более подробную информацию о preStop можно найти здесь.

Как отправить Nginx SIGQUIT

Образ nginx-ingress-controller,

quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.24.1

включает команду для отправки сигналов завершения Nginx.

Выполнение следующего сценария на этом изображении:

/usr/local/openresty/nginx/sbin/nginx -c /etc/nginx/nginx.conf -s quit
while pgrep -x nginx; do 
  sleep 1
done

отправит Nginx SIGQUIT сигнал и будет ждать завершения процесса.

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

Определение хука preStop

Мы можем поместить приведенный выше сценарий в однострочную команду и добавить его в раздел lifecycle спецификации модуля следующим образом:

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 5; /usr/local/openresty/nginx/sbin/nginx -c /etc/nginx/nginx.conf -s quit; while pgrep -x nginx; do sleep 1; done"]

Обратите внимание, что перед скриптом стоит команда sleep 5. Это будет ждать прохождения любых условий гонки, связанных с кубернетами, прежде чем инициировать постепенное завершение работы. На момент написания этой статьи неясно, почему это необходимо, но во время тестирования nginx все равно разрывает соединения, если этот сон не был включен.

Определение terminationGracePeriodSeconds

Хук preStop обязательно даст нам QUIT сигнал, необходимый для корректного завершения работы nginx. Однако по умолчанию Kubernetes дал всего 30 секунд на прохождение всего Termination процесса, прежде чем применить SIGKILL. Это означает, что и ловушка preStop, и сигнал SIGTERM Kubernetes должны быть завершены до истечения 30 секунд, иначе Nginx по-прежнему будет страдать от некорректного завершения работы и разорванных соединений.

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

spec:
  terminationGracePeriodSeconds: 600

Обратите внимание: если 30 секунд достаточно для настройки входящего трафика Nginx, вы можете пропустить эту часть.

Более подробную информацию о льготном периоде завершения и выполнении обработчика ловушек можно найти здесь.

Развертывание исправления с нулевым временем простоя

Тот факт, что мы решили проблему с нулевым временем простоя, не означает, что у нас будет развертывание с нулевым временем простоя, если мы обновим развертывание nginx. Помните, что в старой конфигурации все еще есть проблема. Решение здесь состоит в том, чтобы иметь ДВА балансировщика нагрузки и ДВА контроллера входа nginx и переключать трафик между ними. Направьте трафик на временный балансировщик нагрузки, обновите исходный, пока через него не проходит трафик, а затем направьте трафик обратно на исходный контроллер входящего трафика.

Шаг 1. Создайте второй входной контроллер

Развертываем nginx-ingress-controller через helm:

helm upgrade --install nginx-ingress stable/nginx-ingress --namespace ingress -f nginx/values.yaml

Это создаст nginx-ingress-controller в пространстве имен ingress. Мы определили дополнительную конфигурацию в файле values.yaml.

Нам нужно будет установить второй контроллер, что мы также можем сделать через helm (создали новое пространство имен с именем ingress-temp):

helm upgrade --install nginx-ingress-temp stable/nginx-ingress --namespace ingress-temp -f nginx/values.yaml

ПРИМЕЧАНИЕ. Если вы используете Helm 3, вам нужно сначала создать пространство имен через kubectl:

kubectl create namespace ingress-temp

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

Шаг 2. Перенаправьте трафик на временный контроллер

Оба контроллера Nginx доступны для направления трафика к службам, но ваш DNS-провайдер настроен на взаимодействие только с одним из балансировщиков нагрузки. Измените определение DNS для своих веб-сайтов, чтобы оно указывало на балансировщик нагрузки, созданный из nginx-ingress-temp. Как это сделать, зависит от вашего DNS-провайдера.

Мониторинг потока трафика

Во время этого переключения полезно регистрировать трафик от обоих контроллеров. Мы можем сделать это с kubectl в двух отдельных окнах терминала:

Перед переключением переключателя DNS просмотрите журналы трафика с исходного контроллера (пространство имен ingress):

kubectl logs -lcomponent=controller -ningress -f

Вы должны увидеть весь трафик, проходящий через этот контроллер. Это тот, который мы отключим от сети и обновим с помощью ловушки preStop.

В отдельном окне проследите за журналами трафика временного контроллера, созданного на шаге 1:

kubectl logs -lcomponent=controller -ningress-temp -f

Кроме того, что nginx запускается сам, вы не должны видеть журналы трафика с этого контроллера.

Держите эти окна открытыми. Мы будем следить за изменением потока трафика при переключении DNS.

Изменить DNS на временный контроллер

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

Подробнее об автоматическом создании балансировщиков нагрузки Kubernetes можно найти здесь.

Чтобы получить внешний IP-адрес только что созданного балансировщика нагрузки, выполните следующую команду kubectl:

kubectl get service -ningress-temp

Найдите службу типа LoadBalancer и получите External-IP. Например, для AWS это может выглядеть так:

EXTERNAL-IP
xxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxx.us-west-1.elb.amazonaws.com

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

Отслеживайте изменения в потоке трафика

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

Подождите, пока трафик полностью иссякнет с исходного контроллера. Как только это будет завершено, переходите к шагу 3.

Шаг 3. Повторно разверните исходный контроллер с помощью PreStop Update

После того, как трафик был удален из исходного nginx-ingress-controller, можно безопасно обновить исходное развертывание nginx-ingress-controller с помощью ловушки preStop.

Добавьте в nginx values.yaml следующую конфигурацию:

controller:
  lifecycle:
    preStop:
      exec:
        command: ["/bin/sh", "-c", "sleep 5; /usr/local/openresty/nginx/sbin/nginx -c /etc/nginx/nginx.conf -s quit; while pgrep -x nginx; do sleep 1; done"]
  terminationGracePeriodSeconds: 600

Еще раз обратите внимание, что terminationGracePeriodSeconds НЕОБЯЗАТЕЛЬНО, а количество секунд настраивается в зависимости от потребностей вашего контроллера.

Повторно разверните исходный контроллер с помощью helm:

helm upgrade --install nginx-ingress stable/nginx-ingress --namespace ingress --version 1.6.16 -f nginx/values.yaml

Убедитесь, что контроллер развернут с применением хука preStop:

kubectl get deployment nginx-ingress-controller -ningress -oyaml

Исходный контроллер снова готов принимать трафик.

Шаг 4: Прямой трафик обратно к исходному контроллеру

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

kubectl get service -ningress-temp

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

Мониторинг обратного потока трафика к исходному контроллеру

Вернитесь в окна своего терминала, в которых вы отслеживаете трафик для двух контроллеров. Если время выполнения команды kubectl logs истекло, возможно, вам придется вызвать команду снова.

Если вышеуказанные шаги были выполнены по плану, вы должны увидеть, как трафик падает на временном контроллере и восстанавливается на исходном контроллере.

Еще раз, как только трафик будет полностью истощен, перейдите к последнему шагу.

Шаг 5. Удалите лишнюю инфраструктуру

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

Используйте helm, чтобы удалить временный контроллер, как показано ниже:

helm delete --purge nginx-ingress-temp --namespace ingress-temp

Опять же, если вы используете Helm 3, удалите временное пространство имен через kubectl:

kubectl delete namespace ingress-temp

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

Шаг 6: отпразднуйте!

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

Будущие развертывания Nginx

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

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

Линдси Лэндри, инженер DevOps в Codecademy