Избегайте этих простых ошибок кодирования для стабильной системы

Первоначально опубликовано на моей домашней странице - https://kislayverma.com/programming/code-review-checklist-for-distributed-systems/

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

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

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

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

При вызове удаленных систем

Что происходит при выходе из строя удаленной системы?

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

  1. Определите путь для обработки ошибок. В коде должны быть явно определены пути для обработки ошибок, а не просто позволить вашей системе взорваться на глазах у конечных пользователей. Будь то хорошо спроектированная страница ошибок, журнал исключений с метрикой ошибок или автоматический выключатель с резервным механизмом, ошибки должны обрабатываться явным образом.
  2. Составьте план восстановления. Учитывайте каждое удаленное взаимодействие с вашим кодом и выясните, что нам нужно сделать, чтобы восстановить прерванную работу. Должен ли наш рабочий процесс быть с отслеживанием состояния, чтобы запускаться с момента сбоя? Публикуем ли мы все неудавшиеся полезные нагрузки в очередь повторных попыток / таблицу БД и повторяем их всякий раз, когда удаленная система возвращается в исходное состояние? Есть ли у нас сценарий для сравнения баз данных двух систем и их как-то синхронизации? Явный и предпочтительно системный план восстановления должен быть реализован и развернут до развертывания фактического кода.

Что происходит, когда удаленная система замедляется?

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

  1. Всегда устанавливать тайм-ауты для удаленных системных вызовов: сюда входят тайм-ауты для удаленных вызовов API, публикации событий и вызовов базы данных. Я нахожу этот простой недостаток в таком большом количестве кода, что он шокирует и в то же время не является неожиданным. Проверьте, установлены ли конечные и разумные тайм-ауты для всей удаленной системы при вызовах, чтобы не тратить ресурсы на ожидание, если удаленная система по какой-либо причине перестанет отвечать.
  2. Тайм-аут повторной попытки: сеть и системы ненадежны, и повторные попытки абсолютно необходимы для обеспечения устойчивости системы. Повторные попытки обычно устраняют множество «всплесков» при межсистемном взаимодействии. Если возможно, используйте какую-то отсрочку при повторных попытках (фиксированную, экспоненциальную). Добавление небольшого дрожания к механизму повтора может дать немного передышки. Поместите вызываемую систему, если она находится под нагрузкой, и это может привести к большему успеху. Обратной стороной повторных попыток является идемпотентность, о которой мы поговорим позже в этой статье.
  3. Используйте автоматический выключатель: не так много реализаций, которые поставляются с этой функцией заранее, но я видел, как компании сами пишут свои собственные оболочки. Если у вас есть такой выбор, обязательно воспользуйтесь им. Если вы этого не сделаете, подумайте о том, чтобы инвестировать в его создание. Наличие четко определенной структуры для определения откатов в случае ошибки создает хороший прецедент.
  4. Не обрабатывайте тайм-ауты как сбой - тайм-ауты - это не сбои, а неопределенные сценарии, и их следует обрабатывать таким образом, чтобы поддерживать разрешение неопределенности. Мы должны создать явные механизмы разрешения, которые позволят системам синхронизироваться в случаях, когда происходят тайм-ауты. Это может быть как простые сценарии согласования, так и рабочие процессы с отслеживанием состояния, очереди недоставленных сообщений и многое другое.

Контролируемое пакетирование. Если вы работаете с большим количеством данных, вместо этого выполняйте пакетные удаленные вызовы (вызовы API, чтение БД) один за другим, чтобы убрать накладные расходы сети. Но помните, что чем больше размер пакета, тем больше общая задержка и больше единица работы, которая может дать сбой. Поэтому оптимизируйте пакетирование как с точки зрения производительности, так и с точки зрения отказоустойчивости.

При построении системы другие будут вызывать

  1. Все API ДОЛЖНЫ быть идемпотентными. Это обратная сторона повторных попыток тайм-аутов API. Ваши вызывающие абоненты могут повторить попытку только в том случае, если ваши API безопасны для повторной попытки и не вызывают неожиданных побочных эффектов. Под API я имею в виду как синхронные API, так и любые интерфейсы обмена сообщениями - клиент может опубликовать одно и то же сообщение дважды (или брокер может доставить его дважды).
  2. Явно определите SLA для времени отклика и пропускной способности и код для их соблюдения. В распределенных системах гораздо лучше быстро выйти из строя, чем позволить вызывающим абонентам ждать. По общему признанию, SLA с пропускной способностью сложно реализовать (распределенное ограничение скорости - это трудная проблема, которую нужно решить само по себе), но мы должны быть осведомлены о наших SLA и предусмотреть возможность упреждающего отказа от вызовов, если мы перейдем к нему. Другой важный аспект - это знание времени отклика ваших подчиненных систем, чтобы вы могли определить, какая у вашей системы самая быстрая.
  3. Определите и ограничьте пакетные API. Если вы предоставляете пакетные API, максимальные размеры пакетов должны быть явно определены и ограничены соглашением об уровне обслуживания, которое мы обещаем. Это следствие соблюдения SLA.
  4. Подумайте о наблюдаемости заранее. Наблюдаемость означает способность анализировать поведение системы, не заглядывая в ее внутренности. Подумайте заранее, какие показатели вы должны собирать о своей системе и какие данные вы должны собрать, чтобы вы могли ответить на ранее не задаваемые вопросы. Затем настройте системы на получение этих данных. Мощным механизмом для этого является идентификация моделей предметной области вашей системы и публикация событий каждый раз, когда событие происходит в домене (например, получен идентификатор запроса 123, возвращен ответ на запрос 123 - обратите внимание, как можно использовать эти два события «домена». для получения нового показателя, называемого «время ответа». Необработанные данные ›› предварительно определенные агрегаты).

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

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

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

Далее: Контрольный список проверки проекта для распределенных систем

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