
Что происходит под капотом корпоративного мессенджера, когда нагрузка пробивает отметку в 650 000 запросов в секунду? Обычно в этот момент архитектура распределённых хранилищ начинает проверять на прочность не только диски и сеть, но и нервы команды эксплуатации.
В бэкенде чатов Телемоста за доставку и историю сообщений отвечает отказоустойчивая YDB. Но сообщения — это лишь верхушка айсберга. Помимо них системе нужно ежесекундно проверять составы чатов, актуализировать их названия, сверять права доступа и обслуживать десятки внешних интеграций. И вся эта огромная, динамически меняющаяся масса метаданных живёт в PostgreSQL.
Привет! Меня зовут Никита Звонарёв, я разработчик в Яндекс 360. Последние три с половиной года я пишу на C++ и Python, а параллельно преподаю в СПбГУ. В статье мы детально разберём, как устроен путь запроса к нашим базам данных и за счёт каких архитектурных решений нам удаётся держать такой профиль нагрузки.

Архитектура микросервисов: Fanout, Registry и топология запросов
Если смотреть на верхнеуровневую схему бэкенда, то вся система разбита на изолированные слои. Чтобы понять, как распределяется нагрузка на СУБД, разделим наши сервисы по типу взаимодействия с хранилищем. Из всех компонентов системы на запись в PostgreSQL работает только один сервис, остальные — используют только чтение.

Сервисы модификации сущностей мессенджера: Registry и Files. Основная точка входа для любых мутирующих запросов к метаданным — это сервис Registry (а также его изолированный аналог Files, отвечающий за логику работы с файловыми вложениями).
Архитектура: классические stateless‑сервисы.
Зона ответственности: обработка CRUD‑операций над сущностями. Они отвечают за создание чатов, изменение их составов, обновление конфигураций и прав доступа.
Взаимодействие с БД: Registry напрямую пишет все изменения в PostgreSQL, одновременно оставляя там пометки о том, что эти данные необходимо вычитать другим компонентам системы.
Сервис обработки и рассылки сообщений и пушей: Fanout. Над сервисом записи находится Fanout — ключевой и самый нагруженный компонент бэкенда чатов.
Архитектура: stateful‑сервис с агрессивным внутренним кешированием.
Зона ответственности: непосредственная доставка отправленных сообщений, рассылка пуш‑уведомлений и отдача истории чатов по запросу клиентов.
Взаимодействие с БД: Fanout асинхронно вычитывает маркеры обновлений из PostgreSQL, актуализирует свой стейт и кеши, а саму историю сообщений параллельно пишет в YDB.
Фоновые воркеры и консьюмеры. Помимо основного рантайма, к PostgreSQL подключён ряд инфраструктурных сервисов, работающих в режиме read‑only.
LB Subscriber: сервис‑консьюмер, который подписывается на общесистемные события Яндекса из шины данных (Logbroker). Ему постоянно требуются тяжёлые выборки из Postgres (например, получить все чаты конкретного пользователя), чтобы синхронизировать внутреннее состояние чатов с глобальными изменениями в экосистеме.
GDPR‑воркер: изолированный сервис, обрабатывающий запросы пользователей на выгрузку или удаление персональных данных. Выполняет точечное чтение метаданных.
За счёт агрессивного кеширования в памяти Fanout сильно снижает читающую нагрузку на PostgreSQL.
Топология базы данных и проблема масштабирования
Исторически наше хранилище метаданных представляло собой классический распределённый кластер PostgreSQL:
Запись — один выделенный мастер‑узел.
Чтение — пул из нескольких читающих реплик.
При этом все наши stateless‑ и stateful‑сервисы утилизировали чтение со всех доступных нод, включая мастер, а мутирующие запросы (запись) шли в мастер БД. До определённого момента эта схема работала идеально. Но затем выросла нагрузка со стороны внешних интеграций.
Бэкенд чатов Телемоста перерос рамки одного приложения (у которого и так есть B2B‑ и B2C‑контуры). Мы стали единым платформенным решением для всей экосистемы Яндекса. Пара примеров из десятков таких интеграций: чаты курьеров, ресторанов и заказчиков в Яндекс Еде, оперативная связь в такси и доставке в Яндекс Go, фронтлайн поддержки практически во всех сервисах компании.
Каждая доставка еды, каждая поездка на такси — это создание нового чата с уникальным составом участников, метаданными и конфигурацией.
Дополнительная нагрузка от инфраструктурных сервисов вдобавок к самому контуру Телемоста привела к следующим проблемам с PostgreSQL:
Большой поток на запись. Поток запросов на создание и модификацию чатов от десятков внешних сервисов утилизировал всю пропускную способность единственного мастер‑узла. Мы упёрлись в потолок вертикального масштабирования мастер‑ноды по CPU и дисковому I/O.
Быстрая деградация свободного места на диске. При таком темпе генерации метаданных свободное место на SSD‑массивах таяло на глазах.
Стало очевидно: если не изменить подход к хранению и распределению данных прямо сейчас, бэкенд просто сломается под весом экосистемного трафика. Нужно было кардинально перестраивать архитектуру работы с СУБД.
Базовые подходы: вертикальное vs горизонтальное масштабирование

