points of view by sanja

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

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

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

Удаленная система выходит из строя


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

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

  1. Определите путь обработки ошибок. В коде должны быть явно определены средства для обработки ошибок. Например, правильно разработанная страница ошибок, журнал исключений с метриками ошибок или автоматическое выключение с механизмом резервирования. Главное — ошибки должны обрабатываться явно. Нельзя допускать, чтобы пользователи видели ошибки работы вашего кода.
  2. Составьте план восстановления. Рассмотрите каждое удаленное взаимодействие в вашем коде и выясните, что нужно сделать для восстановления прерванной работы. Запускается ли рабочий процесс с точки сбоя? Публикуются ли все полезные данные во время сбоя в таблице очередей или таблице повторов? И повторяются ли каждый раз, когда удаленная система восстанавливается? Есть ли скрипт для сравнения баз данных двух систем и их синхронизации? Системный план восстановления нужно разработать и реализовать до развертывания фактического кода.

Удаленная система медленно отвечает


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

Некоторые из проблем можно решить прозрачно для кода приложения, если использовать технологии Service Mesh, такие как Istio. Тем не менее нужно убедиться, что такие проблемы обрабатываются вне зависимости от метода:

  1. Установите тайм-ауты для удаленных системных вызовов. Это касается и тайм-аутов для удаленного вызова API и базы данных, публикации событий. Проверьте, установлены ли конечные и разумные тайм-ауты для всех удаленных систем в вызовах. Это позволит не тратить ресурсы на ожидание, если удаленная система перестала отвечать на запросы.
  2. Используйте повторные попытки после тайм-аута. Сеть и системы ненадежны, повторные попытки — обязательное условие устойчивости системы. Как правило, повторные попытки устраняют множество пробелов в межсистемном взаимодействии.

    Если возможно, используйте какой-то алгоритм в ваших попытках (фиксированный, экспоненциальный). Добавление небольшого джиттера в механизм повтора даст передышку вызываемой системе, если она под нагрузкой, и приведет к увеличению скорости ответа. Обратная сторона повторных попыток — идемпотентность, о которой расскажу позже в этой статье.
  3. Применяйте автоматический размыкатель (Circuit Breaker). Существует не так много реализаций, которые поставляются с этим функционалом, например Hystrix. В некоторых компаниях пишут собственных внутренних обработчиков. Если есть возможность, обязательно используйте Circuit Breaker или инвестируйте в его создание. Четкая структура для определения запасных вариантов в случае ошибки — хороший вариант.
  4. Не обрабатывайте тайм-ауты как сбои. Тайм-ауты — не сбои, а неопределенные сценарии. Их следует обрабатывать способом, который поддерживает разрешение неопределенности. Стоит создавать явные механизмы разрешения, которые позволят системам синхронизироваться в случае тайм-аута. Механизмы могут варьироваться от простых сценариев согласования до рабочих процессов с отслеживанием состояния, очередей недоставленных писем и многого другого.
  5. Не вызывайте удаленные системы внутри транзакций. Когда удаленная система замедляется, вы дольше удерживаете соединение с базой данных. Это может быстро привести к исчерпанию соединений с базой данных и, как следствие, к сбою в работе вашей собственной системы.
  6. Используйте интеллектуальное пакетирование. Если работаете с большим количеством данных, выполняйте пакетные удаленные вызовы (вызовы API, чтение БД), а не последовательные — это устранит нагрузку на сеть. Но помните: чем больше размер пакета, тем выше общая задержка и больше единица работы, которая может дать сбой. Так что оптимизируйте размеры пакета для производительности и отказоустойчивости.

Построение системы, которую вызывают другие системы


  1. Убедитесь, что все API идемпотентны. Это обратная сторона повторов при тайм-аутах API. Вызывающие абоненты могут повторить попытку, только если ваши API безопасны для повторения и не вызывают неожиданных побочных эффектов. Под API я имею в виду как синхронные API, так и любые интерфейсы обмена сообщениями — клиент может опубликовать одно и то же сообщение дважды.
  2. Определите SLA — согласованный уровень качества предоставления услуги. Потребуется SLA для времени отклика и пропускной способности, а также код для их соблюдения. В распределенных системах гораздо лучше быстро потерпеть неудачу, чем позволить абонентам ждать.

    SLA с высокой пропускной способностью сложно реализовать: ограничение распределенной скорости — сама по себе сложная задача. Но стоит помнить об этом и иметь возможность обрывать вызовы, если выходите за пределы SLA. Другой важный аспект — знание времени отклика нижестоящих систем, чтобы определить, какую скорость ожидать от вашей системы.
  3. Определите и ограничьте пакетирование для API-интерфейсов. Максимальные размеры пакетов явно определяют и ограничивают обещанным SLA — это следствие соблюдения SLA.
  4. Подумайте заранее о наблюдаемости системы. Наблюдаемость — способность анализировать поведение системы, не обращая внимания на ее внутреннее устройство. Подумайте заранее, какие метрики и данные о системе нужны, чтобы ответить на вопросы, на которые ранее невозможно было получить ответы. И продумайте инструменты для получения этих данных.

    Мощный механизм — разбить логику системы на «домены» и публиковать события каждый раз, когда в «домене» происходит событие. Например, получен запрос с id = 123, возвращен ответ на запрос с id =123. Обратите внимание, как можно использовать эти два «доменных» события, чтобы получить новую метрику под названием «время отклика». Из необработанных данных следуют заранее определенные агрегации.

Общие рекомендации


  1. Используйте агрессивное кэширование. Сеть нестабильна, поэтому кэшируйте столько данных, сколько возможно. Ваш механизм кэширования может быть удаленным, например сервер Redis, работающий на отдельной машине. По крайней мере, вы перенесете данные в свою область управления и уменьшите нагрузку на другие системы.
  2. Определите единицу сбоя. Если API или сообщение представляет несколько единиц работы (пакет), то какова единица сбоя? В случае сбоя вся полезная нагрузка выйдет из строя или, напротив, отдельные блоки будут успешны или потерпят неудачу независимо? Отвечает ли API кодом успеха или неудачей в случае частичного успеха?
  3. Изолируйте внешние доменные объекты на границе системы. Еще один пункт, который, по моему опыту, вызывает проблемы в долгосрочной перспективе. Не стоит разрешать использование доменных объектов других систем во всей вашей системе для повторного использования. Это связывает вашу систему с работоспособностью другой системы. И каждый раз, когда меняется другая система, нужно рефакторить код. Поэтому стоит создавать собственную внутреннюю схему данных и преобразовывать внешние полезные данные в эту схему. И затем использовать внутри собственной системы.

Безопасность


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

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

Удачи!

Что еще почитать:

  1. Как спокойно спать, когда у вас облачный сервис: основные архитектурные советы.
  2. Как технический долг и лже-Agile убивают ваши проекты.
  3. Наш канале в Телеграме о цифровой трансформации.