Как правильно сделать рефакторинг кода

Цель рефакторинга - сделать бота более оптимизированным. Бот для одного приложения с play market, в котором есть сообщества и чаты. somelib - это REST API библиотека для этого приложения. Столкнулся с проблемой большой нагрузки из-за многопоточности. Из-за незнания как работают потоки в python, сделал бота с такой структурой:

bot.py:

import somelib

client = somelib.Client()

# Авторизация
client.login(email=<email>, password=<password>)

# Авторизация в сообществе
sub_client = somelib.SubClient(comId=<comId>, profile=client.profile)

old = []

class Bot:

    def run(self, chatid, sql, db):

        commands = Commands()

        # Название чата
        title = sub_client.get_chat_thread(chatId=chatid).title

        while True:

            # Получение последних трёх сообщений в чате
            msg = sub_client.get_chat_messages(chatId=chatid, size=3)

            # Получение сообщения, айди сообщения, тип сообщения, ник автора, айди автора
            for message, messageId, messageType, author, authorid in zip(msg.content, msg.messageId, msg.type, msg.author, msg.author.userId):

                if messageId in old:
                    pass
                else:
                    print(f"{title} | {author} : {message}")
                    if messageType == 0 and message[0] == "!":

                        if message.lower() == "!help":
                            commands.help(chatid)

                        if message.lower() == "!flip":
                            commands.flip(chatid)

                        if message.lower() == "!top":
                            commands.top(chatid, sql)

                        if message.lower() == "!stats":
                            commands.stats(authorid, author, chatid)

                    old.append(messageId)

commands.py:

import somelib

client = somelib.Client()

client.login(email=<email>, password=<password>)
sub_client = somelib.SubClient(comId=<comId>, profile=client.profile)

class Commands:

    def help(self, chatid):
        ...
    
    def flip(self, chatid):
        ...

    def top(self, chatid, sql):
        ...

    def stats(self, authorid, author chatid)
        ...

main.py:

import sqlite3
from threading import Thread

from lib.bot import Bot

db1 = sqlite3.connect("databases/chat1.db", check_same_thread=False)
sql1 = db1.cursor()
db2 = sqlite3.connect("databases/chat2.db", check_same_thread=False)
sql2 = db2.cursor()

bot = Bot()

if __name__ == '__main__':
    print("START")
    t1 = Thread(target=bot.run, args=("aa1b9614-61df-4d3a-ac4b-a105ee75283c", sql1, db1))
    t2 = Thread(target=bot.run, args=("a5dd10aa-551d-41b9-95f6-96fd3bf087dc", sql2, db2))
    
    t1.start()
    t2.start()

    t1.join()
    t2.join()

В main.py у меня на данный момент 40 потоков, каждый из которых следит за отдельным чатом, и нагрузка на процессор в виртуальной машине 80-90%. Как сделать иначе? Или порекомендуйте статью где можно посмотреть про правильный подход к созданию ботов.


Ответы (2 шт):

Автор решения: Mike Lazko

Выглядит, что класс Bot взял на себя больше отвественности чем нужно. Я бы вынес в отдельную часть обязаность опрашивать чаты и детектить там команды, после чего ставил бы их в очередь, а бот бы из очереди просто читал. Возможно отправлял в асинхрон на исполнение воркерам. Ниже попытался оформить идею в виде схемки. введите сюда описание изображения

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

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

PS. я лично ботов не писал, так что смотрите на это с некоторой долей скептицизма

→ Ссылка
Автор решения: Anomaly

В целом, вам необходимо сменить Фреймворк на асинхронный.
В вашем случае я вижу синхронное выполнение 40 инстансов бота на виртуальной машине. При таком подходе именно за счёт отдачи задачи мониторить чат на поток у вас процессор и умирает. Если ваши пользователи не ведут настолько активный обмен данными с ботом (запросы не каждую миллисекунду), это значит, что время между запросами является «мертвым временем», когда инстанс ничего не делает. Это происходит именно потому, что код у вас выполняется синхронно.

Асинхронный же код делает так:

  1. Создаёт задачу (например с ожиданием сообщения пользователя).
  2. Начинает её выполнение.
  3. Пока задача неактивна сообщает интерпретатору, что «эта задача требует времени, выполняйте другой код/другие задачи».
  4. Как только ответ по поставленной задаче пришёл, он «напоминает» о возвращении к коду после неё.

Т.е. пока задача не вернула ожидаемый результат, программа не стопорится на этом ожидании, она выполняет другие задачи.

В Python 3.5+ (если не ошибаюсь) появился модуль асинхронных задач asyncio и поддержка асинхронных задач в ситаксисе через async/await. При помощи данных нововведений создали новую библиотеку для написания асинхронных Python-ботов. Имя этой библиотеке aiogram. Для дополнительной информации по теме асинхронности советую почитать эту статью.

Отдельной задачей также встаёт работа с базой данных, так как при таком подходе неплохо и с ней взаимодействие сделать асинхронным. Для своего бота на данный момент выбрал асинхронную библиотеку aiomysql. Работает достаточно продуктивно, хотя и имеет несколько нюансов, рассмотрение которых выходит за рамки данного вопроса.

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

→ Ссылка