Перед нами встала классическая задача масштабирования СУБД под высокую нагрузку на запись. Фундаментально у инженера здесь есть два пути.
Вертикальное масштабирование. Суть подхода — наращивание вычислительных ресурсов на существующих узлах и развёртывание дополнительных read‑реплик в кластере.
Плюсы: абсолютно прозрачно для прикладного кода. Логика приложения не меняется, задача решается исключительно силами инфраструктуры («заваливается железом»).
Минусы: рано или поздно система упирается в физический (технологический) тупик. Невозможно бесконечно масштабировать запись (Write‑RPS) на одну мастер‑ноду.
Горизонтальное масштабирование. Суть подхода — разбить данные на кусочки, то есть партиционировать и хранить на отдельных кластерах (шардах). При этом каждый шард представляет собой самостоятельный кластер со своим мастером и пулом реплик.
Плюсы: практически безграничный потенциал масштабирования как по объёму данных, так и по Write‑RPS.
Минусы: высокая цена реализации. Требуется кардинально переписать логику приложения, научить сервис маршрутизировать запросы и решать проблему распределённых транзакций.
Мы долго удерживали нагрузку за счёт вертикального подхода, но в определённый момент упёрлись в аппаратный потолок мастер‑ноды. Больше выделить железа было физически невозможно, а темп прироста интеграций не снижался. Вариантов не осталось — мы приняли решение переходить на горизонтальное шардирование.
Как устроены наши данные
Прежде чем приступать к распилу монолитной базы на шарды, необходимо детально изучить схему данных. Наша СУБД содержит десятки таблиц, но верхнеуровнево всю логику метаданных определяют ключевые сущности:
Пользователи (users): профили в мессенджере. Каждый пользователь идентифицируется глобально уникальным идентификатором — GUID.
Чаты (chats): метаданные самих комнат. Таблица хранит уникальный строковый ID чата, его название и конфигурационные флаги. По строковому идентификатору система также определяет тип чата и его принадлежность к конкретной интеграции (например, Телемост, Яндекс Еда или Яндекс Go).
Главная инфраструктурная нагрузка и сложность распределения данных кроется в связующих и вспомогательных таблицах:
Состав чатов (chat_members): таблица, которая хранит пары GUID пользователя — chat_id.
Вспомогательные таблицы, такие как истории изменений составов чатов, списки заблокированных и удалённых пользователей.
Служебные таблицы для тредов. Технически это изолированные дочерние чаты, привязанные к конкретному родительскому сообщению.
Поскольку наш бэкенд активно обслуживает B2B‑клиентов, схема данных существенно усложняется поддержкой организационных структур. Мы не можем ограничиваться связью чат — конкретный человек, поэтому в базе выделен отдельный домен:
Таблицы сотрудников организаций, состав групп и отделов.
Таблицы сопряжения, которые связывают конкретный chat_id не с отдельными GUID, а целиком со структурами организации (например, со всем отделом какой‑либо компании). При изменении состава отдела внутри компании доступ к чату должен обновляться автоматически.
Наличие кардинально разных сущностей — отдельных пользователей (GUID), чатов (chat_id) и организаций с их группами и отделами — делает выбор единого ключа шардирования нетривиальной задачей.
Трёхмерная структура данных и подходы к шардированию
Связи между нашими сущностями можно представить в виде трёхмерного куба, где грани — это пользователи, чаты и организации.

