Тестирую свой пет-проект. Потому что могу

07.11.2025

Первый шаг моего открытого эксперимента — архитектура и стратегия тестирования GetMeOne. В следующих частях — метрики, JMeter, k6, результаты и оптимизация. Всё как на реальном проекте.

пет-проект

🧪

Предыстория

Когда-то GetMeOne был просто моим личным маленьким пайтон-скриптом. Я устал вручную обновлять страницы OLX и Otodom, когда искал квартиру в Польше, и написал небольшой скрипт, который присылал мне новые объявления в Google Sheets.

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

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

Но перед тем как выпускать его в прод, я решил сделать то, чего обычно никто не делает с пет-проектами — протестировать перформанс. Если бы я был мануальным тестировщиком, то наверное протестил бы функционал. Но я перформанс-инженер, поэтому делаю то, что умею лучше всего, а остальное как пойдёт 😅

Кстати, вот тут можно его попробовать: GetMeOne bot.

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

В следующих статьях я расскажу, как:

  • настраиваю мониторинг, трейсинг и алерты в Grafana;
  • готовлю окружение и скрипты в разных тулах (JMeter, k6 и возможно что-то ещё);
  • анализирую результаты, оптимизирую и, надеюсь, не переписываю всё с нуля.

Это — первая статья из серии о перформансе пет-проекта. Поехали! 🚀

Содержание


Архитектура

GetMeOne — это сервис, который собирает объявления из разных источников (пока только OLX и Otodom), фильтрует их под интересы пользователя и отправляет релевантные результаты в Телеграм.

Система построена по микросервисной архитектуре, где каждый компонент выполняет свою чёткую роль. Звучит неплохо.




Пользователь и интерфейсы

Telegram User взаимодействует с системой через два входа:

  • Telegram Bot — получает новые объявления, управляет фильтрами и языком интерфейса.
  • WebApp (через Telegram WebApp) — создаёт и редактирует фильтры через UI.

Как это работает:

  1. Пользователь открывает WebApp → запрос идёт через HTTPS на Caddy (reverse proxy);
  2. Caddy перенаправляет запрос на WebApp (AioHTTP);
  3. WebApp загружает данные и юзер создаёт новый фильтр;
  4. Изменения сохраняются в таблицах filters и user_prefs.

Telegram Bot, в свою очередь, получает команды (/filters, /language, /start), обновляет настройки и получает уведомления от Notifier.


Поток данных и событий

Здесь начинается самое интересное — как данные путешествуют по системе:

  • Parsers (OLX, Otodom, Otomoto) — парсят сайты, нормализуют данные (приводят к единому формату) и делают upsert в таблицу home, а также insert в new_listings.
  • Filter Service — обрабатывает записи из new_listings (где processed=false), ищет совпадения с пользовательскими фильтрами, записывает результаты в sent_listings и помечает обработанные записи.
  • Notifier — берёт задачи из sent_listings, рендерит шаблоны (Jinja2), отправляет через Telegram Bot API. После успешной отправки обновляет статус.
  • Cronjobs — выполняют фоновую работу: чистят старые записи, архивируют устаревшие объявления и обновляют locations.

База данных (PostgreSQL)

Сердце системы — PostgreSQL. Вот основные таблицы и их роли:

ТаблицаНазначениеКто пишетКто читает
homeНормализованные объявленияParsersFilter, Notifier
new_listingsОчередь новых uid для обработкиParsersFilter
filtersПользовательские фильтрыWebApp/BotFilter
user_prefsНастройки пользователя (язык и т.д.)WebApp/BotNotifier
sent_listingsОчередь задач на отправкуFilter, NotifierNotifier
home_archiveАрхив устаревших объявленийCron
locationsГеоиндексы и справочникиCronWebApp

Да, я использую БД как очередь. Это не самое элегантное решение, но для пет-проекта вполне достаточно.


Мониторинг

Без мониторинга перформанс-тестирование — это стрельба вслепую. Поэтому:

  • Prometheus снимает /metrics со всех сервисов: парсеры, WebApp, Bot, Filter, Notifier, БД, Caddy.
  • Grafana визуализирует всё что нужно: RPS парсеров, latency фильтрации, время доставки, ошибки, использование ресурсов и многое другое.
  • Loki + Promtail собирают централизованные логи (по сервисам, тегам, ошибкам парсеров).

Метрики доступны на эндпоинте /metrics, логи агрегируются через Promtail, алерты настроены в Grafana. Всё как полагается!


Последовательность (end-to-end сценарий)

Как выглядит полный путь обработки объявления:

ЭтапДействиеКомпонент
S1Парсер получает объявления с сайтаParser Service
S2Нормализует и сохраняет в БДParser Service
S3INSERT в new_listingsParser Service
S4Фильтрация по пользователямFilter Service
S5INSERT в sent_listingsFilter Service
S6Формирование сообщенийNotifier
S7Отправка в TelegramNotifier + Bot
S8Маркировка как sentNotifier

Или ещё проще — путь от сайта до Telegram-уведомления:

Представим, что на Otodom появилось новое объявление о квартире.

  1. Parser его находит, извлекает все поля (цена, площадь, адрес и т.д.) и сохраняет в базу. Если это действительно новое объявление, оно попадает в new_listings.
  2. Filter Service замечает новый элемент, подбирает к нему пользователей, чьи фильтры совпадают, и добавляет задачи в sent_listings.
  3. Notifier берёт эти задачи, формирует красивые сообщения и отправляет через Telegram Bot API.
  4. После успешной отправки Notifier помечает записи как «доставленные».

Всё это должно происходить достаточно быстро. Но что значит быстро — это уже вопрос к следующей главе.


Ключевые принципы архитектуры

