Работа с потоками в Django и Gunicorn
Я пытаюсь сделать бота, который каждые несколько минут делает API запрос, выполняет некоторые вычисления и некоторые действия на их основе. Я сделал так: сначала создал модель Bot
, затем модель BotGroup
, которая запускает ботов по расписанию с помощью модуля sched
.
import sched
import threading
from django.db import models
class Bot(models.Model):
on_status = models.BooleanField()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.busy = False
def run(self):
self.busy = do_stuff()
class BotGroup(models.Model):
name = models.CharField(max_length=16)
bots = models.ManyToManyField(Bot)
sch = sched.scheduler(time.time, time.sleep)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.on_status = False
self.events = {}
self.th = None
def start(self, thread):
self.th = thread
self.on_status = True
self._init_bots()
self._run()
def stop(self):
self.on_status = False
for bot in self.bot_set:
if not bot.busy:
if bot.id in self.events.keys():
for event in self.events[bot.id]:
if event in self.sch.queue:
self.sch.cancel(event)
def _init_bots(self):
self.bot_set = self.bots.all()
for bot in self.bot_set:
bot.bg = self
def _check_bots(self):
delay = self._get_delay() # Returns delay
for bot in self.bot_set:
if ((bot.busy or self.on_status and bot.on_status)
and (bot.id not in self.events.keys()
or self.events[bot.id] not in self.sch.queue)):
self.events[bot.id] = self.sch.enterabs(
delay, bot.id, bot.run)
def _run(self):
while self.on_status or self.sch.queue:
self._check_bots()
next_ev = self.sch.run(False)
if next_ev is not None:
time.sleep(min(5, next_ev))
else:
time.sleep(5)
class BotsThread(threading.Thread):
def __init__(self, bg):
super().__init__()
self.bg = bg
self.name = bg.name
def run(self):
self.bg.start(self)
def stop(self):
self.bg.stop()
Затем у меня есть представление 'app:bot'
, в котором есть кнопка запуска/остановки, которая :
{% if bg.on_status %}
<a href="{% url 'app:bot_stop' bg.id %}">STOP</a>
{% else %}
<a href="{% url 'app:bot_start' bg.id %}">START</a>
{% endif %}
Кнопка "Пуск" переходит к просмотру:
def bot_start(request, pk):
if request.method == 'GET':
bg = BotGroup.objects.get(id=pk)
th = BotsThread(bg)
th.start()
th_ready = False
while not th_ready:
time.sleep(1)
th_ready = th.is_alive()
return redirect('app:bot')
else:
return HttpResponseBadRequest('Invalid request')
Проблема начинается в представлении для рендеринга 'app:bot'
, когда я пытаюсь получить экземпляр BotGroup
. Сначала я попробовал сделать это так:
def bot(request):
if request.method == 'GET':
context = {
'bg': BotGroup.objects.get(id=1),
}
return render(request, 'bot.html', context)
else:
return HttpResponseBadRequest('Invalid request')
Но когда я использовал кнопку "Старт", она запускала тему, но на странице по-прежнему было написано START
. Затем я заметил, что если перезагрузить страницу несколько раз, то в конце концов кнопка меняется на "STOP". Но если вы перезагрузите страницу снова через некоторое время, она снова изменится на "START", хотя поток продолжает работать в фоновом режиме. Это приводит к первому вопросу:
- Почему атрибут
bg.on_status
читается какFalse
, хотя поток запущен?
Вторая и гораздо более серьезная проблема проявилась при развертывании на AWS (1 vCPU, 1Gb RAM). Сначала я использовал стандартный конфиг gunicorn:
[Unit]
Description=gunicorn daemon
Requires=app.socket
After=network.target
[Service]
User=app
Group=www-data
WorkingDirectory=/opt/app/src
ExecStart=/opt/app/ve/bin/gunicorn \
--log-level=debug \
--capture-output \
--log-file /var/log/app.log \
--access-logfile - \
--workers 3 \
--bind unix:/run/app.sock \
app.wsgi:application
[Install]
WantedBy=multi-user.target
Когда я попытался запустить бота, сначала он на некоторое время завис, а затем выдал 502. После нескольких попыток он в конце концов загрузил страницу. И так продолжается до сих пор. Я попробовал изменить рабочие gunicorn на gthread
, немного поиграл с числом, но, похоже, это только ухудшило ситуацию по сравнению с дефолтной. Поэтому мой второй вопрос:
- Для этой установки, где приложение использует несколько потоков (пока я запускаю только один экземпляр
BotGroup
, но планирую запустить несколько), какой лучший способ настроить gunicorn для 1vCPU экземпляра AWS ec2?
И поскольку я полный профан в многопоточности:
- Делаю ли я что-то неправильно в целом? Могу ли я улучшить всю настройку?