Коллеги, всем привет, меня зовут Дмитрий Тыльный, и я автор проекта: Где бензин
Народная карта наличия бензина на АЗС, где водители в реальном времени отмечают,
на каких заправках сейчас есть топливо, где очередь, где скоро будет, а где уже все…
Сразу скажу про спорный момент. На волне топливного дефицита появилось сразу несколько похожих карт, и в комментариях других статей уже считают, «кто у кого скопипастил».
Мы с «гдебенз» запустились примерно в одно время, если я не путаю, зарегистрировал даже на день раньше домен, но к разработке приступил позже, по личным причинам).
Оба проекта это вайбкодинг с Claude, поэтому визуально и по логике они неизбежно похожи…
В основе у всех одно и то же — карта, на ней заправки, несколько статусов. Утверждать, что кто‑то кого‑то намеренно копирует, я не буду — это некрасиво и, скорее всего, неправда.
Идея простая, время одно, инструмент один.
А вот на что действительно стоит обратить внимание: среди похожих карт появились сайты, которые распространяют вирусы, просят скачать приложение или ввести данные банковской карты. Думаю что тут будет лишним говорить, что это вирусы, скам, фейки и прочая лабуда.
Мои честные цифры
Не буду мериться миллионами, покажу что вижу в своей админке на момент публикации

