Однажды база данных становится слишком большой. Запросы тормозят, индексы пухнут, реплики для чтения уже не спасают. Да, можно вертикально отмасштабировать - накинуть сервак помощнее, памяти побольше, но в какой-то момент уже не получится растить мощности бесконечно
И тут появляется шардирование - разделение данных на несколько независимых частей, которые называются шардами
То есть мы не просто оптимизируем одну большую таблицу, а распределяем данные между несколькими базами или узлами
Например, пользователи с одними user_id живут в одном шарде, с другими user_id - в другом
shardKey = user_id % 4
0 → shard_0
1 → shard_1
2 → shard_2
3 → shard_3
То есть по user_id мы сразу понимаем, в какой шард идти. В теории круто: было много данных в одной базе, стало меньше данных в каждой отдельной базе
НО не все так просто, потому что мы получили несколько доп. сложностей. Очевидно стала более сложная инфра: была одна БДшка - стало 4. Нужны новые серваки, мониторинг за каждой, поддержка и тд
Но с точки зрения работы с данными - главная боль шардирования в том, что теперь данные распределены
Раньше можно было сделать обычный запрос:
SELECT * FROM orders WHERE user_id = 123;
И база сама разбиралась, где что лежит. После шардирования приложение или специальный routing layer должны сначала понять, в каком шарде лежит user_id = 123
Но и тут проблемы начинаются, когда мы заходим сделать запрос не по shard key
SELECT * FROM orders WHERE status = 'FAILED';
Если status не является ключом шардирования, то мы не знаем, в каком шарде лежат такие заказы
Значит, надо идти во все шарды, выполнить запрос в каждом, собрать результаты, склеить их и отдать клиенту. А если у нас 10 шардов, то это +10 сетевых вызовов и большой риск, что какой-то из этих запросов отвалится
Иногда это ОК, но если таких запросов много, то лучше пересмотреть архитектуру хранения данных
Еще больно становится с JOIN’ами
Классическая ситуация - надо достать user и все его связанные orders. В рамках одной БД это просто. Но если user и его orders лежат на разных шардах, то простым JOIN уже не получится. То есть ключом шардированя у order был бы не order_id, а user_id
❗️Поэтому один из самых важных вопросов при шардировании - по какому ключу шардировать
Плохой shard key может убить всю производительность
Во-первых плохой выбор может привести к распределенному запросу по шардам, что мы уже разобрали, и это not good
Но еще при плохом выборе может быть неравномерное заполнение шардов. Один или несколько шардов будут гореть, на них будет приходить многоп данных и запросов, а остальные будут проставивать.
Это называется hot shard - когда нагрузка распределяется неравномерно, и один узел становится бутылочным горлышком
В реальности, конечно, это всегда компромисс, потому что всегда будет какой-то перекос. Главное предотвратить заранее проблему на этапе проектирования, а то потом будет дорого перераспределять кучу данных
Окей, а где это шардирование включить?
Такой вопрос может возникнуть, особенно у начинающих
В PostgreSQL шардирование не появляется магически из коробки. SQL БДшки вообще по дефолту не продоставляют такую функциональность. Если мы хотим шардировать SQL БД, то у нас есть два варианта:
🟣Писать свой routing layer в приложении
То есть мы обращаемся из кода также в обычный репозиторий userRepo.save(user), но под капотом он считает ключ шардирования и идет сам в нужный шард. Но эту логику надо писать самим. На моих проектах часто это была своя платформенная библиотека
int shardId = userId % shardCount;
DataSource dataSource = shardRouter.getDataSource(shardId);
🟣Есть расширения вроде Citus, которые можно накатывать на Postgres
За счет него БДшка «сама» будет роутить данные, мы просто обращаемся к узлу-координатору, а он дальше распределяет данные. Но это не бесплатно и в этом надо разобраться, а не просто включить галочкой. В общем, свои трудности
Ставь 🔥 , если заходят такие посты
P.S зацени, схему к посту)
#хардовая_польза
