Глубокие проверки работоспособности Kubernetes

от автора

Распределённые системы часто характеризуют как палку о двух концах. В Интернете найдётся множество отличных материалов как об их неприглядных, так и об отличных сторонах. Но этот пост — немного иного характера. Вообще обычно я за распределённые системы в тех случаях, когда они действительно нужны, но в этом посте я расскажу, как одна моя ошибка при работе с распределённой системе привела к далеко идущим последствиям.

Ошибка, которую я допустил, сейчас случается во многих компаниях и может приводить к лавинообразным отказам. Назовём её глубокая проверка работоспособности в Kubernetes.

Запутанный Kubernetes: история о жизнеспособности, готовности и подводных камнях, возникающих при глубокой проверке работоспособности

Kubernetes — это платформа для оркестрации контейнеров. Kubernetes не случайно популярен в среде специалистов, выстраивающих распределённые системы, так как позволяет формализовать инфраструктуру в виде разумной исходно облачной абстракции. В рамках такой абстракции разработчики могут конфигурировать и запускать приложения, не будучи экспертами в сетевых технологиях.

В Kubernetes допускается и рекомендуется конфигурировать пробы нескольких типов: для определения жизнеспособности (liveness), готовности (readiness) и возможности запуска (startup). Концептуально эти пробы просты, и их можно описать следующим образом:

  • Припомощи проб жизнеспособности мы сообщаем Kubernetes,нужно ли перезапустить контейнер. Еслипроба жизнеспособности даст отрицательныйрезультат, то приложение перезапустится.Таким методом можно отлавливатьопределённые проблемы — например, взаимные блокировки — и снова открыватьдоступ к приложению. Например, коллегииз Cloudflare описали здесь,как таким способом перезапускать «застрявших» потребителей Kafka.

  • Пробы готовности используются только с приложениями, работающими на основе http,и такие пробы позволяют просигнализировать,что контейнер готов получать трафик. Под считается готовым к получению трафика, когда готовы все его контейнеры. Если какой-либо контейнер в поде провалил пробу готовности, то он удаляется избалансировщика нагрузки сервисов и не будет получать никаких HTTP-запросов.Но, если провалена проба готовности, то под не перезапускается, а если провалена проба жизнеспособности, то перезапускается.

  • Пробы запуска обычно рекомендуется применять с унаследованными приложениями, на запуск которых требуется немало времени. Как только приложение пройдёт пробы запуска, последующие пробы жизнеспособности и готовности не учитываются.

В оставшейся части поста мы сосредоточимся на пробах готовности применительно к приложениям, работающим на основе HTTP.

Когда моё приложение будет готово?

На первый взгляд, очень простой вопрос, правда? «Приложение готово к работе, как только сможет отвечать на запросы пользователя», — могли бы сказать вы. Рассмотрим приложение платёжной компании, в котором вы можете проверять ваш баланс. Когда пользователь открывает мобильное приложение, оно отправляет запрос на один из многих имеющихся у вас бекенд-сервисов. Сервис, получивший запрос, далее должен:

  • Валидировать пользовательский токен, обратившись для этого к сервису аутентификации.

  • Вызвать тот сервис, на котором ведётся баланс.

  • Отправить в Kafka событие balance_viewed.

  • (Черездругую конечную точку) позволитьпользователю закрыть свой аккаунт, врезультате чего обновляется строка в собственной базе данных этого сервиса.

Следовательно, можно утверждать, что успешная работа приложения, обслуживающего пользователей, зависит от:

  • Доступности сервиса аутентификации

  • Доступности сервиса для проверки баланса

  • Доступности Kafka

  • Доступности нашей базы данных

Граф этих зависимостей будет выглядеть примерно так:

Следовательно, можно написать такую конечную точку проверки готовности, которая возвращала бы JSON и код 200 при доступности всего вышеперечисленного:

{     "available":{         "auth":true,         "balance":true,         "kafka":true,         "database":true     } }

В данном случае «доступность» для разных элементов может пониматься немного по-разному:

  • Работая с сервисами аутентификации и баланса необходимо убедиться, что мы получаем код 200 от их конечных точек готовности.

  • Работая с Kafka, убеждаемся, что можем выдать событие в топик, именуемый healthcheck.

  • Работая с базой данных, выполняем SELECT 1;

