Тестирование на отказоустойчивость: ломаем, чтобы сделать лучше. Часть 2

от автора

Всем привет! Недавно в блоге выходила статья, в которой мой коллега Александр Волков рассказал про применение практик хаос-инжиниринга, продемонстрировал поведение системы при сбоях на примере демосервиса, оценил его отказоустойчивость и предложил стратегии для улучшения архитектуры. А в этой части я, Екатерина Ильина — QA-инженер Cloud.ru, расскажу, как мы решили собственноручно сломать наши сервисы, чтобы сделать их отказоустойчивее.

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

Как мы пришли к тестированию отказоустойчивости

К хаос-инжинирингу обращаются, когда хотят лучше понять сложную систему и вовремя принять меры защиты от потенциальных сбоев, которые рано или поздно, но случаются у всех. С помощью тестирования отказоустойчивости неконтролируемые отказы можно превратить в контролируемые. И от того, как вы будете к ним готовы, будет зависеть и лояльность пользователей по отношению к продукту.

Приняв эту истину, мы у себя в команде решили внедрить тестирование отказоустойчивости и предотвратить часть возможных проблем. Кроме того, толчком к внедрению хаос-инжиниринга послужила параллельно проходившая в компании активность по DRP — подробнее можно почитать у Microsoft. План был рассчитан на всю компанию, и мы в свою очередь выделили несколько пунктов, которые без труда могли смоделировать в нашей системе. Например, сбой на уровне сервиса, потеря связности, отказ системы виртуализации, потеря данных и отработка сценариев по их восстановлению. Согласовав эту активность с продуктовым менеджером, мы с моим коллегой QA приступили к тестированию отказоустойчивости.

Продумываем план тестирования

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

Содержание тест-плана в рабочем пространстве

Содержание тест-плана в рабочем пространстве

Расскажу подробнее про каждый пункт.

Описание и цели тестирования

В описании мы кратко обозначили, что будем делать: «Требуется провести тестирование отказоустойчивости сервисов, отвечающих за переносы данных». И, опираясь на принципы хаос-инжиниринга, DRP и необходимость соблюдать определенный уровень SLA продукта, мы определили следующие цели тестирования отказоустойчивости:

Сократить время недоступности сервисов. Время недоступности сервисов напрямую влияет на удовлетворенность пользователей и бизнес-процессы. Если в рамках тестирования мы заранее сымитируем сбой, то будем знать, какое именно влияние он может оказать на пользователей. А значит, сможем разработать и меры по устранению.

Ограничить область влияния аварии или сбоя на систему. Важно, чтобы сбой в одном компоненте системы не приводил к каскадным отказам других компонентов. Ограничение области влияния проблемы позволяет локализовать ее и предотвратить распространение на другие части системы. Однажды во время тестирования интеграции двух сервисов на dev-окружении мы столкнулись с ситуацией, что API одного из них стал недоступен. И в тот же момент пользователь потерял доступ к сущностям из первого сервиса, которые не должны были от него зависеть. Получается, из-за сбоя в одном сервисе, который не имел отношения к работе основного, произошел каскадный сбой работы всей системы в целом. Такого быть не должно. И тестирование отказоустойчивости позволяет выявить это влияние до его возникновения и, соответственно, оптимизировать сервисы.

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

Определить сценарии восстановления для минимизации времени простоя. Когда в сервисе происходит сбой, то может быть два варианта восстановления: первый — автоматический, если поды снова запустились, и второй — ручной, с привлечением DevOps-инженеров или других членов команды. Отсюда вытекает пятая цель.

Определить и сформировать инструкции по восстановлению системы. Приведу еще один пример проблемы, с которой мы однажды столкнулись. Сервис для хранения логов долгое время работал без рестартов. Случился сбой, и сервис не смог восстановиться в автоматическом режиме, потому что не мог переварить большое количество данных и проинициализироваться. Пришлось привлекать DevOps-инженера, который подкрутил readiness probe — только после этого Kubernetes перестал убивать сервис. Как раз на такой случай мы и должны заготовить инструкции по восстановлению, чтобы эффективно и быстро устранять последствия сбоя.

Основные этапы и ответственные

А дальше мы заполнили календарь основных этапов и в процессе работы отмечали их статус. У нас не было строгих дедлайнов, поэтому сроки определили исходя из загруженности и по верхнеуровневой оценке объема работ. В планируемые сроки мы, кстати, уложились.

Таблица со сроками проведения основных этапов тестирования и статусом их выполнения.

Таблица со сроками проведения основных этапов тестирования

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

Общий план тестирования

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

  1. Выполнение предварительных условий.

  2. Проведение эксперимента.

  3. Регистрация инцидента.

  4. Восстановление работы сервиса.

  5. Тестирование после восстановления работоспособности. 

Расскажу про каждый из них подробнее.

Перед проведением эксперимента важно убедиться, что выполнены предварительные условия:

  • определены цели тестирования — про них мы поговорили выше.

  • выдвинуты гипотезы — предположили, как система должна вести себя при определенных условиях отказа.

  • тестируемый сервис и окружение стабильны — посмотрели на метрики.

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

