Шардирование и CAP
Интуиция: один архив перестал справляться
Представь районный архив, где все карточки жителей хранятся в одном шкафу у одного архивариуса. Пока людей тысяча, всё отлично. Когда их миллион, к окошку тянется очередь, а шкаф уже не закрывается. Выход очевиден: поставить несколько шкафов с отдельными архивариусами и на входе повесить табличку «А–К — в первый, Л–Р — во второй, С–Я — в третий».
Это и есть шардирование. Мы делим данные на части (шарды) и раскладываем их по разным серверам. Каждый шард хранит только свою долю данных и обслуживает только свою долю нагрузки.
Когда одной базы уже мало
Сначала пробуют дешёвые средства: индексы, кэш, реплики для чтения. Шардировать стоит, когда упираешься в запись или в объём. Реплики тут не помогут: каждая реплика всё равно получает все записи и хранит все данные.
| Параметр | Нужно системе | Тянет один узел | Вывод |
|---|---|---|---|
| Записи в секунду | 50 000 | ~10 000 | минимум 5 шардов |
| Объём данных | 20 ТБ | ~2–4 ТБ комфортно | 5–10 шардов |
| С запасом ×2 на рост | — | — | ~10–16 шардов |
На собесе проговаривай вслух: «Реплики масштабируют чтение, шарды масштабируют запись и объём». Одна такая фраза сразу показывает, что ты понимаешь разницу.
Как выглядит система
Роутер знает карту шардов: какой диапазон ключей на каком сервере. Роль роутера может играть библиотека в коде, отдельный прокси (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% данных переезжают. Это гигабайты трафика и часы деградации.
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и забыть про перешардирование. - Не упомянуть горячие шарды. Интервьюер почти наверняка про них спросит.
Ты только что разобрал тему, на которой валятся многие кандидаты. Проговори пример ответа вслух пару раз, и он станет твоим.