Создание систем реального времени на PostgreSQL: за пределами ORM

Команда Atlas Verified
·
13 февраля 2026 г.
·
8 мин чтения
Создание систем реального времени на PostgreSQL: за пределами ORM

Когда приложению нужны обновления в реальном времени, стандартный ответ в 2025 — управляемый сервис: Firebase Realtime Database, Supabase Realtime, Pusher, Ably или размещённый слой WebSocket. Они скрывают сложность отправки обновлений подключённым клиентам. Работают хорошо — пока не перестают.

В Atlas Verified мы начинали с управляемой платформы базы данных с подписками реального времени. По мере роста системы мы столкнулись с пределами этой абстракции. Ограничения пулов соединений, проблемы надёжности реального времени, потребность в пространственных запросах PostGIS и собственной логике триггеров подтолкнули нас в одном направлении: взять слой базы данных под контроль.

Мы перешли на самостоятельно размещённый PostgreSQL и создали систему реального времени с нативным механизмом LISTEN/NOTIFY и Server-Sent Events. Эта публикация рассказывает, почему мы приняли решение, как работает архитектура и чему научились.

Почему мы ушли с управляемой платформы

Управляемые платформы баз данных предлагают привлекательный набор: аутентификация, безопасность на уровне строк, подписки реального времени, автоматически создаваемые API и панель управления — всё сразу. Для ранних продуктов это действительно ценно. Можно создать рабочее приложение без единой строки серверного кода.

Проблемы возникают по мере развития приложения и расхождения требований с предположениями платформы.

Ограничения соединений. Управляемые платформы часто направляют соединения базы через пулер со своими ограничениями. Когда несколько серверных сервисов требуют собственных пулов — сервер API, система фоновых задач, слушатель реального времени — доступные соединения быстро исчерпываются. Мы стали обходить ограничения пулера инженерными средствами вместо решения реальных задач продукта.

Надёжность реального времени. Система реального времени управляемой платформы работала через Change Data Capture на журнале предзаписи PostgreSQL. Это архитектурно элегантно, но создавало виды сбоев, которые мы не могли контролировать. Репликация на основе WAL имеет присущую ей задержку. Сервис реального времени иногда отставал, доставляя устаревшие обновления. Переподключение после сетевых разрывов было ненадёжным. А отладка требовала доступа к инфраструктуре вне нашего контроля.

Требования к расширениям. Наше приложение обрабатывает торговые документы с географическими данными: расположением портов, маршрутами и положением судов. PostGIS, пространственное расширение PostgreSQL, необходим для этих запросов. Управляемая платформа поддерживала PostGIS, но нам требовались конкретные версии и настройки, доступные не всегда.

Собственная логика триггеров. Нам требовались триггеры базы, запускающие специфичные для приложения события, а не общие уведомления «строка изменилась». При изменении статуса обработки документа нужно уведомить конкретного подключённого клиента, который его загрузил. Реальное время платформы работало на уровне таблиц, а нам нужен уровень сущностей.

Решение о миграции было не вопросом стоимости или принципа. Оно было прагматичным: обход ограничений платформы отнимал больше времени, чем самостоятельное создание функций.

LISTEN/NOTIFY: скрытый механизм Pub/Sub PostgreSQL

PostgreSQL содержит встроенный механизм публикации и подписки с версии 9.0, выпущенной в 2010. Несмотря на возраст более 15 лет, LISTEN/NOTIFY удивительно мало используется. Большинство разработчиков не знают о его существовании.

Механизм прост. Любое соединение с базой может выполнить LISTEN для именованного канала и подписаться. Любое соединение может выполнить NOTIFY для отправки сообщения всем слушателям канала. Полезная нагрузка — строка, ограниченная 8,000 байтами.

Что делает LISTEN/NOTIFY мощным для приложений реального времени:

Нулевые дополнительные расходы на инфраструктуру. Не нужен отдельный брокер сообщений для развёртывания, мониторинга или масштабирования. Если есть PostgreSQL, есть pub/sub. Ни Redis, ни RabbitMQ, ни Kafka для этого конкретного случая не нужны.

