Ctrl K
Урок

Шардирование и CAP

Интуиция: один архив перестал справляться

Представь районный архив, где все карточки жителей хранятся в одном шкафу у одного архивариуса. Пока людей тысяча, всё отлично. Когда их миллион, к окошку тянется очередь, а шкаф уже не закрывается. Выход очевиден: поставить несколько шкафов с отдельными архивариусами и на входе повесить табличку «А–К — в первый, Л–Р — во второй, С–Я — в третий».

Это и есть шардирование. Мы делим данные на части (шарды) и раскладываем их по разным серверам. Каждый шард хранит только свою долю данных и обслуживает только свою долю нагрузки.

Когда одной базы уже мало

Сначала пробуют дешёвые средства: индексы, кэш, реплики для чтения. Шардировать стоит, когда упираешься в запись или в объём. Реплики тут не помогут: каждая реплика всё равно получает все записи и хранит все данные.

Параметр Нужно системе Тянет один узел Вывод
Записи в секунду 50 000 ~10 000 минимум 5 шардов
Объём данных 20 ТБ ~2–4 ТБ комфортно 5–10 шардов
С запасом ×2 на рост — — ~10–16 шардов

На собесе проговаривай вслух: «Реплики масштабируют чтение, шарды масштабируют запись и объём». Одна такая фраза сразу показывает, что ты понимаешь разницу.

Как выглядит система

HTTPSзапрос с user_idгде лежит ключ?КлиентклиентAPI-сервиссервисРоутер шардов (shard_key → шард)шлюзКарта шардов (config)кэшШард 1: user_id hash 0–33%БДШард 2: user_id hash 34–66%БДШард 3: user_id hash 67–100%БД
Роутер по ключу шардирования выбирает нужный шард

Роутер знает карту шардов: какой диапазон ключей на каком сервере. Роль роутера может играть библиотека в коде, отдельный прокси (Vitess, mongos) или сама распределённая БД (Cassandra, CockroachDB).

Три способа разложить данные

Способ Как работает Плюсы Минусы
По диапазону user_id 1–1M → шард 1, 1M–2M → шард 2 удобные запросы по диапазону новые ключи бьют в последний шард
По хэшу hash(key) % N равномерное распределение при смене N переезжает почти всё
Consistent hashing ключи и узлы лежат на кольце, ключ уходит к ближайшему узлу по часовой при добавлении узла переезжает ~1/N данных сложнее, нужны виртуальные узлы

Ключ шардирования — самое важное решение. Хороший ключ равномерно распределяет нагрузку и почти всегда есть в запросе. Для мессенджера подойдёт chat_id: все сообщения чата окажутся на одном шарде.

Перешардирование: почему % N больно

Было 3 шарда, стало 4. При hash % N ключ остаётся на месте, только если h % 3 == h % 4. Таких ключей около 25%, остальные ~75% данных переезжают. Это гигабайты трафика и часы деградации.

00
11
22
33
44
55
66
77
88
99
1010
1111
hash от 0 до 11: при N=3 и N=4 шард совпадает только у 0, 1, 2 — 3 ключа из 12, 25%

Consistent hashing решает проблему: новый узел встаёт на кольцо и забирает часть ключей только у соседа. Переезжает примерно 1/4 данных вместо 3/4. Чтобы узлы не получили неравные куски кольца, каждый физический сервер ставят на кольцо 100–200 раз. Это виртуальные узлы.

Горячие шарды

Даже при идеальном хэше бывает горячий ключ: аккаунт знаменитости или товар из рекламы. Весь его трафик идёт в один шард. Что можно сделать:

  • кэш перед шардом для горячих чтений;
  • соль в ключе: celebrity_id#0..9 раскладывает записи по 10 шардам, а при чтении их приходится собирать;
  • вынести горячий ключ на выделенный шард.

CAP простыми словами

В распределённой системе рано или поздно рвётся сеть между узлами. Это P (partition), сетевое разделение. От него не уйти. Пока связи нет, у системы два варианта:

  • C (consistency): отказать в запросе, но никогда не отдавать устаревшие данные. Это CP: банковский счёт, бронирование мест.
  • A (availability): ответить, даже если данные могут быть устаревшими. Это AP: лайки, лента, корзина.

То есть CAP — это не «выбери 2 из 3», а «при разделении сети выбери C или A».

PACELC дополняет картину: if Partition — A or C, Else — Latency or Consistency. Даже когда сеть исправна, приходится выбирать. Можно ждать подтверждения от всех реплик и получить согласованность ценой задержки. Можно ответить сразу и получить скорость ценой возможной рассинхронизации.

Система При разделении В норме
Cassandra, DynamoDB A L (настраивается)
Spanner, CockroachDB C C
PostgreSQL + асинхронная реплика — L

Согласованность в конечном счёте

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

Чтобы пользователь не путался, применяют приёмы:

  • read-your-writes: свои изменения читаешь с лидера;
  • кворумы: W + R > N (например, N=3, W=2, R=2) дают свежее чтение.

Как это звучит на собесе

«Запись упирается в один узел: 50k RPS при потолке около 10k. Реплики тут не помогут, поэтому шардирую по user_id через consistent hashing с виртуальными узлами. Так добавление шарда перемещает примерно 1/N данных. Для горячих ключей ставлю кэш и соль. Ленте хватит eventual consistency, это AP. Платежи держу в CP-хранилище».

Обязательно назови компромиссы: кросс-шардовые JOIN и транзакции становятся дорогими, глобальные запросы превращаются в scatter-gather, а вся схема держится на удачном выборе ключа.

Частые ошибки

  • Шардировать сразу, не попробовав индексы, кэш и реплики.
  • Выбрать ключ, которого нет в большинстве запросов: каждый запрос тогда идёт во все шарды.
  • Говорить «CA-система». Без P распределённой системы не бывает.
  • Использовать hash % N и забыть про перешардирование.
  • Не упомянуть горячие шарды. Интервьюер почти наверняка про них спросит.

Ты только что разобрал тему, на которой валятся многие кандидаты. Проговори пример ответа вслух пару раз, и он станет твоим.