Перевод статьи Create a scalable REST API with Falcon and RHSCL автора Shane Boulden.

В этой статье мы создадим REST API на основе фреймворка Python Falcon, потестируем производительность и попробуем его масштабировать, чтобы справиться с нагрузками.

Для реализации и тестирования нашего API нам понадобятся следующие компоненты:


Почему Falcon?


Falcon — это минималистичный веб-фреймворк для построения веб API, согласно сайту Falcon он до 10 раз быстрее чем Flask. Falcon быстрый!

Начало


Я предполагаю, что у вас уже установлен PostgreSQL (куда же мы без него). Нам необходимо создать БД orgdb и пользователя orguser.

Этому пользователю необходимо в настройках PostgreSQL в файле pg_hba.conf прописать доступ по паролю к только что созданной БД и выдать все права.

Конфигурирование базы данных завершено. Перейдем к созданию нашего Falcon приложения.

Создание API


Для нашего приложения будем использовать Python3.5.

Создадим virtualenv и установим необходимые библиотеки:

$ virtualenv ~/falconenv
$ source ~/falconenv/bin/activate
$ pip install peewee falcon gunicorn

Создадим файл ‘app.py’:

import falcon
from models import *
from playhouse.shortcuts import model_to_dict
import json


class UserIdResource():
     def on_get(self, req, resp, user_id):
         try:
             user = OrgUser.get(OrgUser.id == user_id)
             resp.body = json.dumps(model_to_dict(user))
         except OrgUser.DoesNotExist:
             resp.status = falcon.HTTP_404


class UserResource():
     def on_get(self, req, resp):
         users = OrgUser.select().order_by(OrgUser.id)
         resp.body = json.dumps([model_to_dict(u) for u in users])


api = falcon.API()

users = UserResource()
users_id = UserIdResource()

api.add_route('/users/', users)
api.add_route('/users/{user_id}', users_id)

Теперь опишем модели в файле ‘models.py’:

from peewee import *
import uuid

psql_db = PostgresqlDatabase(
    'orgdb',
    user='orguser',
    password='123456',
    host='127.0.0.1')


def init_tables():
    psql_db.create_tables([OrgUser], safe=True)


def generate_users(num_users):
    for i in range(num_users):
        user_name = str(uuid.uuid4())[0:8]
        OrgUser(username=user_name).save()


class BaseModel(Model):
    class Meta:
        database = psql_db


class OrgUser(BaseModel):
    username = CharField(unique=True)

Мы создали два вспомогательных метода для настройки приложения ‘init_tables’ и ‘generate_users’. Запустим их для инициализации приложения:

$ python
Python 3.5.1 (default, Sep 15 2016, 08:30:32) 
Type "help", "copyright", "credits" or "license" for more information.
>>> from app import *
>>> init_tables()
>>> generate_users(20)

Если вы зайдете в БД orgdb, то в таблице orguser увидите созданных пользователей.

Теперь вы можете протестировать API:

$ gunicorn app:api -b 0.0.0.0:8000
[2017-12-11 23:19:40 +1100] [23493] [INFO] Starting gunicorn 19.7.1
[2017-12-11 23:19:40 +1100] [23493] [INFO] Listening at: http://0.0.0.0:8000 (23493)
[2017-12-11 23:19:40 +1100] [23493] [INFO] Using worker: sync
[2017-12-11 23:19:40 +1100] [23496] [INFO] Booting worker with pid: 23496
$ curl http://localhost:8000/users
[{"username": "e60202a4", "id": 1}, {"username": "e780bdd4", "id": 2}, {"username": "cb29132d", "id": 3}, {"username": "4016c71b", "id": 4}, {"username": "e0d5deba", "id": 5}, {"username": "e835ae28", "id": 6}, {"username": "952ba94f", "id": 7}, {"username": "8b03499e", "id": 8}, {"username": "b72a0e55", "id": 9}, {"username": "ad782bb8", "id": 10}, {"username": "ec832c5f", "id": 11}, {"username": "f59f2dec", "id": 12}, {"username": "82d7149d", "id": 13}, {"username": "870f486d", "id": 14}, {"username": "6cdb6651", "id": 15}, {"username": "45a09079", "id": 16}, {"username": "612397f6", "id": 17}, {"username": "901c2ab6", "id": 18}, {"username": "59d86f87", "id": 19}, {"username": "1bbbae00", "id": 20}]

