Запасной шаблон: проектирование плавной деградации микросервисов
В архитектуре микросервисов сервисы образуют сеть распределенных сетевых вызовов. Хотя это позволяет командам самостоятельно создавать и масштабировать сервисы, это также означает, что общая надежность вашей системы зависит от ее самого слабого звена. Если критически важная служба выходит из строя или перестает отвечать на запросы, это может вызвать каскадный сбой, который нарушит работу всего приложения.
Возврат пользователям общего сообщения «500 Internal Server Error» или пустой страницы в момент сбоя одной зависимости — это плохой пользовательский опыт. Вместо этого устойчивые системы созданы для того, чтобы плавно деградировать, когда что-то идет не так.
Именно здесь на помощь приходит Резервный шаблон. Определив безопасный альтернативный путь выполнения в случае сбоя основного вызова службы, вы можете сохранить работоспособность своего приложения даже в ухудшенном состоянии.
В этом руководстве мы рассмотрим шаблон Fallback, общие стратегии его реализации, как он взаимодействует с другими шаблонами устойчивости и как писать резервную логику на Java (Resilience4j) и Go.
Аналогия из реального мира: запасной план в кофейне
Представьте, что вы заходите в местную кофейню, чтобы купить латте. Бариста вводит ваш заказ, но когда вы подносите карту, на платежном терминале мигает ошибка подключения — у интернет-провайдера магазина произошел сбой.
Кофейня немедленно выключает свет, запирает двери и отправляет всех посетителей домой?
Конечно, нет. Они реализуют запасную стратегию:
- Если у них есть наличные, они спрашивают, можете ли вы заплатить наличными.
- Если вы постоянный клиент, бариста может записать ваше имя и заказ в книгу и попросить вас заплатить при следующем посещении.
- Они могут использовать автономное устройство считывания карт, которое сохраняет токен карты локально и обрабатывает платеж позже, когда восстановится подключение к Интернету.
В разработке программного обеспечения:
- Заказ кофе — это запрос клиента.
- Карточный терминал — это основная нисходящая услуга (например, API платежного шлюза).
- Отключение Интернета — это тайм-аут сети или сбой в работе службы.
- Ledger/Offline Reader — резервный путь выполнения.
Распространенные стратегии отката
В зависимости от бизнес-логики и критичности вышедшего из строя сервиса вы можете выбрать одну из нескольких запасных стратегий:
1. Статические значения по умолчанию
Самая простая стратегия — вернуть безопасное, предварительно настроенное статическое значение. Это очень эффективно для некритических функций, где допустимо отображение пустых данных или данных по умолчанию.
- Пример. Если служба персонализации профиля не работает, верните изображение аватара по умолчанию и общее приветствие.
- Пример. В случае сбоя службы рекомендаций вместо выдачи ошибки верните пустой список или жестко закодированный список универсальных бестселлеров.
2. Кэшированные ответы (устаревшие при повторной проверке)
Если текущие данные недоступны, вы можете вернуться к устаревшим данным, доступным только для чтения, из локального кэша или хранилища с быстрой распределенной памятью, такого как Redis.
- Пример. Если служба инвентаризации продуктов отключается, отобразите количество на складе, кэшированное 5 минут назад, вместе с небольшим сообщением пользовательского интерфейса, указывающим, что данные могут быть не полностью актуальными.
- Пример. Если служба настроек пользователя выходит из строя, загрузите кэшированный профиль пользователя вместо блокировки процесса входа в систему.
3. Альтернативная услуга (мультипровайдер)
При выполнении критической операции, которая должна завершиться успешно, вы можете настроить вторичного поставщика услуг в качестве резервного.
- Пример: если ваш основной платежный шлюз (например, Stripe) возвращает ошибку 5xx или истекает время ожидания, резервный механизм немедленно перенаправляет запрос транзакции на дополнительный шлюз (например, PayPal или Adyen).
- Пример. Если API геокодирования дает сбой, вернитесь к вторичному картографическому поставщику.
4. Очередь на потом (асинхронный буфер)
Для операций записи, которые не требуют немедленной синхронной обработки, резервный вариант может буферизовать запрос в локальной очереди или базе данных для повторной попытки позже.
- Пример. Если служба уведомлений по электронной почте не работает, запишите полезные данные уведомления в очередь недоставленных писем или в таблицу локальной базы данных. Фоновый работник будет читать данные из этой очереди и доставлять электронные письма, как только служба уведомлений снова станет работоспособной.
Трио устойчивости: повторная попытка, автоматический выключатель и резервный вариант
Чтобы создать высокоустойчивую архитектуру, необходимо объединить шаблон «Резервный вариант» с шаблонами Повторить и Выключатель. Они образуют трехуровневую линию защиты:
| Узор | Роль | Действие | Сценарий |
|---|---|---|---|
| Повторить шаблон | Устраняет кратковременные кратковременные сбои. | Повторяет запрос после небольшой задержки (задержка + дрожание). | Кратковременные сбросы сетевых пакетов, сброс сокетов. |
| Автоматический выключатель | Предотвращает истощение ресурсов. | Отключения открываются для немедленной блокировки вызовов неисправной службы (Fail-Fast). | Постоянные простои служб, взаимоблокировки базы данных. |
| Резервный шаблон | Сохраняет пользовательский опыт. | Выполняет альтернативное действие в случае сбоя или блокировки основного вызова. | Когда повторные попытки исчерпаны или автоматический выключатель разомкнут. |
Как они работают вместе
- Входящий запрос попадает на сервисный шлюз.
- Запрос проходит через Выключатель.
- Если автоматический выключатель замкнут, запрос передается оболочке Retry, которая выполняет фактический вызов.
- Если возникает временная ошибка, механизм повтора пытается выполнить вызов еще раз.
- Если все повторные попытки завершились неудачей или если автоматический выключатель уже был разомкнут (не удалось быстро сохранить ресурсы), Обработчик резервного копирования перехватывает сбой и возвращает ухудшенный/кэшированный ответ.
Реализации кода
Давайте посмотрим, как мы можем реализовать резервную логику как в Java, так и в Go.
1. Java (Resilience4j)
В Java Resilience4j является отраслевым стандартом отказоустойчивости. Мы можем определить резервный метод декларативно, используя аннотации, или программно.
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
@Service
public class ProductService {
private final InventoryClient inventoryClient;
private final CacheManager cacheManager;
public ProductService(InventoryClient inventoryClient, CacheManager cacheManager) {
this.inventoryClient = inventoryClient;
this.cacheManager = cacheManager;
}
// Bind this method to a circuit breaker. If it fails or is open, route to fallback
@CircuitBreaker(name = "inventoryService", fallbackMethod = "getInventoryFallback")
public List<String> getProductInventory(String category) {
return inventoryClient.fetchStockByCategory(category);
}
// Fallback method must have the same return type and accept the same parameters,
// plus a Throwable parameter containing the error that triggered it
public List<String> getInventoryFallback(String category, Throwable throwable) {
System.err.println("Primary inventory service failed: " + throwable.getMessage());
// Attempt to fetch from local cache (Strategy 2)
List<String> cachedStock = cacheManager.get("inventory:" + category, List.class);
if (cachedStock != null) {
return cachedStock;
}
// Return static empty list if cache is empty (Strategy 1)
return Collections.emptyList();
}
}
2. Иди (Голанг)
В Go мы можем писать чистые резервные декораторы, используя шаблоны функционального программирования, которые позволяют нам обернуть любой обработчик выполнения вторичным обработчиком.
package main
import (
"context"
"errors"
"fmt"
"time"
)
// Request represents a simple payload
type Request struct {
UserID string
}
// Response represents the returned data
type Response struct {
Data string
Status string
}
// ServiceFunc represents our primary function signature
type ServiceFunc func(ctx context.Context, req Request) (Response, error)
// WithFallback wraps a service function with fallback logic
func WithFallback(primary ServiceFunc, fallback ServiceFunc) ServiceFunc {
return func(ctx context.Context, req Request) (Response, error) {
res, err := primary(ctx, req)
if err != nil {
fmt.Printf("[Warning] Primary call failed: %v. Running fallback...\n", err)
return fallback(ctx, req)
}
return res, nil
}
}
func main() {
// 1. Define primary service that occasionally fails
primaryService := func(ctx context.Context, req Request) (Response, error) {
return Response{}, errors.New("database connection timeout (504)")
}
// 2. Define fallback service that retrieves cached data
fallbackService := func(ctx context.Context, req Request) (Response, error) {
// Simulating cached read
return Response{
Data: fmt.Sprintf("Stale cached profile data for user %s", req.UserID),
Status: "DEGRADED (CACHED)",
}, nil
}
// 3. Wrap them together
resilientService := WithFallback(primaryService, fallbackService)
// 4. Execute
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
req := Request{UserID: "user_992"}
response, err := resilientService(ctx, req)
if err != nil {
fmt.Printf("Operation completely failed: %v\n", err)
} else {
fmt.Printf("Response Received:\n - Status: %s\n - Data: %s\n", response.Status, response.Data)
}
}
Лучшие практики для резервного шаблона
- Не допускайте зависимости резервного пути. Логика резервного восстановления не должна полагаться на те же нижестоящие системы или инфраструктуру, которые только что вышли из строя. Если ваша база данных не работает, возврат к другому запросу к базе данных на том же сервере, скорее всего, также не удастся.
- Выполнять быстро: резервная логика должна выполняться быстро. Избегайте сложных вычислений или медленных вложенных вызовов в резервных путях. Цель — как можно быстрее вернуть ответ пользователю.
- Оповещение и мониторинг: всегда регистрируйте резервные выполнения и увеличивайте счетчики телеметрии. Если ваша система использует резервные пути, это означает, что система работает в ухудшенном состоянии. Вам нужны оповещения, чтобы знать, когда уровень отката превышает обычные пороговые значения.
- Сделайте пользовательский интерфейс устойчивым: тесно сотрудничайте с разработчиками интерфейса, чтобы гарантировать, что пользовательский интерфейс предназначен для приема резервных полезных данных (например, пустых коллекций или частичных состояний), не нарушая макет на стороне клиента.
Заключение
Резервный шаблон — это идеальный страховой полис для надежности микросервисов. Предвидя сбои и обеспечивая элегантную запасную логику, вы превращаете серьезные сбои в малозаметные, управляемые сбои. Сочетание этого шаблона с Повторными попытками и Автоматическими выключателями гарантирует, что ваше приложение сможет противостоять крупным внешним сбоям, продолжая обслуживать пользователей.