Транзакционные гарантии. Сообщения NOTIFY доставляются только при фиксации содержащей их транзакции. Если транзакция вставляет строку и отправляет уведомление, а затем откатывается, уведомление не доставляется. Внешние брокеры сообщений не дают такого свойства без тщательной оркестрации.

Интеграция триггеров. Триггеры PostgreSQL могут вызывать pg_notify при выполнении. База может автоматически уведомлять слушателей при изменении определённых данных без кода приложения в пути записи.

Ограничения реальны, и их стоит понимать:

Нет постоянного хранения. Если при срабатывании NOTIFY никто не слушает, сообщение теряется. Нет повторного воспроизведения, очереди сообщений или гарантий доставки отключённым клиентам. Слушатели должны обрабатывать переподключение и восстановление состояния.

Размер полезной нагрузки. Ограничение уведомлений в 8,000 байт означает, что через NOTIFY нельзя отправлять большие объекты. Мы отправляем минимальную нагрузку: название таблицы, тип операции и первичный ключ, а клиент при необходимости получает полные данные.

Область одной базы данных. LISTEN/NOTIFY работает внутри одного экземпляра PostgreSQL. Он не охватывает реплики или кластеры. Для архитектур, где все записи направляются в один основной экземпляр, это не ограничение.

Наша архитектура: триггеры, SSE и мультиплексирование каналов

Вот как компоненты соединяются.

Триггеры базы данных устанавливаются на таблицах, где важны обновления в реальном времени: документы, сообщения, разговоры, уведомления, проверки поставок и другие. При вставке, обновлении или удалении строки триггер вызывает pg_notify с JSON-нагрузкой, содержащей название таблицы, тип операции, первичный ключ и столбцы фильтрации.

Выделенное соединение слушателя на нашем сервере API выполняет LISTEN в канале уведомлений и получает все события триггеров. Это длительное выделенное соединение PostgreSQL, отдельное от пула запросов, которое только слушает уведомления.

Шина событий получает уведомления слушателя и направляет их подключённым клиентам по подпискам. Клиент, подписанный на документы с определённым идентификатором поставки, получает уведомления только о документах этой поставки.

Server-Sent Events доставляют уведомления в браузер. Каждый подключённый клиент открывает одно SSE-соединение с сервером API, указывая нужные каналы и фильтры. Шина событий сопоставляет входящие уведомления с подписками клиентов и отправляет совпадающие события по соответствующим SSE-соединениям.

Почему SSE вместо WebSockets

Мы неизменно довольны этим решением. Server-Sent Events проще WebSockets во всех аспектах, важных для нашего случая.

Однонаправленность по замыслу. Наши потребности реального времени направлены от сервера клиенту: сервер сообщает об изменениях данных. Клиенту не нужно отправлять сообщения в реальном времени серверу, для этого он использует обычные HTTP-запросы. Однонаправленная модель SSE соответствует реальным требованиям.

Нативная поддержка HTTP. SSE-соединения — стандартные HTTP-запросы с определённым типом содержимого. Они работают через любые HTTP-прокси, балансировщики и CDN без специальной настройки. WebSockets требуют HTTP Upgrade, который некоторые прокси обрабатывают неверно, а некоторые корпоративные межсетевые экраны полностью блокируют.

Автоматическое переподключение. EventSource API браузера нативно обрабатывает переподключение. При разрыве браузер автоматически переподключается с экспоненциальным увеличением задержки. С WebSockets вы реализуете логику сами и как минимум однажды ошибаетесь.

Единственная область преимущества WebSockets — двусторонняя связь — нам просто не нужна. Для приложений с сообщениями от клиента серверу в реальном времени WebSockets — правильный выбор. Для приложений, отправляющих уведомления от сервера клиенту, правильный выбор — SSE.

Создание слоя запросов без ORM

Уходя с управляемой платформы, мы отказались и от её автоматически создаваемого API запросов. Нам нужен был слой доступа к базе для сервера API.

Стандартные варианты — ORM вроде Prisma, TypeORM или Drizzle либо чистый SQL с драйвером. Мы выбрали третий путь: лёгкий конструктор запросов с цепочным API, генерирующий и выполняющий чистый SQL внутри.

Знакомый API. Наш существующий код был написан под клиентскую библиотеку управляемой платформы. Совместимый API означал минимум изменений при миграции. Запросы, прежде выполнявшиеся через клиент платформы, могли работать через наш конструктор с тем же синтаксисом.

