Пользователь
0,0
рейтинг
21 июля 2014 в 19:33

Разработка → Современный Торнадо: распределённый хостинг картинок в 30 строк кода tutorial

Впервые слышите о tornado? Слышали, но боялись асинхронности? Смотрели на него более полугода назад? Тогда я посвящаю эту статью вам.

Подготовка


Писать будем на третьем питоне. Если он не установлен, советую воспользоваться pyenv. Кроме tornado нам понадобится motor — асинхронный драйвер к mongodb:

pip3 install tornado motor


Импортируем необходимые модули


import bson
import motor
from tornado import web, gen, ioloop


Подключаемся к gridfs


Как распределённое хранилище будем использовать gridfs:

db = motor.MotorClient().habr_tornado
gridfs = motor.MotorGridFS(db)

В первой строке мы подключаемся к mongodb и выбираем базу 'habr_tornado'. Далее подключаемся к gridfs (по умолчанию это будет коллекция fs).

Upload handler


class UploadHandler(web.RequestHandler):
    @gen.coroutine
    def get(self):
        files = yield gridfs.find({}).sort("uploadDate", -1).to_list(20)
        self.render('upload.html', files=files)

    @gen.coroutine
    def post(self):
        file = self.request.files['file'][0]
        gridin = yield gridfs.new_file(content_type=file.content_type)
        yield gridin.write(file.body)
        yield gridin.close()
        self.redirect('')

Мы относледовались от tornado.web.RequestHandler. И теперь переопределяя методы get и post пишем обработчики соответствующих http запросов.

Декоратор tornado.gen.coroutine позволяет вместо асинхронных колбэков использовать генераторы. Сточка files = yield gridfs ... визуально мало чем отлечается от синхронного files = gridfs. Но функциональное различие огромно. В случае yield произойдёт асинхронный запрос к базе и ожидание его завершания. То есть пока база данных будет «думать», сайт сможет заниматься обработкой других запросов.

Итак в методе get, мы асинхронно получаем из gridfs мета-информацию о последних загруженных файлах. И направляем её в шаблон.

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

ShowImageHandler


Теперь нам нужно достать из gridfs и отобразить полученное изображение:

class ShowImageHandler(web.RequestHandler):
    @gen.coroutine
    def get(self, img_id):
        try:
            gridout = yield gridfs.get(bson.objectid.ObjectId(img_id))
        except (bson.errors.InvalidId, motor.gridfs.NoFile):
            raise web.HTTPError(404)
        self.set_header('Content-Type', gridout.content_type)
        self.set_header('Content-Length', gridout.length)
        yield gridout.stream_to_handler(self)


Здесь мы обрабатываем только GET хттп запрос. Сначала мы асинхронно получаем файл из gridfs по id. Этот id уникален и был автоматически сгенерирован при сохрании изображения в UploadHandler. Если в процессе возникают исключения (некорректный id или отсутствует файл) — показываем 404-ю страницу. Далее устанавливаем соответствующие заголовки, чтобы браузер идентифицировал ответ как изображение. И асинхронно отдаём тело картинки.

Роутинг


Для привязки наших обработчиков (UploadHandler и ShowImageHandler) к url, создадим экземпляр tornado.web.Application:

app = web.Application([
    web.url(r'/', UploadHandler),
    web.url(r'/imgs/([\w\d]+)', ShowImageHandler, name='show_image'),
])

Параметром мы передаём список описывающий отображение url-регулярок на их обработчики. Группа регулярки ([\w\d]+) как раз и будет передаваться в ShowImageHandler.get как img_id. А параметр name='show_image' мы будем использовать в шаблоне для генерации урл.

Запускаем сервер


app.listen(8000)
ioloop.IOLoop.instance().start()

Теперь результат можно наблюдать в браузере: http://localhost:8000/

Шаблон


<!DOCTYPE html>
<html>
    <h1>Upload an image</h1>
    <form action="" method="post" enctype="multipart/form-data">
        <input type="file" name="file" accept="image/*" onchange="javascript:this.form.submit()">
    </form>

    <h2>Recent uploads</h2>
    {% for file in files %}
        {% set url = reverse_url('show_image', file['_id']) %}
        <a href="{{ url }}"><img src="{{ url }}" style="max-width: 50px;"></a>
    {% end %}
