С чего все началось
Работаю в IT больше 15 лет. Чем только не занимался, но всегда следовал правилу - каждые майские праздники я пытаюсь применить на практике что-то новое.
В этом году я прочитал книгу Event Driven Microservices и загорелся потрогать Kafka как настоящий брокер событий, а не сообщений.
Идей было много, но мне хочется проверить все это под реальной нагрузкой, что сразу привело в телеграм боты, где получить +- 1000 пользователей труда не составляет.
Я все детство провозился с ASCII играми в DOS и идея пришла сама собой. В телеграме я нашел кучу текстовых квестов, но с графикой не было ни одного. Я провел небольшое исследование, как телеграм реагирует на большое количество emoji и выявил пару проблем:
Есть ограничение на 13 символов в строке, до момента, как телега не будет все ломать
На андроид, начиная с 50 символа телеграм меняет стилистику emoji
Первое решается легко, а второе потребовало добавить шапку из элементов:
Когда с идеей определился, настал момент выбирать технологии к реализации:
У меня есть кластер Kubernetes и несколько серверов
Для хранения данных я установил PostgreSQL, так как хорошо его знаю
Написал несколько скриптов на "Bashsible" для потребностей DevOps
Установил Kafka. Версия с Zookeeper и без репликации
В дальнейшем добавил Redis, но об этом позже
Сервисы решил писать на Java со Spring Boot
Для хранения и аналитики логов поставил ELK
Первая проблема с которой я столкнулся - это настройка Webhook в телеграме. Он категорически отказывался принимать мой сертификат с ingress контроллера. Хотя, любой frontend сервис, размещенный на кластер, отлично работал в браузере через https.
Я провозился несколько часов, пока не понял в чем дело. Необходимо загрузить в tls секрет всю цепочку сертификатов. Спасибо доброму человеку, написавшего эту статью: Сшиваем SSL-сертификаты
Вторая - это ограничения Telegram. В первой версии я реализовал MMO. Персонажи обновлялись сразу во всех чатах, где их можно увидеть.
Но лимиты телеграма быстро вернули меня в реальность и я решил оставить эту затею. Затем пришла пора входного потока событий. Если изменять сообщение чаще чем раз в секунду, то сразу попадаешь под ошибку подобную этой:
Too Many Requests: "{"ok":false,"error_code":429,"description":"Too Many Requests: retry after 33","parameters":{"retry_after":33}}"
К этому моменту я разбил всю систему на 3 группы микросервисов:
Получение событий из Telegram. Сервис обладает POST контроллером для получения действий пользователя из чата с ботом. А также Kafka Producer, отправляющего данные сообщения в обработку. Позже, я добавил к этому сервису Redis для проверки частоты кликов пользователя на кнопки в боте
-
Обработчики событий. В нее входят:
Consumer сообщений из входного потока сообщений, и producer в выходной поток сообщений
Сервисы карты мира
Объекты для взаимодействия (мобы, NPC, здания, лут и т.д.)
Пользователи и их характеристики
Квестовый движок (Состояния, диалоги, скрипты)
Отправка событий в Telegram. Сервис получает сообщения из выходного потока и отправляет их в телеграм. Попутно проверяя их на лимиты.
Для чего пригодился Redis
Я искал способ на проверку частоты отправки и наткнулся на такую возможность Redis как PX, т.е. время жизни объекта. При получения события я проверяю на наличие объекта по ключу(id чата пользователя), а затем записываю его опять с параметром px = 1000ms.
Если при получении события объект в Redis существует, то я игнорирую данное событие.
Ключевые фишки PostgreSQL
PostgreSQL очень помог на этапе создания квестового движка. Я активно использую индексы с условием и функциональные констрейнты.
Также, очень помогла и сократила размер кода такая конструкция в запросах как INSERT ... on conflict do
Результат
Я получил горизонтально-масштабируемую систему, позволяющую довольно легко внедрять новый функционал. При повышении нагрузки - я добавляю партиции в топики Kafka и увеличиваю кол-во pod'ов, содержащие consumer’ы.
Я накидал небольшой сюжет и опубликовал все это на нескольких ресурсах. Полет нормальный, народ требует продолжения, MMO и PvP
Если тоже хотите присоединиться к волшебному миру, то добро пожаловать в EmojiHeroesBot
Комментарии (22)
rusik2293
08.07.2022 08:50Прикольная идея, начал играть, иногда по нажатию ничего не происходит, это события в кафке висят?
amazing_mike Автор
08.07.2022 11:31Точно нет) скорее всего это сервис, отвечающий за контроль 1 сообщения в секунду на входе
upagge
09.07.2022 18:21+1Тоже работаю с телеграмм ботами. Правильно понимаю, что редис используется для того, чтобы не дать пользователю осуществить "dos атаку" своими сообщениями? То есть, чтобы он не мог присылать новое сообщение до того, как не было обработано старое?
amazing_mike Автор
09.07.2022 18:38Да, именно так. Все подключение заняло пару строчек кода, так как в нем при добавлении записи проставляю время ее существования. Если запись существует, то я не отправляю данные в следующий сервис, а просто игнорирую.
А вот с идемпотентностью пришлось провозиться, так как телега позволяет до получения ответа отправить новый запрос. Каждая кнопка содержит инкреметальный идентификатор действия и в случае повторных отправок (при плохой сети у пользователя или при проблемах на моем сервере), либо при открытии нового сообщения и нажатия кнопок в старых - я просто произвожу обновление сообщения без совершения действия, отправляя в чат кнопки с актуальными actionId
upagge
09.07.2022 20:32Любопытное решение, спасибо. А пробовали снизить задержку хотя бы до 500? Или появлялись сайд-эфекты?
upagge
09.07.2022 20:34У меня еще вопрос. Создам отдельным тредом))
Я так понимаю есть отдельный сервис, который принимает сообщения от телеги и закидывает их в кафку? Сколько инстансов такого сервиса удается поддерживать? Я меня уже после двух телеграм начинает выдавать ошибки API, видимо потому что пытаюсь параллельно использовать один токен телеграм бота. Не было такой проблемы?amazing_mike Автор
09.07.2022 21:45У меня кубер с ингрессом, под ним я делал 5 инстансов принимающего сервиса, но для телеграмма это неважно. Балансировщик один на вход. Вы вебхуками телеграмма пользуетесь?
upagge
09.07.2022 21:47К сожалению нет, нет технической возможности использовать вебхуки, поэтому использую long polling. Видимо с вебхуками такой проблемы нет))
upagge
09.07.2022 20:37Хотел еще вопрос задать, но решил зайти в бота и сам проверить. Судя по всему решения как проверять, что пользователь удалил сообщение, которое бот пытается обновить, тоже не нашлось? Эта проблема решается /reload?
amazing_mike Автор
09.07.2022 21:47А как такая ситуация возможна?) Пользователь удаляя сообщение - удаляет кнопки)
upagge
09.07.2022 21:49Ну в моих ботах немного другой кейс, пользователь помимо кнопок еще может писать текстом свои запросы. Да и кнопки по факту отправляют текст, который пользователь также прислать сам
mmvds
10.07.2022 08:29С 2017-ого есть вот такая игрушка с картой на базе эмодзи https://t.me/MindQuestBot ммо, пвп, крафт, квесты ивенты и т.д., управление только не инлайн, а кнопками лэйаут
excentro
Все это здорово, но хотелось бы больше техничесских подробностей..
amazing_mike Автор
Да, я напишу про детали реализации. Тут много всего использовал, что интересует больше всего?
excentro
Всё :)
upagge
Поддерживаю, пока статья больше похожа на рекламу игры, айайай))
Хотелось бы побольше деталей, вот например
"Также, очень помогла и сократила размер кода такая конструкция в запросах как INSERT ... on conflict do"
Что к чему почему, как помогла, какая была проблема, никто не знает
amazing_mike Автор
Постгрес позволяет обрабатывать конфликты при добавлении записей, например:
Я получаю шкуру и создаю запись с счетчиком = 1
При получении второй - у меня тот же запрос, но у одного юзера может быть только одна запись - тогда я добавляю в insert конструкцию on conflict (поля констрейнта) do update set count = table_name.count +1
Получается, что логика добавления и обновления находится в одном запросе) Сильно сокращает количество кода.
Но при этом сложнее дебажить и тестировать, но на стадии прототипа скорость важнее ;)