Несколько важных решений, которые делают систему надёжной:

  • Idempotency (идемпотентность):

    • Повторная вставка не создаёт дублей благодаря ON CONFLICT.
    • Ретраи парсеров и нотайфера безопасны — можно перезапускать без страха.
  • Очередь через БД: new_listings и sent_listings — это фактически внутренние очереди. Не Kafka, конечно, но для старта сойдёт.

  • Изоляция компонентов: каждый сервис делает своё дело — парсинг, фильтрация, уведомления, UI, бот. Один упал — остальные работают.

  • Наблюдаемость: все сервисы мониторятся и логируются централизованно.

  • Простота деплоя: всё крутится на одном сервере Hetzner в Docker Compose. Никаких кубернетесов (пока).


Перформанс-требования (SLO)

Когда я начал формулировать SLO (Service Level Objectives), то хотел, чтобы они отражали не просто цифры в таблице, а реальный пользовательский опыт.

Пользователь не видит latency фильтрации или глубину очереди. Он видит одно — через сколько секунд после публикации объявления на сайте, оно появится у него в уведомлениях.

Главное SLO: E2E latency

E2E latency (Source → Telegram) — время от публикации на сайте до получения уведомления.

Цель: ≤ 60 секунд (p95) ⚡

Почему 60 секунд? Потому что если объявление придёт через 5 минут, то квартиру уже кто-то другой снимет. Мы же тут серьёзными вещами занимаемся, правильно?


Компонентные SLO

Чтобы уложиться в 60 секунд end-to-end, я разбил систему на составляющие:

КомпонентМетрикаЦель (p95)Почему именно так
ParserParser latency≤ 5 sHTTP-запрос + парсинг + нормализация + запись
FilterFilter latency≤ 20 sПоиск совпадений среди всех фильтров
NotifierNotifier delay≤ 6 sk=3 сообщения, по 3 секунды между каждым
ИтогоE2E (полный цикл)≤ 60 sС запасом на всякие задержки

Кстати, про то, что такое перформанс-требования и как их правильно составлять, я писал в этой статье.


Ресурсные SLO

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

РесурсМетрикаЦельЗачем это нужно
CPUprocess_cpu_seconds_total≤ 70% (p95)Запас под cronjobs и пиковые нагрузки
Memoryprocess_resident_memory_bytesРост ≤ 10% за 48чКонтроль утечек памяти
Disk I/Onode_disk_io_time_seconds_total≤ 70% busyЧтобы БД не тормозила всё
DB latencydb_query_seconds≤ 50 msSELECT/INSERT должны летать

Эти метрики особенно важны для Soak-теста, где система будет работать под нагрузкой 48 часов подряд.


Подход к тестированию

Тестировать такой сервис — задачка не из тривиальных. Тут не отправишь простой HTTP-запрос и не получишь в открытом виде response time.

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

Как я буду нагружать систему?

Скрипт будет создавать фильтры при увеличении количества пользователей. То есть наша задача — не генерить условные реквесты в минуту, а довести систему до определённого количества созданных фильтров и остановить генерацию.

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

Когда появляется новое объявление, оно проходит всю цепочку:

  1. Parser → new_listings
  2. Filter Service проверяет его против всех фильтров
  3. Notifier отправляет уведомления всем подходящим пользователям

Чем больше фильтров в системе, тем больше работы у Filter Service и Notifier. Вот так и создаётся реальная нагрузка.


Типы тестов

Будет четыре ключевых этапа тестирования (подробнее о типах тестов я писал тут):

  1. Baseline-тест

    • Небольшая нагрузка, чтобы зафиксировать исходное состояние системы.
    • Это наша точка отсчёта для всех дальнейших сравнений.
  2. Load-тест

    • Целевая нагрузка: 3 000 пользователей с 4 500 фильтрами.
    • Проверяем, укладываемся ли в SLO при реалистичной нагрузке.
    • Сравниваем результаты с Baseline.
  3. Capacity-тест

    • Увеличиваем нагрузку до предела.
    • Цель — найти потолок системы. Где она начнёт ломаться?
  4. Soak-тест

    • Запускаем на 48 часов под постоянной нагрузкой.
    • Проверяем стабильность, утечки памяти и поведение Cronjobs.

Важные нюансы

  • Telegram API частично замокан — основной трафик идёт через mock, но небольшой процент (отправка одному юзеру — мне) направляется в Telegram API для проверки поведения;
  • Otodom и OLX остаются реальными источниками. Их не мокаем — симулировать поведение реальных сайтов с пагинацией, анти-ботом и непредсказуемыми изменениями HTML слишком сложно и мало похоже на прод. Трафик будет постоянным: перед прогоном тестов я очищаю базу за последние N дней (по умолчанию 7 дней), чтобы парсеры заново подхватили все свои объявления и нагрузка от внешних сайтов была репрезентативной.

Что дальше?

GetMeOne начинался как маленький скрипт для поиска квартиры, а превратился в полноценный сервис с микросервисной архитектурой, мониторингом и стратегией перформанс-тестирования. Иногда сам не верю, что это происходит 🤯

Теперь этот проект — площадка для открытого эксперимента. Здесь можно наблюдать, как на практике я делаю перформанс и всё это с реальными ошибками, решениями и выводами.

В следующих частях я расскажу

  • Как разворачивал мониторинг и трейсинг (Prometheus, Grafana, Loki)
  • Как готовил тестовое окружение (моки, данные, скрипты)
  • Как запускал тесты в JMeter и k6
  • Что показали результаты и какие были сюрпризы
  • Какие оптимизации пришлось делать (и пришлось ли вообще)

История только начинается. Подписывайтесь в LinkedIn (en) или Telegram (ru), будет интересно! 🚀