Как правильно сделать рефакторинг кода
Цель рефакторинга - сделать бота более оптимизированным. Бот для одного приложения с 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 шт):
Выглядит, что класс Bot взял на себя больше отвественности чем нужно. Я бы вынес в отдельную часть обязаность опрашивать чаты и детектить там команды, после чего ставил бы их в очередь, а бот бы из очереди просто читал. Возможно отправлял в асинхрон на исполнение воркерам. Ниже попытался оформить идею в виде схемки.

Преимущества такого решения, что мы можем независимо оптимизировать или масштабировать эти две части. Всегда выгодно разделять вещи которые требует оперативного ответа и имеют высокую нагрузку через очередь от вещей которые могут быть в фоне выполнены без жестких регламентов по времени и по окончанию оповестить о результате в нужный чат.
В идеале конечно подписаться бы на оповещение о новых сообщениях, чтобы не ддосить приложение, а получать и обрабатывать их по необходимости. Тогда вообще система становится пассивная. Если нет новых сообщений, то ни чего не грузится.
PS. я лично ботов не писал, так что смотрите на это с некоторой долей скептицизма
В целом, вам необходимо сменить Фреймворк на асинхронный.
В вашем случае я вижу синхронное выполнение 40 инстансов бота на виртуальной машине. При таком подходе именно за счёт отдачи задачи мониторить чат на поток у вас процессор и умирает.
Если ваши пользователи не ведут настолько активный обмен данными с ботом (запросы не каждую миллисекунду), это значит, что время между запросами является «мертвым временем», когда инстанс ничего не делает. Это происходит именно потому, что код у вас выполняется синхронно.
Асинхронный же код делает так:
- Создаёт задачу (например с ожиданием сообщения пользователя).
- Начинает её выполнение.
- Пока задача неактивна сообщает интерпретатору, что «эта задача требует времени, выполняйте другой код/другие задачи».
- Как только ответ по поставленной задаче пришёл, он «напоминает» о возвращении к коду после неё.
Т.е. пока задача не вернула ожидаемый результат, программа не стопорится на этом ожидании, она выполняет другие задачи.
В Python 3.5+ (если не ошибаюсь) появился модуль асинхронных задач asyncio и поддержка асинхронных задач в ситаксисе через async/await. При помощи данных нововведений создали новую библиотеку для написания асинхронных Python-ботов. Имя этой библиотеке aiogram.
Для дополнительной информации по теме асинхронности советую почитать эту статью.
Отдельной задачей также встаёт работа с базой данных, так как при таком подходе неплохо и с ней взаимодействие сделать асинхронным. Для своего бота на данный момент выбрал асинхронную библиотеку aiomysql. Работает достаточно продуктивно, хотя и имеет несколько нюансов, рассмотрение которых выходит за рамки данного вопроса.
Надеюсь, автор уже давно решил проблемы своего бота, а данный ответ послужит отправной точкой в асинхронные фреймворки людям, так или иначе посетивших данный вопрос.