Как я чуть не выложил в продакшн тариф «10 терабайт диска за 590 рублей»
Началось всё максимально прозаично: мне нужен был VPS.
Не «какой-нибудь подешевле», а под конкретную задачу — поднять несколько контейнеров, рядом базу, и чтобы это не падало каждый раз, когда кто-то чихнёт. Задача для DevOps на полдня. Выбор хостера, как я наивно думал, — на полчаса.
Полчаса растянулись на несколько дней.
Проблема была не в том, что вариантов мало. Их много. Проблема в том, что все сравнивалки, которые я нашёл, устроены одинаково: слева фильтры — ядра, память, диск, ползунок цены, — справа выдача. То есть от меня ждут, что я приду уже со списком требований. А я прихожу с задачей. Сколько ядер нужно «чтобы не падало» — это как раз то, что я и хочу выяснить.
Дальше начинается второй круг. Открываешь три вкладки с тарифами и понимаешь, что сравнивать их между собой нельзя. У одного цена за месяц, у второго за год со скидкой 20%, у третьего красивая цифра — это первый месяц по акции. Где-то диск NVMe, где-то просто «SSD», а где-то в характеристиках вообще прочерк, и надо лезть в FAQ. И почти везде цены на агрегаторах отставали от реальных на сайте хостера — иногда отставали существенно.
В какой-то момент я поймал себя за сведением всего этого в гугл-таблицу. И вот тут сработал рефлекс: я DevOps, а у DevOps на любую ручную рутину ровно один ответ — автоматизировать. Так появился SravniVPS: агрегатор тарифов VPS/VDS, который сам ходит в API хостеров и сам следит, что у них поменялось.
Дальше — про то, как это работает, и про грабли, которые ждут любого, кто решит читать тарифы из API реальных провайдеров. Все числа настоящие, из продакшна.
Сразу дисклеймер: SravniVPS — мой проект, и он зарабатывает на партнёрских ссылках хостеров. Поэтому статья — про инженерную кухню, а не про то, какой хостинг лучше. Конкретных провайдеров я ниже не называю намеренно: претензии у меня к документации и формату ответов API, а не к компаниям.
10 240 мегабайт, которые чуть не стали гигабайтами
Начну с истории, ради которой всё это и пишу.
Один провайдер в каталоге отдаёт VPS с диском 10 ГБ за 590 рублей в месяц. Нормальная цена, ничего примечательного. Но на сайт чуть не уехал тот же самый тариф — с диском 10 терабайт. За те же 590 рублей.
Причина банальная. Поле с размером диска называлось так, будто в нём гигабайты, — и документация это подтверждала. На самом деле там лежали мегабайты. Я взял число как есть, и 10 240 уехали в каталог гигабайтами вместо десяти. Лечится делением на 1024 — но сначала надо догадаться, что делить вообще нужно. А выглядело это настолько уверенно, что почти проскочило ревью.
Документация почти никогда не совпадает с реальностью
Это главный вывод из всей затеи, и он звучит банально ровно до момента, когда сам на это напорешься.
У каждого API, с которым я работал, нашлось минимум одно расхождение между тем, что написано в доках, и тем, что реально приходит в ответе. Не «неточность формулировки» — а расхождение, на котором код молча делает не то.
Провайдер, у которого характеристики спрятаны в названии. Тип диска (SSD или NVMe) и объём видеокарты для GPU-тарифов не приходят отдельными полями вообще. Их надо вытаскивать регуляркой из текстового названия тарифа. Регулярка по названию тарифа в проде — это, конечно, то, чем хочется гордиться.
Там же выяснилось, что аккаунты на доменах .ru и .com — это два разных аккаунта с разными токенами. Токен от одного домена на другом просто перестаёт работать. Без подсказки причина не очевидна совсем: запрос уходит, ответ приходит, всё «работает», данных нет.
Провайдер, который принёс 160 тарифов вместо 20. Один и тот же тарифный план продаётся сразу в семи дата-центрах, и API честно отдаёт его семь раз подряд — как будто это семь разных тарифов. Первая же синхронизация вывалила в каталог 160 дублей.
Пришлось сравнивать тарифы не по названию, а по полному набору характеристик, и оставлять только уникальные. Кстати, именно в процессе разгребания этих дублей и вылез баг с мегабайтами.
Провайдер, у которого правильный регион нигде не написан. Запрос без региона падает с ошибкой. Запрос с очевидным московским регионом — не падает, но возвращает ровно ноль тарифов. Реальные данные лежат в другом регионе, название которого нигде не задокументировано как основное. Я нашёл его перебором.
У него же в GPU-линейке обнаружились тарифы-близнецы: одинаковый процессор, одинаковая память — 32 ядра и 256 гигабайт, — а цена 92 тысячи рублей и 260 тысяч. Почти втрое. Я долго был уверен, что где-то опять накосячил с единицами измерения. Нет: модель видеокарты и объём видеопамяти тоже оказались текстом внутри названия тарифа.
Как это работает у меня сейчас
Из двенадцати провайдеров в каталоге автоматически обновляются пять. Остальные семь либо не отдают тарифы через API вообще, либо отдают что-то, из чего каталог не собрать.
По расписанию запускается GitLab pipeline: ходит в API провайдеров, сравнивает с тем, что уже лежит в каталоге, и открывает мне на ревью гит-дифф. «Добавилось 3 тарифа, у 5 изменилась цена, 1 пропал».
Я смотрю диффы и жму «принять». Всё.
Никакой магии тут нет — это ровно та работа, которую я иначе делал бы руками, только я её не делаю. Важна не автоматизация как таковая, а то, что человек остаётся в цикле: тариф на 10 ТБ поймался именно на этапе «посмотреть глазами».
Хостер, который молча резал Telegram
Отдельный сюжет — уже не про парсинг, а про эксплуатацию.
На сайте есть форма обратной связи, она пересылает сообщения в Telegram-бота. Локально работает идеально. На боевом сервере — стабильно падает с сетевой ошибкой.
Захожу внутрь контейнера, проверяю руками. DNS резолвит api.telegram.org нормально. Обычный интернет есть, соседний сайт открывается. А соединение именно к серверам Telegram висит до таймаута.
Хостер моего же сервера фильтровал подсети Telegram. Молча. Такое приятно узнавать откуда угодно, только не из логов прода.
Менять хостинг ради одной формы не хотелось, поэтому решил в обход: поднял маленький бесплатный прокси на Cloudflare Workers. Мой сервер стучится в него, а он уже снаружи ходит в Telegram.
Смешное: вторая фича с тем же ботом — автопостинг дайджеста в Telegram-канал — про эту проблему вообще не в курсе. Она постится не с моего сервера, а из среды CI/CD. Один бот, две разные сети, два разных решения.
Побочный эффект, который оказался полезнее основного
Конвейер и так каждый раз считает, что изменилось в ценах. Выбрасывать это было жалко, поэтому я стал складывать изменения в отдельный журнал, а бот при каждом принятом обновлении публикует дайджест в Telegram-канал: что подешевело, что подорожало, что появилось, что исчезло.
Пользователю больше не надо самому ходить и мониторить — дайджест приходит сам. А у меня появился публичный журнал цен, и если завтра кто-нибудь из провайдеров нарисует скидку задним числом, это будет видно в открытой истории.
Изначально я это вообще не планировал. Оно просто лежало рядом.
Что бы я сделал иначе
Одну вещь. С самого начала фиксировал бы реальный ответ каждого API — пусть урезанный, пусть на десять строк — в тестовую заготовку, которую можно гонять офлайн, без сети.
Тогда каждая следующая правка адаптера не рисковала бы молча наступить на те же грабли, что я уже прошёл. Мегабайты вместо гигабайт я поймал глазами. В следующий раз могу и не поймать.
А в остальном доволен. Сейчас в каталоге 12 провайдеров и 161 тариф, пять провайдеров обновляются сами — от меня требуется один клик. По остальным AI собирает черновик, который я всё равно сверяю глазами с сайтом — ровно по той же причине, что и в истории выше.
И тариф с диском в 10 терабайт за 590 рублей в продакшн так и не уехал.
Если интересно, как всё это устроено изнутри — на сайте есть открытая страница методики: источники данных, формулы сортировки и честный список того, чего каталог не умеет. А дайджест изменений цен публикуется в Telegram-канале.