Руководство по плавной деградации в веб-разработке
Плавная деградация - это принцип проектирования программного обеспечения и системной инженерии, который гарантирует, что система продолжает функционировать, хотя и со сниженной производительностью или функциональными возможностями, когда один или несколько ее компонентов выходят из строя или сталкиваются с проблемами.
Вместо того, чтобы полностью выходить из строя, система "постепенно деградирует", сохраняя основные функциональные возможности и обеспечивая минимально приемлемый пользовательский опыт. Какой аспект ухудшается, зависит от типа системы/программного обеспечения.
Например, картографический сервис может перестать выдавать дополнительные сведения о районе города из-за сбоя в работе сети, но по-прежнему позволит пользователю перемещаться по областям карты, которые уже были загружены; веб-сайт может оставаться доступным для навигации и чтения, даже если некоторые скрипты, изображения или расширенные функции не работают.не загружается, например, веб-почта, которая все равно позволит вам редактировать свои электронные письма, даже если вы находитесь в режиме полета.
Концепция "постепенной деградации" отличается от "быстрых до отказа" подходов, при которых система немедленно прекращает работу при обнаружении сбоя. Плавная деградация обеспечивает устойчивость и дизайн, ориентированный на пользователя, обеспечивая доступность критически важных служб во время частичных сбоев.
Как обычно, код для этой статьи доступен на GitHub. Мы будем использовать теги, чтобы проследить наш путь по "деградации" функциональных возможностей.
Реализация плавной деградации в демонстрационном приложении
Чтобы подкрепить наше объяснение, мы будем использовать простое приложение (написанное на Deno/Fresh, но язык / фреймворк в этой статье не имеет значения), которое будет вызывать удаленный API, чтобы получить свежую шутку для пользователя.
Интерфейс довольно прост, а код можно найти в репозитории (в частности, по этому тегу).
Файл islands\Joke.tsx - это компонент preact, отвечающий за отображение случайной шутки в веб-интерфейсе. Он использует перехватчики useState и useEffect для управления состоянием шутки и получения данных при подключении компонента. Шутка извлекается из конечной точки /api/joke, и пользователи могут получить новую шутку, нажав на кнопку. Компонент отображает шутку вместе с кнопкой, которая запускает динамическую выборку новой шутки при нажатии.
Файл routes\api\joke.ts определяет конечную точку API, которая возвращает случайную шутку. Он извлекает шутку из внешнего API (в данном примере мы используем сервис, но подойдет и любой другой подобный сервис) и извлекает настройки и изюминку. Затем ответ форматируется в виде одной строки (setup + punchline) и возвращается клиенту в виде ответа в формате JSON.
Сбои и меры по их устранению
Приложение не делает многого, но с архитектурной точки зрения оно состоит из двух уровней: интерфейса и серверной части с API. Наш интерфейс прост и не подвержен сбоям, но серверная часть, наш API-интерфейс Joke, может давать сбои: она зависит от внешнего сервиса, который находится вне нашего контроля.
Давайте посмотрим на текущую версию API:
import { FreshContext } from "$fresh/server.ts"; export const handler = async (_req: Request, _ctx: FreshContext): Promise<Response> => { const res = await fetch( "https://official-joke-api.appspot.com/random_joke", ); const newJoke = await res.json(); const body = JSON.stringify(newJoke.setup + " " + newJoke.punchline); return new Response(body); };
Первая ошибка: корректная обработка тайм-аутов API
Первый тип сбоя, который мы реализуем, заключается в случайном получении тайм-аута при вызове внешнего API. Давайте изменим код:
import { FreshContext } from "$fresh/server.ts"; export const handler = async ( _req: Request, _ctx: FreshContext, ): Promise<Response> => { // Simulate a timeout by setting a timeout promise const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), 200) ); // Fetch the joke from the external API const fetchPromise = fetch( "https://official-joke-api.appspot.com/random_joke", ); // Race the fetch promise against the timeout const res = await Promise.race([fetchPromise, timeoutPromise]); if (res instanceof Response) { const newJoke = await res.json(); const body = JSON.stringify(newJoke.setup + " " + newJoke.punchline); return new Response(body); } else { return new Response("Failed to fetch joke", { status: 500 }); } };
В этой новой версии мы добавили timeoutPromise, который будет "ускоряться" при вызове нашего внешнего API: если внешний API ответит менее чем за 200 мс (т.е. выиграет гонку), мы получим новую шутку, в противном случае мы получим null в качестве результата. Это разрушительно - наш интерфейс полагается на ответ от API в виде объекта JSON и получает сообщение (Не удалось получить JOKE") и ошибку HTTP 500. В браузере это приведет к следующим эффектам:
Шутка не обновляется, и вы получаете сообщение об ошибке в консоли, потому что сообщение, которое вы получаете от API, не является отформатированным в формате JSON. Чтобы уменьшить случайные тайм-ауты, которые мы ввели в наш API-код, мы можем обеспечить защиту: когда выборка завершается неудачей, мы возвращаем стандартную шутку, отформатированную так, как ожидает интерфейс:
... // Race the fetch promise against the timeout const res = await Promise.race([fetchPromise, timeoutPromise]); if (res === null) { // If the timeout wins, return a fallback response const fallbackJoke = { setup: "[cached] Why did the developer go broke?", punchline: "Because they used up all their cache!", }; const body = JSON.stringify( fallbackJoke.setup + " " + fallbackJoke.punchline, ); return new Response(body); } ...
Чтобы смягчить последствия только что созданного сбоя, мы проверяем, вернул ли вызов значение null; в таком случае полезно иметь резервный файл, который будет возвращен в том же формате, который ожидает интерфейс. Этот простой механизм повысил устойчивость нашего API к сбоям определенного типа: непредсказуемому таймауту внешнего API.
Второй сбой: корректная обработка сетевых ошибок
В примере с тайм-аутом механизм, который мы применили для устранения неполадок, по-прежнему основан на том факте, что сервер с внешним API доступен. Если вы отсоедините сетевой кабель от своего ПК (или активируете режим полета), вы увидите, что интерфейс по-новому выйдет из строя:
Причина в том, что серверная часть не может связаться с внешним API-сервером и, таким образом, возвращает серверной части сообщение об ошибке (для получения дополнительной информации проверьте журналы из Deno). Чтобы смягчить эту ситуацию, мы должны модифицировать серверную часть, чтобы она была в курсе сбоя внешнего API, а затем справиться с этим, предоставив запасной вариант.:
... // If the fetch completes in time, proceed as usual if (res instanceof Response) { const newJoke = await res.json(); const body = JSON.stringify(newJoke.setup + " " + newJoke.punchline); return new Response(body); } else { throw new Error("Failed to fetch joke"); } } catch (_error) { // Handle any other errors (e.g., network issues) const errorJoke = { setup: "[cached] Why did the API call fail?", punchline: "Because it couldn't handle the request!", }; const body = JSON.stringify(errorJoke.setup + " " + errorJoke.punchline); return new Response(body, { status: 500 }); } };
Решение основано на том факте, что вместо того, чтобы возвращать обычное сообщение "Не удалось извлечь Joke", мы заключаем все взаимодействие с внешним сервером API в блок try/catch. Этот блок позволит нам справиться с сетевым сбоем, используя локальную шутку вместо выразительного сообщения об ошибке. Это окончательное решение для устранения возможных ошибок, которые могут возникнуть в серверной части, и оно повышает устойчивость системы.
Смягчение последствий для внешнего интерфейса
В предыдущем разделе мы повысили устойчивость к сбоям, но мы также хотим сохранить ориентированный на пользователя подход как часть постепенной деградации. На данный момент пользователь не знает, свежа ли полученная им шутка или нет. Чтобы расширить эти знания, мы расширим JSON, возвращаемый из серверной части, чтобы отслеживать свежесть шутки. При сбое внешнего API JSON, возвращаемый интерфейсу, будет указывать, что шутка не свежая (значение fresh равно false).:
const errorJoke = { setup: "Why did the API call fail?", punchline: "Because it couldn't handle the request!", fresh: false };
В противном случае, когда внешний API завершается успешно, мы возвращаем объект JSON с полем fresh, равным true:
if (res instanceof Response) { const newJoke = await res.json(); newJoke.fresh = true; const body = JSON.stringify(newJoke); return new Response(body); }
Теперь, когда интерфейс получает свежесть каждой шутки, нам просто нужно показать ее пользователю:
Когда внешний вызов API завершается неудачей, сообщение отображается красным цветом, чтобы пользователь знал, что он получает.
Вывод
В этой статье мы рассмотрели концепцию постепенной деградации, выделив два механизма для устранения системных сбоев. Мы рассмотрели два принципа реализации постепенной деградации: создание устойчивых компонентов, способных противостоять сбоям, и применение подхода, ориентированного на пользователя, чтобы пользователи знали о любых ограниченных функциональных возможностях системы в случае сбоев.