Как открыть веб-сервер с приложениями REST API и HTML / JavaScript из существующего приложения Python? - PullRequest
3 голосов
/ 10 марта 2019

У меня есть приложение Python, которое непрерывно сканирует Интернет.Он использует пакет requests для отправки HTTP-запросов на различные интернет-сайты, такие как GitHub, Twitter и т. Д., И загружает доступные данные в файловую систему.Он также делает HTTP-запросы к REST API репозиториев GitHub и Twitter и загружает много метаданных.Это продолжает делать это в бесконечном цикле.После каждой итерации он вызывает time.sleep(3600) на 1 час перед следующей итерацией.

Теперь я хочу выставить HTTP-сервер на порт 80 из этого приложения, чтобы любой клиент мог подключиться к порту 80 этого приложения.запросить его внутреннее состояние.Например, если кто-то запускает curl http://myapp/status, он должен ответить {"status": "crawling"} или {"status": "sleeping"}.Если кто-то посещает http://myapp/status с помощью своего веб-браузера, он должен отобразить HTML-страницу с указанием статуса.На основании обнаруженного пользовательского агента он будет обслуживать как ответы REST API, так и HTML-страницы.Если по какой-либо причине мое приложение выходит из строя или дает сбой, HTTP-запросы к порту 80, конечно, должны завершаться неудачей.

Как я могу выставить такой HTTP-сервер из приложения?Я подумал об использовании Django, потому что в ходе реализации проекта ему приходится выполнять тяжелую работу, такую ​​как аутентификация, защита от CSRF-атак, принятие пользовательского ввода и выполнение запросов к имеющимся у него данным и так далее.Джанго, кажется, хорош для этой цели.Но проблема с Django заключается в том, что я не могу встроить Django в мое текущее приложение.Мне нужно запустить отдельный сервер uwsgi для обслуживания приложения Django.Та же проблема существует и с Flask.

Как правильно решить такую ​​проблему в Python?

1 Ответ

4 голосов
/ 22 апреля 2019

На мой взгляд, у вас есть два высокоуровневых способа решения этой проблемы:

  1. Имеют отдельные приложения («сервер» и «сканер»), которые имеют общее хранилище данных (база данных, Redis и т. Д.). Каждое приложение будет работать независимо, и сканер может просто обновить свой статус в общем хранилище данных. Этот подход, вероятно, мог бы масштабироваться лучше: если вы раскручиваете его в чем-то вроде Docker Swarm, вы можете масштабировать экземпляры искателя столько, сколько можете себе позволить.
  2. Имейте одно приложение, которое порождает отдельные потоки для искателя и сервера. Поскольку они находятся в одном и том же процессе, вы можете делиться информацией между ними немного быстрее (хотя, если это просто статус сканера, это не должно иметь большого значения) Преимущество этой опции, похоже, заключается в трудности ее раскрутки - вам не понадобится общее хранилище данных и вам не нужно управлять более чем одной службой.

Я бы лично склонялся к (1) здесь, потому что каждая из частей проще. Далее следует решение (1) и быстрое и грязное решение (2).

1. Отдельные процессы с общим хранилищем данных

Я бы использовал Docker Compose для обработки всех служб. Это добавляет дополнительный уровень сложности (так как вам нужно установить Docker), но значительно упрощает управление сервисами.

Весь стек Docker Compose

Опираясь на пример конфигурации здесь Я бы сделал ./docker-compose.yaml файл, который выглядит как

version: '3'
services:
  server:
    build: ./server
    ports:
      - "80:80"
    links:
      - redis
    environment:
      - REDIS_URL=redis://cache
  crawler:
    build: ./crawler
    links:
      - redis
    environment:
      - REDIS_URL=redis://cache
  redis:
    image: "redis/alpine"
    container_name: cache
    expose: 
      - 6379

Я бы организовал приложения в отдельные каталоги, например ./server и ./crawler, но это не единственный способ сделать это. Как бы вы ни организовали их, ваши build аргументы в приведенной выше конфигурации должны совпадать.

Сервер

Я бы написал простой сервер на ./server/app.py, который бы делал что-то вроде

import os

from flask import Flask
import redis

app = Flask(__name__)
r_conn = redis.Redis(
    host=os.environ.get('REDIS_HOST'),
    port=6379
)

