Pull to refresh

Потоковая архитектура бота Telegram

Level of difficultyMedium
Reading time6 min
Views3.2K

В этой статье я изложу свой взгляд на создание эффективной, модульной и масштабируемой архитектуры телеграм-бота, основанной на иерархической структуре сценарных меню и очередях обработки данных. Пожалуюсь на отсутствующие на данный момент элементы управления в чате и предложу свои костыли.

Чтобы было интересно большему числу разработчиков - я пока не буду приводить примеры кода на конкретном языке, а ограничусь концептуальным подходом, но даже так - будет очень много текста. Я предполагаю, что читатель понимает, как работает мессенджер Telegram и уже имеет опыт создания телеграм-ботов и администрирования Linux серверов.

Как это ни банально звучит, но телеграм-бот это программа, которая принимает поток входящих сообщений с телеграм-сервера и отправляет ему поток исходящих сообщений, как результат обработки входящего потока.

Получить поток входящих сообщений от телеграм-сервера бот может двумя способами:

  1. Сервер будет слать боту запросы с новыми сообщениями.

  2. Бот будет запрашивать у сервера новые сообщения.

В случае когда телеграм-сервер сам отправляет сообщения боту - он может в какой-то момент одновременно создать очень много соединений. Это само по себе затратно по ресурсам для обработки кучи параллельно входящих запросов, но еще и потребует дополнительных ресурсов сервера для сохранения данных от кучи параллельно работающих обработчиков этих запросов с возможными блокировками и прочими «радостями» в БД.

Я предпочитаю второй подход, так как в этом случае у бота больше контроля за количеством принимаемых сообщений + бонусом на телеграм-сервере «бесплатно» создается и хранится очередь входящих сообщений, из которой бот по мере своих возможностей выбирает сообщения. Мне кажется очевидным, что очередь для обработки потоков сообщений - это самый простой и эффективный подход. В этом случае каждый, из всего нескольких процессов бота, обрабатывает одновременно только одно сообщение. Для работы такого бота подойдет самый простой сервер со скромными ресурсами ядер и памяти.

Для работы потокового бота я предлагаю создать в БД две очереди обработки и отправки сообщений, хранилище состояний чатов и три параллельно работающих процесса обработчиков этих очередей, назовем их Input, Proc и Output. Проще всего это реализовать с помощью отдельных systemd сервисов, которые буду «пинать» и будить друг друга с помощью POSIX сигналов SIGCONT.

Первый обработчик Input отвечает исключительно за опрос телеграм-сервера в цикле лонг полинга с помощью getUpdates и помещения входящих сообщений в очередь обработки. Обработчики Proc и Output, когда не обрабатывают свои очереди, то «спят», пока их не пнёт Input.

Если Input принял новое сообщение из чата, но для этого чата в очереди обработки всё еще есть необработанное сообщение, то это новое входящее сообщение следует просто игнорировать и удалить, чтобы не засорять чат. Для этого в исходящую очередь Input сразу ставит сообщение deleteMessage с ID игнорируемого (удаляемого) сообщения. Если во время очередной итерации опроса телеграм-сервера Input получил и поставил сообщения в очередь обработки и/или отправки - он пинает Proc и/или Output.

Второй обработчик Proc занимается обработкой сообщений. В цикле он вынимает пачками сообщения и производит их обработку по одному с помощью сценариев и данных о состоянии конкретного чата, о которых я напишу ниже. В результате на каждое вынутое из очереди обработки сообщение он помещает в очередь вывода сообщение-ответ. Как только он завершает обработку очередной пачки - он пинает Output. Если в очереди обработки больше нет сообщений - он уходит в спячку.

Третий обработчик Output вынимает сообщения из очереди отправки и отправляет их на сервер. Главная его задача не превысить ограничения для отправки сообщений. Если в очереди отправки больше нет сообщений - он уходит в спячку.

Как я уже писал выше - я рассматриваю бота как иерархическое дерево из менюшек. Для каждого меню есть свой сценарий обработки. Например в боте @megaport_bot, на котором я сейчас тестирую описываемую архитектуру, на самом верхнем уровне иерархии находится главное меню, где можно выбрать приложение или язык общения с ботом. Все остальные меню находятся ниже в иерархии и при переходе в них можно либо продолжить работу с текущим меню, либо вернутся наверх, либо перейти дальше в нижнее меню.

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

Самое главное в сценарии это, то что для определенного шага и действия, выбираемого на этом шаге, в самом сценарии однозначно описано, какой ввод от пользователя ожидает бот. Всё остальное бот игнорирует. Это позволяет избегать неоднозначности в пользовательском вводе, например когда по целому ряду причин у пользователя может вывестись новое меню, но при этом не будет удалено предыдущее. Т.е. пользователь мог бы использовать кнопки сразу двух меню, но это не возможно, так как сценарий ждет определенного ввода и только из самого последнего меню.

Тут самое время вспомнить о хранилище состояний чатов, которое я предложил создать в БД выше по тексту. Каждый чат в телеграм имеет свой идентификатор, который удобно использовать для хранения состояния конкретного чата. Под состоянием чата я понимаю ID последнего отправленного сообщения (для editMessageText), стек меню, состояние текущего меню и другие данные конкретного пользователя.

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

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

Видимо надо объяснить, что я понимаю под приложением для бота. Как я описал выше - сам бот это набор из трех простых обработчиков очередей, я их называю движком бота. В движке может использоваться один или несколько модулей приложений. Модуль приложения - это набор сценариев меню. Таким образом при необходимости можно убрать или добавить любое приложение или запустить экземпляр движка с этим приложение как отдельного бота.

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

Например, если вы остановитесь в каком-то меню в боте @megaport_bot, а затем перейдете в бота @megaport1_bot и выполните команду /start, то вы продолжите работу там с того же самого меню. Естественно можно вернуться обратно, «рестартануть» и продолжить работу в первом боте. Сразу говорю, что номера 2 и выше хоть и зарезервированы, но сейчас на них не запущены боты.

При создании ботов мне не хватает кнопок, которые я бы назвал «внутритекстовыми». К сожалению инлайн кнопки могут быть расположены в чате только после текста сообщения, что не всегда удобно. Поэтому в своих ботах я часто использую команды в тексте для таких целей, очень часто в настройках. Или например как на скрине выше в меню просмотра мейлов можно было бы не городить костыли с «глазами», а сразу возле каждого мейла вывести «внутритекстовую» кнопку, которая вела бы прямо в меню просмотра этого мейла. Надеюсь Телеграм их когда-нибудь сделает.

С помощью обработчика Proc следует делать только самую простую обработку сообщений, которая не занимает много времени. Это важно, так как задержка в обработке каждого сообщения влияет на скорость реакции бота. По сути этот обработчик выполняет самые простые действия для обеспечения функционирования интерфейса с пользователем. Для обработки более ресурсоёмких и длительных задач лучше использовать отдельные процессы со своими очередями.

Также отдельное и большое внимание при проектировании архитектуры бота следует уделить мультиязычности. Тему мультиязычности и тяжелых задач я отложу для следующей статьи, в которой скорее всего придется более подробно изложить «непонятности» из этой статьи исходя из вопросов в комментариях.

Tags:
Hubs:
Total votes 4: ↑4 and ↓0+4
Comments7

Articles