Рост таблиц рано или поздно упирается в одну машину: индексы перестают помещаться в память, архивная часть перестаёт помещаться на быстрые диски, а записи в секунду упираются в то, что один узел физически не успевает. Шардинг отвечает на этот тупик разделением: таблица режется по строкам на части, части живут на разных узлах, и нагрузка размазывается по железу горизонтально. Звучит просто, за простотой стоит архитектура, которая определяет жизнь системы на годы, и ошибки на этом пути стоят дороже любой другой ошибки хранилища.
PostgreSQL подошёл к шардингу со своей колеёй: сначала нативное партиционирование внутри одного узла, затем расширения, распределяющее таблицы по кластеру, и внешние обёртки для федерации. Ниже разобраны все слои: когда шарды действительно нужны, чем партиции отличаются от шардов, как выбирать ключ разрезания, как переезжать между узлами без остановки и что происходит с запросами, которые лезут через все шарды разом.
Когда одна машина перестаёт справляться и что делать раньше шардов
Шардирование это последняя кнопка масштабирования, и жалеть её следует больше всего. До неё есть лестница более дешёвых решений: индексы под реальные запросы, покрытие горячей части кэшем, вертикальный рост узла, реплики для чтения, разделение нагрузки по характеру, архивация холодной части. Каждая ступень дешевле шардов, работает без изменения логики приложения и не создаёт распределённых проблем, которых до неё не существовало.
Сигналы к шардам честны и измеримы: запись упирается в диск на запись постоянно, и реплики не спасают, потому что каждая реплика обязана принять ту же запись; рабочее множество индексов не помещается в память при любом разумном бюджете; одна таблица доминирует в хранилище и растёт быстрее продукта; обслуживание одной таблицы, переиндексация, выгрузка, мешает всем остальным. Каждый сигнал по отдельности лечится иначе, и только их устойчивое сочетание оправдывает резать таблицу на части.
Оценка объёмов обязана быть числовой: строки в день, байты в месяц, запись в секунду на пике, и трёхлетний горизонт роста. Числа либо показывают, что одной машине хватит на годы с запасом, либо превращают разговор о шардах из тревоги в инженерную задачу с параметрами.
Ранняя подготовка без внедрения это лучший компромисс: схема с явным ключом арендатора или пользователя, отсутствие зависимостей между строками разных арендаторов и обёртка доступа к таблице в коде. Такая дисциплина стоит копейки, а день переезда превращается из переписывания в миграцию.
Нативное партиционирование PostgreSQL первый шаг без распределения
PostgreSQL умеет резать таблицы на партиции внутри одного узла: декларативное разбиение по диапазонам, спискам и хэшу объявляется в схеме, и планировщик сам направляет запросы в нужные куски. Огромная таблица становится набором кусков поменьше: горячий месяц живёт на быстрых дисках, старые месяцы на дешёвых, удаление месяца это отсоединение партиции за мгновение вместо удаления миллионов строк.
Партиции не решают проблему записи на одной машине и не добавляют вычислений, но решают половину бытовых бед больших таблиц: обслуживание по кускам, лёгкая ротация, точечные индексы на горячем куске, и планировщик умеет отсекать лишние партиции по условию запроса. Поездка в партиции это неделя работы, и она оттягивает разговор о шардах на годы для большинства продуктов.
Ключ партиционирования выбирается по естественным границам времени: помесячно, понедельно, и размер куска держится в единицах-десятках миллионов строк, чтобы каждый кусок обслуживался без боли. Слишком мелкая нарезка плодит служебные структуры, слишком крупная возвращает проблему большой таблицы, и золотая середина подбирается по росту, а не по вкусу.
Границы партиций заводятся заранее: расписание создаёт будущие куски до их наступления, и первый запрос нового месяца не встречает отсутствующей партиции. Автоматика создания и уборки это первый десяток строк обслуживающего кода, и они окупаются первым же спокойным утром.
Ключ шарда выбор который определяет судьбу архитектуры
Шардирование начинается с ключа, и ключ это самое важное решение всей схемы. Хороший ключ равномерно разбрасывает строки по узлам, почти все запросы идут в один шард, и новые узлы подключаются без перекройки логики. Плохой ключ создаёт горячие шарды, запросы поперёк всех узлов и бесконечные переезды строк, и переделка ключа после запуска это остановка всей системы.
Кандидаты на ключ известны: идентификатор арендатора в мультитенантных продуктах, идентификатор пользователя в социальных, устройства в телеметрии, счёт в финансовых. Правило выбора одно: ключ обязан совпадать с осью, по которой работает приложение, и запросы одного клиента обязаны попадать в один шард, потому что сцепка строк одного клиента это то, что делает шарды незаметными для логики.
Равномерность проверяется на живом распределении: у большинства продуктов есть гиганты, арендаторы с объёмами в сотни раз выше медианы, и честный расчёт весов показывает, что наивный хэш по арендатору кладёт трёх гигантов на один узел. Решения известны: отдельные узлы для гигантов, срезание больших арендаторов на собственные шарды, и исключения эти оформляются таблицей маршрутизации, а не надеждой.
Число шардов выбирается с запасом: узлов в несколько раз больше, чем нужно сегодня, и переезд между ними это уже знакомая задача ребаланса, а не смена архитектуры. Слишком мелкая нарезка плодит накладные расходы координации, слишком крупная ограничивает рост, и практика выбирает десятки шардов на перспективу.
Citus и FDW два пути распределить таблицы PostgreSQL
Расширение Citus превращает кластер PostgreSQL в распределённое хранилище: таблица с ключом разрезания распределяется по узлам, запросы маршрутизируются автоматически, и приложение продолжает видеть обычную таблицу. Колокация сущностей одного ключа на одном узле сохраняет соединения локальными, а координатор собирает запросы поперёк шардов, и это самый короткий путь к настоящему шардированию без переписывания приложения.
Второй путь это федерация через внешние обёртки: узлы-шарды остаются обычными базами, координатор подключает их таблицы внешними таблицами и маршрутизирует сам. Путь требует больше ручного труда, зато узлы остаются независимыми базами с полной автономией, и гибридные схемы с исключением-гигантов строятся прямее.
Выбор между путями сводится к команде: Citus экономит инженерные недели и берёт заботу о маршрутизации на себя, федерация даёт прозрачность и контроль ценой собственного кода маршрутизации. Продукты с малой командой выбирают первое, команды с особыми требованиями и свободными руками второе, и оба пути живут в производстве годами.
Таблицы без ключа разрезания остаются либо на координаторе целиком, либо копируются на все шарды как справочники. Первый вариант прост и годится для маленьких таблиц, второй требует дисциплины обновления, и выбор зависит от частоты изменений справочника, а не от его размера.
Ребаланс переезд шардов без остановки сервиса
Кластер живёт, и узлы то прибывают, то выбывают, а строки обязаны переезжать. Хороший ребаланс это конвейер: перенос строк в фоне, переключение маршрутизации атомарно, и сервис продолжает работать на обоих наборах в момент переезда. Плохой ребаланс это остановка на выходные, и после первой такой остановки продукт ищет способ не повторять.
Инструменты переноса знакомы каждому, кто мигрировал между базами: потоковая логическая репликация копирует изменения на новый шард, момент готовности совпадает на обоих копиях, короткая блокировка переключает владельца, и разница во времени измеряется секундами. Конвейер переезда строится из тех же блоков: переключение подписки, сверка счётчиков, атомарная замена маршрута.
Планирование переездов это план на кластер: не более одного переезда на узел одновременно, окна по низкому трафику, скорость переноса ограничена, чтобы не душить рабочие запросы. Тысячи строк в секунду переезда это тихая фоновая работа, миллионы в секунду это уже заметный трафик, и ограничитель обязан быть ручкой.
Сверка завершённости обязательна: счётчики строк, контрольные суммы на выборке, минуты сравнительного чтения из обоих шардов. Тихо потерянная строка при переезде обнаруживается через месяц отчётом клиента, и десять минут сверки дешевле месяца разбирательств.
Запросы поперёк шардов агрегации соединения и их цена
Запрос в один шард это норма, и маршрутизация по ключу делает её невидимой. Запрос поперёк шардов это исключение, и исключение обязано оставаться исключением: каждый такой запрос разворачивается в запросы ко всем узлам, сбор результатов на координаторе и агрегацию в его памяти, и цена растёт с числом шардов линейно.
Агрегации по одной размерности считаются честно: суммы, средние и счётчики собираются из частичных результатов каждого шарда, и координатор складывает маленькие ответы, а не гигабайты. Пагинация поперёк шардов дороже: десятая страница это топ из каждого шарда и сортировка на координаторе, и глубокие листания поперёк всех узлов ограничиваются на уровне приложения.
Соединения живут хорошо там, где таблицы колокированы ключом: строки одного арендатора на одном узле соединяются локально, и распределённое соединение не происходит. Соединение таблиц с разными ключами это shuffle или сборка на координаторе, и такие запросы выносятся в витрины и предагрегаты, а не живут в коде страниц.
Транзакции поперёк шардов существуют и стоят дорого: подтверждение в два шага держит блокировки во всех участвующих узлах, и частота таких транзакций обязана стремиться к нулю. Архитектурный ответ это выстраивание изменений так, чтобы одна бизнес-операция меняла строки одного шарда, и исключения оформляются явно, с компенсациями на случай отказа, а не по умолчанию.
Идентификаторы и транзакции глобальные порядки без единого узла
Единая последовательность автоинкремента исчезает вместе с единой таблицей, и глобальные идентификаторы строятся иначе. Варианты известны: случайные универсальные идентификаторы без координации, составные ключи из номера шарда и локальной последовательности, генераторы с долей времени и номера узла. Каждый вариант уникален без переговоров между узлами, и выбор определяется потребностью в сортируемости и длиной ключа.
Внешние ключи поперёк шардов не работают, и целостность переезжает в приложение: строка заказа создаётся в шарде покупателя вместе со строкой корзины, и связи в пределах одного узла остаются обычными. Ссылки между шардами живут как сохранённые идентификаторы без объявления внешнего ключа, и проверки на существование делаются запросом, потому что декларативной гарантии поперёк узлов нет.
Порядок операций поперёк шардов это продуктовая задача, а не техническая: событие в шарде заказа публикуется, шард склада слушает и резервирует, и цепочка шагов с компенсациями переживает отказы лучше, чем распределённая блокировка. Мониторинг цепочек становится частью наблюдения, и идемпотентность повторов обязательна, потому что сеть между узлами это сеть со всеми её правами на таймаут.
Порядок внедрения шардинга свёрнут в короткую лестницу:
- посчитать рост на три года и попробовать дешевле: партиции, реплики, вертикаль;
- выделить ключ разрезания и проверить распределение на живых объёмах;
- ввести обёртку доступа к таблице и запретить запросы мимо неё;
- поднять кластер с запасом шардов и колокацией сущностей ключа;
- прогнать сценарии поперёк шардов и вынести их в витрины;
- отработать ребаланс на тестовом кластере до первой необходимости;
- завести мониторинг горячих узлов и распределения нагрузки по ним.
Шарды не делают систему лучше, они делают её больше: запись растёт с железом, хранение дешевеет на холодных узлах, и границы роста отодвигаются за горизонт продукта. Цена этой величины это дисциплина ключа и уважение к границам узлов, и команды, которые платят её добровольно, получают инфраструктуру, которая переживает их собственный рост.