Репликация
Копируй данные на много узлов ради чтений, локальности и выживания — но каждая копия лжёт, пока не догонит. Лидер-фолловер, мульти-лидер и leaderless по-разному жертвуют доступностью записи ради риска тихо потерять подтверждённую запись.
Пользователь меняет адрес доставки, видит тост «Сохранено», обновляет страницу — а там старый адрес. Меняет снова. Снова старый. Поддержка не может воспроизвести. Происходящее невидимо из любой отдельной консоли: запись легла на лидера, успех вернулся, а чтение обслужил фолловер, который ещё не получил изменение. Ни бага, ни ошибки, ни исключения. Просто две копии правды, которые расходятся на пару сотен миллисекунд, и клиент, который теперь думает, что продукт сломан. Репликация — то, как ты переживаешь смерть узла и обслуживаешь чтения на масштабе, — и одновременно то место, где «я сохранил» тихо перестаёт быть правдой. К концу урока ты будешь знать, почему это происходит, когда подтверждённые записи могут исчезнуть целиком и какое единственное изменение в маршрутизации чинит баг с пропадающим адресом.
Зачем вообще копировать данные
Один узел с твоими данными — это и единая точка отказа, и единый потолок пропускной способности (ровно проблема, которой кончился урок о вертикальном масштабировании). Репликация держит одни и те же данные на нескольких узлах и покупает три разные вещи, которые опасно путать:
- Масштабирование чтений — разнести чтения по многим копиям вместо долбёжки одного узла.
- Локальность — поставить копию рядом с пользователем (реплика в Европе для европейских чтений), чтобы срезать задержку.
- Отказоустойчивость — если узел умер, другой держит данные и может взять нагрузку.
Каждый из этих выигрышей реален — но все трое поставляются с одной скрытой ценой: как только копий больше одной, они могут расходиться, и системе нужно решить, которой верить.
Подвох — тот, что показал hook: как только копий больше одной, они могут расходиться. Поэтому репликация — не фича, которую включают; это модель консистентности, которую выбирают, и выбор — какое расхождение ты терпишь.
Лидер-фолловер (по умолчанию)
Самая частая топология — лидер-фолловер (он же primary-replica, master-slave). Один узел — лидер: все записи идут к нему. Лидер транслирует свои изменения — обычно лог репликации каждой записи — одному или нескольким фолловерам, которые применяют их по порядку. Чтения обслуживает лидер или любой фолловер.
Репликация бывает синхронной или асинхронной, и этот единственный переключатель — сердце урока:
- Синхронная: лидер ждёт подтверждения от фолловера перед тем, как подтвердить запись клиенту. Фолловер гарантированно актуален — но если он медленный или упал, запись блокируется. Сделай всех фолловеров синхронными — и одна медленная реплика тормозит каждую запись.
- Асинхронная: лидер подтверждает запись немедленно и шлёт её фолловерам в фоне. Быстро и доступно — но фолловеры отстают, и запись, которую лидер подтвердил, может ещё нигде больше не существовать.
Большинство продакшн-систем работает полусинхронно: один синхронный фолловер (чтобы подтверждённая запись жила минимум на двух узлах) и остальные асинхронные (чтобы чтения масштабировались, не блокируя записи).
Лаг репликации и read-your-writes
Промежуток между «у лидера есть запись» и «у этого фолловера есть запись» — это лаг репликации. Под нагрузкой или через регион он колеблется от долей миллисекунды до секунд, а при долгой операции или сетевом сбое раздувается до минут. Лаг — не баг; это цена асинхронной репликации.
Лаг порождает семейство аномалий, и адрес из hook — самая частая: read-your-writes (чтение-после-записи — гарантия, что пользователь сразу видит своё изменение). Пользователь делает запись, тут же читает — и попадает на отстающий фолловер, который её не применил, так что собственное изменение будто пропало. Все починки — про маршрутизацию, а не про ускорение репликации:
- Читать собственные недавно записанные данные пользователя с лидера короткое окно после его записи.
- Запоминать позицию записи в логе (токен версии) и маршрутизировать чтение на фолловер, который её догнал.
- Прибить пользователя к одной реплике (с теми компромиссами, что всегда несут sticky-сессии).
▸Почему это работает
Почему не сделать всю репликацию синхронной и закрыть вопрос? Потому что синхронная репликация связывает доступность записи с твоей самой медленной репликой. С одним sync-фолловером записи нужны два узла; с лидером плюс N sync-фолловерами — все N+1, так что добавление реплик ради надёжности снизило бы доступность, обратное желаемому. Это напряжение следующие два урока формализуют как CAP: нельзя иметь одновременно быстрые записи, строгую консистентность чтений и терпимость к отказу реплики. Полусинхрон (один sync, остальные async) — прагматичная середина: он ограничивает риск потери до «потеряли лидера и его один незавершённый sync-ack», не держа каждую запись в заложниках у каждой реплики.
Failover и пропадающие записи
Когда лидер умирает, система должна сделать failover: повысить фолловера до нового лидера. Именно здесь асинхронная репликация может тихо потерять данные. Допустим, лидер подтвердил записи 98, 99 и 100, но успел отправить фолловерам только до 97, прежде чем упал. Последняя запись повышенного фолловера — 97. Записи 98–100 были подтверждены клиентам — пользователи видели успех — и теперь их просто не существует. Хуже: если старый лидер вернётся и присоединится как фолловер, эти записи могут быть на нём, но конфликтовать с новыми, принятыми кластером тем временем; обычное разрешение — выбросить их.
Вот режим отказа, который надо выжечь в памяти: при async-репликации подтверждённая запись — не долговечная запись. Клиенту сказали «сохранено» ещё до того, как данные оказались где-либо кроме лидера, и сбой в этом окне стирает их без следа. Известный сбой GitHub 2012 года и многие другие сводятся ровно к этому — к failover, потерявшему или воскресившему записи. Меры — те же рычаги долговечности: хотя бы одна синхронная реплика, чтобы ack означал «на двух узлах», и аккуратный failover-инструментарий, который отказывается повышать слишком отставшего фолловера или огораживает старого лидера, чтобы тот не писал после замены.
▸Частая ошибка
Тонкий и опасный баг failover — split-brain: сеть распадается на партиции, кластер ошибочно решает, что лидер мёртв, и повышает фолловера, но старый лидер жив и продолжает принимать записи на своей стороне партиции. Теперь два лидера, оба принимают конфликтующие записи, и когда партиция заживёт, у тебя две разошедшиеся истории для примирения — часто одну выбрасывают. Защита — fencing (повышенный узел заставляет старого лидера уйти, или STONITH — «выстрели другому узлу в голову») и кворумное повышение (фолловер становится лидером только при согласии большинства), что уроки leader-election и Raft в треке distributed разворачивают целиком. Мысль уровня композиции: никогда не давай «повысить нового лидера» произойти без механизма, гарантирующего, что старый больше не пишет.
Платёжный сервис хранит записи транзакций. Требования: подтверждённая запись должна пережить сбой одного узла (надёжность), задержка записи должна оставаться ниже 20 мс на p99, сервис должен переносить недоступность одной реплики без блокировки записей. Какой режим репликации подходит?
Мульти-лидер и leaderless
Две другие топологии отдают единственного лидера:
Мульти-лидер разрешает записи на более чем одном узле (например, один лидер на регион или клиент, работающий офлайн). Он покупает локальность записи и офлайн-записи, но цена — конфликты записи: два лидера принимают конфликтующие обновления одной записи, и их надо разрешать — last-write-wins (тихо роняет данные), векторы версий или прикладные слияния (CRDT). Мульти-лидер не убирает проблему консистентности; он переносит её с «устаревших чтений» на «конфликтующие записи».
Leaderless (модель Dynamo) вообще без лидера: клиент пишет на несколько узлов и читает с нескольких, а корректность даёт кворум. С N репликами требуй W подтверждений на запись и R ответов на чтение; если R + W > N, множества чтения и записи обязаны пересечься хотя бы в одном узле, так что чтение гарантированно видит последнюю подтверждённую запись. Крути W = N ради надёжности при многих чтениях или W = 1 ради доступности записи; неравенство пересечения — это ручка. Это мост к следующим двум урокам — leaderless-кворумы держат Dynamo-подобные хранилища доступными при партициях ценой не строгой, а eventual-консистентности, а механика кворума получает полный разбор в юните quorum трека distributed.
Пользователь обновляет профиль, видит «Сохранено», а при самой следующей загрузке показано старое значение. Ошибок нигде нет. Причина и правильная починка?
Твоя async-реплицируемая база подтверждает запись #100 клиенту, затем лидер падает, не отправив #98–100 фолловерам. Повышается фолловер на #97. Что стало с #98–100?
В leaderless-хранилище с N репликами чтение гарантированно видит последнюю подтверждённую запись, когда кворум чтения R и кворум записи W удовлетворяют R + W _______ N — потому что тогда множество узлов, куда ты писал, и множество, откуда читаешь, обязаны разделить хотя бы один узел.
- 01Сравни синхронную и асинхронную лидер-фолловер репликацию и что даёт полусинхрон.
- 02Что такое read-your-writes, почему лаг репликации его ломает и как чинить?
- 03Как async-репликация тихо теряет подтверждённые записи при failover и что защищает?
Репликация держит одни данные на нескольких узлах, чтобы масштабировать чтения, ставить данные рядом с пользователями и переживать смерть узла, — но превращает проблему консистентности в выбор дизайна. В лидер-фолловере все записи бьют в одного лидера, транслирующего лог фолловерам; запись синхронна (долговечна на ≥2 узлах, но блокируется самой медленной репликой) или асинхронна (быстро и доступно, но лидер может подтвердить запись, которой ещё нигде нет), с полусинхроном как обычной серединой. Лаг между лидером и фолловером ломает read-your-writes — пропадающий адрес из hook, — что чинится маршрутизацией собственных недавних чтений на актуальный узел. Главный режим отказа: при async-репликации подтверждённая запись — не долговечная запись, так что сбой до отправки записи и затем failover теряет данные, которые клиент видел успешными; защищайся синхронной репликой и fencing/кворумным повышением против split-brain (два лидера). Мульти-лидер меняет устаревшие чтения на конфликты записи; leaderless (Dynamo) использует кворум, где R + W > N гарантирует пересечение множеств чтения и записи. Механика кворума и выбор лидера получают полный вывод в треке distributed — этот урок остаётся на высоте композиции какое расхождение ты терпишь. Теперь, когда увидишь жалобу «изменение пропало», первый вопрос — попало ли чтение на отстающий фолловер, и ты будешь знать, какой рычаг тянуть.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.