Тестируем API


Оценим производительность нашего API с помощью Taurus. По возможности необходимо развернуть Taurus на отдельной машине.

Устанавливаем Taurus в нашем виртуальном окружении:

$ pip install bzt

Теперь мы можем создать сценарий для нашего теста. Создаем файл bzt-config.yml следующего содержания (не забудьте указать верный IP адрес):

execution: 
   concurrency: 100 
   hold-for: 2m30s
   ramp-up: 1m
   scenario:
     requests:
       - url: http://ip-addr:8000/users/
         method: GET
         label: api
         timeout: 3s

Этот тест будет симулировать веб трафик от 100 пользователей, с нарастанием их числа в течение минуты, и держать нагрузку 2 минуты 30 секунд.

Запустим API с одним воркером:

$ gunicorn --workers 1 app:api -b 0.0.0.0:8000

Теперь мы можем запустить Taurus. При первом запуске он скачает нужные зависимости:

$ bzt bzt-config.yml -report

После установки зависимостей отобразится наша консоль с ходом исполнения теста:



Опцию -report мы используем для загрузки результатов в BlazeMeter и генерации веб-отчета.

Наше API отлично справляется со 100 пользователями. Мы достигли пропускной способности в ~1000 запросов/секунду, без ошибок и со средним временем ответом 0.1с.



Отлично, а что если пользователей будет 500? Изменим параметр concurrency на 500 в нашем файле bzt-config.yml и снова запустим Taurus.



Хм. Похоже наш одинокий воркер не справился с нагрузкой. 40% ошибок — это не дело.

Попробуем увеличить количество воркеров.

gunicorn --workers 20 app:api -b 0.0.0.0:8000



Выглядит лучше. Ошибки еще есть, но пропускная способность увеличилась до ~1500 запросов/секунду, и среднее время отклика уменьшилось до ~270 мс. Такое API уже можно использовать.

Дальнейшая оптимизация производительности


Можно настроить PostgreSQL под железо с помощью PgTune.

На сегодня все. Спасибо, что читали!

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


  1. aavezel
    19.01.2018 18:33

    фреймворк работающий через gunicorn в 2018? вы серьезно?


    1. foxin
      19.01.2018 18:55

      Поделитесь, пожалуйста, стабильными решениями.


    1. floyer
      19.01.2018 21:58

      а чем gunicorn не угодил? хорошее решение для многих ситуаций.


      1. aavezel
        22.01.2018 15:30

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


    1. vassabi
      20.01.2018 22:03

      не подскажете, через что по-вашему должен работать фреймворк в 2018 году?


    1. mozzzg
      22.01.2018 15:10

      Что, собственно, не так? Довольно часто встречаю gunicorn в продакшене.


  1. neopug
    19.01.2018 19:30

    Подскажите, это перевод или авторская статья? Скриншоты и листинги уж больно смахивают на developers.redhat.com/blog/2017/12/29/create-scalable-rest-api-falcon-rhscl


    1. artnest
      22.01.2018 15:15

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


  1. floyer
    19.01.2018 21:56

    Пытался перейти на Falcon с Flask, но документация просто отвратительная, в следствии чего так и не смог разобраться в данном фреймворке.


  1. pythonprogrammer
    19.01.2018 22:10

    Один-единственный человек пилит — вряд ли там что-то дельное может быть…


  1. ebt
    20.01.2018 02:19

    Аргумент про 10x ускорение в сравнении с Flask совершенно нерелевантен. Этот слой никогда не бывает узким местом.


  1. SerhiyRomanov
    20.01.2018 18:59

    Хотелось бы ещё увидеть сравнение с API на aiohttp.web например.