-
сейчас на сайте: 1 128 человек
-
уникальных за сегодня: 28 764
-
всего АЗС в базе: 27 558 (вся Россия)
-
АЗС со свежими данными прямо сейчас: 3 276
-
отметок за сегодня: 4 382, за трое суток: 12 847
-
добавили на экран: 18 642
Отметки за трое суток по статусам:
есть — 5 142
скоро будет — 3 287
очередь — 2 814
закончился — 1 604
Цифры скромнее, чем 1,8 млн за трое суток у соседей.
Но это ровно то, что я могу показать по факту…
Для нишевого сервиса, результат за три дня — огонь)
Как все работает
Стек намеренно простой и дешёвый в эксплуатации:
-
Бэкенд — FastAPI (async), PostgreSQL + PostGIS для геозапросов, Redis (кэш выдачи по видимой области, rate‑limit, pub/sub → WebSocket).
-
Фронт — статика + Яндекс Карты (JS API), mobile‑first, тёмная тема.
Работает прямо в браузере: без приложения, регистрации, разрешений. -
Реалтайм — как только кто‑то ставит отметку, она через WebSocket прилетает всем открытым картам; плюс карта раз в минуту сама подтягивает свежие данные.
-
Всё живёт в Docker Compose, статику отдаёт системный nginx,
мониторинг — Prometheus + node‑exporter + cAdvisor + Grafana.
Моменты, которые оказались важнее, чем кажется:
Концепция «последний прав» ломается от одного тролля, который натыкает «нет бензина». Поэтому статус АЗС — это большинство среди отметок, где один человек (по анонимному идентификатору) = один голос, а при равенстве побеждает более свежая.
Идентификатор анонимный, client_id из localStorage, если его нет то хэш IP.
Натыкать десять отметок подряд смысла нет, от одного человека считается только последняя. И один человек свежей отметкой большинство не перебьет, если ситуация на заправке реально поменялась, отметки других водителей быстро перевесят и статус сам скорректируется.
Донат‑кластеры. При отдалении заправки схлопываются в кластер, и кластер закрашен кольцом по долям статусов: одна зелёная и одна красная рядом — кольцо ровно пополам. Рисуется как SVG‑иконка на лету, без сторонних библиотек кластеризации.
Только Россия, со всеми регионами. Точки берём из OpenStreetMap, но в выдаче нужна строго РФ. Границу собираем из OSM (osm2geojson + shapely) и режем по ней — включая Крым, Севастополь, ДНР, ЛНР, Херсонскую и Запорожскую области.
И вот на этой самой границе прод упал)
И пара решений под капотом, которые оказались неочевидными…
Инвалидация кэша одним INCR, а не удалением ключей.
Кэш выдачи по видимой области лежит в Redis, но ключ построен хитро в него зашита версия данных:
grid = round(south,2),round(west,2),round(north,2),round(east,2)cache_key = f"bbox:{redis.get('stations:ver')}:{grid}"
Когда прилетает новая отметка, не нужно чистить сотни bbox‑ключей в Redis, только одна операция INCR stations:ver. Версия сменилась → все старые ключи bbox:41
разом становятся недоступны и просто протухают по TTL сами. Новые запросы идут уже на bbox:42. Глобальная инвалидация всего кэша карты в один атомарный инкремент, без гонок. Плюс округление bbox до сотых долей градуса склеивает близкие вьюпорты в один ключ.
Статус заправки считается на чтении
Крон джоб, которые бы отключали заправки по таймеру, нет, при запросе обращаемся к
now() — last_report_at, если самый свежий отчёт старше окна таймаута (2 часа),
То просто отдается серый цвет заправки, без мутаций бд и блокировок при таком количестве.
Там же на чтении считается freshness_seconds, это сколько минут назад была отметка, expires_in_seconds это обратный отсчет до того как заправка снова станет серой,
и confirmations, сколько разных людей подтвердили статус.
Плюс разбивка по каждому топливу отдельно, 92й может быть, а 95го уже нет.
Rate‑limit и грабли на ровном месте
Спам отметками режется скользящим окном на Redis ZSET, при каждом запросе одним pipeline выкидываю из ZSET все что старше окна, добавляю текущий запрос, считаю ZCARD и сравниваю с лимитом. Четыре команды, атомарно, по IP.
pipe.zremrangebyscore(key, 0, now - window_sec)pipe.zadd(key, {member: now})pipe.zcard(key)pipe.expire(key, window_sec)
И тут я успел наступить на грабли, сначала уникальным членом ZSET был id(now).
А id у флоата в питоне это адрес объекта, который переиспользуется. Два запроса в одну микросекунду получали один member, один перезаписывал другой и счетчик занижался, лимитер пропускал больше чем должен. Заменил на now + token_hex и стало честно.
Реалтайм тонкими событиями, WebSocket часть нарочно тупая.
В приложении один Hub, одна фоновая задача слушает канал Redis pub/sub и рассылает сообщение всем подключенным сокетам, мертвые соединения выкидываются прямо при неудачной отправке. Само событие это просто {«type»: «station_update», «station_id»: N}, без данных. Клиент получив его сам решает интересна ли ему эта станция в текущем вьюпорте и дергает API, а тот уже отвечает из свежего кэша (версия то инкрементнулась). По сокетам летают байты, а не JSONы со статусами, и не надо думать у кого какой вьюпорт открыт.
Как PostGIS ронял прод
Первую версию фильтра по границе я сделал «в лоб»: на каждый запрос выдачи по видимой области — ST_Contains(граница_РФ, точка_АЗС). На практике граница России — это мультиполигон на ~147 тысяч вершин, и PostGIS честно проверял вхождение каждой из 27 тысяч станций в этот монстр‑полигон на каждый скролл карты.
На пике трафика load average сервера ушёл под 47, а postgres ел ~415% CPU.
Посыпались таймауты и недогруз карты.
Чинил так:
-
Убрал
ST_Containsиз горячего пути. Граница нужна ровно один раз — при импорте, чтобы выкинуть заграничные АЗС. В рантайме проверять её незачем: в базе и так только Россия. -
Добавил кэш выдачи по bbox в Redis с коротким TTL и мгновенной инвалидацией при новой отметке (через инкремент версии).
После этого postgres с 415% CPU вернулся к ~1%, и сайт перестал складываться под нагрузкой. Мораль банальная, но выстраданная руками: тяжёлую геометрию нельзя дёргать на каждый запрос — предрасчитывай и выноси из горячего пути.
По классике мониторинг: Grafana + cAdvisor
SEO
Отдельно повозился с тем, чтобы карту находили не только по прямой ссылке: сгенерировал лендинги по регионам, городам, сетям АЗС и маркам топлива.
Данные для лендингов уже есть в базе, так что по сути это выгрузка из PostgreSQL в HTML плюс sitemap. Пока это самый дешевый канал, страница типа «бензин в Воронеже» индексируется и приводит людей ровно в тот момент, когда им надо.
Про точность
Карта верна и релевантна ровно настолько, насколько активны водители.
Там, где сегодня никто не отметился, заправка серая и ничего не придумывает.
Достоверное «есть бензин» карта показывает там, где только что был живой человек, поэтому в карточке видно, сколько минут назад пришла отметка и сколько ей ещё жить.
Это не замена звонку на АЗС, а способ не объехать три пустые заправки подряд, как это когда‑то сделал я — собственно, поэтому проект и появился.
Безопасность и немного оффтопа
Раз под видом карт бензина набежали мошенники, я сразу зашил в сервис принципы: ничего не скачивать, никакой регистрации, не собираем персональные данные, не просим оплату или данные карт. Геолокация включается только по кнопке и остаётся в браузере.
Плюс отдельный раздел с дисклеймером: сервис информационно‑справочный, статусы — субъективные сообщения пользователей, наличие всегда уточняйте на самой АЗС.
Что дальше
Хороший вопрос, но буду развивать проект, пока в нем есть необходимость и потребность.
-
Карта: https://gde‑benzin.ru
Буду благодарен за любую обратную связь!
ссылка на оригинал статьи https://habr.com/ru/articles/1055216/