Съвети и трикове за Kubernetes: особености на гратис затваряне в NGINX и PHP-FPM

Типично условие за реализиране на CI/CD в Kubernetes: приложението трябва да може преди пълно спиране да не приема нови клиентски заявки, но най-важното — успешно да завършва вече съществуващите.

Kubernetes tips & tricks: особености на graceful shutdown в NGINX и PHP-FPM

Спазването на това условие позволява да се постигне нулево време на простоя по време на деплой. Въпреки това, дори при употреба на много популярни стекове (като NGINX и PHP-FPM), могат да се появят трудности, които да доведат до ръст на грешките при всеки деплой…

Теория. Как живее pod

Подробности за жизнения цикъл на pod-а вече публикувахме. тази статия. В контекста на разглежданата тема ни интересува следното: в момента, когато pod преминава в състояние Прекратяване, нови заявки не се изпращат към него (pod се изтрива от списъка с endpoints за услугата). По този начин, за да се избегне простоя по време на деплой, от наша страна е достатъчно да решим проблема с коректното спиране на приложението.

Също така трябва да помним, че grace period по подразбиране е 30 секунди: след това pod ще бъде терминално спрян и приложението трябва да успее да обработи всички заявки до този период. Забележка: въпреки че всяка заявка, която трае повече от 5-10 секунди, вече е проблемна, и graceful shutdown няма да се справи с нея…

За да разберем по-добре какво се случва, когато pod приключва работата си, е достатъчно да проучим следната схема:

Kubernetes tips & tricks: особености на graceful shutdown в NGINX и PHP-FPM

А1, B1 — Получаване на изменения за състоянието на pod-а
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 можем да завършим процесите или в режим на бързо затваряне, или в режим на учтиво затваряне. Очевидно е, че ни интересува именно последният вариант.

Следва всичко да е просто: необходимо е да добавим в preStop-hook командата, която ще изпраща сигнал за учтиво затваряне. Това може да се направи в 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? Как обработва той учтивото затваряне? Нека разберем.

PHP-FPM

Случаят с PHP-FPM има по-малко информация. Ако се ориентираме в официалния мануал по PHP-FPM, ще се спомене, че приемат следните POSIX сигнали:

  1. SIGINT, SIGTERM — бързо затваряне;
  2. SIGQUIT — учтиво затваряне (това, от което имаме нужда).

Останалите сигнали за това задание не са необходими, затова тяхното разглеждане ще пропуснем. За правилно завършване на процеса е необходимо да се напише следният preStop-hook:

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

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

Практика. Възможни проблеми с учтивото затваряне

NGINX

Най-напред е полезно да помним: освен извършването на командата nginx -s quit има още една стъпка, на която си заслужава да обърнете внимание. Стигали сме до проблема, при който NGINX вместо сигнал SIGQUIT все пак изпраща SIGTERM, което води до натрупване на заявки без коректно приключване. Подобни случаи могат да бъдат намерени, например, тук. За съжаление, не успяхме да установим конкретната причина за такова поведение: подозирахме въпросните версии на NGINX, но те не се потвърдиха. Симптоматиката в случая се състои в това, че в логовете на контейнера NGINX бяха наблюдавани съобщения «open socket #10 left in connection 5», след което pod-ът спираше.

Можем да наблюдаваме такъв проблем, например, по отговорите на необходимия ни Ingress:

Kubernetes tips & tricks: особености на graceful shutdown в NGINX и PHP-FPM
Статус кодовете в момент на деплой

В този случай получаваме точно 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 hook. Възможно е причината да се крие точно в това.

PHP-FPM… и не само

Проблемът с PHP-FPM се описва тривиално: той не изчаква завършването на дъщерните процеси, а ги приключва, в резултат на което възникват 502 грешки по време на деплой и други операции. На bugs.php.net от 2005 година има няколко съобщения за грешки (например, тук и тук), в които се описва този проблем. А в логовете вероятно няма да видите нищо: PHP-FPM ще отчете приключването на своя процес без каквито и да е грешки или външни уведомления.

