Эволюция PostgreSQL-хранилища размещений в Авито

Эволюция PostgreSQL-хранилища размещений в Авито

Data&AI Insights

📖 Источник: habr.com

Эволюция PostgreSQL-хранилища размещений в Авито: ретроспектива развития Listing Fee

Статья посвящена развитию продукта Listing Fee — одного из первых продуктов монетизации Авито, который с 2014 года превратился в ключевую точку входа для размещения миллионов объявлений ежедневно. Евгений Константинов, backend-инженер Авито, подробно описывает технические и архитектурные решения, с которыми столкнулась команда при росте нагрузки и объёма данных, а также рассказывает о борьбе с блокировками, ростом таблиц, дедлоками и авариями, вплоть до внедрения шардирования и перехода к eventual consistency. Материал основан на выступлении на конференции Saint HighLoad++ 2025.


Устройство размещений на Авито и роль Listing Fee

Listing Fee — это процесс публикации объявлений на площадке Авито, запущенный в 2014 году как один из первых продуктов монетизации. В терминах сервиса, listing — это не код, а сам процесс публикации объявлений. Аналогия с транспортом: размещения — это билеты, льготы — бесплатные лимиты, а проездные — пакеты размещений. Контроль осуществляется постоянно, с нагрузкой около 10 тысяч проверок в секунду.

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

В 2020 году к Listing Fee добавился альтернативный способ монетизации — CPA (Cost Per Action). В отличие от Listing Fee, где оплата происходит сразу за размещение, CPA предполагает оплату за результат (звонки, клики, чаты). При этом размещение становится почти бесплатным или безлимитным, что вызвало резкий рост количества объявлений и увеличило нагрузку на сервис.

Таким образом, Listing Fee превратился из продукта монетизации в точку входа для всех размещений, что вызвало серьёзные вызовы в работе с нагрузкой и данными.


Акт 1. Борьба с блокировками

В 2020 году, при росте нагрузки, команда унаследовала от монолита не только функциональность, но и технические подходы, включая работу с блокировками в PostgreSQL — механизмами, запрещающими одновременное изменение одних и тех же данных несколькими процессами. Блокировки увеличивали время ответа и создавали дополнительную нагрузку на базу.

Пессимистичный сценарий блокировок

Изначально блокировки накладывались по пользователю — самый пессимистичный способ, от которого быстро отказались в пользу блокировок по ресурсам (лимитам или пакетам размещений).

Долгое ожидание и перенос ожидания на бэкенд

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

Повторные попытки блокировки теперь выполнялись на бэкенде, что позволило перераспределить время ожидания из базы в сервис.

Межтабличные дедлоки

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

Оптимизация бизнес-логики с учётом CPA и безлимитных пакетов

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

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

Архитектурные изменения: асинхронность и очереди

В системе были две основные точки входа: синхронные пользовательские сценарии (мобильные и веб) и автозагрузка — инструмент для массовой загрузки и редактирования тысяч объявлений.

Автозагрузка, составлявшая до 70% размещений, была переведена в асинхронный режим с собственной очередью для управления нагрузкой. Очередь была разбита на партиции, что позволило группировать размещения по ресурсам и отправлять их пачками.

Для одиночных объявлений добавили очереди по сценариям, что дало прирост производительности от 3 до 5 раз.

Итоги первого этапа

  • Отказ от пессимистичных блокировок по пользователю в пользу блокировок по ресурсам.
  • Перенос ожидания блокировок из базы в бэкенд с использованием NOWAIT.
  • Оптимизация бизнес-логики для безлимитных размещений.
  • Введение асинхронной автозагрузки с очередями и батчами.
  • Рост нагрузки продолжился: если в 2020 году дневной объём размещений вырос на миллион за год, то к 2023 году — на миллион каждые три месяца.

Акт 2. Борьба за место на диске

К 2023 году размер таблицы размещений достиг примерно 1 ТБ, что значительно превышало рекомендуемый DBA Авито предел в 5-10 ГБ. Поддерживать такую таблицу с постоянными обновлениями и чтениями было сложно.

Проблемы с удалением данных через DELETE FROM

Первым решением было удаление старых данных с помощью DELETE FROM с гибкими правилами. Однако этот подход имел серьёзные недостатки:

  • Высокая нагрузка на базу из-за большого объёма удалений.
  • Отсутствие реального уменьшения размера таблицы, так как Vacuum не всегда освобождал страницы.
  • Двойная нагрузка из-за необходимости удаления данных и в архивном Postgres, куда шла логическая репликация.
  • Риск переполнения счётчика транзакций (Tx Wraparound) в PostgreSQL.

При переполнении счётчика PostgreSQL запускает агрессивный Vacuum, который сильно снижает производительность сервиса, иногда на несколько часов.

Итоги DELETE FROM

Удаление строк оказалось непригодным для таблиц такого размера (1 ТБ+), поскольку не уменьшало размер базы, создавало нагрузку и риски для стабильности.

Внедрение партиционирования и DETACH PARTITION