Если любая из этих операций закончится неуспешно, то мы вернём false для ключа JSON, а также ошибку HTTP 500. Такая ситуация расценивается как провал пробы готовности, в результате Kubernetes выведет этот под из балансировщика нагрузки сервисов. На первый взгляд может показаться, что это разумно, но на самом деле такая практика иногда приводит к лавинообразным отказам. Тем самым мы, пожалуй, обнуляем одно из самых значительных достоинств микросервисов (изоляция отказов).

Представьте себе следующий сценарий: сервис аутентификации отказал, а все остальные сервисы, используемые в нашей компании, опираются на него при глубокой проверке готовности:

В таком случае из-за отказа сервиса аутентификации все наши поды будут удалены из сервиса балансировки нагрузки. Отказ станет глобальным:

Хуже того, у нас почти не будет метрик, по которым можно было бы определить причину этого отказа. Поскольку запросы не доходят до наших подов, мы не знаем, насколько нарастить все те метрики Prometheus, которые мы аккуратно расставили в нашем коде. Вместо этого придётся внимательно рассмотреть все те поды, которые помечены в нашем кластере как неготовые.

После этого нам придётся достучаться до конечной точки, отвечающей за их готовность, чтобы выявить, какая зависимость стала причиной отказа и далее проследить всё дерево. Сервис аутентификации мог лечь из-за того, что отказала одна из его собственных зависимостей.

Примерно так:

Тем временем, наши пользователи видят следующее:

 upstream connect error or disconnect/reset before headers. reset reason: connection failure

Не самое качественное сообщение об ошибке, верно? Мы можем сделать гораздо лучше и сделаем.

Итак, как же судить о готовности приложения?

Приложение готово, если может выдать отклик. Это может быть отклик, свидетельствующий об отказе, но в таком случае он всё равно выполняет часть бизнес-логики. Например, если отказал сервис аутентификации, то мы можем (и должны) сначала повторить попытку, запрограммировав при этом экспоненциальную выдержку, тем временем увеличивая значение счётчика отказов. Если любой из этих счётчиков достигнет порогового значения, которое вы сочтёте неприемлемым (в соответствии с SLO, определёнными в вашей организации), то вы сможете объявить инцидент с чётко очерченной зоной поражения.

Остаётся надеяться, что в это время отдельные сегменты вашей бизнес-системы смогут продолжить работу, так как не вся система зависела от этого отказавшего сервиса.

Как только инцидент исправлен, следует обдумать, нужна ли сервису данная зависимость, и что можно сделать, чтобы от неё избавиться. Например, можно ли перейти на такую модель аутентификации, в которой меньше сохраняется состояние? Можно ли использовать кэш? Можно ли при некоторых последовательностях пользовательских действий автоматически их прерывать? Следует ли вынести в другой сервис некоторые рабочие потоки, для которых не требуется столько зависимостей — чтобы в будущем успешнее изолировать отказы?

Заключение

Исходя из разговоров с коллегами, могу предположить, что этот пост получится довольно холиварным. Кто-то может счесть меня идиотом уже потому, что я вообще реализую глубокие проверки работоспособности, так как естественно они могут приводить к лавинообразным отказам. Другие поделятся этим постом в своих каналах и спросят: «мы что, неправильно проверяем готовность?» — и тут в дискуссию вмешается сеньор и станет доказывать, что их случай особый, поэтому у них такие проверки целесообразны. Может быть, и так, в таком случае мне хотелось бы подробнее почитать о вашей ситуации.

Создавая распределённую систему, мы дополнительно её усложняем. Работая с распределёнными системами, всегда стоит проявлять здоровый пессимизм и сразу задумываться о возможных отказах. Я имею в виду, не ждать отказа постоянно, а просто быть готовым к нему. Нужно понимать взаимосвязанную природу наших систем и знать, что от единой точки отказа проблемы расходятся как рябь на воде.

Основной вывод из моей истории о Kubernetes — не отказываться от глубоких проверок работоспособности полностью, а пользоваться ими аккуратно. Здесь очень важен баланс: взвесить все достоинства подробных проверок работоспособности и соотнести их с вероятностью обширного отказа системы. Мы совершенствуемся как разработчики, когда учимся на своих ошибках и помогаем в этом другим. Так нам удаётся сохранять устойчивость, работая со всё более сложными системами.

Мэтт

Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud в нашем Telegram-канале

Перейти ↩

📚 Читайте также:


ссылка на оригинал статьи https://habr.com/ru/articles/845410/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *