Дизайн системы: приготовьтесь к худшему
При разработке нового приложения или новой функции одним из важных моментов, на который следует обратить внимание, является способность вашего приложения к самовосстановлению в случае сбоя. Ваш код может быть отличным и соответствовать бизнес-требованиям, НО достаточно ли он хорош, чтобы делать это также во время неожиданных сбоев? Достаточно ли отказоустойчиво ваше приложение, чтобы автоматически преодолевать эти сбои?
Иногда вы не будете неявно проинструктированы о реализации механизмов устойчивости и отката в рамках бизнес-требований, поскольку сценарии, которые он должен обрабатывать, могут быть очень техническими и ориентированными на разработку. Эти механизмы относятся к семейству нефункциональных требований.
О каких «неожиданных неудачах» мы говорим?
Что ж, существуют различные сценарии, которые могут привести ваше приложение к сбою/нестабильному состоянию. Например:
- Тайм-аут/служба недоступна для внешнего API, используемого вашим приложением
- Ошибки в третьей стороне, которые используются вашим приложением
- Сбой операций базы данных
- Непредвиденная нагрузка на приложение, вызывающая снижение производительности и, следовательно, отказ в приеме входящих запросов.
- Непредвиденная ошибка, которая приводит к потере данных
- долгоживущие процессы, которыми управляет приложение, застревают в своем состоянии из-за потери данных
Наличие приложения без соответствующих механизмов самовосстановления и отката не будет работать в долгосрочной перспективе. Эти «неожиданные сбои» в конечном итоге произойдут, это повредит вашему SLA и качеству обслуживания клиентов. Это может привести к потере данных, которую будет трудно восстановить, и вам потребуется ручное вмешательство и некоторые магические действия, чтобы решить проблему. Теперь подумайте о приложениях, которые обрабатывают сотни тысяч запросов в минуту: небольшой сбой может привести к серьезным последствиям, которые трудно исправить вручную и которые будут стоить вашей организации много времени и денег.
Поэтому вы должны готовиться к худшему.
Давайте рассмотрим некоторые распространенные сценарии и предложим решения, которые могут повысить уровень отказоустойчивости вашего приложения и резервные возможности, чтобы вы могли спокойно спать по ночам :)
Механизм повтора
Особенно при работе с операциями ввода-вывода, такими как отправка HTTP-запросов, чтение/запись из базы данных или файла и т. д., вероятность того, что вы столкнетесь с ошибкой, является обычным явлением. Например: служба недоступна, внутренняя ошибка сервера, тайм-аут операции и многое другое.
Многие из этих ошибок могут быть временными и прекращаться в течение нескольких секунд, а это означает, что существует высокая вероятность того, что следующая попытка через несколько секунд сработает.
Итак… почему рушится ваш бизнес-процесс только из-за небольшой временной проблемы? Попробуй снова! Используйте механизм повтора операции, который гарантирует, что, если проблема носит временный характер, ваше приложение автоматически преодолеет ее.
Обратите внимание на следующее:
- Иметь возможность настраивать и ограничивать количество повторных попыток, которые пытается выполнить ваше приложение. Бесконечное/большое количество повторных попыток может привести к зависанию вашего приложения и отклонению других запрошенных запросов, поскольку оно будет занято при бесконечных повторных попытках.
- внедрить экспоненциальный режим выпечки. Экспоненциально увеличивайте интервал между повторными попытками.
Это повысит вероятность того, что временная проблема (на сервере/базе данных/третьей стороне) была решена и ваша следующая попытка будет успешной. Кроме того, использование экспоненциального интервала также дает льготу для третьей стороны, которая может быть под нагрузкой и, следовательно, не может обслуживать своих клиентов. Выполнение немедленных повторных попыток без задержки может усугубить проблему. - Убедитесь, что вы понимаете, при каких ошибках следует повторять попытку, а при каких нет. Не каждая ошибка может быть устранена повторной попыткой. Например, предположим, что ваш http-запрос не удался с кодом состояния «Неверный запрос» (400). Это означает, что что-то с данными запроса не так и было отклонено сервером. Сколько бы попыток ни делало ваше приложение, оно закончится с одним и тем же результатом — сбоем.
Выполнение повторных попыток для таких ошибок является излишним и повлияет на производительность вашего приложения. Вы должны различать разные ошибки и решать, какую повторную попытку можно сделать.
Существует несколько библиотек с открытым исходным кодом, которые уже предоставляют вам все соответствующие функции механизма повторных попыток, поэтому вам не нужно писать их с нуля. Например, в .NET у вас есть библиотека Polly.
Не забудьте задать вопрос, что делать, если все повторные попытки не увенчались успехом? Это деловой вопрос, и ответ может меняться между сценариями.
Определите время ожидания для ваших процессов
Некоторые приложения управляют долгоживущими процессами. Процесс имеет состояние, которое указывает на прогресс. Ход процесса иногда зависит от длительных операций или асинхронного ответа от третьей стороны. Поскольку что-то может пойти не так (а они всегда идут не так), ваш процесс может навсегда застрять в нефинальном состоянии и остаться «открытым». Например, третья сторона не смогла отправить ответ.
Хорошо, так что они останутся открытыми. В чем проблема?
Скорее всего, другие системы зависят от ваших процессов и ждут своего результата. Ваше приложение затормозит весь бизнес-процесс, что повлияет на SLA других служб и взаимодействие с пользователем.
Кроме того, эти открытые процессы затруднят расследования и анализ данных, поскольку они ведут себя по-разному и могут повредить вашей статистике.
Я не хочу этого! Как мне это решить?
Во-первых, определите подходящее и приемлемое время, в течение которого ваш процесс может оставаться в незавершенном состоянии. Это деловой вопрос, и он должен определяться SLA, которому вы обязаны, и оценкой общего разумного времени, которое должно занять операции внутри процесса. Например, если мой процесс зависит от третьей стороны, среднее время ответа которой составляет 10 минут, вы можете установить тайм-аут в 20–30 минут.
Теперь можно переходить к реализации. Базовая реализация может заключаться в создании запланированной операции, которая запускается каждые X минут и ищет открытые процессы более Y минут (значение времени ожидания). Если найдено, просто переведите процесс в конечное состояние, это может быть состояние отказа/отмены.
Таким образом, у вас есть возможность изящно завершить/отменить процесс. Это делается управляемым образом и предоставляет вашему приложению возможность уведомлять о соответствующем результате своих ожидающих клиентов.
Уведомление! шаблон сам по себе не решил проблему, НО предоставил вам возможность автоматически управлять им и выполнять резервный поток. Возможность сообщить о проблеме и придерживаться обещанного SLA.
Давайте рассмотрим реальный сценарий использования этого шаблона:
Моя команда в Payoneer отвечает за процесс автоматического утверждения документов клиентов. Процесс утверждения документа — это автономный процесс, состоящий из нескольких шагов и включающий интеграцию с внешними поставщиками, которые предоставляют результаты анализа посредством асинхронной связи.
В нашей системе реализован шаблон тайм-аута, и по истечении этого времени система перемещает документы клиента для ручной проверки представителем вместо ожидания автоматического результата.
Таким образом, мы не влияем на соглашение об уровне обслуживания, которое у нас есть перед клиентом, в отношении времени, которое требуется для проверки его документов.
Не ретранслируйте только на обратный вызов/веб-перехватчик — внедрите механизм опроса
При работе с асинхронной связью ответ возвращается клиенту в качестве обратного вызова. Это может быть сервер, запускающий веб-перехватчик с данными, или сервер, публикующий событие через шину событий/брокер сообщений.
Существуют сценарии, когда сообщение не было получено клиентом или было получено, но не было правильно обработано и, следовательно, было отброшено, что привело к потере данных. В этих сценариях клиент все еще ожидает ответа, но сервер уже отправил его и больше не будет отправлять.
в предыдущем разделе мы упомянули шаблон тайм-аута, который не решает проблему, но позволяет изящно завершить ее и включить управляемый резервный поток. Внедрив механизм опроса, вы сможете автоматически решать некоторые вопросы и оставаться на «счастливом пути» бизнес-потока.
Вместо того, чтобы просто ждать, пока сервер предоставит вам ответ асинхронно путем нажатия, реализуйте запланированный процесс, который будет выполняться каждые X минут и будет искать запросы, ожидающие более Y минут. Для этих запросов ваше приложение отправит синхронный запрос на сервер для получения данных ответа. Если исходный запрос был завершен для обработки сервером, он сможет отправить вам результат. В этом случае ваше приложение продолжит бизнес-процесс так же, как если бы данные были получены вебхуком. В противном случае исходный запрос все еще обрабатывается сервером, и нет возможности отправить его обратно. Вам просто нужно продолжать ждать.
Внедрив этот механизм, вы смогли автоматически решить непредвиденные проблемы, связанные с асинхронной связью, и тем самым избежать сбоя потока или ручного вмешательства для восстановления данных с сервера.
Уведомление! Не все сервисы, с которыми вы будете интегрироваться, будут иметь возможность предоставлять вам конечные точки для опроса в дополнение к предлагаемой ими асинхронной связи. Если их нет, стоит спросить, можно ли это сделать в будущем.
Это правда, что это решение снижает производительность, НО при выборе правильных интервалов оно будет бесшовным, а преимущество, которое оно дает во время сбоев, бесценно.
Избегайте блокировки поставщика
Обычно приложение использует внешних поставщиков для достижения своих бизнес-целей. Невозможно, чтобы каждая организация разрабатывала каждую часть логики самостоятельно, особенно если логика сложна и далека от основной деятельности организации. Поэтому вы, вероятно, предпочтете интегрироваться с поставщиком, который сможет предоставить вам часть логики, необходимой для завершения вашего бизнес-процесса.
Итак, после исследования и POC вы нашли подходящего поставщика для ваших нужд.
Интеграция прошла гладко и… ВУАЛЯ! вы запустили свой бизнес и обслуживаете сотни тысяч клиентов в день.
Теперь позвольте мне задать вам вопрос. Что произойдет, если однажды этот внешний вендор перестанет работать? что будет, если он обанкротится? Если его взломали и сервис будет недоступен долгое время? Или, может быть, вы просто не будете удовлетворены производительностью поставщика или, может быть, тем, как он управляется, поскольку каждый месяц появляются новые критические изменения, которые заставляют вашу команду разработчиков работать круглосуточно, чтобы настроить интеграцию?
Особенно при работе с основными и конфиденциальными бизнес-потоками этот сценарий имеет решающее значение и может парализовать часть вашей организации и оказать огромное влияние на ваших клиентов.
Вернемся снова к процессу автоматического утверждения документов клиента, которым управляет моя команда в Payoneer. В рамках этого процесса мы используем внешних поставщиков для извлечения соответствующего текста из документа и его обработки. Мы решили интегрироваться с двумя поставщиками, которые обеспечивают нам этот результат и имеют приоритет между ними, поэтому, если один выходит из строя или имеет много ложных срабатываний, мы можем переключиться на другого с нулевым временем простоя бизнес-потока. Это, конечно, также дает нам уверенность в том, что если что-то случится с одним из них, у нас всегда будет другой, на которого можно положиться.
Конечно, вам не всегда нужно создавать эту резервную копию. Это вопрос соотношения цены и качества и того, насколько критичен бизнес-поток, с которым вы имеете дело.
Схема автоматического выключателя
В отличие от шаблона повторных попыток, который предполагает устранение временных сбоев, шаблон разрыва цепи здесь предназначен для предотвращения вызова приложением операции, которая имеет высокую вероятность сбоя.
Думаю, вы используете сторонний API через REST API, который в настоящее время имеет ошибку, которая приводит к сбою всех запросов. Предположим, что ETA для исправления составляет 4 часа.
При использовании шаблона повторных попыток приложение несколько раз попытается вызвать запрос, который в конечном итоге завершится ошибкой. Эти бесполезные повторные попытки повлияют на производительность приложения и третьей стороны.
Вместо этого шаблон разрыва цепи позволит приложению быстро выйти из строя и попытаться постепенно восстановиться, когда проблема будет решена. Это позволяет избежать возникновения ошибок и снижения производительности.
Итак, как это работает?
Механизм действует как прокси для операции, которую вы хотите выполнить.
Имеет 3 состояния:
- Закрыто — означает, что операция разрешена для регулярного выполнения.
В этом состоянии подсчитывается количество отказов. Если количество сбоев превысит заданный порог за определенный интервал времени, механизм перейдет в открытое состояние. - Открытый — это означает, что механизм предотвратит выполнение операции, поскольку он испытал множество сбоев, и поэтому он предпочтет быстро завершиться ошибкой, не пытаясь ее выполнить. В этом состоянии у нас есть тайм-аут. При достижении механизм переходит в полуоткрытое состояние.
- Полуоткрытый — это означает, что разрешено и передается ограниченное количество запросов на выполнение операции. Это состояние предназначено для проверки того, что проблема решена. Если это ограниченное количество операций выполняется успешно, механизм предполагает, что проблема решена, и переводит состояние механизма в закрытое. В противном случае он возвращается к Open, поскольку проблема все еще существует.
Выделенные конечные точки для простого и эффективного ручного вмешательства
Сказав все это, иногда мы не готовы к автоматическому решению для любого случая, который может возникнуть. Иногда наше автоматизированное решение приходит только после того, как мы впервые столкнулись со сценарием и поняли, как решить его с помощью автоматизации. Поэтому в таких случаях ручное вмешательство неизбежно. В большинстве случаев эти ручные вмешательства требуют, чтобы вы прошли через «внутренности» службы, например: напрямую изменили значения через базу данных или вручную реструктурировали сообщение и отправили его напрямую через брокера сообщений.
Также здесь мы можем быть заранее подготовлены, чтобы это ручное вмешательство было управляемым, безопасным и эффективным. Просто создайте внутренний API в своем приложении, который позволит выполнять эти обычные ручные операции.
Например, создайте конечную точку, которая позволит вам отменить процесс, которым управляет ваше приложение. Или создайте конечную точку, которая позволит повторно опубликовать результат старого процесса, чтобы другая система могла использовать его снова, если он был потерян или никогда не запускался.
Создавая этот внутренний API, хотя он требует, чтобы кто-то его запускал, он имеет много преимуществ:
- Операция управляется приложением. Приложение является владельцем изменения и знает об этом.
- Операция была протестирована QA, так как она была частью разработки. Таким образом, вы снижаете вероятность ошибок
- иногда операция не так проста, как просто обновление значения в БД. Иногда есть и побочные действия, которые необходимо выполнить. Всеми этими побочными действиями можно управлять с помощью внутреннего API для простоты использования.
- Сохранение времени
Заключить
Ваше приложение ДОЛЖНО быть всегда готово к худшему!
Чем больше ваше приложение будет знать о самовосстановлении и иметь резервные решения, тем больше повысится его производительность, влияние на качество обслуживания клиентов будет низким, тем быстрее вы сможете преодолевать сбои и экономить драгоценное время и деньги для вашей организации.
Во время разработки вашего приложения/функции вы должны обратить на это внимание. Вы должны задать правильные вопросы и выявить слабые места, чтобы подготовить соответствующее обращение и избежать неприятных сюрпризов в производстве.