Тестирую свой пет-проект. Потому что могу
Первый шаг моего открытого эксперимента — архитектура и стратегия тестирования 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.
Как это работает:
- Пользователь открывает WebApp → запрос идёт через HTTPS на Caddy (reverse proxy);
- Caddy перенаправляет запрос на WebApp (AioHTTP);
- WebApp загружает данные и юзер создаёт новый фильтр;
- Изменения сохраняются в таблицах
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 | Нормализованные объявления | Parsers | Filter, Notifier |
new_listings | Очередь новых uid для обработки | Parsers | Filter |
filters | Пользовательские фильтры | WebApp/Bot | Filter |
user_prefs | Настройки пользователя (язык и т.д.) | WebApp/Bot | Notifier |
sent_listings | Очередь задач на отправку | Filter, Notifier | Notifier |
home_archive | Архив устаревших объявлений | Cron | — |
locations | Геоиндексы и справочники | Cron | WebApp |
Да, я использую БД как очередь. Это не самое элегантное решение, но для пет-проекта вполне достаточно.
Мониторинг
Без мониторинга перформанс-тестирование — это стрельба вслепую. Поэтому:
- Prometheus снимает
/metricsсо всех сервисов: парсеры, WebApp, Bot, Filter, Notifier, БД, Caddy. - Grafana визуализирует всё что нужно: RPS парсеров, latency фильтрации, время доставки, ошибки, использование ресурсов и многое другое.
- Loki + Promtail собирают централизованные логи (по сервисам, тегам, ошибкам парсеров).
Метрики доступны на эндпоинте /metrics, логи агрегируются через Promtail, алерты настроены в Grafana. Всё как полагается!
Последовательность (end-to-end сценарий)
Как выглядит полный путь обработки объявления:
| Этап | Действие | Компонент |
|---|---|---|
| S1 | Парсер получает объявления с сайта | Parser Service |
| S2 | Нормализует и сохраняет в БД | Parser Service |
| S3 | INSERT в new_listings | Parser Service |
| S4 | Фильтрация по пользователям | Filter Service |
| S5 | INSERT в sent_listings | Filter Service |
| S6 | Формирование сообщений | Notifier |
| S7 | Отправка в Telegram | Notifier + Bot |
| S8 | Маркировка как sent | Notifier |
Или ещё проще — путь от сайта до Telegram-уведомления:
Представим, что на Otodom появилось новое объявление о квартире.
- Parser его находит, извлекает все поля (цена, площадь, адрес и т.д.) и сохраняет в базу. Если это действительно новое объявление, оно попадает в
new_listings. - Filter Service замечает новый элемент, подбирает к нему пользователей, чьи фильтры совпадают, и добавляет задачи в
sent_listings. - Notifier берёт эти задачи, формирует красивые сообщения и отправляет через Telegram Bot API.
- После успешной отправки 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) | Почему именно так |
|---|---|---|---|
| Parser | Parser latency | ≤ 5 s | HTTP-запрос + парсинг + нормализация + запись |
| Filter | Filter latency | ≤ 20 s | Поиск совпадений среди всех фильтров |
| Notifier | Notifier delay | ≤ 6 s | k=3 сообщения, по 3 секунды между каждым |
| Итого | E2E (полный цикл) | ≤ 60 s | С запасом на всякие задержки |
Кстати, про то, что такое перформанс-требования и как их правильно составлять, я писал в этой статье.
Ресурсные SLO
Помимо функциональных метрик, нужно следить за инфраструктурой. Никому не нужна система, которая работает быстро, но жрёт все ресурсы и падает каждый час.
| Ресурс | Метрика | Цель | Зачем это нужно |
|---|---|---|---|
| CPU | process_cpu_seconds_total | ≤ 70% (p95) | Запас под cronjobs и пиковые нагрузки |
| Memory | process_resident_memory_bytes | Рост ≤ 10% за 48ч | Контроль утечек памяти |
| Disk I/O | node_disk_io_time_seconds_total | ≤ 70% busy | Чтобы БД не тормозила всё |
| DB latency | db_query_seconds | ≤ 50 ms | SELECT/INSERT должны летать |
Эти метрики особенно важны для Soak-теста, где система будет работать под нагрузкой 48 часов подряд.
Подход к тестированию
Тестировать такой сервис — задачка не из тривиальных. Тут не отправишь простой HTTP-запрос и не получишь в открытом виде response time.
Вся магия происходит на фоне: парсеры работают постоянно, фильтры обрабатывают данные асинхронно, уведомления отправляются с задержками. Это напоминает мне один проект, где мы тестировали репликацию данных — всё на фоне через очереди.
Как я буду нагружать систему?
Скрипт будет создавать фильтры при увеличении количества пользователей. То есть наша задача — не генерить условные реквесты в минуту, а довести систему до определённого количества созданных фильтров и остановить генерацию.
Звучит просто, но вся магия начинается дальше.
Когда появляется новое объявление, оно проходит всю цепочку:
- Parser → new_listings
- Filter Service проверяет его против всех фильтров
- Notifier отправляет уведомления всем подходящим пользователям
Чем больше фильтров в системе, тем больше работы у Filter Service и Notifier. Вот так и создаётся реальная нагрузка.
Типы тестов
Будет четыре ключевых этапа тестирования (подробнее о типах тестов я писал тут):
-
Baseline-тест
- Небольшая нагрузка, чтобы зафиксировать исходное состояние системы.
- Это наша точка отсчёта для всех дальнейших сравнений.
-
Load-тест
- Целевая нагрузка: 3 000 пользователей с 4 500 фильтрами.
- Проверяем, укладываемся ли в SLO при реалистичной нагрузке.
- Сравниваем результаты с Baseline.
-
Capacity-тест
- Увеличиваем нагрузку до предела.
- Цель — найти потолок системы. Где она начнёт ломаться?
-
Soak-тест
- Запускаем на 48 часов под постоянной нагрузкой.
- Проверяем стабильность, утечки памяти и поведение Cronjobs.
Важные нюансы
- Telegram API частично замокан — основной трафик идёт через mock, но небольшой процент (отправка одному юзеру — мне) направляется в Telegram API для проверки поведения;
- Otodom и OLX остаются реальными источниками. Их не мокаем — симулировать поведение реальных сайтов с пагинацией, анти-ботом и непредсказуемыми изменениями HTML слишком сложно и мало похоже на прод. Трафик будет постоянным: перед прогоном тестов я очищаю базу за последние N дней (по умолчанию 7 дней), чтобы парсеры заново подхватили все свои объявления и нагрузка от внешних сайтов была репрезентативной.
Что дальше?
GetMeOne начинался как маленький скрипт для поиска квартиры, а превратился в полноценный сервис с микросервисной архитектурой, мониторингом и стратегией перформанс-тестирования. Иногда сам не верю, что это происходит 🤯
Теперь этот проект — площадка для открытого эксперимента. Здесь можно наблюдать, как на практике я делаю перформанс и всё это с реальными ошибками, решениями и выводами.
В следующих частях я расскажу
- Как разворачивал мониторинг и трейсинг (Prometheus, Grafana, Loki)
- Как готовил тестовое окружение (моки, данные, скрипты)
- Как запускал тесты в JMeter и k6
- Что показали результаты и какие были сюрпризы
- Какие оптимизации пришлось делать (и пришлось ли вообще)
История только начинается. Подписывайтесь в LinkedIn (en) или Telegram (ru), будет интересно! 🚀