Коли ви керуєте автоброкером в Азербайджані та імпортуєте авто з пошкодженнями зі Сполучених Штатів, ваші проблеми з програмним забезпеченням виглядають інакше, ніж у стартапу з Кремнієвої долини. Ви не оптимізуєте систему для мільйона одночасних користувачів. Ви оптимізуєте її для чіткості, безперебійної роботи та можливості самостійно виправити помилки опівночі, координуючи дії з аукціонним будинком у дванадцяти часових поясах від вас. Саме в такій ситуації я опинився, коли створював AutoMakler. Платформа обробляє все: від скрапінгу живих аукціонів та перевірки Carfax до оцінки вартості доставки та обробки платежів. Це реальна робоча система, що обслуговує реальних клієнтів, і вона працює на тому, що більшість розробників назвали б агресивно нудним стеком.
Стек, який ніхто не хоче презентувати
Тут немає React. Немає Vue. Немає Redis, Celery та WebSocket-сервера. Бекенд — це FastAPI з чистим Python. База даних — PostgreSQL. Фронтенд — це HTML, що рендериться на сервері за допомогою шаблонів Jinja2, Bootstrap та дрібки чистого JavaScript. Для скрапінгу я використовую Playwright. Усе працює як єдиний процес Python, який безпосередньо віддає HTML.
Тут немає етапу збірки. Немає папок node_modules, які потрібно перевіряти, немає транспілерів, які потрібно налаштовувати, і немає постійної зміни фронтенд-фреймворків, за якими треба встигати. Коли я розгортаю систему, я просто переміщую Python-файли та шаблони, а не оркеструю конвеєр бандлерів. Ця простота — не компроміс. У цьому і полягає весь сенс.
Як ставити завдання в чергу без брокера повідомлень
Скрапінг живого автомобільного аукціону не може відбуватися синхронно. Один цикл скрапінгу може тривати кілька секунд, поки Playwright завантажує сторінку, виконує JavaScript і витягує дані. Блокувати користувача на цей час — не варіант. Стандартний підхід передбачає встановлення Redis, налаштування Celery та запуск пулу воркерів. Я пропустив усе це.
Замість цього AutoMakler використовує Postgres як власну чергу завдань. Коли користувач запускає скрапінг, додаток записує новий рядок у таблицю tasks зі статусом pending. Фонове завдання asyncio підхоплює цей рядок і запускає скрапінг у браузері. Тим часом браузер кожні три секунди опитує легковажний ендпоінт, щоб перевірити статус. Коли статус рядка змінюється на completed, сторінка оновлюється і відображає результати.
Ця схема працює, тому що інтервал опитування достатньо короткий, щоб система здавалася чуйною, але достатньо довгий, щоб не перевантажувати сервер. Три секунди — це ціла вічність для комп'ютера і майже непомітно для людини, яка чекає на відповідь від зовнішнього сайту аукціону. База даних обробляє паралелізм нативно, і оскільки завдання — це просто рядки в Postgres, я можу переглянути чергу за допомогою простого SQL-запиту, замість того щоб копатися в логах Celery або ключах Redis.
Як підтримувати сервер у робочому стані без пулу воркерів
Автоматизація браузера потребує багато пам'яті. Запустіть занадто багато екземплярів Playwright одночасно — і ваш сервер впаде. Традиційне рішення — керований пул воркерів з лімітами паралелізму, що часто базується на тій самій комбінації Redis та Celery. Я ж використовую один рядок коду на Python: asyncio.Semaphore.
Семафор обмежує кількість одночасних екземплярів браузера, які можуть працювати. Коли надходить новий запит на скрапінг, він або миттєво займає вільний слот, або чекає, поки один звільниться. Усе це відбувається в межах того самого процесу. Немає зовнішнього оркестратора, який може вийти з ладу, немає воркерів, які можуть тихо впасти, і немає додаткової інфраструктури для моніторингу. Використання пам'яті залишається передбачуваним, а код, який захищає сервер, знаходиться поруч із кодом, який його використовує, а не прихований у маніфесті розгортання.
Маршрутизація грошей за допомогою одного URL-адреси callback
Обробка платежів принесла обмеження, яке я не міг змінити. Мій платіжний шлюз дозволяє лише одну URL-адресу callback на один рахунок мерчанта, але мені потрібно було обробляти транзакції для двох окремих проєктів через цей єдиний рахунок. Створення другого профілю мерчанта означало б додаткові комісії, додатковий комплаєнс і зайву паперову роботу, на яку у маленького брокера немає часу.
Рішенням було закодувати назву проєкту безпосередньо в рядок ID замовлення перед тим, як відправляти клієнта на шлюз. Коли callback надходить на мій сервер, AutoMakler декодує цей ID, визначає, до якого проєкту належить платіж, і спрямовує сповіщення до відповідного внутрішнього обробника. Існуюча логіка залишилася недоторканою. Це аддитивний дизайн: я не переписував процес оплати, я просто зробив так, щоб ідентифікатор ніс трохи більше контексту. Це той тип хаку, який здається очевидним озираючись назад, але економить години архітектурної гімнастики.
Чат, що працює без WebSockets
Чат підтримки клієнтів — це зазвичай те місце, де інженери здаються і додають WebSockets. Мені потрібен був вбудований обмін повідомленнями, але я також хотів зберегти мінімальне навантаження на інфраструктуру. Тому я використав ту саму стратегію опитування (polling), яка забезпечує збір даних (scrapes) з аукціонів.
Повідомлення зберігаються в Postgres. Коли користувач надсилає повідомлення, воно записується в таблицю. Клієнт опитує сервер на наявність оновлень, і інтерфейс відображає нові повідомлення та підтвердження прочитання майже в реальному часі. Щоб це працювало швидко навіть при зростанні таблиці розмов, я додав частковий індекс Postgres, який охоплює лише непрочитані повідомлення для активних розмов. База даних не витрачає ресурси на сканування старої історії, а планувальник запитів може виконувати більшість пошуків у чаті за допомогою вузького діапазонного сканування індексу.
Для чату підтримки, де затримка в кілька секунд є прийнятною, цього цілком достатньо. Користувачі отримують необхідний зворотний зв'язок, а мені ніколи не доводилося налагоджувати застаріле WebSocket-з'єднання або керувати окремим сокет-сервером.
Чесні недоліки
Ця архітектура передбачає реальні компроміси, і стверджувати протилежне було б нечесно. Опитування (polling) створює багато зайвих запитів. Кожні три секунди кожен активний клієнт звертається до сервера. Навантаження на пропускну здатність і запити вище, ніж вимагало б постійне сокет-з'єднання. Якщо процес Python перезапускається, будь-яке фонове завдання, що виконується в цей момент, миттєво переривається, оскільки немає зовнішнього воркера, який міг би підхопити його знову. Я приймаю це, тому що завдання невеликі, а вартість повторної спроби низька. Невдалий збір даних браузером може бути просто перезапущений користувачем.
У цього підходу також є межа. Якщо AutoMakler колись знадобиться обслуговувати тисячі одночасних зборів даних, однопроцесна модель з опитуванням почне працювати на межі можливостей. Але це не той бізнес, яким я займаюся. Мені потрібна надійність для десятків одночасних користувачів, а не
