Как сделать интернет-аукцион: долго, дорого, с блэкджеком

25.06.2013

Когда перед нами заказчик поставил задачу сделать аукцион, мы взялись за нее не раздумывая. А что: каталог товаров есть, пользователи есть, нужно просто запустить таймер для каждого лота и принимать ставки. После выигрыша лота идет оформление заказ как в интернет-магазине. Собственно цена и сроки проекта верстались из расчета интернет-магазин + немного непонятной фигни. Ну срок в 3 месяца.

И мы его сделали. stavkazolota.ru. Получился действительно интернет-магазин. Каждый товар выставлялся на торги и превращался в лот. Для лота система генерировала начальное время торгов, конечное время. В период активности лота аукцион принимал ставки. Простой механизм: из браузера на сервер посылается AJAX запрос со ставкой. Если время не вышло и ставка корректная она принимается. Самая важная часть аукциона — обновление лотов в браузере пользователя. Ведь другие пользователи также делают ставки и при просмотре лота мы должны видеть в реальном времени, что кто-то другой сделал ставку. «В реальном времени» — это в идеале синхронно. Ставку должны видеть сразу все. Самое первое и очевидное решение: периодически посылать AJAX запросы на сервер и просить сервер обновить информацию о просматриваемых лотах. Так и было сделано. Раз в 5 секунд мы получали данные с сервера. Когда кто-то делал ставку — время торгов увеличивалось на 15 секунд. В результате 5ти секундная задержка была незаметна.

Через пару месяцев на аукционе стало расти количество лотов (до 1000), да и посещаемость возрастала. Ставки могли происходить раз в секунду, онлайн сидело человек 50. Тут начались первые проблемы со скоростью работы. Иногда лот не обновлялся уже не 5, а 15, 20 секунд. Возросла нагрузка на сервер в целом: много запросов HTTP, много запросов к БД. Латали как могли: оптимизировали БД, убирали индексы с часто изменяемых таблиц, перестали хранить историю ставок для прошедших лотов. Не помогало., запросы никуда не денешь.

Нужен был другой подход к обновлению лотов. Спасением оказались WebSockets в реализации socket.io. Решение работало на тот момент даже в IE6, при наличии флэша. Web-Sockets и PHP сразу не подружились. Сделать что-то стабильно работающее в качестве демона на сервере из PHP скрипта может быть и можно, но видимо это для упорных. В то же время на Хабре часто стали появляться статьи про NodeJS. Это стало настоящим открытием и работающим решением для проекта в 2011 году. NodeJS легок в развертывании, быстр и для него уже тогда было написано множество хороших модулей. В том числе и soket.io.

Итак, аукцион был перенесен с AJAX polling на WebSockets. Мы получили постоянный коннект с браузером пользователя. Весь код NodeJS сервера умещался на экране. Он выполнял простую задачу — держал соединение с браузером и слушал на скрытом от мира порту HTTP команды от сайта на обновление лотов. Например, произошла ставка, сайт подключается к NodeJS и сообщает ему, что лот обновлен. NodeJS пролистывает всех подключенных клиентов и тем, у кого открыт этот лот отсылает новые данные. Получилось круто. NodeJS работал месяцами без перезапусков. Он  принимал и передавал данные.

Но проект продолжал набирать популярность. Ставки уже происходили несколько раз в секунду, пользователи бродили по сайту, используя хитрые фильтры, генерировали много запросов на чтение в том время как торги приводили к запросам на запись — БД захлебывалась. Но это было не самое страшное. Мы столкнулись с негативной стороной ZendFramework — его тормознутостью. Пока он подключит свои 200 файлов, пока сделает десяток запросов к БД просто так, чтобы инициализироваться, у нас уже образуется очередь из запросов. Все серверные оптимизации уже были на максимуме. Часть сайта лежала на диске в памяти, работал APC, memcached кэшировал частые запросы, тяжелые блоки.

