Как бороться с большими объемами данных в запросах, например шардирование
Что проверяют
Умеете ли вы масштабировать хранилище по порядку, от дешёвого к дорогому, и понимаете ли, во что обходится шардирование. Следом почти всегда спрашивают, как выбрать ключ шардирования и как выполнить запрос, которому нужны данные со всех шардов.
Как отвечать
Сначала покажите, что шардирование это последний шаг, а не первый:
- запрос и индексы:
EXPLAIN ANALYZE, покрывающие индексы, выборка только нужных колонок; - пагинация по ключу:
WHERE id > $1 ORDER BY id LIMIT 100вместоOFFSET, который читает и выбрасывает все предыдущие строки; - партиционирование: в PostgreSQL декларативные партиции по дате, старые партиции отключаются и архивируются целиком;
- реплики и кеш: чтение уводят на реплики, горячие данные в Redis.
Если одна машина всё равно не справляется с объёмом или записью, переходите к шардированию:
- ключ: высокая кардинальность, равномерное распределение и, главное, чтобы основные запросы попадали в один шард. Для сервиса заказов это обычно
user_id; - стратегия: по диапазону (просто, но последний шард горячий), по хэшу (равномерно, но без диапазонных запросов), через таблицу соответствия или консистентное хеширование, чтобы новый шард не переносил все ключи;
- маршрутизация в Go: пул соединений на шард (
pgxpool.Poolили*sql.DB) и функцияshardFor(key). Запросы на все шарды идут параллельно черезerrgroupсSetLimitи общимcontextс таймаутом, результаты сливаются в приложении; - готовые решения: Vitess для MySQL (сам написан на Go), Citus для PostgreSQL, распределённые CockroachDB и YugabyteDB.
Отдельно назовите цену: межшардовые JOIN и транзакции, глобально уникальные ID (UUIDv7 или Snowflake вместо автоинкремента), решардинг.
Где спотыкаются
Предлагают шардирование сразу, не проверив план запроса и индексы. Интервьюер обычно ждёт, что сначала выжмут одну базу.
Берут hash(key) % N. При добавлении шарда почти все ключи меняют адрес, и решардинг превращается в перенос всей базы.
Собирают ответы всех шардов в один слайс целиком и забывают rows.Close(). Память растёт вместе с данными, а незакрытые rows держат соединения, и пул быстро кончается.