Следующий этап — проведение эксперимента, который включает в себя:

  • генерацию сбоя, 

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

  • мониторинг поведения сервиса со стороны пользователя.

И тут мы задались вопросами: какие именно сбои хотим сгенерировать? Сколько по времени каждый из них будет длиться? Где мониторить состояние сервиса? Какое будет ожидаемое поведение системы? Отвечу на всё по порядку. 

Мы решили использовать сервис Chaos Mesh — о нем Александр подробно рассказывал в первой части статьи. Сервис предоставляет различные типы сбоев, и мы использовали наиболее подходящие из них для наших сервисов. В основном это: недоступность пода, разделение сети, прерывание соединения, задержка в запросе или ответе. Многое еще не успели опробовать в связи с инфраструктурными ограничениями, но обязательно сделаем это в будущем.

Длительность сбоя решили брать исходя из механизма ретраев в сервисе и времени хранения записей в кеше, плюс еще некоторое время на фиксацию деградации сервиса. Например, если система имеет ретраи, которые в сумме занимают около 5 минут, и использует кеш, который обновляется каждые 10 минут, имеет смысл провести испытание длительностью 5-15 минут. Это позволит увидеть реакцию на истечение всех циклов ретраев и работу системы без обновления кеша. Для первого эксперимента мы выбрали длительность 10 минут.

Когда мы начали тестировать отказоустойчивость, для наших сервисов уже был настроен дашборд с метриками в Grafana, которым пользовалась вся команда. Он нам вполне подошел. Среди всех метрик мы определи наиболее важные для нас: готовность пода к работе со временем его запуска, количество рестартов пода, CPU, потребление памяти, сетевая активность подов, включая входящий и исходящий трафик, количество ответов по кодам ошибок. 

Дашборд в Grafana с метриками для отслеживания влияния сбоя на сервис

Дашборд в Grafana с метриками для отслеживания влияния сбоя на сервис

Также мониторили влияние сбоя на пользователей — что они увидят, открыв UI сервиса, какие действия смогут, а какие не смогут выполнять, что нарушится в их повседневной работе. Именно это влияние мы будем стремиться устранить с командой разработки.

Следующий этап после того, как сбой внедрен и очевидны его последствия для системы — регистрация инцидента. Этот этап необходим для того, чтобы убедиться, что проведенный сбой и метрики, которые мы в него закладываем (сервисные метрики, логфайлы, результаты трассировки и дополнительные артефакты), достаточны, чтобы однозначно определить, где произошла проблема. И в случае возникновения реального инцидента у нас уже была исчерпывающая и понятная информация для быстрого реагирования и восстановления работы системы.

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

Работу сервиса восстановили (фууух), для подтверждения проводим повторное тестирование, чтобы убедиться, что все последствия аварии устранены. Для этого запускаем все функциональные тесты и проверяем метрики. И, если все показатели зеленые, считаем, что эксперимент завершен.

Тестирование сервисов

Общий план тестирования составили и теперь приступаем к его реализации — проведению тестирования отказоустойчивости. В этой части на примере одного из сервисов Cloud․ru ML Space расскажу и покажу, как мы провели первый эксперимент со сбоем.

☝️ Эксперимент, который мы рассмотрим, был намеренно упрощен и доведен до абстрактного уровня, чтобы гарантировать защиту конфиденциальной информации. Хотя пример не включает все детали, он всё же успешно демонстрирует ключевую концепцию и дает возможность понять процедуру тестирования, которую мы использовали.

Но прежде, чем начать тестировать, нужно ответить на вопрос что тестировать: какие сервисы проверять и какие именно типы сбоев к ним применять? Для этого мы провели анализ архитектуры — давайте на нее посмотрим. 

Изучаем архитектуру сервисов

Одна из функциональностей, за которую отвечает наша команда, — перенос пользовательских данных между разными хранилищами, такими как объектное хранилище S3, «горячее» хранилище NFS и базы данных. Переносы бывают в ручном или автоматическом режиме с заданной периодичностью и заданным способом. Также есть поддержка уведомлений о статусе переноса и его логировании. За этот процесс отвечает несколько взаимосвязанных между собой сервисов. 

Абстрактная схема архитектуры системы, которая отвечает за перенос данных

Абстрактная схема архитектуры системы, которая отвечает за перенос данных

На схеме видно множество сервисов и связей между ними, что указывает на возможность и необходимость проведения тестирования сбоев для каждого из них. Однако здесь есть единая точка отказа — пользовательские данные нельзя будет перенести при недоступности сервиса А, который отвечает за коннекторы к хранилищам данных. Это ключевой сервис — от него зависит работа всей системы переносов в целом. Поэтому сбои в этом сервисе мы начали рассматривать в первую очередь и разработали ряд требований и ожиданий от работы системы в случае его деградации.

Составляем протокол тестирования

И тут мы переходим к основной части — проведению эксперимента со сбоем. Для удобства протоколирования и наглядности мы создали таблицу, куда вносили все сценарии и ход работы:

