Требования к разработке приложения в Kubernetes
Сегодня я планирую рассказать, как нужно писать приложения и какие есть требования для того, чтобы ваше приложение хорошо работало в Kubernetes. Чтобы с приложением не было никакой головной боли, чтобы не приходилось придумывать и выстраивать какие-то «костыли» вокруг него — и работало всё так, как это задумывалось самим Kubernetes.
Эта лекция в рамках «Вечерней школы Слёрма по Кубернетес». Вы можете просмотреть открытые теоретические лекции Вечерней Школы на Youtube, сгруппированные в плейлист. Для тех же, кому удобнее текст, а не видео, мы подготовили эту статью.
Зовут меня Павел Селиванов, на текущий момент я являюсь ведущим DevOps инженером компании Mail.ru Cloud Solutions, мы делаем «облака», мы делаем мэнедж-кубернетисы и так далее. В мои задачи сейчас как раз-таки входит помощь в разработке, раскатывание эти облаков, раскатывание приложения, которые мы пишем и непосредственно разработка инструментария, который мы предоставляем для наших пользователей.
Я DevOps-ом занимаюсь, думаю, что последние, наверное, года три. Но, в принципе, то, чем занимается DevOps, я занимаюсь, наверное, лет пять уже точно. До этого я занимался скорей больше админскими вещами. С Kubernetes я начал работать очень давно — уже, наверное, прошло порядка четырех лет с тех пор, как я начал с ним работать.
Вообще начинал я, когда у Kubernetes была версии 1.3, наверное, а может быть и 1.2 — когда он был еще в зачаточном состоянии. Сейчас он уже совсем не в зачаточном состоянии находится — и очевидно, что на рынке есть огромный спрос на инженеров, которые в Kubernetes хотели бы уметь. И у компаний есть очень большой спрос на таких людей. Поэтому, собственно, и появилась эта лекция.
Если говорить по плану того, о чем я буду рассказывать, это выглядит вот так, в скобочках написано (TL; DR) — «too long; don«t read». Моя сегодняшняя презентация будет представлять из себя бесконечные списки.
На самом деле, я сам не люблю такие презентации, когда их делают, но тут такая тема, что я когда готовил эту презентацию, я просто не придумал на самом деле, как по-другому организовать эту информацию.
Потому что по большому счету эта информация — это «ctrl+c, ctrl+v», из, в том числе нашей Вики в разделе DevOps, где у нас написаны требования к разработчикам: «ребята, чтобы ваше приложение мы запустили в Kubernetes, оно должно быть вот таким».
Поэтому получилась презентация таким большим списком. Извините. Постараюсь рассказывать максимально, чтобы было нескучно по возможности.
Что мы с вами сейчас будем разбирать:
- это, во-первых, логи (журналы приложения?), что с ними делать в Kubernetes, как с ними быть, какими они должны быть;
- что делать с конфигурациями в Kubernetes, какие для Kubernetes лучшие-худшие способы конфигурировать приложение;
- поговорим о том, что такое проверки доступности вообще, как они должны выглядеть;
- поговорим о том, что такое graceful shutdown;
- поговорим еще раз про ресурсы;
- еще раз затронем тему хранения данных;
- и в конце я расскажу, что такое термин этот загадочный cloud-native приложение. Cloudnativeness, как прилагательное от этого термина.
Логи
Я предлагаю начать с логов — с того, куда эти логи нужно в Kubernetes пихать. Вот вы запустили в Kubernetes приложение. По классике, раньше приложения всегда писали логи куда-нибудь в файлик. Плохие приложения писали логи в файлик в домашней директории разработчика, который запустил это приложение. Хорошие приложения писали логи в файлик куда-нибудь в /var/log
.
Соответственно, дальше, у хороших админов в инфраструктурах были настроены какие-нибудь штуки, которые эти логи могут ротировать — тот же самый rsyslog элементарно, который смотрит на эти логи и когда с ними что-то происходит, их становится много, он создает резервные копии, складывает туда логи, удаляет старые файлики, больше, чем за неделю, за полгода и еще за сколько-нибудь. По идее, у нас должно быть предусмотрено, чтобы просто тупо из-за того, что приложение пишет логи, место на серверах продакшна (боевых серверах?) не кончалось. И, соответственно, весь продакшн не останавливался из-за логов.
Когда мы переходим в мир Kubernetes и запускаем там все то же самое, первое, на что можно обратить внимание — это на то, что люди, как писали логи в файлике, так и продолжают их писать.
Оказывается, если мы будем говорить про Kubernetes, что правильным местом для того, чтобы из докер-контейнера куда-то написать логи, это просто писать их из приложения в так называемые Stdout/Stderr, то есть потоки стандартного вывода операционной системы, стандартного вывода ошибок. Это самый правильный, самый простой и самый логичный способ, куда девать логи в принципе в докере и конкретно в Кубернетисе. Потому что, если ваше приложение пишет логи в Stdout/Stderr, то дальше это уже задача докера и надстройки над ним Kubernetes, что с этими логами делать. Докер по умолчанию будет складывать свои специальные файлики в JSON формате.
Тут уже возникает вопрос, что вы с этими логами будете делать дальше. Самый простой способ, понятно, у нас есть возможность сделать kubectl logs
и посмотреть эти логи этих «подов». Но, наверное, это не очень хороший вариант — с логами нужно что-то дальше делать.
Пока еще поговорим заодно, раз уж мы затронули тему логов, о такой штуке, как логи должны выглядеть. То есть, это не относится напрямую к Kubernetes, но когда мы начинаем задумываться о том, что делать с логами, хорошо бы задуматься и об этом тоже.
Нам нужен какой-то инструмент, по-хорошему, который эти логи, которые у нас докер складывает в свои файлики, возьмет и куда-то их отправит. По большому счету, обычно мы внутри Kubernetes запускаем в виде DaemonSet какой-нибудь агент — сборщик логов, которому просто сказано, где лежат логи, которые складывает докер. И этот агент-сборщик их просто берет, возможно, даже по пути как-то парсит, возможно обогащает какой-то дополнительной метаинформацией и, в итоге, отправляет на хранение куда-то. Там уже возможны вариации. Самое распространенное, наверное, Elasticsearch, где можно хранить логи, их можно оттуда удобно доставать. Потом с помощью запроса, с помощью Kibana, например, строить по ним графики, строить по ним алерты и так далее.
Самая главная мысль, еще раз ее хочу повторить, о том, что внутри докера, в частности, внутри Kubernetes, складывать ваши логи в файлик — это очень плохая идея.
Потому что во-первых, логи внутри контейнера в файлике проблематично доставать. Нужно сначала зайти в контейнер, сделать туда exec, а потом уже смотреть логи. Следующий момент — если у вас логи в файлике, то в контейнерах обычно минималистичное окружение и там нет утилит, которые нужны обычно для нормальной работы с логами. Их погрепать, посмотреть, открыть в текстовом редакторе. Следующий момент, когда у нас логи лежат в файлике внутри контейнера, в случае, если этот контейнер удалится, вы понимаете, логи погибнут вместе с ним. Соответственно, любой рестарт контейнера — и логов уже нет. Опять же, плохой вариант.
И последний момент — это то, что внутри контейнеров у вас обычно существует ваше приложение и все — оно обычно является единственным запущенным процессом. Ни о каком процессе, который бы ротировал файлы с вашими логами, речи вообще не идет. Как только логи начинают записываться в файлик, это значит, что, извините, продакшн сервера мы начнем терять. Потому что, во-первых, их проблематично найти, их никто не отслеживает, плюс их никто не контролирует — соответственно, файлик растет бесконечно, пока просто место на сервере не кончится. Поэтому, еще раз говорю, что логи в докере, в частности, в Kubernetes, в файлик — это затея плохая.
Следующий момент, тут я хочу опять же об этом поговорить — раз уж мы затрагиваем тему логов, то хорошо бы поговорить и о том, как логи должны выглядеть, для того, чтобы с ними было удобно работать. Как я говорил, тема не относится напрямую к Kubernetes, но зато очень хорошо относится к теме DevOps. К теме культуры разработки и дружбы двух этих разных отделов — Dev и Ops, чтобы всем было удобно.
Значит в идеале, на сегодняшний день, логи нужно писать в JSON формате. Если у вас какое-нибудь непонятное приложение ваше самописное, которое пишет логи в непонятных форматах, потому что вы там вставляете какой-нибудь print или что-то типа того, то самое время уже нагуглить какой-нибудь фрэймворк, какую-нибудь обертку, которая позволяет реализовывать нормальное логирование; включить там параметры логирования в JSON, потому что JSON простой формат, его парсить просто элементарно.
Если у вас JSON не прокатывает по каким-нибудь критериям, неизвестно каким, то хотя бы пишите логи в том формате, который можно парсить. Тут, скорее, стоит задумываться о том, что, например, если у вас запущена куча каких-нибудь контейнеров или просто процессов с nginx, и у каждого свои настройки логирования, то, наверное, кажется, вам парсить их будет очень неудобно. Потому что на каждую новый nginx instance вам нужно написать собственный парсер, потому что они пишут логи по-разному. Опять же, наверное, тут стоило задуматься о том, чтобы все эти nginx instance имели одну и ту же конфигурацию логирования, и абсолютно единообразно писали все свои логи. То же самое касается абсолютно всех приложений.
Я в итоге еще хочу масла в огонь добавить, о том, что в идеале, логов многострочного формата стоит избегать. Тут какая штука, если вы когда-то работали со сборщиками логов, то, скорее всего, вы видели, что они вам обещают, что они умеют работать с многострочными логами, умеют их собирать и так далее. На самом деле, нормально, полноценно и без ошибок собирать многострочные логи, не умеет, по-моему, на сегодняшний день ни один сборщик. По-человечески, чтобы это было удобно и без ошибок.
Но stack trace — это всегда многострочные логи и как их избегать. Тут вопрос в том, что лог — это запись о событии, а стактрэйс фактически логом не является. Если мы логи собираем и складываем их куда-нибудь в Elasticsearch и по ним потом рисуем графики, строим какие-нибудь отчеты работы пользователей на вашем сайте, то когда у вас вылазит stack trace — это значит, что у вас происходит какая-то непредвиденная, необработанная ситуация в вашем приложении. И stack trace имеет смысл закидывать автоматически куда-нибудь в систему, которая их умеет трэкать.
Это то ПО (то же Sentry), которое сделано специально для того, чтобы работать со stack trace. Оно может создавать сразу же автоматизированно задачи, назначать на кого-то, аллертить, когда стактрейсы происходят, группировать эти стактрэйсы по одному типу и так далее. В принципе, не имеет особого смысла говорить о стактрэйсах, когда мы говорим о логах, потому что это, все-таки, разные вещи с разным предназначением.
Конфигурация
Дальше у нас по поводу конфигурации в Kubernetes: что с этим делать и как приложения внутри Kubernetes должны конфигурироваться. Вообще, я обычно говорю о том, что докер — он не про контейнеры. Все знают, что докер — это контейнеры, даже те, кто особо с докером не работал. Повторюсь, Докер это не про контейнеры.
Докер, по моему мнению, это про стандарты. И там стандарты всего практически: стандарты сборки вашего приложения, стандарты установки вашего приложения.
И эта штука — мы ей пользовались и раньше, просто с приходом контейнеров это стало особо популярно — эта штука называется ENV (environment) переменные, то есть переменные окружения, которые есть в вашей операционной системе. Это вообще идеальный способ конфигурировать ваше приложение, потому что, если у вас есть приложения на JAVA, Python, Go, Perl не дай бог, и они все могут читать переменные database host, database user, database password, то идеально. У вас приложения на четырех разных языках конфигурируются в плане базы данных одним и тем же способом. Нет больше никаких разных конфигов.
Все можно сконфигурировать с помощью ENV переменных. Когда мы говорим про Kubernetes, там есть прекрасный способ объявлять ENV переменные прямо внутри Deployment. Соотвественно, если мы говорим про секретные данные, то секретные данные из ENV переменных (пароли к базам данных и т.д.), мы можем сразу запихать в секрет, создать секрет кластер и в описании ENV в Deployment указать, что мы не непосредственно объявляем значение этой переменной, а значение этой переменной database password будем читать из секрета. Это стандартное поведение Kubernetes. И это самый идеальный вариант конфигурировать ваши приложения. Просто на уровне кода, опять же к разработчикам это относится. Если вы DevOps, можно попросить: «Ребята, пожалуйста, научите ваше приложение читать переменные окружения. И будет нам счастье всем».
Если еще и все будут читать одинаково названные переменные окружения в компании, то это вообще шикарно. Чтобы не было такого, что одни ждут postgres database, другие database name, третьи database еще что-нибудь, четвертые dbn какой-нибудь там, чтобы, соответственно, единообразие было.
Проблема наступает, когда у вас переменных окружения становится так много, что просто открываешь Deployment —, а там пятьсот строчек переменных окружения. В таком случае, переменные окружения вы уже просто переросли — и дальше уже не надо себя мучить. В таком случае, имело бы смысл начать пользоваться конфигами. То есть, обучить ваше приложение использовать конфиги.
Только вопрос в том, что конфиги — это не то, что вы думаете. Config.pi — это не тот конфиг, которым удобно пользоваться. Или какой-нибудь конфиг в вашем собственном формате, альтернативно одаренном — это тоже не тот конфиг, который я подразумеваю.
То, о чем я говорю, это конфигурация в приемлемых форматах, то есть, на сегодняшний день самым популярным стандартом является стандарт .yaml. Его понятно, как читать, он человекочитаемый, его понятно, как читать из приложения.
Соответственно, помимо YAML, можно еще, например, попользоваться JSON, парсить примерно так же удобно, как YAML в плане чтения оттуда конфигурации приложения. Читать людьми заметно неудобнее. Можно попробовать формат, а-ля ini. Его читать прям совсем удобно, с точки зрения человека, но его может быть неудобно автоматизированно обрабатывать, в том плане, если вы когда-то захотите генерить свои конфиги, вот ini формат уже может быть неудобно генерить.
Но в любом случае, какой бы формат вы ни выбрали, суть в том, что с точки зрения Kubernetes — это очень удобно. Вы весь свой конфиг можете положить внутри Kubernetes, в ConfigMap. И потом этот configmap взять и попросить внутри вашего пода замонтировать в какую-нибудь определенную директорию, где ваше приложение будет конфигурацию из этого configmap читать так, как будто бы это просто файлик. Это, собственно, то, что хорошо делать, когда у вас в приложении становится конфигурационных опций много. Или просто структура какая-нибудь сложная, вложенность есть.
Если у вас есть configmap, то очень прекрасно вы можете научить свое приложение, например, автоматически отслеживать изменения в файле, куда замонтирован configmap, и еще и автоматически релоадить ваше приложение при изменении конфигов. Это вообще идеальный вариант был бы.
Об этом я уже тоже, опять же, говорил — секретную информацию не в конфигмап, секретную информацию не в переменные, секретную информацию в секреты. Оттуда эту секретную информацию подключать в диплойменты. Обычно мы все описания кубернетовских объектов, диплойменты, конфигмапы, сервисы храним в git. Соответственно, запихивать пароль к базе данных в git, даже если это ваш git, который у вас внутренний в компании — плохая идея. Потому что, как минимум, git помнит все и удалить просто оттуда пароли не так просто.
Health check
Следующий момент — это о такая штука, которая называется Health check. Вообще, Health check — это просто проверка того, что ваше приложение работает. При этом мы чаще всего говорим о неких веб-приложениях, у которых, соответственно, с точки зрения хэлсчека (лучше не переводить здесь и далее) это будет какой-нибудь специальный URL, который они обрабатывают стандартно, обычно делают /health
.
При обращении на этот URL, соответственно, наше приложение говорит или «да, окей, у меня все хорошо 200» или «нет, у меня всё не хорошо, какой-нибудь 500». Соответственно, если у нас приложение не http, не веб-приложение, мы сейчас говорим какой-нибудь демон, мы можем придумать, как делать хэлсчеки. То есть, необязательно, если приложение не http, то все работает без хэлсчека и сделать это никак нельзя. Можно обновлять периодически информацию в файлике какую-нибудь, можно придумать к вашему daemon специальную какую-нибудь команду, типа, daemon status
, которая будет говорить «да, все нормально, daemon работает, он живой».
Для чего это нужно? Первое самое очевидное, наверное, для чего нужен хэлсчек — для того, чтобы понимать, что приложение работает. То есть, просто тупо, когда оно сейчас поднято, оно выглядит как рабочее, чтобы точно быть уверенным, что оно работает. А то получается так, что под с приложением запущен, контейнер работает, инстенс работает, все нормально —, а там пользователи уже оборвали все телефоны у техподдержки и говорят «вы что там, …, заснули, ничего не работает».
Вот хэлсчек — это как раз-таки такой способ увидеть с точки зрения пользователя, что оно работает. Один из способов. Давайте говорить так. С точки зрения Kubernetes, это еще и способ понимать, когда приложение запускается, потому что мы понимаем, что есть разница между тем, когда запустился контейнер, создался и стартанул и когда непосредственно в этом контейнере запустилось приложение. Потому что если мы возьмем какое-нибудь среднестатистическое java-приложение и попробуем его запустить в доке, то секунд сорок, а то и минуту, а то и десять, оно может прекрасно стартовать. При этом можно хоть обстучаться в его порты, оно там не ответит, то есть оно еще не готово принимать трафик.
Опять же, с помощью хэлсчека и с помощью того, что мы обращаемся сюда, мы можем понимать в Kubernetes, что в приложении не просто контейнер поднялся, а прям приложение стартануло, оно на хэлсчек уже отвечает, значит туда можно запускать трафик.
То, о чем я говорю сейчас, называется Readiness/Liveness пробы в рамках Kubernetes, соответственно, readiness пробы у нас как раз-таки отвечают за доступность приложения в балансировке. То есть если readiness пробы выполняются в приложении, то значит все окей, на приложение идет клиентский трафик. Если readiness пробы не выполняются, то приложение просто не участвует, конкретно этот instance, не участвует в балансировке, оно убирается с балансировки, клиентский трафик не идет. Соответственно, Liveness пробы в рамках Kubernetes нужны для того, чтобы в случае, если приложение «залипло», его «рестартануть». Если liveness проба не работает у приложения, которое объявлено в Kubernetes, то приложение не просто убирается с балансировки, оно именно рестартится.
И тут такой важный момент, о котором хотелось бы сказать, с точки зрения практики, обычно все-таки чаще используется и чаще нужна readiness проба, чем liveness проба. То есть просто бездумно объявлять и readiness, и liveness пробы, потому что Kubernetes так умеет, а давайте использовать всё, что он умеет — не очень хорошая идея. Объясню почему. Потому что есть пунктик номер два в пробах — это о том, что неплохо было бы проверять нижележащий сервис в ваших хэлсчеках. Это значит, что если у вас есть веб-приложение, отдающее какую-то информацию, которую в свою очередь оно, естественно, должно где-то взять. В базе данных, например. Ну, и сохраняет в эту же базу данных информацию, которая в это REST API поступает. То соответственно, если у вас хэлсчек отвечает просто типа обратились на слешхэлс, приложение говорит »200, окей, все хорошо», а при этом у вас у приложения база данных недоступна, а приложение на хэлсчек говорит »200, окей, все хорошо» — это плохой хэлсчек. Так не должно работать.
То есть, ваше приложение, когда к нему приходит запрос на /health
, оно не просто отвечает,»200, ок», оно сначала сначала идет, например, в базу данных, пробует подключиться к ней, делает там что-нибудь совсем элементарное, типа селект один, просто проверяет, что в базе данных коннект есть и в базу данных можно выполнить запрос. Если это все прошло успешно, то отвечает »200, ок». Если не прошло успешно, то говорит, что ошибка, база данных недоступна.
Поэтому, в связи с этим, опять же возвращаюсь к Readiness/Liveness пробам — почему readiness проба скорее всего вам нужна, а liveness проба под вопросом. Потому что, если вы будете описывать хэлсчеки именно так, как я сейчас сказал, то получится у нас недоступна в части instanceв или со всех instance
в база данных, к примеру. Когда у вас объявлена readiness проба, у нас хэлсчеки начали фэйлиться, и приложения соответственно все, с которых недоступна база данных, они выключаются просто из балансировки и фактически «висят» просто в запущенном состоянии и ждут, пока у них базы данных заработают.
Если у нас объявлена liveness проба, то представьте, у нас сломалась база данных, а у вас в Kubernetes половина всего начинает рестартовать, потому что liveness-проба падает. Это значит, нужно рестартовать. Это совсем не то, что вы хотите, у меня даже был в практике личный опыт. У нас было приложение, которое было чатом и которое было написано в JS и входило в базу данных Mongo. И была как раз-таки проблема в том, что это было на заре моей работы с Kubernetes, мы описывали readiness, liveness пробы по принципу того, что Kubernetes умеет — значит будем использовать. Соответственно, в какой-то момент Mongo немножко «затупила» и проба начала фэйлиться. Соответственно, по ливнесс пробе поды начали «убиваться».
Как вы понимаете, они когда «убиваются», это чат, то есть, к нему висят куча соединений от клиентов. Они тоже «убиваются» — нет не клиенты, только соединения — все не одновременно и из-за того, что не одновременно убиваются, кто-то раньше, кто-то позже, они не одновременно стартуют. Плюс стандартный рандом, мы не можем предсказать с точностью до миллисекунды время старта каждый раз приложения, поэтому они это делают по одному instance. Один инфоспот поднимается, добавляется в балансировку, все клиенты приходят туда, он не выдерживает такой нагрузки, потому что он один, а их там работает, грубо говоря, десяток, и падает. Поднимается следующий, на него вся нагрузка, он тоже падает. Ну, и каскадом просто продолжаются эти падения. В итоге чем это решилось — просто пришлось прям жестко остановить трафик пользователей на это приложение, дать всем instance подняться и после этого разом запустить весь трафик пользователей для того, чтобы он уже распределился на все десять instances.
Не будь бы это liveness проба объявленной, которая заставила бы это все рестартовать, приложение бы отлично с этим справилось. Но у нас отключается все из балансировки, потому что базы данных недоступны и все пользователи «отвалились». Потом, когда эта база данных стала доступна, все включается в балансировку, но приложениям не нужно заново стартовать, не нужно тратить на это время и ресурсы. Они все уже здесь, они готовы к трафику, соответственно, просто открывается трафик, все отлично — приложение на месте, все продолжает работать.
Поэтому, readiness и liveness пробы это разное, даже более того, можно теоретически делать разные хэлсчеки, один типа рэди, один типа лив, например, и проверять разные вещи. На readiness пробах проверять бэкенды ваши. А на liveness пробе, например, не проверять, с точки зрения того, что liveness пробы это вообще просто приложение отвечает, если вообще оно в состоянии ответить.
Потому что liveness проба по большому счету это когда мы «залипли». Бесконечный цикл начался или еще что-нибудь — и больше запросы не обрабатываются. Поэтому имеет смысл даже их разделять — и разную логику в них реализовывать.
По поводу того, чем нужно отвечать, когда у вас происходит проба, когда вы делаете хэлсчеки. Просто это боль на самом деле. Те, кто знаком с этим, наверное, будут смеяться —, но серьезно, я видел сервисы в своей жизни, которые в ответ на запросы в стопроцентных случаях отвечают »200». То есть, который успешен. Но при этом в теле ответа они пишут «ошибка такая-то».
То есть статус ответа вам приходит — все успешно. Но при этом вы должны пропарсить тело, потому что в теле написано «извините, запрос завершился с ошибкой» и это прям реальность. Это я видел в реальной жизни.
И чтобы кому-то не было смешно, а кому-то очень больно, все-таки стоит придерживаться простого правила. В хэлсчеках, да и в принципе в работе с веб-приложениями.
Если все прошло успешно, то отвечайте двухсотым ответом. В принципе, любой двухсотый ответ устроит. Если вы очень хорошо читали рэгси и знаете, что одни статусы ответа отличаются от других, отвечайте подходящим: 204, 5, 10, 15, что угодно. Если не очень хорошо, то просто «два ноль ноль». Если все пошло плохо и хэлсчек не отвечает, то отвечаете любым пятисотым. Опять же, если вы понимаете, как нужно отвечать, чем отличаются разные статусы ответа друг от друга. Если не понимаете, то 502 — ваш вариант отвечать на хэлсчеки, если что-то пошло не так.
Такой еще момент, хочу немного вернуться по поводу проверки нижележащих сервисов. Если вы начнете, например, проверять все нижележащие сервисы, которые стоят за вашим приложением — вообще все. То у нас получается с точки зрения микросервисной архитектуры, у нас есть такое понятие, как «низкое зацепление» — то есть, когда ваши сервисы между собой минимально зависимы. В случае выхода одного из них из строя, все остальные без этого функционала просто продолжат работать. Просто часть функционала не работает. Соответственно, если вы все хэлсчеки завяжете друг на друга, то у вас получится, что в инфраструктуре упало что-то одно, а из-за того, что оно упало, все хэлсчеки всех сервисов тоже начинают фэйлиться — и инфраструктуры вообще всей микросервисной архитектуры больше нет. Там все вырубилось.
Поэтому я хочу повторить это еще раз, что проверять нижележащие сервисы нужно те, без которых ваше приложение в ста процентах случаях не может выполнять свою работу. То есть, логично, что если у вас REST API, через который пользователь сохраняет в базу или достает из базы, то в случае отсутствия базы данных, вы не можете работать гарантированно с вашими пользователями.
Но если у вас пользователи, когда вы их достаете из базы данных, дополнительно обогащаются еще какими-нибудь метаданными, еще из одного бэкенда, в который вы входите перед тем, как отдать ответ на фронтенд — и этот бэкенд недоступен, это значит, что вы отдадите ваш ответ без какой-то части метаданных.
Дальше у нас тоже одна из больных тем при запуске приложений.
На самом деле, это не только Kubernetes касается по большому счету, просто так совпало, что культура какая-то разработки массово и DevOps в частности начала распространяться примерно в то же время, что и Kubernetes. Поэтому по большому счету, что оказывается нужно корректно (graceful) завершать работу вашего приложения и без Kubernetes. И до Kubernetes люди так делали, но просто с приходом Kubernetes мы про это начали массово говорить.
Graceful Shutdown
В общем, что такое Graceful Shutdown и вообще для чего это нужно. Это о том, что когда ваше приложение по какой-то причине завершает свою работу, вам нужно выполнить app stop
— или вы получаете, например, сигнал операционной системы, ваше приложение его должно понимать и что-то с этим делать. Самый плохой вариант, конечно, это когда ваше приложение получает SIGTERM и такой типа «SIGTERM, висим дальше, работаем, ничего не делаем». Это прям откровенно плохой вариант.
Практически настолько же плохой вариант, это когда ваше приложение получает SIGTERM и такой типа «сказали же сегтерм, все значит завершаемся, не видел, не знаю никаких пользовательских запросов, не знаю что у меня там за запросы сейчас в работе, сказали SIGTERM, значит завершаемся». Это тоже плохой вариант.
А какой вариант хороший? Первым пунктом, учитываем завершение операций. Хороший вариант — это когда ваш сервер все-таки учитывает то, что он делает, если ему приходит SIGTERM.
SIGTERM — это мягкое завершение работы, оно специально такое, его можно перехватить на уровне кода, его можно обработать, сказать о том, что сейчас, подождите, мы сначала закончим ту работу, которая у нас есть, после этого завершимся.
С точки зрения Kubernetes, как это выглядит. Когда мы говорим поду, который работает в кластере Kubernetes, «пожалуйста, остановись, удались» или у рестарт нас происходит, или обновление, когда Kubernetes пересоздает поды — Kubernetes присылает в под сообщение как раз-таки SIGTERM, ждет какое-то время, причем, вот это время, которое он ждет, оно тоже конфигурируется, есть такой специальный параметр в диплойментах и он называется Graceful ShutdownTimeout. Как вы понимаете, неспроста он так называется и неспроста мы об этом сейчас говорим.
Там можно конкретно сказать, сколько нужно подождать между тем, когда мы послали в приложение SIGTERM и когда мы поймем, что приложение кажется от чего-то офигело или, «залипло» и не собирается завершаться — и нужно послать ему SIGKILL, то есть, жестко завершить его работу. То есть, соответственно, у нас работает какой-то демон, он обрабатывает операции. Мы понимаем, что в среднем у нас операции, над которыми работает демон, не длятся больше 30 секунд за раз. Соответственно, когда приходит SIGTERM, мы понимаем, что наш демон максимум может после SIGTERM дорабатывать 30 секунд. Мы его пишем, например, 45 секунд на всякий случай и говорим, что SIGTERM. После этого 45 секунд ждем. По идее, за это время демон должен был завершить свою работу и завершиться сам. Но если вдруг он не смог, значит, он, скорее всего, залип — он больше не обрабатывает наши запросы нормально. И его можно через 45 секунд смело, собственно, прибить.
Причем тут, на самом деле, даже 2 аспекта можно учитывать. Во-первых, понимать, что, если у вас запрос пришел, вы начали с ним как-то работать и не дали ответ пользователю, а вам пришел SIGTERM, например. То имеет смысл доработать и дать ответ пользователю. Это пункт номер раз в этом плане. Пункт номер два тут будет то, что если вы пишите свое приложение, вообще архитектуру строите таким образом, что у вас пришел запрос на ваше приложение, дальше вы запустили какую-то работу, файлики начали откуда-то качать, базу данных перекачивать и еще что-то. В общем, ваш пользователь, ваш запрос висит полчаса и ждет, пока вы ему ответите — то, скорее всего, вам нужно над архитектурой поработать. То есть просто учитывать даже здравый смысл, что если у вас операции короткие, то имеет смысл SIGTERM игнорировать и дорабатывать. Если у вас операции длинные, то не имеет смысла в данном случае игнорировать SIGTERM. Имеет смысл переработать архитектуру так, чтобы таких длинных операций избежать. Чтобы пользователи просто не висели, не ждали. Не знаю, сделать там какой-нибудь websocket, сделать обратные хуки, которые ваш сервер уже будет отправлять клиенту, что угодно другое, но не заставлять пользователя полчаса висеть и ждать просто сессию, пока вы ему ответите. Потому что непредсказуемо, где она может порваться.
Когда ваше приложение завершается, следует отдавать какой-нибудь адекватный exit-код. То есть если ваше приложение попросили закрыться, остановиться, и оно нормально само смогло остановиться, то не нужно возвращать какой-нибудь там типа exit-код 1,5,255 и так далее. Все что не нулевой код, по крайней мере, в Linux системах, я в этом точно уверен, считается неуспешным. То есть считается, что ваше приложение в таком случае завершилось с ошибкой. Соответственно, по-хорошему, если ваше приложение завершилось без ошибки, вы говорите 0 на выходе. Если ваше приложение завершилось с ошибкой по каким-то там поводам, вы говорите не 0 на выходе. И с этой с информацией можно работать.
И последний вариант. Плохо, когда у вас пользователь кидает запрос и полчаса висит, пока вы его обработаете. Но в целом еще хочется сказать о том, что вообще стоит со стороны клиента. Неважно у вас мобильное приложение, фронтенд и так далее. Необходимо учитывать, что вообще сессия пользователя может оборваться, может произойти, что угодно. Может быть запрос послан, например, недообработаться и не вернуться ответ. Ваш фронтенд или ваше мобильное приложение — вообще любой фронтенд, давайте говорить так — должны это учитывать. Если вы работаете с websocket`ами, это вообще самая страшная боль, которая была в моей практике.
Когда разработчики каких-нибудь очередных чатов не знают, что, оказывается, websocket может порваться. У них когда там элементарно на проксе что-нибудь происходит, мы просто конфиг меняем, и она делает reload. Естественно, все долгоживущие сессии рвутся при этом. К нам прибегают разработчики и говорят: «Ребята, вы чего, у нас там у всех клиентов чат поотваливался!». Мы им говорим: «Вы чего? Ваши клиенты не могут переподключаться?». Они говорят: «Нет, нам надо, чтобы сессии не рвались». Короче говоря, это бред на самом деле. Нужно учитывать сторону клиента. Особенно, я же говорю, с долгоживущими сессиями типа websocket, что она может порваться и незаметно для пользователя нужно уметь такие сессии переустановить. И тогда вообще все идеально.