Репликация, шардирование и консистентность: что происходит с данными, когда один сервер уже не справляется, — и какие компромиссы от вас ждут на собеседовании и в проде.
Листайте стрелками ← →, свайпом или кнопками внизу. В схемах нажимайте кнопки и двигайте ползунки. Предыдущие части: подход, масштабирование.
Сначала вопросы: как читаем (по ключу, по диапазону, поиском, агрегатом), сколько пишем, нужны ли транзакции между сущностями. Ответ определяет класс хранилища, а не мода.
Подробно о каждой технологии — в презентации «Бэкенд-стек». На собеседовании назовите паттерн, выбор и цену: «Cassandra, потому что 200 тыс. записей в секунду и чтение только по chat_id; платим отсутствием join и транзакций».
Выберите паттерн доступа.
ANY 1 (r1, r2) в Postgres, semi-sync в MySQL).Выберите режим и где стоит r2. RTT: та же зона ~0,3 мс, соседняя зона ~1,5 мс, другой регион 60–90 мс.
Асинхронная реплика отстаёт на миллисекунды, а под нагрузкой или во время долгого запроса — на секунды. Пользователь сохраняет профиль, страница перечитывает его с реплики — и показывает старое.
Сохраните имя и сразу откройте профиль. Переключайте стратегии чтения.
Лидер перестал отвечать: умер или просто отрезан сетью? Снаружи не отличить. Если назначить нового лидера, а старый жив, — лидеров два, и оба принимают запись.
Разрежьте сеть, запишите с обеих сторон и восстановите связь — с защитой и без.
В Dynamo-подобных базах (Cassandra, ScyllaDB, Riak) пишет любой узел: координатор шлёт запись всем N репликам и ждёт W ответов, чтение опрашивает R реплик и берёт самую свежую версию.
W + R > N — множества записи и чтения пересекаются, чтение увидит последнюю подтверждённую запись.N − W узлов, чтение — N − R.Двигайте N, W, R, нажимайте на узлы, чтобы ронять их, пишите и читайте.
Несколько лидеров (по одному в каждом ДЦ, офлайн-клиенты, совместное редактирование) или кворумы без лидера — значит, две записи одного ключа могут случиться одновременно. Кто-то должен их слить.
Корзина {молоко}. В Европе удаляют молоко и кладут книгу, в США одновременно кладут чайник. Выберите стратегию и синхронизируйте.
Чем выше ступень, тем меньше странностей видит клиент — и тем больше координации, задержки и недоступности при сбоях. Требование выбирают для каждой операции, а не для всей системы.
CAP говорит одно: при разрыве сети (P) узел, который не видит остальных, либо отвечает и рискует отдать старые данные (A), либо отказывает (C). «Выбрать два из трёх» — неверная формулировка: разрывы не выбирают.
PACELC добавляет главное: Else, без разрыва, выбор между задержкой (L) и консистентностью (C). Это цена каждого запроса, а не только аварии.
Три реплики: две в ДЦ A, одна в ДЦ B. Разрежьте сеть, попробуйте писать и читать в CP- и AP-режиме; посмотрите, как RTT между ДЦ влияет на задержку.
Cassandra, коэффициент репликации 3, запись и чтение с CONSISTENCY QUORUM. Одна из трёх реплик ключа упала. Что будет?
UnavailableException. С ONE система переживёт и это, но чтение сможет вернуть устаревшее значение.key#0..key#7 — и кэш перед базой.Выберите нагрузку и способ разбиения. Красная черта — потолок одного шарда, ~12 тыс. запросов в секунду.
Таблица шардирована по user_id, а искать нужно по городу. Индекс по городу можно хранить двумя способами.
shard = hash(key) % N — при переходе с N на N + 1 шардов меняет адрес почти каждый ключ: переезжает доля N/(N + 1) данных. Консистентное хеширование кладёт шарды и ключи на кольцо, ключ принадлежит следующему шарду по часовой — и при добавлении шарда переезжает только ~1/(N + 1).
Несколько точек на шард (виртуальные узлы) выравнивают нагрузку. Тот же приём — в вероятностных структурах. Добавляйте и убирайте шарды в разных режимах.
partition = hash(key) % partitions. Добавили партиции — ключи переехали, порядок сообщений по ключу нарушился.
# Redis Cluster: слот = CRC16(key) mod 16384
CLUSTER KEYSLOT user:42 → 15880
CLUSTER KEYSLOT {user:42}:cart → 15880 # тот же слот
# multi-key операции только в одном слоте
# MongoDB: ключ шардирования и чанки
sh.shardCollection("app.msgs", { chat_id: "hashed" })
sh.status() # какие чанки на каком шарде
Ключ шардирования выбирают по самому частому запросу: он должен попадать в один шард. Менять его потом — значит переливать все данные.
Мессенджер. Таблица сообщений шардирована по хешу message_id. Самый частый запрос — последние 50 сообщений чата. Что не так?
(channel_id, bucket), где bucket — окно времени, чтобы огромные чаты не превращались в бесконечную партицию. По времени — все записи в один последний шард.Запрос и транзакция внутри шарда — дешёвые. Через шарды — нужен двухфазный коммит: координатор спрашивает всех «готовы?» (PREPARE), и только если все сказали «да», рассылает COMMIT.
PlaceOrderuuidv7() в Postgres 18).bigint, растёт со временем. Нужно раздать узлам номера и следить за часами.Генерируйте ID и смотрите, из чего он состоит. Попробуйте перевести часы назад.
Первичный ключ — B-дерево. Растущие ключи всегда попадают в самую правую страницу: она горячая и лежит в памяти. Случайные ключи попадают в случайную страницу.
Вставляйте ключи в индекс. Кэш вмещает 12 страниц.
Таблица на 500 млн строк, первичный ключ — UUIDv4. Индекс ключа 40 ГБ, RAM 32 ГБ. Вставки становятся всё медленнее, процессор при этом свободен. Почему?
bigint из Snowflake вставки идут в правый край индекса, который всегда горячий. Коллизии UUIDv4 на таком объёме практически невозможны.
-- Postgres: лаг реплик на лидере
SELECT client_addr, state, sync_state, replay_lag
FROM pg_stat_replication;
-- на реплике: до какой позиции дошли
SELECT pg_last_wal_replay_lsn(),
now() - pg_last_xact_replay_timestamp() AS lag;
# Cassandra: узлы, токены, где лежит ключ
nodetool status
nodetool getendpoints app messages 'chat:42'
# Redis Cluster: слоты и баланс
redis-cli --cluster check 10.0.0.1:6379
# Kafka: отставание потребителей (модели чтения)
kafka-consumer-groups.sh --describe --group projections
W + R > N.Распределённые данные — это выбор, что именно вы готовы увидеть устаревшим.