Шаблон протокола проведения эксперимента

Шаблон протокола проведения эксперимента

Заполнение таблицы рассмотрим на примере сервиса А, который мы выбрали на предыдущем шаге. Также отмечу, что все запланированные эксперименты мы проводили на dev-среде — для начала этого достаточно, чтобы уже выявить отклонения от ожиданий в работе системы. 

☝️ Не забудьте предупредить команду о начале проведения эксперимента и возможной недоступности сервисов.

Итак, первая колонка — описание события или сбоя, звучит оно так: «Сбой в работе сервиса А». Тут все кратко и понятно. Дальше тип сбоя и сам сценарий — мы решили, что в рамках первого эксперимента будем имитировать недоступность сервиса, и, используя инструмент Chaos Mesh, смоделируем сценарий неисправности подов в течение определенного времени. Для этого выбираем тип сбоя PodChaos — Pod Failure и делаем все поды деплоймента недоступными в течение 10 минут.

Pod Failure: injects fault into a specified Pod to make the Pod unavailable for a period of time.

Затем заполнили «Мониторинг сервиса во время сбоя»:

  • ключевые метрики — прикрепили ссылку на наш дашборд для мониторинга,

  • зависимые сервисы — записали все связи подопытного сервиса с другими, которые тоже могут пострадать от сбоя, 

  • влияние на пользователя — по пунктам перечислили, как сбой может сказаться на пользователе.

В остальных колонках также записали ожидаемый результат, и вот что получилось до запуска сбоя:

Протокол тестирования в процессе проведения тестирования

Протокол тестирования в процессе проведения тестирования

Теперь мы готовы внедрять сбой. Обязательно отмечаем время начала и окончания запущенного сбоя. Это важно для последующего отслеживания времени восстановления сервиса.

В Chaos Mesh наш эксперимент выглядел вот так:

Страница с экспериментом в Chaos Mesh

Страница с экспериментом в Chaos Mesh

Инициируем сбой, и следим за влиянием на систему, фиксируя показатели в таблице. Как я упоминала ранее, мы проводили эксперимент вдвоем с коллегой: собирались на звонке, и один из нас генерировал сбой, отслеживал метрики и влияние на пользователя, в то время как другой стенографировал показатели в протокол. Такой подход позволил нам избежать потери важных деталей — не пришлось переключаться между экранами с экспериментом, дашбордами, пользовательским интерфейсом объекта тестирования и одновременно вести протоколирование. 

Уже через минуту после запуска сбоя было замечено влияние на сервис А — поды деплоймента начали рестартовать, график потребления памяти пошел вверх, стремительно выросло количество ошибок с кодом 500. Также наблюдались ошибки доступа к сервису А во всех зависимых сервисах. Влияние на пользователей оказалось более ощутимым, чем мы предполагали — когда все ручки сервиса начали возвращать ошибку 500 internal server error, пользователю отображалась только совсем пустая страница, и не было никаких информирующих сообщений о том, что сервис временно недоступен («Попробуйте позже»).

Возросшее потребление памяти в сервисе А во время сбоя

Возросшее потребление памяти в сервисе А во время сбоя

Так мы подробно расписали все, что увидели, в таблице в соответствующих колонках и разметили по приоритетам: что совпало с ожиданием и обрабатывается корректно выделили зеленым, что требует доработки и можно закинуть в бэклог — желтым, а красным то, что совсем не соответствует ожиданиям и следует поправить в первую очередь. Часть пунктов мы оставили с пометкой вопроса (а как мы хотим это обрабатывать?), чтобы вынести на обсуждение с командой.

Далее — регистрация инцидента. Здесь отметили, что все метрики на нашем дашборде показывают актуальное состояние сервисов во время сбоя, сервисных логов достаточно для локализации проблемы, а вот алерты о наличии отклонений на dev-окружении отсутствуют, и их необходимо добавить.

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

Последний этап — мониторинг сервиса после восстановления. По сервисным метрикам влияние сбоя прекратилось через 25 секунд после окончания его генерации, а обработка пользовательских запросов полностью восстановилась и вернулась к прежней скорости через две минуты. Результат приемлем. Так мы убедились, что метрики действительно отражают восстановление работоспособности сервиса. В колонке «Мониторинг сервиса после восстановления» записали, во сколько было устранено влияние, и что финальный прогон E2E-тестов прошел успешно — все зеленые.

В итоге наш протокол имел следующий вид:

 Заполненный протокол после проведения тестирования

 Заполненный протокол после проведения тестирования

Это был только первый из наших экспериментов. Все остальные эксперименты мы оформляли в этой же таблице аналогичным образом.

Анализируем результаты

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

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

Наши выводы

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

Ну и, на мой взгляд, большая польза от проделанной работы и погружения в инструмент хаос-инжиниринга — его внедрение в рабочий процесс почти на постоянной основе. Теперь на большую часть задач мы смотрим не только с функциональной точки зрения, но и с нефункциональной, что позволяет предотвратить или минимизировать негативное влияние различных сбоев на пользователя и бизнес-процессы.

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

Вам может быть интересно:


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