</html>

Здесь вам всё должно быть знакомо по django или jinja. Единственное отличие: end вместо endfor

Результат


Итак мы получили быстрый, масшабируемый, асинхронный по своей сути, но написаный в псевдо-синхронном стиле хостинг картинок. А главное, теперь вы знаете как устроены: роутинг, обработчики запросов и шаблоны в tornado. А так же умеете асинхронно работать с mongodb и gridfs в частности.

Но ...


Вы наверняка заметили одно узкое место: file = self.request.files['file'][0]. Да, действительно, мы грузим весь файл изображения в память прежде чем записать его в базу. И вы наверное, подумываете что можно воспользоваться чем-то типа NginxHttpUploadModule. Однако теперь это можно сделать и средствами tornado: tornado.web.stream_request_body. Возможно, это мы и сделаем в одном из следующих уроков.

Cсылки




Ваше мнение


Понравилось ли? Стоит ли продолжать? Исправления? Пожелания?
@Imbolc
карма
22,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (16)

  • +4
    Быстрая статья :)
    Хотелось бы больше об асинхронных штуках. Использовал торнадо для вебсокетов, но с асинхронностью так и не подружился.
  • +5
    да, по торнадо — однозначно продолжать.
  • +3
    Интересует по торнадо — подробнее про концепт coroutine. И поглубже про yield.
  • 0
    Писал на торнадо аснхронный http прокси сервер, была беда, после недели работы, кончалась память, и его убивал oom. Как сейчас там с утечками?
    • 0
      Я с утечками не сталкивался ни разу. Возможно, это было что-то специфичное для прокси, какие-нибудь зависшие соединения.
    • +3
      было но давно, исправлено в какой-то версии 3.x с год назад см. changelog
  • +1
    Отличначя статья, просто и понятно. Спасибо.
  • 0
    А «распределенный» из заголовка статьи — это относится к возможности сконфигурить монгодб в кластер? А где тогда хоть пару строк про конфигурацию распределенности как базы так и самого приложения? Или в Торнадо есть своя уличная магия на этот счет? ;)
    • +1
      Всё верно — распределённость за счёт распределённости хранилища. Немного магии есть: tornado.process, но пользоваться ей не рекомендуется. А настройки монги и nginx мне показались лишними в этой вводной статье, тут же даже настроек самого Application нету :)
      • 0
        Спасибо. Про tornado.process — интересно почему не рекомендуется, из-за расширенных возможностей выстрелить в ногу при рестарте или IO операциях?
        • 0
          А вот тут подробно написано.
  • +1
    У меня ламерский вопрос. Почему для веб-сервисов используют медленные управляемые языки? Разве это не увеличивает нагрузку? Или узкое место — канал и диск?
    • +3
      Если вы про Python, Ruby и т.п., то тут чаще роль играет скорость разработки. Как-то в одной из презентаций яндекса рассказывалось, как они писали один из своих сервисов (вроде афишу), и для прототипирования было решено взять Django. Начали, посмотрели, подумали, так и остались на джанге (сейчас правда уже не знаю на чём, может поменялось). Во всяком случае подобный довод я слышу чаще всего, их много разных.
    • +3
      Очень многие задачи упираются в IO, а не в CPU. Чаще всего в БД. Для таких задач то, что они реализованы на Python или Ruby, не имеет существенной разницы с точки зрения производительности — от переписывания на Java/C#/C++/asm сильно быстрее не станет. А вот скорость разработки будет различаться в разы.
    • 0
      Если что-то упирается в производительность процессора/памяти, то это «что-то» обычно переписывают на чем-то более высоко-производительном. Частая связка, например, Ruby+Scala.
    • 0
      Как написали выше, то все упирается в жесткий диск, а не CPU. Если так-уж и происходит что ваше приложение где-то уперлось в CPU то обычно выносят эту часть системы на другой язык.

      К примеру обработка фото на пайтоне займет много времени и cpu конечно-же. Чтоб это более не являлось узким горлышком, его переписывают C++ и состыковывают с пайтоном через Python C API в качестве пакета. В итоге вы получаете тяжелый кусок кода на C++, который меньше жрет процессора, а всё остальное работает на пайтоне.

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