Операционная стоимость — Примеры

Пример 1 — Когда сервис запасов замедляется

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

app.get("/orders/:id", async (req, res) => {
  const order = await ordersService.get(`/orders/${req.params.id}`);
  const stock = await stockService.get("/stock");
  res.json({ ...order, stock });
});

Решение

app.get("/orders/:id", async (req, res) => {
  const requestId = req.headers["x-request-id"] ?? crypto.randomUUID();

  async function call(service, path, { timeout, fallback }) {
    try {
      return await withTimeout(service.get(path, { requestId }), timeout);
    } catch {
      return fallback;
    }
  }

  const order = await call(ordersService, `/orders/${req.params.id}`, { timeout: 300, fallback: null });
  const stock = await call(stockService, "/stock", { timeout: 200, fallback: null });

  res.json({ ...order, stock, degraded: stock === null, requestId });
});

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

Пример 2 — Удаление поля во время постепенного развёртывания

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

{
  "current":  { "id": "ord_812", "status": "placed", "total": 42.5 },
  "proposed": { "id": "ord_812", "status": "placed" }
}

Решение

{
  "current": { "id": "ord_812", "status": "placed", "total": 42.5 },
  "next":    { "id": "ord_812", "status": "placed", "total": 42.5,
               "amount": { "value": 42.5, "currency": "GBP" } },
  "retire":  "total, после того как старые клиенты мигрируют"
}

Постепенное развёртывание, канареечный выпуск и отстающий сервис означают, что несколько версий работают одновременно, поэтому разброс версий — постоянное состояние, а не переходный этап: изменение интерфейса либо выдерживает предыдущую версию, либо становится запланированной миграцией, что и фиксирует строка retire. Поддерживать оба имени по средствам только тогда, когда этот сервис можно развернуть отдельно, и это настоящая проверка границы, а совместный выпуск сервисов «только один раз» — вот как возвращается связанность.