Добре е да уточним, че самият проблем може в по-малка или по-голяма степен да зависи от самото приложение и да не се прояви например в мониторинга. Ако все пак се сблъскате с него, то на ум най-напред идва простото решение: добавете preStop hook със sleep(30). То ще позволи да се приключат всички заявки, които са били преди това (а нови не приемаме, тъй като pod-ът вече е в състояние Прекратяване), а след 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 и самия Яндекс.Танк — от нашия опит.

Най-важното тук е да проверявате промените поетапно. След добавяне на ново коригиране, пускайте теста и наблюдавайте дали резултатите са се променили в сравнение с предишното стартиране. В противен случай ще бъде трудно да се открият неефективни решения, а в перспектива може дори да навредите (например, да увеличите времето за деплой).

Друг нюанс е да следите логовете на контейнера по време на терминирането му. Записва ли се информация за плавно спиране? Има ли грешки в логовете при обращение към други ресурси (например, към съседния контейнер PHP-FPM)? Грешки на самото приложение (както в описания по-горе случай с NGINX)? Надявам се, че въвеждащата информация от тази статия ще помогне за по-добро разбиране на това, което се случва с контейнера по време на терминирането му.

И така, първото стартиране на теста се проведе без lifecycle и без допълнителни директиви за сървъра на приложенията (process_control_timeout в PHP-FPM). Целта на този тест беше да се установи приблизителното количество грешки (и дали изобщо съществуват). Също така от допълнителната информация следва да се знае, че средното време за деплой на всеки под е било около 5-10 секунди до пълното му готовност. Резултатите са следните:

Kubernetes tips & tricks: особености на graceful shutdown в NGINX и PHP-FPM

На информационната панела на Яндекс.Танк се наблюдава изблик на 502 грешки, който се е случил в момента на деплоя и е продължил средно до 5 секунди. Предполага се, че това се е дължало на прекъснати съществуващи запитвания към стария под, когато той е бил терминиран. След това се появиха 503 грешки, които станаха резултат от спряния контейнер NGINX, който също е прекъснал връзките поради бэкенда (поради което Ingress не е успял да се свърже с него).

Нека виждаме как process_control_timeout в PHP-FPM ще ни помогне да изчакаме завършването на child-процесите, т.е. да коригираме такива грешки. Повторен деплой вече с използване на тази директива:

Kubernetes tips & tricks: особености на graceful shutdown в NGINX и PHP-FPM

По време на внедряване вече няма 500-ки грешки! Внедряването преминава успешно, graceful shutdown работи.

Но е важно да си спомним момента с Ingress контейнерите, малък процент от грешките, които можем да получим, са заради временно забавяне. За да ги избегнем, остава да добавим конструкция със sleep и да повторим внедряването. Все пак, в нашия конкретен случай не бяха видими изменения (грешки отново няма).

Заключение

За коректно завършване на процеса очакваме от приложението следното поведение:

  1. Да изчака няколко секунди, след което да престане да приема нови свързвания.
  2. Да изчака приключването на всички заявки и да затвори всички keepalive връзки, които не изпълняват заявки.
  3. Да завърши своя процес.

Но не всички приложения умеят да работят по този начин. Едно от решенията на проблема в реалностите на Kubernetes е:

  • добавяне на pre-stop hook, който да изчака няколко секунди;
  • изследване на конфигурационния файл на нашия бекенд за съответните параметри.

Примерът с NGINX показва, че дори приложението, което по принцип трябва да реагира коректно на сигналите за приключване, може да не го направи, поради което е критично важно да проверяваме наличието на 500 грешки по време на внедряването на приложението. Също така това позволява да погледнем на проблема по-широко и да не се съсредоточаваме само върху отделен pod или контейнер, а да разглеждаме цялата инфраструктура.

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

Отговаряйки на обратната връзка относно статията: трябва да се уточни, че проблемите и начините за решаването им тук са описани относно NGINX Ingress. За други случаи има и други решения, които, възможно, ще разгледаме в следващите материали от цикъл.

P.S.

Друго от цикъла K8s tips & tricks:

Източник: habr.com

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster