Директор по развитию цифровой облачной платформы Platform V Матвей Ульянычев рассказывает в своей статье для Tproger, почему высоконагруженные приложения могут «падать», а также делится полезными базовыми советами по построению и тестированию распределенных систем.
Источник: tproger.ru: "Не падай кодом: повышаем доступность высоконагруженных приложений"
Всем привет, меня зовут Матвей Ульянычев, я руковожу развитием цифровой облачной платформы Platform V компании «СберТех». При разработке приложений мы используем продукты, компоненты, инструменты и методологии нашей платформы, а также применяем большое количество типов, видов и методов тестирования для того, чтобы в конечном итоге обеспечить максимальную доступность клиентских и корпоративных сервисов.
В этой статье я расскажу о том, почему высоконагруженные приложения могут «падать», дам полезные для небольших и начинающих команд базовые советы по построению и тестированию распределенных систем и поделюсь набором инструментов в свободном доступе.
Разберемся с наиболее частыми причинами, почему приложения могут не работать:
- Внутренние ошибки: например, вышли за границы массива.
- Ошибки инфраструктуры, на которой приложение запущено: например, отвалилась сеть или база данных.
- Ошибки, связанные с производительностью stateful-слоя (увеличение времени отклика от баз данных и т. д.)
- Ошибки интеграции со внешними системами: они не отвечают, или без предупреждения поменялись форматы интеграции.
- Проблемы при «накатах» новых версий приложений: проблемы с обратной совместимостью, возникновение ошибок, которые меняют метаданные (например, неправильно обновились справочники планов счетов и т. д.)
Остановимся на способах предотвращения и устранения этих причин.
[object Object]
Программная часть очень важна в случае интеграции с внешними системами либо при взаимодействии с сервисами в рамках микросервисной архитектуры. Распределенные системы определяют некоторые типовые паттерны, например, Circuit Breaker, Retry и Timeout. Тут важно понимать, готовы ли разработчики команды в них погружаться.
Так как микросервисы могут быть написаны на разных языках, в каждом языке необходима своя библиотека с реализацией интеграционных паттернов. Чтобы этого избежать, интеграционные паттерны можно реализовать на транспортном уровне. Например, эти задачи на себя может взять интеграционный продукт, благодаря которому реализация паттернов в каждом микросервисе не требуется. Из open source решений можно взять Istio или LinkerD. Но следует иметь в виду, что использование ПО с открытым кодом в enterprise-разработке требует серьезных компетенций и немалых вложений в доработку и сопровождение.
[object Object]
Важной частью работы любого приложения являются данные. Чем крупнее система, тем более важным становится вопрос их сохранности, доступности, масштабируемости. Ситуация дополнительно усложняется в случае, если система должна быть территориально распределена. Если stateless-логику можно раскидать по территориально-разнесенным дата-центрам, то повторить тоже самое для stateful-логики сложно.
Наш опыт показывает, что в крупных, высоконагруженных приложениях сделать stateful-слой геораспределенным зачастую выгоднее путем выделения промежуточного сервиса хранения данных. Он осуществляет горизонтальное масштабирование по данным и изолирует потребителей от вопросов, связанных с доступом к данным и их обслуживанием, позволяет компенсировать снижение скорости и рост задержек при взаимодействии между ЦОД-ами, реплицировать данные по тонким каналам связи, определять приоритеты, распределять stateful-слой приложения в нескольких дата-центрах без существенных затрат. Это позволит безопасно накатывать обновления.
Еще один вариант — положиться на репликацию средствами СУБД. Но здесь есть ограничение: вы жестко привязываетесь к конкретной реализации базы данных и не сможете ее сменить. Также важно помнить, что большинство СУБД не может работать в кластере типа active/active, когда предусмотрена работа двух СУБД одновременно).
[object Object]
Это необходимо при работе различных стратегий деплоя Kubernetes. В случае высоконагруженных приложений одновременно может работать 2-4 версии сервиса с реализацией стратегии «канареечного» развертывания на ограниченное множество пользователей или серверов.
Если раньше такие стратегии управлялись руками, то сегодня в тренде подход Progressive delivery, когда решение о добавлении трафика в процессе релиза принимается автоматически на основе прикладных или технических метрик.
Реализовать такой подход могут такие open source продукты, как Spinaker, Argo rollouts и Flux. Алгоритмы в их основе позволяют принять решение о том, что релиз был неудачным, и откатиться на предыдущую версию. Продукты похожи, и конкретный выбор зависит от предпочтений команды.
[object Object]
Для обеспечения устойчивой работы приложений важна не только техническая, но и бизнес-составляющая. Методология исследования пользовательского опыта — A/B-тестирование — позволяет выявить ошибки, которые тяжело отследить на внутреннем тестировании. В свою очередь, это позволяет снизить зону поражения до определенного количества клиентов, так как тестирование функционала производится на ограниченной части аудитории.
Сегодня в разработке, особенно enterprise-уровня, популярно вычисление radius blast (зоны поражения), когда перед запуском релиза определяется вероятность возникновения тех или иных рисков. Есть много open source ПО, с помощью которого можно реализовать такой подход. Один из примеров -популярный движок политик Open policy agent.
[object Object]
Сколько бы вы не прикладывали усилий для обеспечения надежности приложений, у сервисов все равно могут быть проблемы с доступностью. Для того, чтобы быстро устранять ошибки или понимать, когда можно откатиться к предыдущей версии, необходимо внедрять культуру мониторинга и обеспечивать так называемую observability системы — ее наблюдаемость, т. е. способность понимать текущее состояние на основы данных, которые она создает (логи, метрики и трейсы).
Для хорошего observability важно обеспечить трейсинг и мониторинг в стандартных форматах. Пример — Prometheus exposition format и Open Metrics с понятными дашбордами для сопровождения. Также лучше использовать долговременное хранилище, а не стандартный Prometheus, который хранит данные без потерь в течение ограниченного количества времени. Это необходимо, если планируется строить какой-либо корреляционный анализ или же данных так много, что они могут лежать в хранилище 2-3 дня, а это чрезвычайно мало, чтобы понять причину сбоев.
Повторюсь, форматы должны быть стандартными, чтобы можно было работать с ними на базе наработок community. Соответственно, если делается логирование, надо решить вопрос со структурированием логов. По структурированным логам можно удобно искать информацию в СУБД, скажем, в Clickhouse, но при этом придется потратить время на их структурирование.
Если же логирование нужно срочно, а времени на подготовку структуры нет, будьте готовы к тому, что в будущем придется вложиться в тюнинг производительности решений полнотекстового поиска.
[object Object]
Для того, чтобы предотвратить «падения» высоконагруженных приложений, даже небольшим командам важно проводить много тестов на разных циклах разработки. Помимо упомянутых ранее, можно использовать юнит- и контрактное тестирование, тестирование производительности, отказоустойчивости и безопасности, а также фаззинг-тестирование.
Остановлюсь на последнем подробнее. Суть фаззинг- тестирования в том, чтобы приложению на входе передать неправильные или случайные данные, после чего можно выявить возможные проблемы. Оно необходимо, чтобы предотвратить падения и зависания, нарушения внутренней логики, утечки памяти, а также проверить код приложения.
Вот неплохой инструмент для фаззинга в свободном доступе. Удобно, что в нем можно использовать спецификацию OpenAPI-сервиса для генерации тестового плана. Однако если запускать его в режиме максимальной нагрузки, то тестирование сервиса может занимать часы.
[object Object]
Вот несколько простых вопросов, ответы на которые помогут вам подробнее взглянуть на обеспечение доступности ваших приложений и найти причины проблем. Чем больше ответов «да» — тем более доступны ваши приложения.
- Предусмотрены ли автоматические механизмы самодиагностики модулей приложения?
- Предусмотрена ли в коде реакция на сбои или увеличенное время отклика смежных модулей и внешних систем?
- Предусмотрена ли в архитектуре защита от чрезмерной нагрузки на сервисы приложения?
- Предусмотрены ли метрики мониторинга для отслеживания деградации производительности вашего приложения (например, время отклика совокупно и отдельных частей приложения)?
- Возможно ли обновление версии приложения без прерывания обслуживания? Если да, то предусмотрены ли меры по восстановлению работоспособности в случае сбоя обновления?
- Полностью ли автоматизированы процессы раскатки приложения? В частности, умеете ли автоматически изменять схему данных в базе данных и откатывать при сбоях?
- Как код поведет себя при сбоях в работе баз данных? В частности, корректно ли поведет себя код если увеличится время отклика СУБД?
- Насколько критичным является сценарий отказа ЦОД, в котором размещено приложение? Если да, то как этот сценарий митигируется архитектурой?
- Предусмотрены ли планом тестирования интеграционные тесты, имитирующие нестандартное поведение пользователя и/или внешней среды?
Очень важно собрать и описать все способы устранения возможных причин недоступности приложений, чтобы сотрудники, в том числе и новые, могли ознакомиться с ними и оперативно начать применять.
Идеальный вариант — использовать комплексные решения, например, готовые цифровые платформы, где все эти наборы действий уже реализованы на программном уровне, и разработчикам не приходится самостоятельно придумывать и реализовывать их.
