Шардирование и репликация
Когда данные Go-сервиса перестают влезать в одну машину или одна машина перестаёт держать поток записей, данные раскидывают по нескольким инстансам — и весь интерес начинается после слова «раскидать». Сначала нужно отличать партиционирование (разрезать одну таблицу на куски внутри одного инстанса БД) от шардирования (разнести данные по разным инстансам), потом выбрать ключ шарда, по которому строка попадёт на свою машину, найти способ маршрутизации запроса к нужному шарду, пережить ребалансировку при росте кластера и как-то отвечать на запросы, которым нужны данные из многих шардов сразу. Параллельно с этим идёт репликация: шард почти никогда не живёт в одиночку — у него есть реплики ради надёжности и масштабирования чтений.
Главная ловушка темы — считать, что шардирование «просто разносит данные по серверам» и на этом всё. На деле цена прячется в деталях: ключ шарда низкой кардинальности (вроде пола пользователя) создаёт горячий шард и перекос нагрузки; популярный пользователь или вирусный пост дают горячий ключ внутри шарда; асинхронная репликация приносит лаг и устаревшие чтения, а синхронная — медленнее; кросс-шард JOIN, агрегация и поиск без ключа дороги и съедают выигрыш. Отдельный пласт — выбор хранилища (SQL против NoSQL под реальные паттерны доступа) и развязка боевой нагрузки OLTP от аналитической OLAP, чтобы тяжёлый отчёт не клал прод. Эта тема разбирает распределение данных по слоям — от различия партиций и шардов до конвейера из OLTP в OLAP.
Карта темы
- Партиционирование против шардирования — партиционирование режет одну таблицу на куски (range/list/hash) внутри одного инстанса БД, чтобы планировщик пропускал лишние партиции, а шардирование разносит данные по разным инстансам.
- Зачем шардировать — поднять пропускную способность на запись, размазать нагрузку на CPU, гео-распределить данные ближе к пользователям и хранить объём, который не влезает в один сервер; шард всегда живёт с репликами.
- Выбор ключа шарда — высокая кардинальность (user_id хорошо, пол плохо), равномерное распределение нагрузки и стабильные паттерны доступа; плохой ключ даёт горячий шард или кросс-шард на каждом запросе.
- Горячие ключи — один ключ или шард забирает непропорционально много трафика (популярный пользователь, вирусный пост); лечится разбиением горячего диапазона, солью к ключу, кэшем или выделенным шардом.
- Маршрутизация шардов — как запрос находит свой шард: строка DSN с координатами подключения, прокси со знанием карты или узел-координатор, который планирует и форвардит (например Citus для Postgres), с разменом по латентности и риском узкого места.
- Ребалансировка шардов — срабатывает, когда шард распух, перегрелся, кластер вырос или сжался или сменился ключ шарда; диапазоны двигают с минимальным простоем, а консистентное хеширование минимизирует объём переезда.
- Кросс-шард операции — кросс-шард JOIN, агрегация и поиск строки без ключа дороги; их избегают денормализацией, fan-out с последующим merge или поисковым индексом (Elasticsearch).
- Репликация — primary/master принимает записи, реплики отдают чтения; асинхронная репликация быстра, но даёт лаг и устаревшие чтения, синхронная согласованна, но медленнее, а failover повышает реплику до primary.
- SQL против NoSQL — SQL (реляционная модель, ACID, джоины, жёсткая схема, сильная согласованность) против NoSQL (документы, key-value, wide-column, гибкая схема, простое горизонтальное масштабирование, часто eventual consistency); выбор по паттернам доступа.
- От OLTP к OLAP — держать транзакционную (OLTP, Postgres) и аналитическую (OLAP) нагрузку врозь, стримя изменения через CDC или доменные события и Kafka в OLAP (ClickHouse), даталейк (S3) и хранилище (Snowflake).
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Путать партиционирование с шардированием | Ждёшь масштаба по записи от партиций внутри одного инстанса — потолок одной машины никуда не делся |
| Шардировать без реплик | Падение одного узла теряет его данные и часть кластера — нет ни надёжности, ни чтений в масштабе |
| Брать ключ шарда низкой кардинальности (пол, статус) | Несколько перегруженных шардов и простаивающие остальные — перекос нагрузки |
| Выбирать ключ без учёта паттернов доступа | Каждый запрос ходит кросс-шард, потому что нужная строка не находится по ключу |
| Игнорировать горячий ключ внутри шарда | Один популярный пользователь или вирусный пост кладёт целый шард |
| Делать маршрутизацию через единственный координатор без резерва | Узел маршрутизации становится узким местом и единой точкой отказа |
| Ребалансировать обычным хешем по числу узлов | При добавлении узла переезжает почти весь кластер вместо малой доли |
| Строить продукт на кросс-шард JOIN и поиске без ключа | Каждый такой запрос веером бьёт по всем шардам — дорого и медленно |
| Считать асинхронную реплику источником свежих данных | Лаг репликации даёт устаревшие чтения и аномалию read-after-write |
| Тянуть NoSQL «для масштаба» без реальной причины | Теряешь джоины и сильную согласованность там, где хватило бы простого Postgres |
| Гонять аналитику тяжёлыми запросами по боевому OLTP | Отчёт нагружает транзакционную БД и роняет латентность основного сервиса |
Значение для собеседований
Распределение данных — обязательная тема senior-уровня Go-интервью в части system design, и проверяют не знание слова «шардирование», а понимание его цены. Интервьюер смотрит, отличаете ли вы партиционирование от шардирования, выбираете ли ключ шарда по кардинальности и паттернам доступа, помните ли, что шард живёт с репликами, и понимаете ли, что асинхронная репликация приносит лаг, а кросс-шард операции дороги.
Что обычно проверяют:
- В чём разница партиционирования (одна таблица, один инстанс) и шардирования (данные по разным инстансам) и зачем вообще шардировать.
- Как выбрать ключ шарда (высокая кардинальность, равномерная нагрузка, стабильные паттерны доступа) и чем грозит плохой ключ.
- Что такое горячий ключ и горячий шард и как с ними бороться (разбить диапазон, посолить ключ, закэшировать, выделить шард).
- Как запрос находит свой шард (DSN, прокси, координатор вроде Citus) и где у маршрутизации узкое место и точка отказа.
- Когда и как ребалансировать шарды и почему консистентное хеширование минимизирует переезд данных.
- Чем дороги кросс-шард JOIN, агрегация и поиск без ключа и как их избегать (денормализация, fan-out + merge, поисковый индекс).
- Как устроена репликация (primary принимает записи, реплики отдают чтения), чем асинхронная отличается от синхронной и что такое лаг репликации и failover.
- Как выбирать между SQL и NoSQL по паттернам доступа и согласованности и почему разумно начинать с простого Postgres.
- Зачем разделять OLTP и OLAP и как стримить изменения через CDC и Kafka в ClickHouse, S3 и Snowflake.
Типичный неверный ответ: «шардирование — это просто разнести данные по нескольким серверам». Это запускает разбор того, что без реплик падение узла теряет данные, что ключ низкой кардинальности создаёт горячий шард, что маршрутизация и ребалансировка — отдельная инженерная задача, что кросс-шард JOIN бьёт веером по всем шардам, что асинхронная репликация даёт устаревшие чтения, и что аналитику нельзя гонять по боевому OLTP.