Борьба за производительность пошла экстенсивным путем. Мы ушли с HETZNER, где был хороший сервер с то ли 24, то ли 32гб оперативки и каким-то начальным Xeonом. Решили попробовать облако. clodo.ru. Было создано 2 сервера: фронтенд, для работы сайта и бэкенд, для работы NodeJS и БД. MySQL перевели на репликацию. Все что на запись шло на мастер на бэкенде, чтение было на фронтенде со слейва. Что привлекло в clodo: расположение в России, лучшие чем у HETZNER ресурсы CPU. Удавалось уже выдерживать более сотни торгующихся онлайн. Но количество запросов к тяжелому PHP не давало шанса на масштабируемость всей системы.

Было принято решение переносить критичную к времени выполнения логику на JavaScript в NodeJS. В результате туда убрали почти весь аукцион кроме работы сайта. NodeJS выставлял лоты на торги, принимал ставки, закрывал лоты, принимал уведомления пользователей. Если что-то NodeJS должен был сказать сайту, то посылался HTTP запрос на  внутренний API сайта. Так, например отправлялась почта — через PHP.

Далее, на отдельных участках, где пролетает много данных и их нужно хранить (лоты, ставки) MySQL заменялся на Redis. Redis стал вторым открытием после NodeJS. Поначалу конечно использовали memcached, но потом оказалось, что Redis умеет информацию еще и сохранять на диск и мы не теряем данные всего аукциона при перезапуске сервера. Clodo как хостинг серверов не оправдал надежд из-за частых сбоев и технических работ, а также из-за непонятных провалов в производительности. Поэтому мы снова вернулись на HETZNER, а Clodo останется как точка присутствия для статики, поближе к Москве.

В этом виде, на 50% на JavaScript, на 50% на PHP сайт работает сейчас: stavakzolota.ru. Заказчик ставит цель: 40 тысяч лотов на торгах. При нынешних 6 тысячах. Но даже с учетом длинного пройденного пути проекта это не кажется нереальным.

Следующий шаг проекта: отказ от БД для ведения торгов. MySQL, даже быстрый Redis все равно являются внешними хранилищами. Лучшее хранилище — переменные приложения. Аукцион будет загружать слепок торгов из MySQL при старте и сохранять свое состояние по ходу торгов. Операции с лотами будут происходить внутри запущенного на сервере приложения на NodeJS, в его переменных. PHP останется только для генерации страниц сайта и оформления заказа.

Разрабатывая сайты в потоке привыкаешь с одним и тем же технологиям, мыслишь в их рамках. Когда возникают нетипичные задачи приходится развиваться, изучать новое. Краткая история развития и ошибок этого проекта показывает как сложно с накатанного пути прийти к правильной архитектуре с виду простой задачи — аукциона. У меня не получилось найти отечественный аналог подобного аукциона. Аукционов масса, но если открыть страницу со списком лотов, то видно как их разработчики решили проблему производительности: убрали экшен из торгов. Торги идут вяло и долго. Нет таймеров, не видно закрывающихся десятками лотов на одной странице. А аукцион — это азарт, игра, победа. Аукцион с активными торгами реально работает и продает. У них — стратегия, у нас — шутер.

Веб-студия должна всегда браться за сложные проекты. Мы гордимся дизайном. Но при этом сделали то что казалось технически неподъемный для нас http://epson.ru/. Успешный и посещаемый интернет-магазин http://sumka34.ru/. Самый крупный аукцион ювелирки: http://stavkazolota.ru/. Магазин с громадной базой: http://5karmanov.ru/. Соцсеть http://unigeo.ru/. Соцсеть http://dissp.com/. Все это становится возможным для региональной студии благодаря ориентации на нестандартные проекты на фреймворке, а не CMS. Свои программисты и их профессиональный рост — это важнейший фактор развития.

Кстати о программистах. Нам нужен веб-программист. Требования и условия те же: http://magwai.ru/vacancy/veb-zend-framework. Приходите, у нас в разработке есть несколько проектов, на которых можно круто проапгрейдиться.  Тому кто потянет, смогу поставить задачу, решение которой потребует развития и по серверной (нетипичные серверы) и по клиентской части (бутстрапы, js фреймворки). Нагрузки, сайты на нескольких серверах в hetzner, clodo, amazon. Тут было бы желание, а задача найдется.