Согласованность: чем платишь, когда данные лежат в двух местах
CAP как настоящий компромисс, а не лозунг; строгая и eventual-согласованность; отставание реплик и чтение собственных записей; и почему повторы заставляют каждую запись быть идемпотентной.
Прошлый урок закончился неприятной нотой. Ты добавил реплики чтения, чтобы выдержать больше трафика, и самой первой покупкой стала вторая копия правды, которая может не согласиться с первой. Этот урок — про это несогласие: когда оно случается, сколько стоит и как не дать ему дойти до пользователя в виде бага.
В тот момент, когда данные оказались на двух машинах, появляется вопрос, которого раньше не было: какая копия правильная? На одной машине ответ тривиален, потому что она одна. На двух машинах нужно решить две разные вещи. Первая: что чтению разрешено вернуть, пока запись ещё распространяется. Вторая: что делать, когда машины вообще не могут друг с другом связаться. У обоих решений есть настоящая цена, и ни одно нельзя обойти, выбрав железо получше. Именно об этом спорят под именем CAP, поэтому стоит разобраться в этом споре точно.
Ты наверняка слышал CAP как «выбери два из трёх»: Consistency, Availability, Partition tolerance. Эта версия запоминается, повторяется везде и вводит в заблуждение так, что приводит к плохим решениям. Прочитанная буквально, она намекает, что можно выбрать согласованность и доступность и просто отказаться от устойчивости к разделению. Нельзя. Разделение — это не настройка, от которой ты отказываешься; это выдернутый сетевой кабель, перезагружающийся коммутатор, насытившийся канал между датацентрами, пауза сборщика мусора, достаточно длинная, чтобы вторая машина сдалась. Если твоя система работает более чем на одной машине, соединённой сетью, разделения будут случаться, и устойчивость к разделению в меню не значится.
Заметь, чего на самом деле стоит каждая ветка, потому что ломаются они совершенно по-разному. Отказ отвечать — это громко: пользователь видит страницу с ошибкой, кнопка оплаты возвращает 503, и ты узнаёшь об этом от своего мониторинга через секунды. Расхождение копий — это тихо: каждый запрос успешен, сайт быстрый, и два пользователя смотрят на два разных баланса без единого признака, что что-то не так. Что ты хочешь, зависит от функции, а не от компании. Для банковского перевода громкий отказ очевидно правилен, потому что уверенно неверный ответ про деньги человека хуже, чем отсутствие ответа. Для счётчика лайков или просмотров тихий отказ очевидно правилен, потому что никому не плохо от того, что счётчик на две секунды отличается на единицу.
WHEN THE LINK BREAKS, PICK ONE BEHAVIOUR
refuse the write A returns 503 until the link returns
accept and diverge A returns 200, B stays stale
accept a quorum only A needs 2 of 3 nodes to answer
last write wins conflicts resolved by timestamp
the partition is never optionalЧетыре настоящих варианта во время разделения. Заметь, что три из них — вариации «отказать» или «разойтись», а не третья ось.Ещё одно уточнение важно, потому что оно меняет проектные решения каждый день. Согласованность — это не только про сбои; она стоит задержки даже когда всё здорово. Строгая согласованность означает, что запись не завершена, пока достаточно копий не подтвердят, что она произошла. Если эти копии лежат в трёх датацентрах на трёх континентах, а сетевой round trip между ними 70 мс, то запись, которую должно подтвердить большинство, платит за эту дистанцию на каждом отдельном запросе. Поэтому компромисс CAP иногда переформулируют как выбор между согласованностью и задержкой даже когда ничего не сломано: согласие требует времени, пропорционального расстоянию между копиями, а расстояние не обсуждается.
Теперь два обиходных слова. Строгая согласованность означает, что как только запись подтверждена, любое последующее чтение где угодно видит её. Нет момента, в котором корректное чтение может вернуть старое значение. Цена в том, что каждое чтение либо ждёт согласия, либо маршрутизируется через единственное место, обладающее полномочиями, а это ровно так медленно и хрупко, как звучит. Eventual-согласованность обещает обратное, и стоит сформулировать это точно, потому что обычно формулируют плохо: после того как записи прекратились, копии сходятся, и если новых записей нет, они согласятся за некоторое ограниченное время. Про то, что чтение видит в промежутке, обещаний нет. Этот промежуток — не баг модели, это и есть модель.
BANK BALANCE LIKE COUNTER
money leaves an account a stranger taps a heart
a wrong read costs money a wrong read costs nothing
must be strong eventual is fine
cost: one writer at a time cost: two users see 41 vs 42
reject a write rather than lie show 41 now, 42 in 800 msОдна и та же распределённая система, держащая оба вида данных сразу, с двумя разными обещаниями про них.Важное следствие: согласованность — это не свойство системы, это свойство данных. Одно и то же приложение может держать банковский баланс под строгой согласованностью, а счётчик лайков под eventual, и никого это не удивляет, стоит произнести это вслух. Спроси, чего стоит неверный ответ. Если устаревшее чтение баланса счёта позволит кому-то снять деньги дважды, это чтение обязано быть строгим, и за него стоит платить задержкой и доступностью. Если устаревшее чтение счётчика лайков показывает 41 вместо 42 в течение восьмисот миллисекунд, цена в том, что никто не замечает, и платить там за строгую согласованность было бы чистым расточительством. Выбор для каждой функции — это и есть всё умение, и это бизнес-суждение, выраженное техническим языком.
Между строгой и eventual есть промежуточный вариант, который решает большинство реальных жалоб, и ты встретил его в прошлом уроке: чтение собственных записей. Он слабее строгой согласованности, потому что другие пользователи могут видеть старое значение, и сильнее обычной eventual, потому что тот, кто сделал изменение, всегда видит его. Стоит он одного правила маршрутизации: чтения пользователя, который недавно писал, идут на primary. Большинству продуктов нужно ровно это и ничего больше — стоит вспомнить, когда кто-то предлагает сделать всю систему строго согласованной, чтобы починить страницу профиля.
Теперь часть, которая связывает всё вместе, и это понятие отличает работающую распределённую систему от демонстрации. Любой сетевой вызов может отказать так, что ты не знаешь, что произошло. Запрос мог потеряться по дороге туда, а мог быть обработан, а ответ потерялся на обратном пути. Для клиента эти два случая выглядят одинаково: таймаут. Поэтому клиент повторяет, и если первая попытка на самом деле прошла, повтор — это второй настоящий платёж, второе письмо, вторая отгрузка. Не повторять тоже нельзя, потому что отказ от повторов превращает любой кратковременный сбой в видимую пользователю ошибку. Это то же рассуждение, что и доставка хотя бы раз, и оно относится к любому RPC, а не только к очередям сообщений.
CREATE TABLE payments (
id bigserial PRIMARY KEY,
idem_key text NOT NULL UNIQUE,
account_id bigint NOT NULL,
amount_cents integer NOT NULL
);
-- the retry sends the same idem_key, so the second insert loses
INSERT INTO payments (idem_key, account_id, amount_cents)
VALUES ('pay-9f2c-1', 42, 1290)
ON CONFLICT (idem_key) DO NOTHING;Единственная структура, делающая повторную запись безопасной: клиент передаёт ключ, а база следит, чтобы ключ был использован один раз.Есть короткий путь, избавляющий от части этой машинерии: сделать операцию идемпотентной по построению. Присвоение значения заданному состоянию, например статус равен shipped, можно повторить сколько угодно раз с тем же результатом, и ключ ему не нужен. Прибавление к значению, например баланс равен баланс плюс 100, так нельзя, потому что повтор меняет результат. Заметь: это свойство операции, а не кода, который её вызывает. Там, где бизнес-правило позволяет, предпочитай операцию, которую безопасно повторить, а те, что нельзя, помечай явным ключом идемпотентности, чтобы небезопасный случай был осознанным и видимым решением, а не недосмотром.
Последняя трудная задача — заставить две отдельные системы меняться вместе. Внутри одной базы транзакция даёт тебе атомарность: несколько инструкций либо проходят, либо все откатываются, и ты никогда не видишь наполовину применённое состояние. Через два сервиса с двумя базами такая гарантия в том же виде недоступна. Классический протокол, двухфазная фиксация, заставляет координатора попросить каждого участника подготовиться, а затем велеть всем зафиксироваться. Он работает и имеет знаменитую слабость: координатор — единая точка отказа, и если он умрёт после фазы подготовки, участники остаются держать блокировки и ждать, иногда долго. Поэтому его применение в основном ограничено системами, которые могут позволить себе задержку и стоимость согласования.
Практическая альтернатива — принять, что промежуточные состояния будут, и проектировать с учётом них. Saga разбивает работу на локальные транзакции, по одной на сервис, каждая фиксируется сама по себе, плюс последовательность компенсирующих действий, отменяющих предыдущие шаги, когда падает последующий. Оформление заказа становится таким: зарезервировать товар, списать с карты, и если создание доставки падает — вернуть деньги и освободить товар. Заметь, от чего это честно отказывается. Есть окно, в котором деньги уже списаны, а заказ ещё не подтверждён, и пользователи увидят это окно, если не спрятать его за статусом вроде «обрабатывается». Это настоящая цена, а выгода в том, что ни один сервис не держит блокировку в ожидании другого, поэтому ни один сбой не замораживает весь поток. Большинство систем, которым это нужно, приходят к саге, потому что она падает маленькими восстановимыми кусками вместо того, чтобы застрять одним большим.
Практика
Платёжный сервис и сервис проверки на мошенничество работают в двух датацентрах. Связь между ними рвётся. Команда должна решить, что платёжный сервис делает с приходящими запросами. Какое рассуждение верное?
Подсказки
- Сеть — не один из вариантов, от которого можно отказаться.
- Спроси, чего стоит уверенно неверный ответ про деньги.
- Реальная альтернатива — либо громко отказать, либо ответить неверно.
Сопоставь каждую гарантию с обещанием, которое она на самом деле даёт, и с тем, чего она стоит.
Подсказки
- Одна из них обещает что-то только про того, кто писал.
- Сходимость — обещание про будущее, а не про следующее чтение.
- Спроси, кому разрешено видеть старое значение и как долго.
Платёжный API ниже вызывают с повтором по таймауту. Сделай его безопасным: один и тот же ключ не должен создавать второй платёж, а повторный вызов должен по-прежнему сообщать результат первого.
Код выполняется у тебя, а не у нас — мы его не запускаем. Проверь решение по каждому требованию и отметь выполненные.
Подсказки
- Хранилище помнит, что уже видело, с ключом идемпотентности в роли ключа.
- Повтор должен вернуть сохранённый результат, а не создать новую запись.
- Верни объект с id платежа и признаком, создал ли его этот вызов.
Вопросы по этому уроку
Это бесплатно?
Да. Первый уровень каждого предмета бесплатный и не требует аккаунта. Дальше уровни и старшие грейды платные — именно это позволяет проекту существовать.
На какой возраст рассчитано?
Для любознательных подростков примерно с 12 лет и для взрослых, начинающих с нуля. Опыт программирования не предполагается: уроки по сетям и физике не требуют математики сверх школьной.
Нужно ли уже уметь программировать?
Нет. Логика, физика и сети начинаются с нуля. Код ты пишешь в разделе языков — и он тоже начинается с самого начала.
Нужно что-то устанавливать?
Нет. Всё работает в браузере: теория, практика и интерактивные симуляции. Скачивать нечего и настраивать нечего.
Сколько времени занимает один предмет?
Урок — от 10 до 15 минут. Грейд «джун» это примерно 12–20 уроков, то есть несколько недель регулярных занятий, а не один вечер.
Обязательно идти по порядку?
Внутри грейда уровни открываются по очереди: каждый опирается на предыдущий. А между предметами ты свободен — начинай с логики, физики или сетей.