Kubernetes становится стандартом разработки, при этом порог входа в него остается довольно высоким. Вместе с архитектором отдела администрирования сервисов Selectel Михаилом Вишняковым собрали список рекомендаций для разработчиков приложений, которые мигрируют их в оркестратор. Знание перечисленных пунктов позволит избежать потенциальных проблем и не создавать ограничений на месте преимуществ k8s.
Наш субъективный список — под катом. Пополните его своими рекомендациями в комментариях!
Для кого этот текст
Для разработчиков, у которых нет DevOps-экспертизы в команде — нет штатных специалистов. Они хотят переехать в Kubernetes, потому что за микросервисами будущее, а k8s — лучшее решение для оркестрации контейнеров и ускорения разработки за счет автоматизации доставки кода в окружения. При этом они могли локально что-то поднимать в Docker, но под Kubernetes еще ничего не разрабатывали.
Поддержку k8s такая команда может отдать на аутсорс — нанять компанию-подрядчика, отдельного специалиста или, например, воспользоваться соответствующими услугами провайдера IT-инфраструктуры. Так, в Selectel есть DevOps as a Service — услуга, в рамках которой опытные DevOps-специалисты компании возьмут любой инфраструктурный проект под полную или частичную опеку: перенесут ваши приложения в Kubernetes, внедрят DevOps-практики, ускорят time-to-market.
Как бы то ни было, есть нюансы, о которых важно знать разработчикам на этапе планирования архитектуры и разработки, — до того, как проект упадет в руки DevOps-специалистов (и они заставят править код под работу в кластере). Эти тонкости мы и подсвечиваем в тексте.
Текст будет полезен и для тех, кто пишет код с нуля и планирует запустить его в Kubernetes, и для тех, у кого уже есть готовое приложение, которое нужно мигрировать в k8s. Во втором случае можно пройтись по списку и понять, в каком месте стоит проверить соответствие вашего кода требованиям оркестратора.
База
На старте убедитесь, что вы знакомы с «золотым» стандартом The Twelve-Factor App. Это публичный гайд, описывающий принципы архитектуры современных веб-приложений. Скорее всего, некоторые из пунктов вы уже применяете.
Зависимости. Явно объявляйте и изолируйте зависимости.
Конфигурация. Сохраняйте конфигурацию в среде выполнения.
Сторонние службы (Backing Services). Считайте сторонние службы (backing services) подключаемыми ресурсами.
Сборка, релиз, выполнение. Строго разделяйте стадии сборки и выполнения.
Процессы. Запускайте приложение как один или несколько процессов, не сохраняющих внутреннее состояние (stateless).
Привязка портов (Port binding). Экспортируйте сервисы через привязку портов.
Параллелизм. Масштабируйте приложение с помощью процессов.
Утилизируемость (Disposability). Максимизируйте надежность с помощью быстрого запуска и корректного завершения работы.
Паритет разработки/работы приложения. Держите окружения разработки, промежуточного развертывания (staging) и рабочего развертывания (production) максимально похожими.
Журналирование (Logs). Рассматривайте журнал как поток событий.
Задачи администрирования. Выполняйте задачи администрирования/управления с помощью разовых процессов
Подробно на этом гайде останавливаться не будем. О нем на Хабре уже писали достаточно подробно. Проверьте, насколько ваше приложение соответствует стандартам разработки.
Теперь перейдем к выделенным рекомендациям и нюансам.
Отдавайте предпочтение stateless-приложениям
Почему?
Для реализации отказоустойчивости stateful-приложения потребуется значительно больше усилий и экспертизы.
Нормальное поведение для Kubernetes — отключение и перезапуск нод. Это происходит при автохилинге, когда нода перестает отвечать и пересоздается, либо при автомасштабировании в сторону уменьшения нод (например, часть из них уже не загружены, и оркестратор исключает их для экономии ресурсов).
Поскольку ноды и поды в k8s могут быть динамически удалены и воссозданы, приложению стоить быть готовым к этому. Оно не должно писать никаких данных, требующих сохранения, в контейнер, в котором запущено.
Что делать
Нужно организовать приложение так, чтобы данные писались в базы данных, файлы — в S3-хранилище, а, допустим, кэш — в Redis или Memcache. Благодаря тому, что приложение хранит данные «на стороне», мы существенно облегчаем масштабирование кластера под нагрузкой, когда нужно добавить дополнительные ноды, и репликацию.
В stateful-приложении, где данные хранятся в подключенном Volume (грубо говоря, в папочке рядом с приложением), все осложняется. При масштабировании stateful-приложения придется заказывать «вольюмы», обеспечить, чтобы они правильно подключались и создавались в правильной зоне. А что делать с этим «вольюмом», когда реплику понадобится убрать?
Да, некоторые бизнес-приложения правильно запускать как stateful. Однако в таком случае необходимо делать их более управляемыми в Kubernetes. Нужен Operator, определенные агенты внутри, выполняющие необходимые действия… Яркий пример здесь — postgres-operator. Все это в разы более трудоемко, чем просто кинуть код в контейнер, указать ему пять реплик и смотреть, как все работает без дополнительных плясок с бубном.
Позаботьтесь о наличии endpoints для проверки состояния приложений
Зачем?
Мы уже отметили, что k8s сам следит за поддержанием приложения в требуемом состоянии. Это включает в себя проверку работы приложения, перезапуск подов, признанных сбойными, отключение нагрузки, перезапуск на менее загруженных нодах, завершение подов, выходящих за установленные лимиты потребления ресурсов.
Чтобы кластер мог корректно отслеживать состояние приложения, стоит заранее позаботиться о наличии endpoints для проверок состояния, так называемых liveness и readiness probes. Это важные механизмы Kubernetes, которые, по сути занимаются тыканьем палочкой в контейнер — проверкой жизнеспособности приложения (работает ли оно должным образом).
Что делать
Механизм liveness probes помогает определить, когда пришло время перезапустить контейнер, чтобы приложение не замкнулось, а продолжало работать.
Readiness probes действуют не так радикально: они позволяют Kubernetes понять, когда контейнер готов принимать трафик. При этом в случае неуспешной проверки под просто будет исключен из балансировки — новые запросы перестанут на него поступать, но принудительное завершение не последует.
Можно использовать эту возможность, чтобы дать приложению «переварить» поступивший поток запросов без падения реплики. Как только несколько проверок готовности пройдут успешно, под реплики вернется в балансировку и снова начнет получать запросы. Со стороны nginx-ingress это выглядит как исключение адреса реплики из upstream.
Такие проверки — полезная функция Kubernetes, но при неправильной настройке liveness probes они могут навредить работе приложения. Например, если вы попытаетесь развернуть обновление приложения, которое проваливает liveness/readiness-проверки, оно откатится в пайплайне или приведет к деградации по производительности (в случае правильной настройки подов). Также известны случаи каскадного перезапуска подов, когда k8s «схлопывает» один под за другим из-за провалов проверки liveness probes.
Проверки можно отключить как функцию, и это стоит сделать, если вы не до конца осознаете специфику и последствия их использования. В остальном важно прописать нужные эндпоинты и сообщить о них DevOps’ам.
Если у вас есть HTTP endpoint, который может быть исчерпывающим индикатором, вы можете настроить и liveness-, и readiness-пробы на работу с ним. Используя один и тот же endpoint, убедитесь, что ваш под будет перезапущен, если этот endpoint не сможет вернуть корректный ответ.
Дополнительное чтение:
Старайтесь сделать потребление приложения более предсказуемым и равномерным
Зачем?
Практически всегда контейнеры внутри пода Kubernetes ограничены по ресурсам в рамках некоторых (иногда весьма небольших) значений. Масштабирование в кластере происходит горизонтально, увеличением количества реплик, а не размеров одной реплики. В Kubernetes мы имеем дело с лимитами памяти (limits) и лимитами по CPU (процессорные ограничения). Ошибка в процессорных ограничениях могут привести к троттлингу — исчерпанию доступного по лимитам процессорного времени для контейнера. А если мы пообещали больше памяти, чем есть, при росте нагрузки и упирании в потолок Kubernetes начнет вытеснять (evict) наименее приоритетные поды с ноды.
Конечно, лимиты настраиваются. Всегда можно подобрать то значение, при котором k8s не будет «убивать» поды из-за ограничений в памяти, но, тем не менее, лучшей практикой будет более предсказуемое потребление ресурсов приложением. Чем равномернее потребление приложения, тем более плотно можно планировать нагрузку.
Что делать
Оцените свое приложение: подсчитайте, сколько примерно запросов оно обрабатывает, сколько памяти занимает. Сколько подов нужно запустить, чтобы нагрузка распределялась между ними равномерно. История, когда какой-то под регулярно потребляет больше, чем остальные, — невыгодная для пользователя. Такой под будет постоянно перезапускаться со стороны Kubernetes, подвергая опасности отказоустойчивую работу приложения. Расширять лимит ресурсов под потенциальную пиковую нагрузку — тоже не вариант. В таком случае ресурсы будут простаивать все время без нагрузки, а значит — деньги на ветер.
Параллельно с установкой лимитов важно следить за показателями мониторинга под. Это может быть kube-Prometheus-Stack, VictoriaMetrics или хотя бы Metrics Server (больше подходит для очень базового масштабирования, в его консоли можно посмотреть статистику с kubelet — сколько потребляют поды). Мониторинг поможет уже в продакшене найти проблемные места и пересмотреть логику распределения ресурсов.
Настройка мониторинга в Kubernetes c помощью kube-Prometheus-Stack. Источник
Есть достаточно специфический нюанс, который касается процессорного времени. Разработчикам приложений под Kubernetes важно иметь его в виду, чтобы после не сталкиваться с проблемами при деплое и не переписывать код под требования SRE-специалистов.
Допустим, на контейнер выдается 500 милли-CPU — примерно 0,5 процессорного времени одного ядра на 100 ms реального времени. Упрощенно говоря, если приложение будет утилизировать процессорное время в несколько непрерывных потоков (допустим, их четыре) и «выест» все отведенные 500 милли-CPU за 25 ms времени, то остальные 75 ms будет заморожено системой до наступления следующего периода квоты.
Иллюстрацией такого поведения могут стать стейджинговые базы данных, запущенные в k8s с небольшими лимитами, когда под нагрузкой запросы, условно, на 5 ms резко проваливаются до 100 ms.
Если на графиках ответов видно, что нагрузка все растет и растет, а потом резко возрастает latency, то, скорее всего, вы столкнулись с этим нюансом. Нужно заняться resource management — давать больше ресурсов репликам или увеличивать число реплик, снижая нагрузку на каждую.
Дополнительное чтение:
- Задание ресурсов CPU для контейнеров и подов
- Про настройку потребления CPU в Kubernetes
- Ресурсы в Kubernetes – процессор (CPU)
- Настройка Kube-Prometheus-Stack для мониторинга Kubernetes
ConfigMaps, секреты, переменные окружения — используйте эти сущности Kubernetes
В Kubernetes есть несколько объектов, которые могут сильно облегчить жизнь разработчикам. Изучите их, чтобы сэкономить себе время в будущем.
ConfigMap — это объект Kubernetes, который используется для хранения несекретных данных в паре «ключ-значение». Поды могут использовать их как переменные окружения или как файлы конфигурации в Volume.
Допустим, вы разрабатываете приложение, которое можно запустить локально (для непосредственно разработки) и, например, в облаке. Вы создаете для вашего приложение переменную окружения — например, DATABASE_HOST, которое будет использоваться приложением, чтобы подключиться к базе данных. Вы устанавливаете эту переменную локально, на локал-хост. Но, когда вы запускаете приложение в облаке, необходимо указать другое значение — например, hostname внешней базы данных.
Переменные окружения позволяют использовать один и тот же Docker-образ и для локального использования, и для деплоя в облаке. Не нужно пересобирать образ для каждого отдельного параметра. Так как параметр динамический, может меняться, его можно указывать через переменную окружения.
То же самое относится к конфиг-файлам для приложений. Это файлы, которые хранят определенные настройки для приложения, чтобы оно работало корректно. Обычно при сборке Docker-образа указывается дефолтный конфиг либо конфиг-файл, который нужно загрузить в Docker. Для разных окружений (dev, prod, test и т.д.) нужны разные настройки, которые иногда еще нужно менять — для тестирования, например.
Чтобы не собирать отдельные Docker-образы для каждого окружения, для каждого отдельного конфига, можно монтировать конфиг-файлы в Docker при запуске пода. Так приложение подхватит нужные конфигурационные файлы, которые мы указали, а мы будем использовать один Docker-образ для подов.
Обычно, если конфиг-файлы большие, используют Volume, чтобы монтировать их в Docker как файлы. Если же конфиг-файлы содержат короткие значения, удобнее использовать переменные окружения. Все зависит от требований вашего приложения.
Еще одна полезная абстракция Kubernetes — это secrets. Секреты похожи на ConfigMaps, но предназначены для хранения конфиденциальных данных — паролей, токенов, ключей и т.д. Использование secrets означает, что вам не нужно включать секретные данные в код приложения. Ведь они могут быть созданы независимо от подов, которые их используют, — это снижает риск раскрытия данных. Секреты могут быть использованы как файлы в «вольюмах», смонтированных в одном или нескольких контейнерах пода. Также их можно использовать как переменные окружения для контейнеров.
Дисклеймер: в этом пункте мы описываем только про функционал Kubernetes «из коробки». Про более специализированные решения для работы с секретами, такие как Vault, не рассказываем.
Зная о наличии таких фичей в Kubernetes, разработчику не придется пересобирать весь контейнер, если в подготовленной конфигурации что-то изменилось — например, сменился пароль.
Дополнительное чтение:
- Больше о ConfigMaps в официальной доке Kubernetes
- Подробнее о секретах
- Как подхватить все пары «ключ-значение» из ConfigMap или секрета в под
Обеспечьте graceful shutdown контейнера с помощью SIGTERM
Почему?
Бывают ситуации, когда Kubernetes «убивает» приложение до того, как оно успевает освободить ресурсы. Это не очень хороший сценарий. Лучше, если перед этим оно успеет, например, ответить на входящие запросы, не принимая новые, завершить транзакцию или сохранить данные в базу данных.
Что сделать
Успешной практикой здесь будет обработка приложением сигнала SIGTERM. При завершении работы контейнера в его PID 1 прилетает сначала сигнал SIGTERM, затем приложению дается немного времени на корректное завершение (дефолтное значение — 30 сек). Далее, если контейнер не завершился сам, прилетает уже SIGKILL — сигнал принудительного завершения. Продолжать принимать соединения после получения SIGTERM не стоит.
Многие фреймворки (например, Django) умеют делать это «из коробки». Возможно, в вашем приложении SIGTERM уже работает. Убедитесь, что это так.
Дополнительное чтение:
Коротко — о еще нескольких важных пунктах
Приложение не должно зависеть от того, на какой из подов приходит запрос
При переезде приложения в Kubernetes мы ожидаемо сталкиваемся с автомасштабированием — возможно, вы переехали именно из-за него. В зависимости от нагрузки оркестратор добавляет или удаляет реплики приложения.
Важно, чтобы приложение при этом не зависело от того, на какой из подов приходит запрос клиента — например, поды со статикой. Либо нужно синхронизировать состояние и отдавать идентичный ответ на запрос из любого пода. Также ваш бэкенд должен уметь работать на несколько реплик, не повреждая данные.
Ваше приложение — за реверс-прокси и должно отдавать ссылки по HTTPS
В Kubernetes есть сущность Ingress, которая по сути обеспечивает реверс-прокси для приложения — как правило, это nginx с кластерной автоматизацией. Для приложения достаточно уметь работать по HTTP и понимать, что снаружи ссылка будет с HTTPS.
Помните, что в Kubernetes приложение находится за реверс-прокси, а не торчит напрямую в интернет, и ссылки, соответственно, надо отдавать с HTTPS. Когда приложение возвращает ссылку с HTTP, Ingress сам переписывает ее на HTTPS. В результате это может привести к зацикливанию и ошибке Too many redirects.
Как правило, избежать такого конфликта можно обычным переключением параметра в используемой вами библиотеке — поставить галочку, что приложение находится за реверс-прокси. Но, если вы пишете приложение с нуля, о работе Ingress за реверс-прокси важно помнить.
Оставьте работу с SSL-сертификатами Kubernetes
Разработчикам можно не думать, как «прикрутить» добавление сертификатов в Kubernetes. Изобретать велосипед здесь не нужно и даже вредно. Для этого, как правило, используют отдельный сервис в оркестраторе — cert-manager, который можно доустановить.
Ingress Controller в Kubernetes позволяет использовать SSL-сертификаты для терминации TLS-трафика. Можно применять как Let's Encrypt, так и заранее выпущенные сертификаты. При необходимости можно создать специальный secret для хранения выпущенных SSL-сертификатов.
А какие рекомендации для разработчиков, которые готовят приложение под Kubernetes есть у вас? Делитесь мнением в комментариях!
Автор: Ульяна Малышева