Без магии. ORM генерируют SQL, который вы не видите и иногда не контролируете. При медленном запросе нужно читать созданный SQL, часто мало похожий на вызов ORM, который его породил. Наш конструктор создаёт предсказуемый SQL: каждый цепочный метод соответствует определённой SQL-конструкции, и созданный запрос всегда можно изучить.

Вложенные объединения. Торговые данные реляционны. У поставки есть документы, у документов — узлы, у узлов — проверки. Возможность выражать вложенные выборки и получать вложенный результат без ручного написания сложного JOIN каждый раз значительно улучшает опыт разработки.

Результат — слой запросов в несколько сотен строк кода, обрабатывающий подавляющее большинство взаимодействий с базой и создающий SQL, который всегда можно прочитать и оптимизировать.

Бюджетирование пулов соединений

При самостоятельном размещении PostgreSQL бюджет соединений принадлежит вам. Параметр max_connections задаёт абсолютный предел, и каждое соединение потребляет память.

Мы обнаружили, что представление соединений как бюджета — конечного ресурса, общего для сервисов, — предотвращает класс проблем, с которыми сталкиваются команды без такого планирования.

Каждый сервис объявляет максимальное число соединений. Сумма максимумов должна оставаться значительно ниже предела PostgreSQL с запасом для разовых соединений: сеансов psql, скриптов миграции и мониторинга. Это кажется очевидным, но большинство команд не делает этого до исчерпания соединений в промышленной эксплуатации. Заранее планируя бюджет, мы избежали этого конкретного вызова в 3 часа ночи.

Безопасные миграции в большом масштабе

Миграция с управляемой платформы на собственный PostgreSQL означала перенос сотен таблиц, функций, триггеров и индексов. Но более сложной задачей стало создание системы миграций, продолжающей работать по мере развития приложения.

Наша автоматическая система работает так: файлы миграций названы временными метками и находятся в отдельном каталоге. Исполнитель сканирует каталог, сравнивает с таблицей применённых миграций и выполняет ожидающие по порядку. Каждая миграция идёт внутри транзакции: при сбое она полностью откатывается и блокирует развёртывание до исправления.

Исполнитель миграций автоматически запускается при каждом развёртывании до старта приложения. Изменения схемы применяются как часть развёртывания без ручных шагов. Разработчик создаёт SQL-файл с временной меткой, отправляет в репозиторий, и следующее развёртывание применяет его.

Ключевые безопасные решения: миграции используют идемпотентные защиты, чтобы не отказывать при частичном применении. Каждая должна быть самостоятельным SQL-скриптом без участия человека. При сбое транзакция откатывается целиком: приложение не запускается с частично изменённой схемой.

Что мы получили

Переход на собственный PostgreSQL с собственной системой реального времени потребовал значительных инженерных вложений. Но отдача оказалась существенной:

Надёжность. Уведомления реального времени поступают через миллисекунды после изменения данных. Нет задержки репликации WAL и сбоев стороннего сервиса на критическом пути.

Возможность отладки. При проблеме мы можем проследить весь путь: триггер срабатывает, уведомление отправляется, шина маршрутизирует, SSE доставляет. Каждый этап в нашем коде, журналах и инфраструктуре.

Гибкость. Для добавления реального времени в новую таблицу достаточно одной SQL-миграции, устанавливающей триггер. Без настройки платформы, сторонней панели или обращения в поддержку.

Производительность. Прямые соединения PostgreSQL с настроенным пулом превосходят маршрутизируемые соединения через инфраструктуру управляемой платформы. После миграции задержка запросов заметно снизилась.

Компромисс — операционная ответственность. Мы управляем базой, резервным копированием, мониторингом репликации и планированием мощности. Для команды, знакомой с эксплуатацией PostgreSQL, это посильно. Для незнакомой команды абстракции управляемой платформы могут оправдывать свои ограничения.


Atlas Verified работает на самостоятельно размещённом PostgreSQL с собственной инфраструктурой реального времени, ежедневно обрабатывая тысячи проверок соответствия торговым требованиям по всей мировой цепочке поставок.

Понравилась статья?

Поделитесь со своими контактами