С чего все началось

Работаю в 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 группы микросервисов:

  1. Получение событий из Telegram. Сервис обладает POST контроллером для получения действий пользователя из чата с ботом. А также Kafka Producer, отправляющего данные сообщения в обработку. Позже, я добавил к этому сервису Redis для проверки частоты кликов пользователя на кнопки в боте

  2. Обработчики событий. В нее входят:

    1. Consumer сообщений из входного потока сообщений, и producer в выходной поток сообщений

    2. Сервисы карты мира

    3. Объекты для взаимодействия (мобы, NPC, здания, лут и т.д.)

    4. Пользователи и их характеристики

    5. Квестовый движок (Состояния, диалоги, скрипты)

  3. Отправка событий в Telegram. Сервис получает сообщения из выходного потока и отправляет их в телеграм. Попутно проверяя их на лимиты.

Для чего пригодился Redis

Я искал способ на проверку частоты отправки и наткнулся на такую возможность Redis как PX, т.е. время жизни объекта. При получения события я проверяю на наличие объекта по ключу(id чата пользователя), а затем записываю его опять с параметром px = 1000ms.

Если при получении события объект в Redis существует, то я игнорирую данное событие.

Ключевые фишки PostgreSQL

PostgreSQL очень помог на этапе создания квестового движка. Я активно использую индексы с условием и функциональные констрейнты.

Также, очень помогла и сократила размер кода такая конструкция в запросах как INSERT ... on conflict do

Результат

Я получил горизонтально-масштабируемую систему, позволяющую довольно легко внедрять новый функционал. При повышении нагрузки - я добавляю партиции в топики Kafka и увеличиваю кол-во pod'ов, содержащие consumer’ы.

Я накидал небольшой сюжет и опубликовал все это на нескольких ресурсах. Полет нормальный, народ требует продолжения, MMO и PvP

Если тоже хотите присоединиться к волшебному миру, то добро пожаловать в EmojiHeroesBot

Комментарии (22)


  1. excentro
    08.07.2022 07:59
    +3

    Все это здорово, но хотелось бы больше техничесских подробностей..


    1. amazing_mike Автор
      08.07.2022 11:29

      Да, я напишу про детали реализации. Тут много всего использовал, что интересует больше всего?


      1. excentro
        08.07.2022 16:58

        Всё :)


    1. upagge
      09.07.2022 18:23

      Поддерживаю, пока статья больше похожа на рекламу игры, айайай))

      Хотелось бы побольше деталей, вот например

      "Также, очень помогла и сократила размер кода такая конструкция в запросах как INSERT ... on conflict do"

      Что к чему почему, как помогла, какая была проблема, никто не знает


      1. amazing_mike Автор
        09.07.2022 18:45

        Постгрес позволяет обрабатывать конфликты при добавлении записей, например:

        1. Я получаю шкуру и создаю запись с счетчиком = 1

        2. При получении второй - у меня тот же запрос, но у одного юзера может быть только одна запись - тогда я добавляю в insert конструкцию on conflict (поля констрейнта) do update set count = table_name.count +1

        Получается, что логика добавления и обновления находится в одном запросе) Сильно сокращает количество кода.

        Но при этом сложнее дебажить и тестировать, но на стадии прототипа скорость важнее ;)


  1. Brogahnl
    08.07.2022 08:20
    +4

    image


  1. rusik2293
    08.07.2022 08:50

    Прикольная идея, начал играть, иногда по нажатию ничего не происходит, это события в кафке висят?


    1. amazing_mike Автор
      08.07.2022 11:31

      Точно нет) скорее всего это сервис, отвечающий за контроль 1 сообщения в секунду на входе


  1. Kondmv
    08.07.2022 13:24
    +3

    Пара идей для реализации: Boulder Dash или River Raid.


  1. upagge
    09.07.2022 18:21
    +1

    Тоже работаю с телеграмм ботами. Правильно понимаю, что редис используется для того, чтобы не дать пользователю осуществить "dos атаку" своими сообщениями? То есть, чтобы он не мог присылать новое сообщение до того, как не было обработано старое?


    1. amazing_mike Автор
      09.07.2022 18:38

      Да, именно так. Все подключение заняло пару строчек кода, так как в нем при добавлении записи проставляю время ее существования. Если запись существует, то я не отправляю данные в следующий сервис, а просто игнорирую.

      А вот с идемпотентностью пришлось провозиться, так как телега позволяет до получения ответа отправить новый запрос. Каждая кнопка содержит инкреметальный идентификатор действия и в случае повторных отправок (при плохой сети у пользователя или при проблемах на моем сервере), либо при открытии нового сообщения и нажатия кнопок в старых - я просто произвожу обновление сообщения без совершения действия, отправляя в чат кнопки с актуальными actionId


      1. upagge
        09.07.2022 20:32

        Любопытное решение, спасибо. А пробовали снизить задержку хотя бы до 500? Или появлялись сайд-эфекты?


        1. amazing_mike Автор
          09.07.2022 21:42

          Для обновлений сообщений никак(( сразу лочит


  1. upagge
    09.07.2022 20:34

    У меня еще вопрос. Создам отдельным тредом))
    Я так понимаю есть отдельный сервис, который принимает сообщения от телеги и закидывает их в кафку? Сколько инстансов такого сервиса удается поддерживать? Я меня уже после двух телеграм начинает выдавать ошибки API, видимо потому что пытаюсь параллельно использовать один токен телеграм бота. Не было такой проблемы?


    1. amazing_mike Автор
      09.07.2022 21:45

      У меня кубер с ингрессом, под ним я делал 5 инстансов принимающего сервиса, но для телеграмма это неважно. Балансировщик один на вход. Вы вебхуками телеграмма пользуетесь?


      1. upagge
        09.07.2022 21:47

        К сожалению нет, нет технической возможности использовать вебхуки, поэтому использую long polling. Видимо с вебхуками такой проблемы нет))


  1. upagge
    09.07.2022 20:37

    Хотел еще вопрос задать, но решил зайти в бота и сам проверить. Судя по всему решения как проверять, что пользователь удалил сообщение, которое бот пытается обновить, тоже не нашлось? Эта проблема решается /reload?


    1. amazing_mike Автор
      09.07.2022 21:47

      А как такая ситуация возможна?) Пользователь удаляя сообщение - удаляет кнопки)


      1. upagge
        09.07.2022 21:49

        Ну в моих ботах немного другой кейс, пользователь помимо кнопок еще может писать текстом свои запросы. Да и кнопки по факту отправляют текст, который пользователь также прислать сам


  1. Mastermind-S
    10.07.2022 07:09
    +1

    Читая заголовок, я подумал, что ты будешь ловить спам ботов...


  1. easty
    10.07.2022 07:56

    А где про генератор игрового поля?)


  1. mmvds
    10.07.2022 08:29

    С 2017-ого есть вот такая игрушка с картой на базе эмодзи https://t.me/MindQuestBot ммо, пвп, крафт, квесты ивенты и т.д., управление только не инлайн, а кнопками лэйаут