Kubernetes tips & tricks: асаблівасці выканання graceful shutdown у 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 - Атрыманне змен аб стане пода
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. SIGINT, SIGTERM - 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:

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-хук. Цалкам магчыма, што прычына крыецца якраз у гэтым.

PHP-FPM… і не толькі

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

Варта ўдакладніць, што сама праблема можа ў меншай ці большай ступені залежаць ад самога дадатку і не праяўляцца, напрыклад, у маніторынгу. Калі вы ўсё ж сутыкнецеся з ёй, то на розум спачатку прыходзіць просты workaround: дадаць preStop-хук са 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 і самога Яндэкс.Танка - прыкладам з нашага вопыту.

Самае галоўнае тут - правяраць змены паэтапна. Пасля дадання новага выпраўлення запускайце тэставанне і гледзіце, ці змяніліся вынікі ў параўнанні з мінулым запускам. У адваротным выпадку будзе складана выявіць неэфектыўныя рашэнні, а ў даляглядзе можна і зусім толькі нашкодзіць (напрыклад, павялічыць час дэплою).

Іншы нюанс - глядзіце логі кантэйнера падчас яго тэрмінацыі. Ці фіксуецца там інфармацыя аб graceful shutdown? Ці ёсць у логах памылкі пры звароце да іншых рэсурсаў (напрыклад, да суседняга кантэйнера PHP-FPM)? Памылкі самога прыкладання (як у апісаным вышэй выпадку з NGINX)? Спадзяюся, што ўступная інфармацыя з гэтага артыкула дапаможа лепш разабрацца, што ж адбываецца з кантэйнерам падчас яго тэрмінавання.

Такім чынам, першы запуск тэсціравання адбываўся без lifecycle і без дадатковых дырэктыў для сервера прыкладанняў (process_control_timeout у PHP-FPM). Мэтай гэтага тэсту было выяўленне прыблізнай колькасці памылак (і ці ёсць яны ўвогуле). Таксама з дадатковай інфармацыі варта ведаць, што сярэдні час дэплою кожнага пода складала каля 5-10 секунд да стану поўнай гатоўнасці. Вынікі такія:

Kubernetes tips & tricks: асаблівасці выканання graceful shutdown у NGINX і PHP-FPM

На інфармацыйнай панэлі Яндекс.Танка бачны ўсплёск 502 памылак, які адбыўся ў момант дэплою і працягваўся ў сярэднім да 5 секунд. Як мяркуецца, гэта абрываліся існуючыя запыты да старога pod'у, калі ён тэрмінаваўся. Пасля гэтага з'явіліся 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, які будзе чакаць некалькі секунд;
  • вывучэнне канфігурацыйнага файла нашага бэкенда на прадмет адпаведных параметраў.

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

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

Адказваючы на ​​зваротную сувязь па артыкуле: варта абмовіцца, што праблемы і шляхі іх рашэння тут апісваюцца ў дачыненні да NGINX Ingress. Для іншых выпадкаў ёсць іншыя рашэнні, якія, магчыма, мы разгледзім у наступных матэрыялах цыклу.

PS

Іншае з цыклу K8s tips & tricks:

Крыніца: habr.com

Дадаць каментар