Двойное списание случается не потому, что кто-то ПЛОХОЙ ИНЖЕНЕР

Есть одна технология, которую большинство разработчиков начинает уважать только после первого серьёзного инцидента. Это не Kafka. Не Saga. Не распределённые транзакции.

Это идемпотентность.

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

Мой самый дорогой UUID

Я познакомился с идемпотентностью именно так. Делал сервис, который списывал деньги за подписку через внешний API.

В документации было обязательное поле:

Idempotency-Key

И комментарий примерно такой:

Обязательный. Можно передать любую случайную строку.

Ну случайную так случайную. Сгенерировал UUID. Передал его в запросе. Закрыл задачу. Через несколько дней выяснилось, что это был один из самых дорогих UUID в моей карьере.

Что произошло:

  • Пользователь нажал кнопку оплаты.
  • Запрос ушёл.
  • Ответ где-то потерялся.
  • Мой сервис решил: "Похоже, запрос не дошёл. Надо повторить."
  • Сервис честно отправил ещё один запрос.
  • Платёжная система честно обработала его ещё раз.
  • Вместо одной подписки получилось два списания.

Дальше всё по классике:

  • Саппорт.
  • Возвраты.
  • Разбор логов.
  • Поиск виноватых (нет, поиск решения, у нас же есть postmortem, но об этом в другой раз).

Retry — зло

После таких историй многие делают неправильный вывод, что retry — это зло.

Нет. Retry вообще ни при чём. Наоборот. Retry — одна из лучших вещей, которые можно сделать для распределённой системы. Сеть падает. Балансировщики рвут соединения. Ответы теряются.

Повторные запросы — это абсолютно нормальная история.

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

  • Из-за retry.
  • Из-за таймаута.
  • Из-за мобильного интернета.
  • Из-за балансировщика.
  • Из-за пользователя, который пять раз нажал кнопку «Оплатить».

Причина вообще не важна. Важно, чтобы деньги списались один раз.

Самые дорогие инциденты часто появляются не из-за плохих решений, а из-за хороших, придуманных хорошими инженерами. Кто-то хотел сделать систему надёжнее. Добавил retry. Настроил автоматические повторы. Сделал всё правильно. Кроме одной детали. Не научил систему понимать, что два одинаковых запроса — это одна и та же бизнес-операция. Иногда именно такие мелочи оказываются самыми дорогими.

Пиши в комментариях, а у тебя было?

Селькин Андрей
TGM / Go-разработчик из Fintech. Инженерные заметки: https://t.me/andrei_selkin_outbox
22
11