Отношения между ними перекрёстные: пользователь состоит во множестве чатов, чат включает множество пользователей, а организации объединяют пользователей в группы, которые имеют коллективный доступ к чатам. Выбор стратегии шардирования для такой связанной структуры — это всегда поиск компромисса, ведь любой разрез куба по одной из осей усложняет выборку по двум другим.
Мы рассмотрели несколько концептуальных подходов к горизонтальному масштабированию.
Вариант 1. Миграция на распределённую СУБД
Первая идея — сменить целевую базу данных и перейти на решение, которое поддерживает распределённое хранение и автоматическое партицирование из коробки, например YugabyteDB, YDB или Greenplum® (последний, пусть и основан на PostgreSQL, больше заточен под OLAP‑нагрузку, нежели OLTP).
Минусы подхода: полная смена СУБД для зрелого сервиса — это колоссальные риски, необходимость переписывать сервис доступа к данным (DAL) и сложнейшая миграция терабайтов метаданных в продакшене без простоя.
Вариант 2. Проксирование на уровне инфраструктуры
Второй подход — вынести логику маршрутизации запросов за пределы приложения. Сделать это можно либо на уровне умного пуллера соединений к базам, либо на уровне балансировщика перед инстансами приложения.
Технически это выглядит как внедрение прокси‑сервиса, который парсит входящий запрос (SQL‑выражение или API‑вызов), извлекает из него ключ шардирования (например, chat_id) и перенаправляет трафик на целевой под приложения или конкретный шард PostgreSQL.
Существуют готовые экосистемные решения, реализующие распределённый сервис прямо поверх классических баз данных. Вот несколько примеров:
Vitess: изначально разработан инженерами YouTube (и активно используется в Slack) для горизонтального масштабирования MySQL.
SPQR (Stateless Postgres Query Router): наша внутренняя яндексовая разработка с открытым исходным кодом. Это легковесный и умный роутер запросов для PostgreSQL, который умеет горизонтально масштабировать кластер, оставаясь прозрачным для клиентских приложений.
Минусы подхода: для эффективной работы системы нужно специально подготовить данные — идеально, когда все данные хорошо дробятся по одному ключу.
Вариант 3. Шардирование на уровне приложения (Application‑Level Sharding)
Подход, при котором вся бизнес‑логика маршрутизации данных зашивается непосредственно в код сервиса. Каждый под приложения на основе переданных параметров сам вычисляет целевой шард и открывает соединение с нужной физической базой.
Почему мы отсекли альтернативы
Взвесив все за и против, мы начали последовательно исключать неподходящие варианты. Полная миграция на иную СУБД в наших реалиях оказалась нежизнеспособной по двум причинам:
Риски миграции. Перенос терабайтов связанных метаданных с гарантированно нулевым даунтаймом и обеспечением возможности прозрачного отката (rollback) при нашей связности таблиц — высокорисковая и трудно осуществимая задача.
Специфика диалектов. Предстояло бы полностью переписать сотни сложных, оптимизированных под Postgres SQL‑запросов, учитывая особенности синтаксиса и планов выполнения новой СУБД.
Шардирование на уровне балансировщика, пуллера или готовых роутеров вроде SPQR идеально работает в изолированных доменах. Оно показывает максимальную эффективность, когда данные можно жёстко раздробить по одному ключевому признаку, а количество кросс‑шардовых запросов (cross‑shard queries) невелико. Однако наш трёхмерный кубик данных (чаты, пользователи, организации) не позволял обойтись простым линейным делением.
Поскольку готовые инфраструктурные роутеры не справляются с нашей трёхмерной структурой данных, мы пошли по пути кастомного шардирования на уровне приложения (Application‑Level Sharding). Чтобы разрезать наш трёхмерный куб, нужно было выбрать оптимальный признак деления — ключ шардирования. Мы детально проанализировали три классические стратегии:
Стратегия |
Для чего подходит |
Почему не подошла нам |
По пользователям (user_id) |
Индивидуальные сервисы (Яндекс Почта, Яндекс Диск), где пользователь работает преимущественно со своими данными |
В мессенджере чат — это коллективная сущность. При шардировании по пользователям создание чата на 10 человек потребовало бы синхронной записи в 10 разных шардов |
По организациям (org_id) |
B2B‑платформы с жёсткой изоляцией клиентов (исторический подход Slack). Хорошо работает, если компании не пересекаются между собой |
Не покрывает наш огромный B2C‑контур (обычные пользователи Телемоста) и сквозные экосистемные интеграции (вроде чатов курьер — клиент в Яндекс Еде) |
По чатам (chat_id) |
Высоконагруженные мессенджеры с интенсивным сетевым взаимодействием и частым созданием чатов |
Наш выбор. Переносит всю нагрузку по распределению данных на идентификатор конкретного чата |
Выбор chat_id в качестве ключа шардирования обеспечил нам два фундаментальных преимущества на этапе записи:
Локализация дискового пространства. Именно метаданные чатов и их истории изменений создавали большую часть оверхеда и утилизировали свободное место на SSD. Привязав все вспомогательные таблицы к ID чата, мы получили возможность линейно распределять эту массу по физическим дискам разных шардов.
Атомарность транзакций. Большинство пишущих операций Мессенджера связано с модификацией чатов — в данном случае все необходимые для таких операций данные будут лежать в одном месте.
Благодаря шардированию по chat_id данные самого чата и всех его участников (таблица chat_members) находятся на одном и том же физическом шарде.
Главный профит: нам удалось полностью избежать распределённых пишущих транзакций. Любая запись или апдейт состава чата выполняются локально, внутри одной классической транзакции Postgres на конкретном мастере. Нам не пришлось писать сложную логику двухфазных коммитов (2PC) или распределённых саг, которые сложны в поддержке системы.
В этой схеме есть очевидный компромисс: если все данные чата залочены внутри своего шарда, то как эффективно выполнить тяжёлый читающий запрос вроде «найти все чаты, в которых состоит пользователь X»? Ведь для этого приложению теоретически нужно обойти вообще все шарды в кластере. К счастью, этот запрос в современной реализации Мессенджера проходит через сервис Fanout, который агрессивно кеширует результат такого запроса.
Проблема маршрутизации: как найти нужный шард
После того как мы выбрали chat_id в качестве ключа шардирования, перед нами встала следующая классическая задача: как приложению, имея на руках строковый идентификатор чата, определить, на каком физическом шарде лежат его данные?
В общем случае этот вызов сводится к трём вариантам решения.
Подход 1. Фиксированное число шардов (хеш по модулю). Самый простой и прямолинейный способ распределения данных.
Приложение берёт хеш‑функцию от идентификатора чата, получает числовое значение и берёт остаток от его деления по модулю N, где N — общее количество физических шардов в кластере:
Shard ID=Hash(chat_id)(modN)
Плюсы: не требует внешней инфраструктуры или вспомогательных баз. Маршрутизация происходит прямо в памяти пода приложения.
Подход 2. Логические партиции. Более гибкий, гибридный подход, при котором все сущности, принадлежащие к одной партиции, хранятся на одном шарде.
Все чаты делятся на фиксированное количество логических партиций. Номер партиции вычисляется как часть от хеша ID чата. При этом в системе заводится сервис, который хранит карту соответствия (mapping table) «Логическая партиция X → Физический шард Y» и умеет отвечать на запрос, который, по сути, вычисляет для чата его шард Y.
Подход 3. Хранить полноценный маппинг. Самый прямолинейный, но инфраструктурно дорогой способ.
В системе создаётся выделенный высокопроизводительный распределённый реестр (внешний сервис + база данных), который хранит жёсткую связь вида chat_id → shard_id для каждого существующего чата. При любом запросе приложение сначала делает запрос в этот реестр, узнаёт адрес целевого шарда и только потом идёт за метаданными.
Казалось бы, первый вариант с хешем по модулю выглядит максимально соблазнительно из‑за малых затрат на разработку. Однако мы сразу от него отказались, так как он несёт в себе огромные архитектурные риски при долгосрочной эксплуатации платформы из‑за проблемы решардирования.
При использовании формулы взятия хеша по модулю количество шардов N намертво зашивается в логику роутинга. Как только нам потребуется добавить новые физические ноды (увеличить N), формула изменится для всего массива данных. Это означает, что при добавлении всего одного шарда большинство из существующих чатов мгновенно «сменят прописку» с точки зрения алгоритма. Нам пришлось бы делать сложную миграцию и параллельно проводить полное глобальное решардирование — перемещать терабайты данных между несколькими шардами. Осуществить такую миграцию на живом продакшене с высоким Write‑RPS без даунтайма крайне тяжело, практически невозможно.
Выбор между вторым и третьим вариантом решения проблемы вызвал внутри команды архитектурные споры. Для двухуровневого шардирования в инфраструктуре Яндекс 360 уже есть готовое, проверенное временем решение — сервис «Шарпей», созданный командой Почты. Однако в ходе проектирования мы натолкнулись на серьёзные долгосрочные риски.
Главная проблема логических партиций — непредсказуемость распределения данных. Если профиль нагрузки изменится, партиции начнут неравномерно раздуваться. В практике наших коллег бывали случаи, когда из‑за высокой активности пользователей одна логическая партиция разрасталась настолько, что физически переставала помещаться на один шард.
Поскольку мы выступаем платформой для десятков внешних интеграций (от Яндекс Go до поддержки), мы не можем прогнозировать паттерны поведения пользователей на годы вперед. Рано или поздно это привело бы нас к необходимости сложного решардирования на живом кластере, чего мы хотели избежать.
В итоге мы пошли по третьему пути — хранению жёсткого маппинга chat_id → shard_id. Для этого мы задействовали альтернативную конфигурацию сервиса из Почты — строковый «Шарпей».
Главное преимущество, которое нам дало это решение, заключается в том, что мы получили мгновенное горизонтальное масштабирование. Когда на текущих физических шардах мы видим, что место начинает приближаться к концу, мы просто добавляем в кластер новые пустые шарды. Строковый «Шарпей» начинает аллоцировать свежесозданные чаты на эти новые шарды из коробки. Старые данные при этом вообще не сдвигаются с места, а проблема решардирования становится куда менее актуальной, особенно если оставить достаточный запас места в старом шарде.
Главный недостаток такого решения — большой объём самого маппинга. Нам приходится хранить сотни миллионов строковых ключей. Однако инфраструктура строкового «Шарпея» оказалась достаточно производительной и легко утилизировала этот объём, обеспечивая константное время ответа при маршрутизации запросов.
Как изменилась архитектура бэкенда
Внедрение кастомного шардирования повлияло на дизайн сервиса. Наша высокоуровневая схема обросла новыми связями, а ключевым изменением стало появление строкового «Шарпея».

