Идемпотентность и гарантии
Самый взрослый блок темы. Здесь довольно чётко отделяют тех, кто разбирал реальные инциденты с двойными списаниями, от тех, кто читал главу про надёжность.
Стержень: сеть ненадёжна, ответ теряется, повтор неизбежен. Всё проектирование крутится вокруг одной задачи - сделать повтор безопасным, а не запретить его.
// Формулировки: «что такое идемпотентность?», «клиент получил таймаут - что дальше?», «как обеспечить доставку ровно один раз?»
Ключ идемпотентности
Операция идемпотентна, если её повтор с теми же данными оставляет систему в том же состоянии, что и однократное выполнение. Чтение идемпотентно само по себе, а вот создание платежа - нет, и его такой делают специально.
Механика простая. Клиент до первой попытки генерирует уникальный идентификатор операции и присылает его в заголовке. Сервер выполняет операцию и запоминает результат вместе с этим ключом. Пришёл повтор с тем же ключом - сервер не создаёт второй платёж, а отдаёт сохранённый ответ.
Определять дубли по совпадению суммы и получателя нельзя, хотя соблазн велик. Два одинаковых перевода одному человеку в один день - совершенно законная ситуация: перевёл тысячу, потом вспомнил и перевёл ещё тысячу. Отличить это от повтора по содержимому невозможно в принципе.
// Ключ генерирует именно клиент, а не сервер, и это принципиально. Только клиент знает, что вот эта попытка - повтор той же самой операции, а не новая. Сервер такого знания не имеет и получить не может.
Гарантии доставки
Гарантия «минимум один раз» означает, что сообщение не потеряется, но может прийти дважды: если подтверждение доставки не получено, отправитель шлёт повторно. Разбираться с дублями обязан получатель.
Гарантию «ровно один раз» на практике строят как «минимум один раз» плюс отсев повторов на приёме. И причина, по которой её не делают напрямую, вполне фундаментальна: отправитель не может отличить потерю самого сообщения от потери подтверждения о нём. Он видит одно и то же - тишину. Значит, выбирать приходится между риском потери и риском дубля, третьего варианта нет.
Практика почти всегда выбирает дубль как меньшее зло: потерянный платёж хуже задвоенного, потому что задвоенный виден и лечится, а потерянный не виден никому.
// Отсюда требование к получателю, и его надо записывать в контракт явно: либо обработка идемпотентна сама по себе, либо получатель хранит идентификаторы уже обработанных сообщений и молча отбрасывает повторы.
Повторы, размыкатель, компенсации
Повторять надо с растущей паузой и случайной добавкой к ней, ограничив число попыток. Ретраи каждые сто миллисекунд наращивают нагрузку ровно в тот момент, когда сервису и так плохо, и мешают ему подняться: он не успевает разгрести очередь, потому что клиенты добавляют новые запросы быстрее.
Случайная добавка нужна отдельно. Без неё тысяча клиентов, одновременно получивших ошибку, подождёт одну и ту же секунду и ударит синхронно - и так на каждой итерации.
Размыкатель цепи работает на уровень выше: накопив долю ошибок, он вообще перестаёт слать запросы и сразу отвечает отказом, не тратя время на ожидание таймаута. Это спасает вызывающую сторону от исчерпания потоков и каскадного падения, когда одна упавшая зависимость утаскивает за собой полсистемы.
// Согласованность между системами без распределённых транзакций обеспечивают сагой: это цепочка шагов, у каждого из которых есть своя компенсация. Не удалось начислить бонусы - возвращаем списанные деньги. Компенсацию описывает бизнес, и обязательно для каждого шага: шаг без компенсации превращает сагу в наполовину выполненную операцию.
Как отвечать: «Клиент отправил платёж и получил таймаут. Что должно быть в контракте?»
Метод проверки статуса операции по ключу клиента. Таймаут не означает провал: операция могла спокойно выполниться, а потеряться мог только ответ на обратном пути - и клиент этих двух случаев не различает, он видит одну и ту же тишину. Без способа спросить о судьбе операции ему остаётся либо гадать, либо повторять и рисковать двойным списанием. Поэтому в контракте нужны две вещи: ключ идемпотентности, по которому повтор безопасен и вернёт прежний результат, и метод запроса статуса, отвечающий однозначно - выполнена, отклонена или ещё в обработке. Автоматический повтор на стороне сервера тут не помогает вообще: сервер не знает, дошёл ли до клиента его ответ.
Кандидат разбирает саму природу неопределённости после таймаута и закрывает её двумя конкретными элементами контракта. Последняя фраза снимает популярное возражение «а пусть сервер сам повторит».
На чём валятся
- −− Не дают клиенту метода проверки статуса по его ключу и оставляют его гадать после таймаута.
- −− Ищут дубли по сумме и получателю, хотя два одинаковых платежа бывают законными.
- −− Повторяют запросы без паузы и лимита попыток, продлевая чужой сбой.
- −− Ставят распределённую транзакцию между системами вместо саги с компенсациями.
- −− Описывают шаги саги, но забывают компенсацию хотя бы для одного из них.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 13, остальные разбираются в тренажёре.
- Что означает гарантия доставки «хотя бы один раз»?A)Сообщение придёт ровно однаждыB)Сообщение может продублироватьсяC)Сообщение может потерятьсяD)Порядок сообщений сохраняется
показать ответ и разбор
+B)Сообщение может продублироваться// разбор: At-least-once означает, что потери исключены, но при неподтверждённой доставке сообщение отправят повторно. Дубликаты неизбежны, и разбираться с ними должен получатель — обычно через идемпотентную обработку или хранение уже обработанных идентификаторов.
- Внешний сервис отвечает медленно и с ошибками. Клиент повторяет запрос каждые 100 мс. Чем это опасно?A)Повторы исказят данные в ответеB)Повторы нарушат формат запросаC)Повторы добьют сервис и продлят сбойD)Повторы приведут к смене кода ответа
показать ответ и разбор
+C)Повторы добьют сервис и продлят сбой// разбор: Агрессивные повторы наращивают нагрузку ровно тогда, когда сервису плохо, и мешают ему подняться. Правильная схема — пауза с ростом интервала и случайной добавкой, ограниченное число попыток и размыкатель, который перестаёт долбить заведомо больной сервис и быстро отдаёт отказ.
- Что делает размыкатель цепи (circuit breaker)?A)Шифрует канал между сервисамиB)Распределяет нагрузку между узламиC)Ограничивает размер сообщенияD)Перестаёт слать запросы к сбойному сервису
показать ответ и разбор
+D)Перестаёт слать запросы к сбойному сервису// разбор: Накопив долю ошибок, размыкатель переходит в открытое состояние и сразу отвечает отказом, не тратя время на заведомо провальные вызовы. Это спасает вызывающую сторону от исчерпания потоков и каскадного падения. Через паузу он пробует пропустить единичный запрос и при успехе возвращается к работе.
- Списание в одной системе прошло, начисление в другой упало. Как это лечат без распределённых транзакций?A)Общей транзакцией на две базыB)Компенсирующей операциейC)Блокировкой обеих систем на времяD)Ежедневной ручной сверкой
показать ответ и разбор
+B)Компенсирующей операцией// разбор: Распределённая транзакция через две системы дорога и хрупка, поэтому применяют сагу: последовательность шагов, у каждого из которых есть компенсация. Не удалось начислить — выполняется возврат списанного. Бизнес обязан описать компенсацию для каждого шага, иначе система застревает в промежуточном состоянии.
- Клиент отправил платёж, получил таймаут и не знает, прошёл ли он. Что должен предусмотреть контракт?A)Автоматический повтор на стороне сервераB)Увеличенный таймаут для платёжных операцийC)Метод проверки статуса по ключу операцииD)Уведомление на почту о результате
показать ответ и разбор
+C)Метод проверки статуса по ключу операции// разбор: Таймаут не означает провал: операция могла выполниться, а потеряться мог ответ. Клиенту нужен способ спросить о судьбе операции по своему ключу и получить однозначный ответ — выполнена, отклонена или в обработке. Без такого метода остаётся гадать либо рисковать повторным списанием.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.