0
Обложка: Не падай кодом: повышаем доступность высоконагруженных приложений

Не падай кодом: повышаем доступность высоконагруженных приложений

Матвей Ульянычев
Матвей Ульянычев
Директор по развитию Platform V «СберТеха»

Всем привет, меня зовут Матвей Ульянычев, я руковожу развитием цифровой облачной платформы Platform V компании «СберТех». При разработке приложений мы используем продукты, компоненты, инструменты и методологии нашей платформы, а также применяем большое количество типов, видов и методов тестирования для того, чтобы в конечном итоге обеспечить максимальную доступность клиентских и корпоративных сервисов.

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

Разберемся с наиболее частыми причинами, почему приложения могут не работать:

  1. Внутренние ошибки: например, вышли за границы массива.
  2. Ошибки инфраструктуры, на которой приложение запущено: например, отвалилась сеть или база данных.
  3. Ошибки, связанные с производительностью stateful-слоя (увеличение времени отклика от баз данных и т. д.)
  4. Ошибки интеграции со внешними системами: они не отвечают, или без предупреждения поменялись форматы интеграции.
  5. Проблемы при «накатах» новых версий приложений: проблемы с обратной совместимостью, возникновение ошибок, которые меняют метаданные (например, неправильно обновились справочники планов счетов и т. д.)

Остановимся на способах предотвращения и устранения этих причин.

Используйте наработанные паттерны при построении распределенных систем

Программная часть очень важна в случае интеграции с внешними системами либо при взаимодействии с сервисами в рамках микросервисной архитектуры. Распределенные системы определяют некоторые типовые паттерны, например, Circuit Breaker, Retry и Timeout. Тут важно понимать, готовы ли разработчики команды в них погружаться.

Так как микросервисы могут быть написаны на разных языках, в каждом языке необходима своя библиотека с реализацией интеграционных паттернов. Чтобы этого избежать, интеграционные паттерны можно реализовать на транспортном уровне. Например, эти задачи на себя может взять интеграционный продукт, благодаря которому реализация паттернов в каждом микросервисе не требуется. Из open source решений можно взять Istio или LinkerD. Но следует иметь в виду, что использование ПО с открытым кодом в enterprise-разработке требует серьезных компетенций и немалых вложений в доработку и сопровождение.

Не забывайте о stateful-слое

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

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

Еще один вариант — положиться на репликацию средствами СУБД. Но здесь есть ограничение: вы жестко привязываетесь к конкретной реализации базы данных и не сможете ее сменить. Также важно помнить, что большинство СУБД не может работать в кластере типа active/active, когда предусмотрена работа двух СУБД одновременно).

Обеспечивайте обратную совместимость для API и СУБД

Это необходимо при работе различных стратегий деплоя Kubernetes. В случае высоконагруженных приложений одновременно может работать 2-4 версии сервиса с реализацией стратегии «канареечного» развертывания на ограниченное множество пользователей или серверов.

Если раньше такие стратегии управлялись руками, то сегодня в тренде подход Progressive delivery, когда решение о добавлении трафика в процессе релиза принимается автоматически на основе прикладных или технических метрик.

Реализовать такой подход могут такие open source продукты, как Spinaker, Argo rollouts и Flux. Алгоритмы в их основе позволяют принять решение о том, что релиз был неудачным, и откатиться на предыдущую версию. Продукты похожи, и конкретный выбор зависит от предпочтений команды.

Применяйте A/B-тестирование

Для обеспечения устойчивой работы приложений важна не только техническая, но и бизнес-составляющая. Методология исследования пользовательского опыта — A/B-тестирование — позволяет выявить ошибки, которые тяжело отследить на внутреннем тестировании. В свою очередь, это позволяет снизить зону поражения до определенного количества клиентов, так как тестирование функционала производится на ограниченной части аудитории.

Сегодня в разработке, особенно enterprise-уровня, популярно вычисление radius blast (зоны поражения), когда перед запуском релиза определяется вероятность возникновения тех или иных рисков. Есть много open source ПО, с помощью которого можно реализовать такой подход. Один из примеров -популярный движок политик Open policy agent.

Внедряйте мониторинг

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

Для хорошего observability важно обеспечить трейсинг и мониторинг в стандартных форматах. Пример — Prometheus exposition format и Open Metrics с понятными дашбордами для сопровождения. Также лучше использовать долговременное хранилище, а не стандартный Prometheus, который хранит данные без потерь в течение ограниченного количества времени. Это необходимо, если планируется строить какой-либо корреляционный анализ или же данных так много, что они могут лежать в хранилище 2-3 дня, а это чрезвычайно мало, чтобы понять причину сбоев.

Повторюсь, форматы должны быть стандартными, чтобы можно было работать с ними на базе наработок community. Соответственно, если делается логирование, надо решить вопрос со структурированием логов. По структурированным логам можно удобно искать информацию в СУБД, скажем, в Clickhouse, но при этом придется потратить время на их структурирование.

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

Придавайте значение тестам

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

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

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

Чек-лист: обеспечиваем доступность высоконагруженного приложения

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

  • Предусмотрены ли автоматические механизмы самодиагностики модулей приложения?
  • Предусмотрена ли в коде реакция на сбои или увеличенное время отклика смежных модулей и внешних систем?
  • Предусмотрена ли в архитектуре защита от чрезмерной нагрузки на сервисы приложения?
  • Предусмотрены ли метрики мониторинга для отслеживания деградации производительности вашего приложения (например, время отклика совокупно и отдельных частей приложения)?
  • Возможно ли обновление версии приложения без прерывания обслуживания? Если да, то предусмотрены ли меры по восстановлению работоспособности в случае сбоя обновления?
  • Полностью ли автоматизированы процессы раскатки приложения? В частности, умеете ли автоматически изменять схему данных в базе данных и откатывать при сбоях?
  • Как код поведет себя при сбоях в работе баз данных? В частности, корректно ли поведет себя код если увеличится время отклика СУБД?
  • Насколько критичным является сценарий отказа ЦОД, в котором размещено приложение? Если да, то как этот сценарий митигируется архитектурой?
  • Предусмотрены ли планом тестирования интеграционные тесты, имитирующие нестандартное поведение пользователя и/или внешней среды?

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

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