Наш CRUD‑сервис (Registry) и сервис обработки сообщений (Fanout) теперь не просто ходят в Postgres напрямую, а перед каждым обращением к БД за шардированными данными они выполняют легковесный запрос в строковый «Шарпей», чтобы узнать, какой именно физический шард обслуживает целевой chat_id.
Чтобы не заносить сложную логику походов к распределённому кластеру PostgreSQL со всей экосистемы, мы перестроили схему взаимодействия сервисов. Главное изменение — полный запрет на прямой доступ к СУБД для сторонних компонентов.
Такие сервисы, как LB Subscriber и GDPR, которые раньше самостоятельно читали данные из базы, теперь полностью изолированы от сервиса хранения. Для них мы реализовали специализированные эндпоинты («ручки») внутри сервиса Registry. Теперь Registry выступает в роли единого фасада для работы с метаданными.
В результате топология упростилась: прямой доступ к инстансам PostgreSQL сохранили только три компонента — Registry, Files (аналог Registry для работы с файлами в сообщениях) и Fanout.
Исторически сервис доступа к данным в Registry был устроен линейно: реализация API‑эндпоинта вызывала компонент RegistryDB (где были инкапсулированы сырые SQL‑запросы), а тот через клиент СУБД шёл в Postgres.
Чтобы внедрить горизонтальное масштабирование и не сломать обратную совместимость, мы переписали эту цепочку, внедрив полиморфизм и компонентную прослойку маршрутизации:
Перехват запроса в ShardedDB. Реализация эндпоинта теперь вызывает класс ShardedDB (наследник старого RegistryDB). Он перехватывает контекст выполнения и извлекает ключ сущности (например, chat_id).
Обработка в Sharding System. ShardedDB передаёт ключ во внутренний модуль — Sharding System. Этот класс определяет статус сущности: переведена ли она на схему распределённого хранения или нет.
Обработка нешардированного трафика. Если эндпоинт или конкретный чат ещё не перенесены на новую архитектуру, запрос проваливается в базовый RegistryDB и направляется на нулевой шард PostgreSQL. Система продолжает работать в штатном режиме.
Запрос в SharpeiClient. Если данные шардированы, Sharding System через SharpeiClient делает запрос к строковому «Шарпею», передаёт ему строковый ID и мгновенно получает физический номер целевого шарда.
Исполнение через пул клиентов. Контекст возвращается в ShardedDB. Компонент выбирает из пула соответствующий инстанс PostgreSQL‑клиента (для каждого физического шарда в коде инициализирован свой изолированный клиент) и выполняет SQL‑запрос точно по адресу.
Благодаря абстракции Sharding System внутри прикладного кода, для верхнеуровневой бизнес‑логики Registry процесс выбора базы остался абсолютно прозрачным. Разработчикам фич не нужно думать о пулах соединений и адресах нод: они просто вызывают метод, а сервис доступа к данным сам решает, куда направить запрос — в нулевой шард или в распределённый кластер.
Проблема кросс‑шардовых запросов
Несмотря на то что большинство транзакций в нашей системе изолированы в рамках одного chat_id, полностью избавиться от кросс‑шардовых запросов на чтение невозможно (например, когда сервису нужно составить список чатов для пользователя).
Для выполнения таких операций сервис ShardedDB использует следующих подход:
Приложение параллельно опрашивает все физические шарды PostgreSQL.
Каждый шард возвращает своё подмножество строк.
Сервис Registry агрегирует эти списки в единый упорядоченный массив в оперативной памяти.
Поскольку данные собираются из независимых физических баз, на этапе агрегации критически важно гарантировать их консистентность. Каждая сущность в нашей системе обладает уникальным идентификатором (ключом, Key) и монотонно растущим номером версии (Version).
При слиянии потоков данных из разных шардов мы анализируем пересечения ключей и делим результаты на три сценария:
Идеальный сценарий. Со всех шардов вернулись абсолютно уникальные, непересекающиеся списки ключей. Они беспрепятственно склеиваются в единый ответ клиенту.
Дубликаты. В результатах встречаются одинаковые ключи, у которых совпадают и номера версий, и содержимое. Это классический побочный эффект решардирования (например, когда чат переезжает на новый шард, но на старом его ещё не успели вычистить). Registry просто схлопывает эти строки, отдавая пользователю валидный объект.
Конфликт данных. Ключи в результатах совпали, но номера версий или содержимое строк различаются. Ситуация означает, что из‑за бага в логике приложения один и тот же чат появился в двух разных шардах. Иногда это происходит не из‑за крупного факапа, а, например, когда мы забыли вычистить до конца данные по некоторой сущности. В случае конфликта мы возвращаем строку с наибольшей версии в агрегированном ответе.
На этапе релиза в продакшен мы обвесили этот сервис жёсткими алертами, вывели в сервис мониторинга две метрики (sharding_duplicates_count и sharding_conflicts_count) и завели на них алерт.
Сейчас обе эти метрики в нашем продакшене находятся строго на нуле. Но пристальный контроль за ними обязателен: если график конфликтов уползёт вверх даже на единицу — это сигнал к немедленному расследованию инцидента командой эксплуатации, так как речь идёт о нарушении целостности данных.
Асинхронное чтение в Fanout
Для рефакторинга сложного stateful‑сервиса Fanout мы применили иной подход для поддержки загрузки из шардированного хранилища.
Исторически логика чтения обновлений в Fanout была устроена следующим образом: API‑эндпоинт обращался к модулю StateImporter. Этот компонент через асинхронный механизм PostgresListener непрерывно «слушал» поток обновлений СУБД и наполнял данными потокобезопасный StateCache в оперативной памяти пода.
Когда перед нами встала задача научить Fanout читать данные из новых шардов, мы поняли, что переписывать внутренности StateImporter — это огромный риск нарушить логику инвалидации кешей. Вместо этого мы создали компонент‑обёртку — ShardedImporter.
Как это работает:
ShardedImporter инкапсулирует массив объектов StateImporter. Их количество строго соответствует числу физических шардов в кластере.
Каждый экземпляр StateImporter получил свой персональный PG Listener, который подключён к конкретному шарду PostgreSQL и асинхронно вычитывает маркеры изменений только из него.
Все импортеры параллельно пишут обновления в один и тот же глобальный StateCache. Реализацию самого кеша нам вообще не пришлось менять — он остался единым для всего инстанса Fanout’а.
Главная прелесть такого подхода — нам удалось на 100% переиспользовать кодовую базу маршрутизации. Когда ShardedImporter нужно обработать входящий запрос и понять, какому именно внутреннему импортеру делегировать задачу, он вызывает знакомые нам по Registry классы ShardingSystem и SharpeiClient.
Благодаря тому, что оба сервиса написаны на C++, мы просто вынесли всю логику интеграции со строковым «Шарпеем» в общую библиотеку. В итоге рантайм‑роутинг в Fanout заработал мгновенно, без написания дублирующего кода.
Внедрение распределённой архитектуры потребовало от нас тотального покрытия системы интеграционными и регрессионными тестами. И здесь нас ждал классический инженерный сюрприз: когда мы запустили обновлённый бэкенд в CI, то некоторые тестовые сценарии начали показывать неожиданное поведение. Однако детальный разбор логов показал, что абсолютное большинство обнаруженных багов вообще не были связаны с шардированием — просто до этого мы их не замечали.
Вместо заключения
Горизонтальное масштабирование СУБД на живом, высоконагруженном B2B/B2C‑сервисе — это сложный, но выполнимый вызов.
В качестве итога мы собрали ключевые инсайты, которые помогли нашей команде успешно пережить этот крупный сдвиг в архитектуре данных без даунтайма и потери консистентности:
Думайте о масштабировании на вырост. Выбирая стратегию распределения данных, оценивайте не только текущие боли, но и то, как система будет вести себя через два‑три года при десятикратном росте трафика. Избегайте поспешных компромиссов, которые закроют вам путь к дальнейшему горизонтальному расширению.
Бейте в самую большую болевую точку. Не пытайтесь распилить весь монолит разом — это верный способ погрязнуть в бесконечной работе. Мы начали с чатов, потому что именно они генерировали максимальный Write‑RPS и утилизировали диски. Если в вашем проекте профиль нагрузки смещён в сторону тяжёлого чтения — ваш выбор способа шардирования и целевой СУБД может (и наверняка должен) быть совершенно иным.
Не изобретайте велосипеды. Если в вашей компании или в опенсорсе уже есть проверенные временем инфраструктурные решения (в нашем случае это строковый «Шарпей»), адаптируйте и переиспользуйте их. Написание собственного решения с нуля — это колоссальный оверхед на разработку и последующую поддержку.
Выслушайте критику внутри команды. Обязательно выносите черновики решения на широкое архитектурное ревью. Коллеги, уже прошедшие через боль разработки подобных систем, гарантированно подсветят скрытые риски и слепые зоны, которые вы могли упустить на этапе проектирования.
Снижайте риски через Observability. Любое, даже самое гениальное теоретическое решение разобьётся о суровую реальность продакшена, если оно непрозрачно для эксплуатации. Обкладывайте тестами каждый измененный DAL‑компонент, выводите детальные метрики ошибок (как наши счётчики конфликтов версий), повесьте на них алерты и непрерывно мониторьте, особенно в моменты поэтапного переключения трафика.
Спасибо за внимание! Делитесь в комментариях вашим опытом кастомного шардирования СУБД.
u007
Получается, телемост и мессенджер - одно и то же внутри?
Кстати, можно попросить в телемосте режим HQ для звука? Для репетиторов иностранного языка надо. Сейчас кодек уж сильно зажатый, верха срезает, портит артикуляцию.