250 000 регистраций через Telegram-бот на национальном конкурсе
Бот для «Yoshlar ovozi» занимает меньше 300 строк кода. Через него прошло больше 250 тысяч регистраций. Рассказываю, что в нём выдержало нагрузку, что сломалось в данных и как мы переделали всё это в следующих конкурсах.
Mirodil OrtiqovБэкенд-разработчик
Когда Агентство по делам молодёжи пришло с задачей, она звучала просто: нужен бот, через который молодые исполнители со всей страны подадут заявку на вокальный конкурс «Yoshlar ovozi». Имя, адрес, возрастная категория, направление, телефон. Всё.
Простые задачи в масштабе страны перестают быть простыми. Вот цифры, которые накопились в базе бота за все сезоны:
Последняя цифра — первое, что удивило нас самих. Об этом ниже.
Почему бот, а не сайт
Аудитория конкурса — молодёжь от 10 до 30 лет, в том числе из районов, где интернет медленный, а сайты открываются через раз. Telegram есть у всех. Его не надо устанавливать, в нём не надо придумывать пароль, а номер телефона можно отправить одной кнопкой.
Сайт с формой здесь проиграл бы по трём пунктам: его надо найти, он долго грузится на слабом 3G, и форму на 7 полей на телефоне заполнять неудобно. В боте каждый вопрос приходит отдельным сообщением, и человек отвечает на него, как в обычной переписке.
Как был устроен бот
Первая версия была очень прямолинейной: aiogram 2, SQLite в одном файле, один процесс на long polling, Docker. Никакого Redis, никакой админки. Состояние регистрации хранилось прямо в таблице заявок, в колонке state.
state = db.get_user_state(user_id) # последняя заявка этого пользователя
if state == "region":
if message.text in regions:
db.update_user_region(user_id, message.text)
await message.answer("Tuman (shahar)ni tanlang", reply_markup=districts_kb)
elif state == "district":
db.update_user_district(user_id, message.text) # без проверки — это ошибка
Каждое нажатие «Boshlash» создавало новую строку. Это сделали специально: один человек мог зарегистрировать нескольких участников подряд. Бот даже подсказывал после успешной заявки: «Если есть другие участники, нажмите Boshlash ещё раз».
Отсюда и среднее 5,6 участника на аккаунт. Мы закладывали это для случаев, когда у ребёнка нет своего Telegram и заявку подаёт родитель или руководитель кружка. Но масштаб такой «групповой» подачи мы не предсказывали.
Нагрузку этот простой стек выдержал. Каждый шаг регистрации — одна короткая запись в базу, а SQLite с одним писателем справляется с таким потоком без проблем. Узким местом оказался не сервер, а мы сами в одном месте: проверка подписки на каналы.
Сначала бот перед каждым сообщением проверял, подписан ли человек на два канала организаторов. Это два лишних запроса к Telegram API на каждое нажатие, а за все сезоны таких нажатий набрались миллионы. В итоге проверку отключили одной строкой: return True в начале функции. Некрасиво, но честно.
Где сломались данные
Сервер жил, а вот данные — не очень. Когда мы закрыли сезон и посмотрели на выгрузку целиком, картина была такая:
- в поле «район» оказалось 3 385 разных значений, хотя в Узбекистане около двухсот районов и городов;
- в поле «махалля» — почти 45 тысяч разных написаний;
- в поле «направление» кроме четырёх правильных вариантов нашлись «Estrada» с большой буквы, «16-22 yosh» и даже «Sport».
Причина одна. На шаге с районом бот принимал любой текст, а махаллю вообще просили ввести вручную. Клавиатура с кнопками у нас была one_time_keyboard: после одного нажатия она прячется, и часть людей просто печатала ответ сама. Бот послушно сохранял.
Где люди бросали регистрацию
Начатых регистраций было около 274 тысяч, завершённых — 250 тысяч. Около 24 тысяч человек ушли на полпути. Мы посмотрели, на каком шаге они застряли:
Самый большой провал — на первом же вопросе. Человек нажал «Boshlash», увидел «F.I.Sh. kiriting» и ушёл. Думаю, часть просто хотела посмотреть, что внутри. Второе место — телефон: кто-то не доверяет кнопке «Отправить номер», кто-то не понимает, что её надо нажать, а не набирать номер вручную.
Махалля — на четвёртом месте, хотя это самый неприятный шаг: надо вспомнить и напечатать официальное название. В следующих ботах мы его переделали первым.
Что на самом деле нужно организаторам
Организаторам не нужна была красивая админка. Им нужны были две вещи: цифры на сегодня и таблица.
Для цифр была команда /getall: бот присылал сводку на текущий момент по ташкентскому времени — сколько всего заявок, по областям, по направлениям, по возрастам. Её отправляли руководству прямо из чата.
Для таблицы была команда /get: pandas собирал заявки в Excel и бот отправлял файл. Никаких логинов и паролей, только проверка, что Telegram ID есть в списке админов.
По распределению всё вышло ожидаемо: эстраду выбрали больше половины участников, следом шло традиционное исполнение (макомы, катта ашула), затем бахшичилик и опера. Самая большая возрастная группа — 16–22 года.
Kitobxon: вторая попытка, уже с базой махаллей
Когда мы делали боты для конкурсов чтения «Yosh Kitobxon» и «Kitobxon Oila», ошибки «Yoshlar ovozi» лежали у нас перед глазами. Поэтому второй заход был совсем другим:
- Django 4.2 + PostgreSQL вместо SQLite и админка, где организаторы сами меняют тексты, каналы и сроки конкурсов.
- Справочник из 9 442 махаллей и сёл, загруженный из Excel. Область → район → махалля выбираются только кнопками, и бот отвечает «выберите из предложенных», если пришёл текст.
- FSM aiogram вместо колонки
stateв таблице заявок: профиль пользователя отдельно, участие в конкретном конкурсе отдельно. - Возраст по дате рождения, а не по выбранной категории. В «Yosh Kitobxon» участвуют от 10 до 30 лет, и бот сам отсекает тех, кто не подходит.
- Несколько конкурсов в одном боте, у каждого свои даты начала и конца регистрации.
- Excel по каждому конкурсу отдельно, а не одна общая таблица.
Каскад из 9 442 вариантов в виде кнопок звучит страшно, но в одном районе обычно несколько десятков махаллей, так что клавиатура остаётся разумной. После этого вопросы «что за махалля „Navbaxor MFY 2-uy“?» у организаторов закончились.
Что я бы проверил перед следующим национальным конкурсом
Если к вам придёт похожая задача, вот список, который я теперь держу в голове с первого дня:
- Поля, по которым будет статистика, принимают только значения из справочника
- Справочник регионов, районов и махаллей загружен до запуска, а не «потом допишем»
- Вопрос с самым большим отвалом (обычно первый) сделан максимально коротким и понятным
- Телефон принимается только кнопкой
request_contact, с понятным текстом рядом - Проверка подписки на каналы — один раз при старте, а не на каждое сообщение
- Отдельная команда или админка с цифрами «на сейчас» для организаторов
- Выгрузка в Excel, которую можно открыть без обработки
- Один пользователь может подать несколько заявок, и это видно в отчёте
Самое неожиданное в этом проекте: бот на 300 строк не упал под четвертью миллиона заявок. Проблемы были не в коде, который обрабатывал нагрузку, а в коде, который принимал ответы. Если вы тоже собираете заявки на конкурс или мероприятие, посмотрите, как мы делаем Telegram-боты, и кейсы Yoshlar ovozi и Yosh Kitobxon. А если вы ещё выбираете между ботом и сайтом, у нас есть отдельный текст о том, что делать первым.