Вторым подходом стало партиционирование таблицы. Удаление данных сводилось к отключению (DETACH) старых партиций, которые затем можно было дропнуть или переместить.

Для партиционирования требовались:

  • Последовательный ключ для партиционирования.
  • Временность данных с понятным сроком годности.

У размещений срок действия есть (обычно 30 дней), но платные услуги продвижения позволяют продлевать объявления практически без ограничений, вплоть до нескольких лет.

Решение — добавить специальную партицию с ключом dt = -infinity для хранения «вечных» объявлений. Перед удалением старой партиции данные с активными объявлениями переносили в эту партицию.

Этот механизм напоминает Vacuum Freeze, но реализован на уровне бизнес-логики и до сих пор работает.

Проблемы с ключом партиционирования по datetime

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

Анализ показал, что в среднем объявление живёт около 30 дней, то есть попадает в одну-две партиции. Был реализован вероятностный подход: запросы сначала идут только в одну-две партиции, остальные опрашиваются при необходимости.

Более 99% запросов успешно обслуживались таким образом.

«Аварии первого числа»

После запуска партиционирования возникли регулярные сбои в первый день месяца — «аварии первого числа». Причина — смена партиций, при которой Postgres не запускал ANALYZE автоматически, что приводило к выбору неэффективных планов запросов (Seq Scan или неправильные индексы).

Решение — запускать ANALYZE вручную по расписанию в первый день месяца после накопления данных. Анализ стал «волшебным» инструментом, решавшим многие проблемы с индексами и производительностью.

Удаление проблемного индекса также помогло избежать неудачного выбора плана.

Однако отказ от крон-задачи ANALYZE привёл к смещению аварий на утро первого числа, что стало заметно всей компании. С тех пор дежурные следили за выполнением ANALYZE в этот период.

Другие аварии и особенности партиционирования

  • DETACH требует эксклюзивной блокировки, что иногда конфликтовало с агрессивным Vacuum и вызывало сбои.
  • Default-партиция, используемая для «вечных» данных, вызывала эксклюзивные блокировки при ATTACH, что оказалось проблемным. Решение — создать дополнительную резервную партицию.
  • Дедлоки на DDL при удалении и пересоздании триггеров возникали из-за неопределённого порядка обработки дочерних партиций. Это приводило к взаимным блокировкам. После семи итераций команда настроила эффективное удаление данных.

Акт 3. Борьба за выживание

С ростом данных и нагрузки началась финальная борьба за стабильность и выживание сервиса.

Нагрузка на pgBouncer и CPU

Команда заметила, что узким местом стал не PostgreSQL, а его балансировщик соединений pgBouncer, у которого не было метрик CPU. CPU уходил в потолок, что не отражалось в привычных метриках активных и ожидающих соединений.

Отказались от тяжёлых функций (например, внутренней аутентификации) и минимизировали использование JSON, но это не решило проблему.

В итоге под угрозой полного отказа Авито разрешили установить второй инстанс pgBouncer, что снизило нагрузку.

Переход к eventual consistency и шардированию

Для масштабирования команда перешла к режиму eventual consistency, распилив транзакции на саги с гибридным подходом: централизованное резервирование ресурсов и две асинхронные ветки — подтверждение списания ресурсов и отмена по таймауту.

Это позволило масштабироваться по ресурсам и подготовило почву для шардирования.

Шардирование было реализовано с разделением на 14 инстансов PostgreSQL и 10 инстансов MongoDB. JSON-файлы, которые росли слишком быстро и мешали, вынесли в MongoDB.

Запуск шардирования состоялся в конце декабря, а первое переключение партиций в январе. Для контроля процесса оставили дежурного.

Каждый шард не превышал 200 ГБ, что позволило Postgres работать в комфортном режиме. Кроны с ANALYZE были убраны, управление данными осталось через партиции.


Итоги и заключение

К 2025 году Авито выросло из сервиса объявлений в платформу с более чем 10 миллионами размещений в день. За последние пять лет команда:

  • Перешла от пессимистичных блокировок к управлению ожиданиями и оптимизации бизнес-логики.
  • Решила проблему роста таблиц через партиционирование и отказ от DELETE FROM.
  • Пережила «аварии первого числа» и настроила регулярный запуск ANALYZE.
  • Внедрила шардирование и переход к eventual consistency.
  • Изменила архитектуру, чтобы соответствовать новым требованиям и нагрузкам.

Главный урок — продукт со временем меняется и перестаёт быть тем, чем задумывался изначально. Важно регулярно задавать себе вопрос: "Чем сегодня является мой продукт? Нужно ли менять архитектуру, чтобы соответствовать новым реалиям?"


Материал подготовлен по мотивам выступления Евгения Константинова на конференции Saint HighLoad++ 2025, прошедшей 22-23 июня.


📢 Информация предоставлена телеграм-каналом: Data&AI Insights

🤖 Data&AI Insights - Ваш источник инсайтов о данных и ИИ

Report Page