Привет! Меня зовут Сергей Птушкин, в этом посте я поделюсь с вами нашим чеклистом для оперативного и безболезненного переезда в Kubernetes. У SM Lab очень много разных продуктов, а как следствие — разных команд разработчиков и администраторов. У всех своя архитектура, стек, любимые языки программирования, SLA и требования по нагрузке.
Поэтому при переезде приложений в Kubernetes нам нужно готовить их с учетом всех особенностей и требований, а также передавать компетенции devops-инженерам и разработчикам. В процессе получается еще и выяснить их собственные потребности при эксплуатации этих приложений.
Итак, давайте разберем на примере нашей ситуации. Мы переезжали в Kubernetes из Mesos и Oracle Weblogic и знали, что разработчики тестируют приложения при помощи docker-compose или локально на станциях. Нам нужно было придумать единый подход для следующих возможностей:
запуск в контейнерной среде
корректный старт и завершение приложений
планирование ресурсов для приложений и расчету количества реплик
работоспособность приложения при параллельной работе нескольких реплик
проверка работоспособности приложений
ограничения по использованию локального диска контейнера
предоставление и сбор метрик
сбор и формат логов
хранение чувствительных данных
обеспечение безопасности
В процессе нашей команде стало поступать множество обращений как от разработчиков, так и от администраторов информационных систем. У многих из них не было базового понимания абстракций и примитивов кубера, так что мы подробно рассказывали им про лучшие практики. Но даже если вы опытный специалист, все равно при приемке продукта на сопровождение можно забыть уточнить у разработчиков какую-то важную особенность приложений. А разработчики использовали свой собственный подход к решению типовых платформы, не зная тонкостей Kubernetes.
Так что стало ясно, что без подробного чеклиста для девопс-специалистов и разработчиков не обойтись.
Мы решили, что чеклист должен помогать девопс-специалисту выяснять у разработчиков все особенности приложений — деплой, параметризацию, корректное завершение, обеспечение доступности и многое другое. Разработчики же с этим чеклистом на руках смогут вносить изменения в архитектуру с учетом работы в Kubernetes. Скажем, иногда приходится уменьшать ресурсы, но увеличивать при этом количество реплик. Или же выделять отдельный порт для метрик и проверок доступности. В общем, случаев хватает.
И если точно следовать чеклисту, то можно добиться очень гладкой миграции. Бонусом это дает нам уверенность, что все возможности Kubernetes используются как надо и в полной мере, а также мы получаем стандартизированные подходы к эксплуатации приложений.
Кому подойдет
Администраторам, перед которыми стоит задача миграции продуктов в Kubernetes. Обладая хорошими знаниями, они могут предложить разработчикам лучшие решения для эксплуатации приложений в k8s. А разработчики, уже знакомые с платформой, смогут самостоятельно разрабатывать архитектуру своего продукта с учетом лучших практик. Не стоит забывать, что при архитектурном проектировании новых приложений проще закладывать требуемые возможности по списку. Большинство перечисленных в чек-листе требований подходят не только для запуска в k8s, но и соответствуют хорошим практикам разработки сервисов для любой платформы контейнеризации.
— Не томите, покажите уже чеклист
Чеклист довольно обширный и в формате таблицы сделал из этого поста полотнище на несколько метров, так что мы убрали его под спойлер.
А вот и он
Ключевые требования | Описание |
Приложение имеет Readiness probe | Endpoint, который используется для проверки готовности приложения принимать трафик. Readiness probe позволяет проверить готово ли приложение принимать трафик. Например, наше приложение должно как-то обрабатывать данные в БД. Но по какой-то причине база данных не отвечает конкретному инстансу приложения. Получается, что приложение не может правильно работать. В этом случае мы настраиваем Readiness Probe так, чтобы оно проверяло коннект с БД. Если проверка завершится с ошибкой, то Kubernetes не будет перенаправлять трафик на этот под, при этом под не будет перезапущен. Главное отличие от Liveness Probe в том, что Readiness Probe не перезапускает приложение, а просто ждет, когда проверка снова будет проходить успешно. Если вы не установите Readiness probe, то kubelet будет предполагать, что приложение готово к приему трафика, как только контейнер запустится. При использовании universalChart определите в своем values.yaml блок с секцией readinessProbe. Пример: readinessProbe: httpGet: path: /sys/health/readiness scheme: HTTP port: 8080 periodSeconds: 10 timeoutSeconds: 2 successThreshold: 2 failureThreshold: 4 |
Приложение имеет Liveness probe | Endpoint, который используется для проверки живучести приложения. Liveness probe позволяет перезапустить зависшее приложение. Бывают ситуации, когда приложение зависает и не отвечает на запросы, тогда его нужно перезапустить. В этом помогает Liveness Probe: если проверка завершится с ошибкой, то Kubernetes перезапустит этот под. Вам не следует использовать Liveness probe для обработки Fatal Errors в вашем приложении и запрашивать у Kubernetes перезапуск приложения. Вместо этого вы должны позволить приложению завершить работу. Liveness probe следует использовать в качестве механизма восстановления только в том случае, если процесс не отвечает. При использовании universalChart определите в своем values.yaml блок с секцией livenessProbe. Пример: livenessProbe: httpGet: path: /sys/health/liveness scheme: HTTP port: 8080 periodSeconds: 10 timeoutSeconds: 2 successThreshold: 1 failureThreshold: 3 |
Приложение умеет аварийно завершать работу при Fatal Error | Если приложение выдает неустранимую ошибку, вы должны позволить ему завершиться сбоем. Примеры неустранимых ошибок: - uncaught exception - опечатка в коде (для динамических ЯП) - не удалось загрузить заголовок или зависимость Пожалуйста, обратите внимание, что вы не должны сигнализировать о сбое проверке жизнеспособности (Liveness probe). Вместо этого вы должны немедленно выйти из процесса и позволить kubelet перезапустить контейнер. |
Endpoint и параметры для проверок Liveness и Readiness не должны быть идентичными! | Когда проверки жизнеспособности (Liveness probe) и готовности (Readiness probe) указывают на один и тот же Endpoint, эффект от проверок объединяется. Приложение сигнализирует, что оно не готово или не работает, kubelet выключает контейнер из раздачи в сервисе и одновременно перезапускает его. |
Readiness probe и зависимости от смежных сервисов (служб) | Желательно, чтобы проверка Readiness не включала зависимости от таких служб, как: БД Миграции в БД APIs Прочие сервисы и службы Так как это может привести к каскадному сбою (Cascading Failures) и приложение будет выведено из раздачи, т. е. на него не будет поступать трафик. Имеет ли это смысл или нет - все зависит от особенностей работы вашего приложения. Если приложение вообще не может работать без смежного сервиса или службы, то возможно такое поведение оправдано. В целом, если ваше приложение технически готово и работает, даже если оно не может функционировать идеально, то Readiness Probe (проверка готовности) не должна быть провалена. Хорошим компромиссом является реализация «Degraded mode». Например, если нет доступа к базе данных, то можно отвечать на запросы (для чтения), которые могут храниться в локальном кэше, и возвращать код 503 (service unavailable) для запросов на запись в БД. |
Приложение умеет повторять попытку подключения к зависимым службам | Когда приложение запускается, оно не должно завершаться сбоем из-за того, что зависимость, такая как база данных, не готова. Вместо этого приложение должно продолжать пытаться подключиться к базе данных до тех пор, пока это не увенчается успехом. Kubernetes ожидает, что компоненты приложения могут быть запущены в любом порядке. Когда вы убедитесь, что ваше приложение может повторно подключиться к зависимости, такой как база данных, вы знаете, что можете предоставить более надежный и отказоустойчивый сервис. |
Приложение умеет корректно обрабатывать SIGTERM и завершать свою работу | Может пройти некоторое время, прежде чем такой компонент, как kube-proxy или Ingress контроллер, будет уведомлен об изменениях в Endpoints. Таким образом, трафик все еще может поступать в приложение несмотря на то, что Pod помечен как Terminating. Приложение должно прекратить принимать новые запросы для всех оставшихся соединений и закрыть их, как только исходящая очередь будет исчерпана. После этого приложение должно корректно завершить свою работу. |
Приложение не должно обрабатывать новые запросы в течение Graceful shutdown периода | Возможно, вы захотите рассмотреть возможность использования событий жизненного цикла контейнера (container lifecycle events), таких как preStop hook, для настройки того, что будет происходить перед удалением Pod'а. При использовании universalChart определите в своем values.yaml блок с секцией lifecycleHooks: Пример: lifecycleHooks: preStop: exec: command: [ "sh", "-c", "sleep 30" ] |
При написании Docker file используйте инструкцию CMD (exec form) для корректной обработки SIGTERM сигнализации | Командная оболочка (Shell), поставляемая по умолчанию с Alpine Linux, не передает сигнализацию дочерним процессам, поэтому необходимо использовать инструкцию CMD в executable form. CMD [ "java", "-jar", "app.jar" ] Вы можете быть уведомлены в логах о том, что Pod будет терминирован, перехватив SIGTERM сигнализацию в вашем приложении. Также следует обратить внимание на пересылку SIGTERM нужному процессу в вашем контейнере. |
Закрывайте все бездействующие (idle) keep alived сокеты | Если приложение не закрывает TCP-соединение при обращении к другому сервису (например, используя TCP keep-alive или пул подключений), то оно подключится только к одному Pod'у и не будет использовать другие Pod'ы в этом сервисе. Но что произойдет когда Pod будет удален? В идеале запрос должен быть отправлен в другой Pod. Но так как открыто долгоживущее (long-live) соединение, то приложение продолжит использовать его. С другой стороны, вы не должны резко завершать долгоживущие (long-live) соединения. Вместо этого вы должны завершить их работу перед выключением приложения. |
Всегда запускать более одной реплики вашего приложения в продуктивном окружении | Запуск более одного экземпляра ваших Pod'ов гарантирует, что удаление Pod'а не приведет к простою. Также для распределения реплик по зонам доступности (в кластере Kubernetes) используйте опцию zoneAwareS etup: true, которая обеспечивает включение TopologySpreadConstraints в universalChart. Исключением из правил могут быть модули импорта или ETL, они должны работать в единственном экземпляре и уметь корректно завершать длительные задачи по расписанию. |
Избегайте размещения Pod-ов на одном узле | Даже если вы запустите несколько копий своих модулей, нет никаких гарантий, что потеря узла не приведет к отключению вашего сервиса. Рассмотрим следующий сценарий: у вас есть 11 реплик на одном узле кластера. Если узел становится недоступным, 11 реплик теряются, и у вас возникает время простоя. Вы должны применить правила Anti-affinity к своим развертываниям (Deployments), чтобы модули были распределены по всем узлам кластера. |
Всегда устанавливайте Pod Disruption Budgets (PDB), если запускаете более одной реплики вашего приложения в продуктивном окружении | Pod Disruption Budget (PDB) — это минимальное количество реплик приложения, которые Kubernetes постарается сохранить для стабильной работы. PDB защищает приложение от некоторых, но не всех типов простоев. Существует два типа причин простоев: Непреднамеренный сбой происходит случайно из-за ошибки ПО или сбоя оборудования. Например, выход узла из строя, недоступность сети или Kernel Panic в ядре. Добровольное прерывание происходит осознанно по инициативе администратора или разработчика. Например, Rolling Update для выпуска новой версии приложения или отключение узла для техобслуживания/обновления. Когда узел кластера выводится на техобслуживание/обновление, все модули на этом узле удаляются и переносятся на другой. Но что, если ваше приложение находится под высокой нагрузкой и вы не можете потерять более 50% своих Pod'ов? Вывод узла кластера на техобслуживание может повлиять на доступность вашего приложения. Чтобы защитить ваши развертывания (Deployments) от непредвиденных событий, которые могут привести к одновременному отключению нескольких модулей, вы можете определить бюджет для сбоев модулей (Pod Disruption Budgets). Представьте, что вы говорите: "Kubernetes, пожалуйста, убедитесь, что для моего приложения всегда запущено по крайней мере 5 модулей". В universalChart при развертывании приложения от 2-ух реплик - PDB включается автоматически. Вам также необходимо определить (в своем values.yaml файле) параметры для PDB. Пример: pdb: matchLabel: <label_for_matching_pods> minAvailable: 50% |
Всегда задавайте requests и limits ( CPU и Memory ) для вашего приложения | Ограничения ресурсов используются для ограничения объема процессора и памяти, которые могут использовать ваши контейнеры. Планировщик Kubernetes использует их в качестве одной из метрик, чтобы решить, на какой узел лучше всего разместить ваш Pod. Неограниченное количество модулей (без ограничений по ресурсам) может привести к чрезмерному использованию ресурсов и потенциальному сбою узла (и kubelet). То же самое относится и к ограничениям процессора. Запросы (requests) — это ресурсы, которые контейнер гарантированно получит. То есть это своего рода минимальные системные требования, которые нужны приложению для работы. Kubernetes всегда планирует размещение подов на узлах так, чтобы обеспечить необходимые запросы для всех подов. Ограничения (limits) — это максимальные значения выделяемых ресурсов. Kubernetes гарантирует, что приложение никогда не получит больше ресурсов, чем указано в ограничениях. Если под попытается превысить ресурс, Kubernetes поступит по-разному в зависимости от типа ресурса. CPU — сжимаемый ресурс, его можно искусственно ограничить. Поэтому если под запросит больше CPU, чем указано в ограничениях, он их просто не получит. Возможно, это скажется на производительности приложения, но оно продолжит работать. RAM — несжимаемый ресурс, его невозможно ограничить. Ведь если приложению просто не выделить запрашиваемую память, оно, скорее всего, перестанет нормально работать. Поэтому если под запросит больше памяти, чем указано в ограничениях, Kubernetes перезапустит под. По умолчанию для всех продуктовых немспейсов используются LimitRanges. Если вы не задали ресурсы для вашего контейнера, то будут использоваться дефолтовые параметры. Они имеют следующие значения: spec: limits: - default: cpu: 250m memory: 512Mi defaultRequest: cpu: 250m memory: 512Mi max: cpu: "2" memory: 16Gi maxLimitRequestRatio: cpu: "4" memory: "2" min: cpu: 50m memory: 64Mi type: Container При использовании universalChart определите в своем values.yaml блок с секцией resources, чтобы указать необходимые ресурсы для вашего контейнера с приложением. Пример: resources: limits: cpu: 2 memory: 4Gi requests: cpu: 1 memory: 2Gi |
Установите значение cpu. requests : 1 или ниже (для приложений с низкой вычислительной нагрузкой) | Если у вас нет заданий с высокой вычислительной нагрузкой, рекомендуется установить cpu.requests : 1 или ниже. |
Установите соответствующее качество обслуживания (QoS) для ваших модулей (Pod'ов) | Когда узел перегружен (т.е. использует слишком много ресурсов), Kubernetes пытается выселить часть модулей (Pod'ов) на этом узле. Kubernetes ранжирует и выселяет модули в соответствии с четко определенной логикой. |
Надеюсь, вам тоже пригодится в работе. А если вы считаете, что в чеклисте отсутствуют какие-то важные пункты, будем рады видеть вас в комментариях.