Не всё должно быть таблицей

Какие гарантии действительно нужны

30 мин
Чему научишься
  • объяснять, зачем данные держат сразу на нескольких серверах;
  • видеть, почему копия может отвечать устаревшими данными;
  • решать, что важнее в конкретной задаче: свежий ответ или ответ вообще.

В соседней стойке, всего в полуметре от кэша «Котомаркета», стоит его двойник: такой же сервер с теми же данными. Станция подаёт питание, и оба загораются одновременно.

Комендант заметно успокаивается: «Раз есть второй — значит, данные в безопасности. Можно читать с любого?»

КВЕРИ: С любого — можно. А вот ответы иногда будут разными. Сейчас увидишь.

Две одинаковые серверные стойки рядом, между ними тянется тонкая светящаяся нить; курсант со спины смотрит то на одну, то на другую, робот-кот сидит на полу между ними
Два сервера с одними данными. Между ними — нить, по которой данные идут не мгновенно.

Узел и копия

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

Второй узел получает изменения первого и старается хранить то же состояние. Такую постоянно обновляемую копию по-английски называют (replica). Она похожа на второй экран с теми же рабочими данными: на основном узле что-то меняется, и изменение отправляется дальше.

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

В Redis новые данные записывают на основной узел, а тот пересылает изменения копии. Оба узла здесь — Redis: основная база магазина на PostgreSQL в этом опыте не участвует.

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

Копии на других узлах встречаются и за пределами Redis. Например, такой механизм есть у PostgreSQL и MongoDB. В работе состояние узлов смотрят в настройках серверов и панелях наблюдения: там проверяют, какой узел основной и успевают ли остальные получать изменения.

В ячейке ниже копия отстаёт ровно на две секунды. Ключ city:42 хранит город доставки покупателя 42. Срок жизни ему не нужен, а название города — одно слово, поэтому двойные кавычки тоже не нужны.

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

Обычная строка с cli обращается к основному узлу. Через запятую после команды учебной консоли можно передать пометку node='copy': тогда команда уйдёт копии, а в выводе её ответы идут после «копия>». В настоящей работе к копии подключаются отдельно — запускают redis-cli с её адресом.

Основной узел уже знает новое значение, а копия пока отвечает старым.
python · redis

Сначала основной узел отвечает «Казань». Это ожидаемо: именно туда только что записали новое значение.

Копия в тот же момент отвечает «Новосибирск». Она не потеряла Казань и не решила спорить с основным узлом. Изменение ещё не успело до неё дойти, поэтому копия отвечает последним значением, которое уже получила.

Проходит две секунды. Теперь копия тоже отвечает «Казань». Значит, отставание закончилось: оба узла снова хранят одинаковое значение.

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

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

Реплика живёт вместе с системой и догоняет основной узел; резервный снимок хранит сохранённое состояние.
Пока изменение в пути, два узла отвечают по-разному.

Когда связь обрывается

Задержка сама по себе заканчивается: изменение едет и рано или поздно доходит. Гораздо сложнее случай, когда дорога между узлами исчезает совсем.

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

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

cut_link() — учебная функция курса: в настоящем Redis её нет, она изображает обрыв сети. С этого момента копия ничего нового не получает. Причём заранее неизвестно, когда связь восстановится.

Опыт с начала: Новосибирск, пять секунд — копия успевает его получить, — обрыв связи и только потом Казань.
python · redis
Связи нет — копия отвечает тем, что успела получить.

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

Если страница читает именно с копии, остаётся два варианта.

  • Ответить тем, что есть. Страница откроется, но покупатель увидит старый город. Хранилище доступно, хотя ответ сейчас неверный.
  • Отказаться отвечать, пока связь не вернётся. Старого города покупатель не увидит, зато не увидит и саму страницу.

Решение зависит от задачи. Для корзины или рекомендаций старый ответ обычно терпим: страницу лучше показать, даже если часть информации немного отстаёт. При оплате разумнее временно остановиться, чем принимать решение по старому остатку.

Есть ещё один выбор, который касается момента записи. Система может ответить сразу после того, как новое значение принял основной узел. Тогда пользователь быстрее получает подтверждение, но в этот момент новое значение может существовать только там.

Можно сначала дождаться, пока изменение примет и копия, и только потом считать запись законченной. Уверенности больше: новое значение уже хранится ещё в одном месте. Цена понятная — ответ на запись придётся ждать дольше.

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

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

Во время разделения сети узлы уже не могут одновременно сверить новое состояние. Тогда приходится решить, что важнее для конкретного запроса: продолжать отвечать доступными данными или отказаться от ответа, если нельзя подтвердить их свежесть. К CAP мы вернёмся в главе 7, когда узлов станет шесть.

КВЕРИ: Второй сервер страхует первый. Заодно появляется новый вопрос: если они отвечают по-разному, кому верить?

🧪 Модель: в настоящей системе иначе

В учебном Redis копия отстаёт ровно на заданное число секунд, а связь рвётся одной строкой. В настоящей системе задержка всё время меняется, а сеть может пропасть без предупреждения.

Поэтому в работе смотрят не только на сам факт наличия копии. Нужно ещё видеть, получает ли она свежие изменения и насколько успела отстать. Иначе второй сервер формально работает, но отвечает состоянием, которое основной узел уже давно изменил.

Проверь себя
Покупатель сменил адрес доставки и сразу открыл страницу заказа. Страница читает данные с копии. Что он может увидеть?
Практика: напиши код
Покупатель 56 переезжает в Екатеринбург. Запиши новый город на основной узел (ключ city:56) и дождись, пока его увидит копия: в конце ячейки копия должна отвечать «Екатеринбург». В этом задании связь между узлами работает, а копия отстаёт на 3 секунды.
python · redis

ТерминыЧто значат новые слова

словообычными словами
узелодин сервер, на котором работает хранилище
копия ()второй рабочий узел, который получает изменения основного и старается хранить то же состояние
задержка копиивремя между изменением на основном узле и появлением этого изменения на копии
разделение сетиузлы работают, но связь между ними нарушена и свежие изменения не могут перейти из одной части системы в другую
CAPправило для системы из нескольких узлов: при разделении сети нельзя одновременно гарантировать самый свежий ответ и ответ на каждый запрос
Главное из урока
  • Данные держат на нескольких узлах, чтобы поломка одного сервера не оставила магазин без работающей копии данных.
  • Копия в этом уроке постоянно получает изменения основного узла. Это рабочая часть системы, а не сохранённый отдельно старого состояния.
  • Изменения приходят на копию с задержкой. Пока новое значение в пути, копия отвечает предыдущим.
  • Если связь между узлами пропала, оба сервера могут продолжать работать, но перестают обмениваться свежими изменениями.
  • Во время такого разрыва приходится выбирать: показать доступные, но возможно устаревшие данные или временно не отвечать. Что лучше, зависит от задачи.
  • Запись тоже можно подтверждать по-разному: ответить после основного узла или дождаться ещё и копии. Во втором случае ответ приходит позже.

Теперь видно, почему наличие второго сервера само по себе ещё не отвечает на вопрос «данные точно свежие?». Комендант ждёт опись галереи — пора сделать первую ставку.