@app.route('/status')
def index():
    stat = r_conn.get('crawler_status')
    try:
        return stat.decode('utf-8')
    except:
        return 'error getting status', 500

app.run(host='0.0.0.0', port=8000)

Наряду с этим ./server/requirements.txt файл с зависимостями

Flask
redis

И, наконец, ./server/Dockerfile, который сообщает Docker, как построить ваш сервер

FROM alpine:latest
# install Python
RUN apk add --no-cache python3 && \
    python3 -m ensurepip && \
    rm -r /usr/lib/python*/ensurepip && \
    pip3 install --upgrade pip setuptools && \
    rm -r /root/.cache
# copy the app and make it your current directory
RUN mkdir -p /opt/server
COPY ./ /opt/server
WORKDIR /opt/server
# install deps and run server
RUN pip3 install -qr requirements.txt
EXPOSE 8000
CMD ["python3", "app.py"]

Стоп, чтобы проверить, что все в порядке

На этом этапе, если вы откроете приглашение CMD или терминал в каталоге с помощью ./docker-compose.yaml, вы сможете запустить docker-compose build && docker-compose up, чтобы проверить, что все собирается и работает успешно. Вам нужно будет отключить раздел crawler файла YAML (так как он еще не был записан), но вы должны иметь возможность раскрутить сервер, который общается с Redis. Если вам это нравится, раскомментируйте раздел crawler YAML и продолжайте.

Процесс сканирования

Так как Docker обрабатывает перезапуск процесса поиска, вы можете просто написать это как очень простой скрипт на Python. Что-то вроде ./crawler/app.py может выглядеть как

from time import sleep
import os
import sys

import redis

TIMEOUT = 3600  # seconds between runs
r_conn = redis.Redis(
    host=os.environ.get('REDIS_HOST'),
    port=6379
)

# ... update status and then do the work ...
r_conn.set('crawler_status', 'crawling')
sleep(60)
# ... okay, it's done, update status ...
r_conn.set('crawler_status', 'sleeping')

# sleep for a while, then exit so Docker can restart
sleep(TIMEOUT)
sys.exit(0)

А потом, как и прежде, вам нужен ./crawler/requirements.txt файл

redis

И (очень похоже на сервер) ./crawler/Dockerfile

FROM alpine:latest
# install Python
RUN apk add --no-cache python3 && \
    python3 -m ensurepip && \
    rm -r /usr/lib/python*/ensurepip && \
    pip3 install --upgrade pip setuptools && \
    rm -r /root/.cache
# copy the app and make it your current directory
RUN mkdir -p /opt/crawler
COPY ./ /opt/crawler
WORKDIR /opt/crawler
# install deps and run server
RUN pip3 install -qr requirements.txt
# NOTE that no port is exposed
CMD ["python3", "app.py"]

Wrapup

В 7 файлах у вас есть два отдельных приложения, управляемых Docker, а также экземпляр Redis. Если вы хотите масштабировать его, вы можете посмотреть опцию --scale для docker-compose up. Это не обязательно простейшее решение, но оно управляет некоторыми неприятными моментами в управлении процессами. Для справки я также сделал для него репозиторий Git здесь .

Чтобы запустить его как автономный сервис, просто запустите docker-compose up -d.

Отсюда вы можете и должны добавить более качественную регистрацию в сканер. Конечно, вы можете использовать Django вместо Flask для сервера (хотя я более знаком с Flask, и Django может вводить новые зависимости). И, конечно, вы всегда можете сделать это более сложным.

2. Отдельный процесс с резьбой

Это решение не требует Docker, и для управления им требуется только один файл Python. Я не буду писать полное решение, если OP не захочет, но базовый эскиз будет выглядеть примерно так:

import threading
import time

from flask import Flask

STATUS = ''

# run the server on another thread
def run_server():
    app = Flask(__name__)
    @app.route('/status')
    def index():
        return STATUS
server_thread = threading.Thread(target=run_server)
server_thread.start()

# run the crawler on another thread
def crawler_loop():
    while True:
        STATUS = 'crawling'
        # crawl and wait
        STATUS = 'sleeping'
        time.sleep(3600)
crawler_thread = threading.Thread(target=crawler_loop)
crawler_thread.start()

# main thread waits until the app is killed
try:
    while True:
        sleep(1)
except:
    server_thread.kill()
    crawler_thread.kill()

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

...