Рубрики
Без рубрики

Постройте готовый к производству API с ограничителем скорости за 15 минут

Этот учебник показывает, как создать готовый к производству RESTful API с ограничителем скорости.

Автор оригинала: Pietro Grandinetti PhD.

Этот пост представляет собой учебник о том, как создать готовый к производству API с ограничителем скорости за 15 минут и со 100% воспроизводимостью.

Как только вы закончите читать его, у вас будет:

  • Научился делать API готовым к производству и воспроизводимым.
  • Узнал, как добавить ограничение скорости в API.
  • Научился использовать готовые к производству хранилища для кэширования API.
  • Есть полностью воспроизводимый шаблон проекта (через GitHub), который вы можете использовать для запуска аналогичного приложения.

История

На прошлой неделе я столкнулся с хорошим проектом, Flask-limiter . Я нашел его очень полезным для добавления ограничителя скорости к среднему API, который я создавал. Это было довольно просто, так что я действительно рекомендую этот пакет.

Однако в ретроспективе документации пакета немного не хватает воспроизводимости для производственной среды. Как я уже много раз говорил, воспроизводимость имеет значение .

Я хочу показать здесь, что я сделал для развертывания API Flask в производстве с ограничителем скорости, чтобы каждый мог воспроизвести его быстро и легко.

Инструменты, которые вам понадобятся:

  • Колба и Колба-ограничитель.
  • Gunicorn, хороший производственный сервер.
  • Docker & Docker-Compose.
  • ..- больше ничего!

Обзор сообщений

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

Затем, прежде чем играть с ограничителем скорости, я проиллюстрирую, как сделать API воспроизводимым и готовым к производству. Я считаю, что делать это именно в этот момент очень важно, потому что концептуально API-это просто микросервис в мультисервисной архитектуре. Как таковой, он должен быть прочным сам по себе.

Затем я представлю концепции ограничения скорости и покажу, как построить прототип, который интегрируется с API. Это займет всего пару минут!

Наконец, я покажу, как использовать надежное, готовое к производству хранилище (memcached) для развертывания системы, состоящей из двух микросервисов: API и кэш-хранилища.

Весь учебник можно воспроизвести с помощью моего GitHub репозитория для этого проекта.

API (или веб-сайт)

Настройка минимального API очень проста, благодаря официальной документации Flask .

Однако во многих примерах используется быстрый и грязный однофайл app.py . На мой взгляд, это ЕЩЕ НЕ готово к производству.

Production – это когда вы перестаете взламывать код и вместо этого думаете о системах и их надежности. Код должен быть в хорошей форме, чтобы его можно было легко поддерживать. Учебник Flask хорошо объясняет, как организовать код для готового к производству приложения (или API).

Прежде всего, создайте новый virtualenv и активируйте его.

python -m venv ~/.virtualenvs/prod-api
source ~/.virtualenvs/prod-api/bin/activate

Настройте корневой каталог проекта.

mkdir prod-api
cd prod-api

Ради этой статьи логика API будет простой, поэтому мне понадобится только Flask, Flask-limiter и Unicorn, то есть готовый к производству веб-сервер.

pip install flask
pip install flask-limiter
pip install gunicorn
pip freeze > requirements.txt

Для моих проектов Python я начинаю с файла .gitignore , который доступен в официальном репо GitHub.

git init
wget https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore -O .gitignore

Я хочу иметь одну конечную точку /test и другую /resource/test . Это упрощенная версия реального случая, когда ваш API имеет некоторую конечную точку по умолчанию, прослушивающую / и многие другие конечные точки для управления конкретными ресурсами. Все согласно спокойным идеям .

Для этого я сначала создам логику для ресурса REST. Поместите следующий код в новый файл flaskr/resource.py .

# flaskr/resource.py

from flask import (
    Blueprint, request, jsonify
)

bp = Blueprint('resource', __name__, url_prefix='/resource')


@bp.route('/test', methods=('GET', 'POST'))
def test():
    if request.method == 'POST':
        response = {'message': 'This was a POST'}
    else:
        response = {'message': 'This was a GET'}
    return jsonify(response), 200

Приведенный выше код использует объекты Flasks Blueprint для создания очень простой конечной точки, которая эхом отдается вызывающему абоненту независимо от того, отправили ли они запрос GET или POST .

Чертеж должен быть использован из основного приложения. Итак, я должен создать еще один файл flaskr/__init__.py . Вот код для этого файла.

# flaskr/__init__.py

import os

from flask import Flask

def create_app(test_config=None):
    # create and configure the app
    app = Flask(__name__, instance_relative_config=True)

    if test_config is None:
        # load the instance config, if it exists, when not testing
        app.config.from_pyfile('config.py', silent=True)
    else:
        # load the test config if passed in
        app.config.from_mapping(test_config)

    # ensure the instance folder exists
    try:
        os.makedirs(app.instance_path)
    except OSError:
        pass

    # a simple endpoint that says hello
    @app.route('/test')
    def hello():
        return 'Hello, World!'

  # register the blueprint
    from . import resource
    app.register_blueprint(resource.bp)

    return app

Это приложение с одним чертежом достаточно сложное, чтобы позволить мне смоделировать реальный, готовый к производству сценарий, но оно также достаточно простое, чтобы позволить мне сосредоточиться на реальной цели-использовании колбы-ограничителя в производстве воспроизводимым способом.

Прежде чем идти дальше, я хочу проверить, что все работает. Я могу запустить сервер разработки в локальном режиме и отправить несколько запросов.

В одном сеансе оболочки выполните:

FLASK_APP=flaskr flask run

и вы увидите запуск сервера разработки. Вы также увидите красное предупреждение, которое гласит:

ПРЕДУПРЕЖДЕНИЕ: Это сервер разработки. Не используйте его в производственном развертывании.

и это как раз одна из проблем, которую мы вскоре решим.

А пока запустите еще один сеанс оболочки и протестируйте три конечные точки: GET/test , getResource/test и POST/resource/test :

➜  prod-api git:(master) ✗ curl localhost:5000/test                
Hello, World!
➜  prod-api git:(master) ✗ curl -XPOST localhost:5000/resource/test
{"message":"This was a POST"}
➜  prod-api git:(master) ✗ curl localhost:5000/resource/test       
{"message":"This was a GET"}

Кажется, все работает, и ваш каталог должен быть следующим (не беспокойтесь о подпапке __pycache__ ):

.
├── flaskr
│   ├── __init__.py
│   ├── __pycache__
│   └── resource.py
├── instance
└── requirements.txt

Сделайте приложение готовым к производству

Еще до того, как углубиться в тонкости ограничителя скорости, я хочу немного остановиться и убедиться, что приложение Flask, и только приложение, готово к производству.

Проблемы, которые необходимо решить для достижения этой цели, заключаются в следующем:

  • Используйте надежный сервер (gunicorn) вместо сервера разработки.
  • Убедитесь, что приложение полностью воспроизводимо, чтобы его можно было легко развернуть в облаке, а код легче поддерживать и расширять.

Вот где Докер облегчит вам жизнь.

Dockerfile для достижения этих целей очень прост:

FROM python:3.7-slim

RUN apt-get update

COPY requirements.txt /tmp/requirements.txt
RUN pip install -r /tmp/requirements.txt

COPY . .

CMD ["gunicorn", "flaskr:create_app()", "-b", "0.0.0.0:5000", "-w", "3"]

При этом вы можете запустить API на любом сервере с помощью двух команд в оболочке:

docker build -t prod-api .
docker run --rm -p 5000:5000 prod-api

Это будет работать независимо от вашего облачного провайдера (AWS, GCP и т. Д.), Независимо от типа сервера (Linux, Windows и т. Д.). Во всех случаях API по-прежнему будет работать на сервере и прослушивать порт 5000. Затем вы можете подключить балансировщик нагрузки, обратный прокси-сервер и т. Д., Но это история для другой статьи.

Необходимость в ограничителе скорости

Если вы предоставляете сервис через API, который позволяет пользователям получать доступ к ресурсам (что на практике означает доступ к базам данных, файлам, вложенным вызовам API и сложной логике), то вам определенно следует рассмотреть возможность добавления ограничителя скорости в API.

Ограничение скорости API просто означает установку максимального количества запросов в заданный промежуток времени для каждого ресурса. Например, вы можете разрешить только 1 запрос каждые 10 секунд на ресурсе /resource/test . Один в десять секунд-это немного слишком строго, но вы понимаете: пользователь не должен злоупотреблять API, потому что это плохо повлияет на других пользователей.

Вот почему вам нужна дополнительная часть программного обеспечения, которая будет блокировать пользователей, когда они отправляют слишком много запросов за короткий промежуток времени. Эта часть программного обеспечения называется rate-limiter.

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

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

Я решил первую проблему с помощью старого трюка: я зашел на страницу проблем проекта (в GitHub) и искал ключевое слово “blueprint”. И я нашел решение немедленно!

Тогда давайте его реализуем! Создайте новый файл flaskr/core.py и поместите в него следующий код.

# flaskr/core.py

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

Затем отредактируйте flaskr/resource.py чтобы связать ограничитель с чертежом. Вот измененный файл, и я отметил, что это новые строки.

# flaskr/resource.py

from flask import (
    Blueprint, request, jsonify
)

from .core import limiter  # <------------ New line


bp = Blueprint('resource', __name__, url_prefix='/resource')

# Set a default limit of 1 request per second,
# which can be changed granurarly in each route.
limiter.limit('1/second')(bp)      # <------------ New line


@bp.route('/test', methods=('GET', 'POST'))
@limiter.limit('1 per 10 second') # <------------ New line
def test():
    if request.method == 'POST':
        response = {'message': 'This was a POST'}
    else:
        response = {'message': 'This was a GET'}
    return jsonify(response), 200

Ограничитель также должен быть инициализирован и связан с основным приложением, что требует некоторых изменений в flaskr/__init__.py файл. Вот обновленный файл с комментариями к новым строкам.

# flaskr/__init__.py

import os

from flask import Flask

from .core import limiter  # <------- New line


def create_app(test_config=None):
    # create and configure the app
    app = Flask(__name__, instance_relative_config=True)
    limiter.init_app(app) # <--------------------------------- New line

    if test_config is None:
        # load the instance config, if it exists, when not testing
        app.config.from_pyfile('config.py', silent=True)
    else:
        # load the test config if passed in
        app.config.from_mapping(test_config)

    # ensure the instance folder exists
    try:
        os.makedirs(app.instance_path)
    except OSError:
        pass

    # a simple endpoint that says hello
    @app.route('/test')
    def hello():
        return 'Hello, World!'

  # register the blueprint
    from . import resource
    app.register_blueprint(resource.bp)

    return app

Примечание Для тестирования нескольких сценариев я добавил ограничение скорости только в конечных точках ресурса ( /resource/test both GET and POST ), но не в корневой конечной точке ( GET/test ).

На этом этапе у вас должна быть структура папок, подобная следующей (опять же, не беспокойтесь о файлах .pyc :

.
├── Dockerfile
├── flaskr
│   ├── core.py
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── core.cpython-39.pyc
│   │   ├── __init__.cpython-39.pyc
│   │   └── resource.cpython-39.pyc
│   └── resource.py
├── instance
└── requirements.txt

Посмотрим, сработает ли это!

Создайте и запустите заново образ Docker:

docker build -t prod-api .
docker run --rm -p 5000:5000 prod-api

Затем в другом сеансе оболочки вызовите дважды (быстро) одну и ту же конечную точку:

➜  prod-api git:(master) ✗ curl -XPOST localhost:5000/resource/test
{"message":"This was a POST"}
➜  prod-api git:(master) ✗ curl -XPOST localhost:5000/resource/test


429 Too Many Requests

Too Many Requests

1 per 1 second

Это сработало! Ограничитель корректно отклонил второй запрос, так как после отправки первого прошло менее 10 секунд.

Давайте проверим с корневой конечной точкой.

➜  prod-api git:(master) ✗ curl localhost:5000/test                
Hello, World!
➜  prod-api git:(master) ✗ curl localhost:5000/test
Hello, World!
➜  prod-api git:(master) ✗ curl localhost:5000/test
Hello, World!
➜  prod-api git:(master) ✗ curl localhost:5000/test
Hello, World!

Это тоже работает! Поскольку я не установил никаких ограничений на эту конечную точку, API позволяет мне отправлять столько запросов, сколько мне нравится.

На данный момент у нас есть образ Docker, работающий на хорошем производственном сервере (gunicorn) с API Flask, который имеет чертежи с включенным ограничением скорости.

Мы уже закончили?

Еще не совсем.

В чем проблема? Проблема в том, что по умолчанию Flask-limiter использует хранилище “в памяти”.

Подожди, что? Да, ограничитель должен использовать физическое хранилище.

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

“В памяти” означает, что ограничитель просто хранит указатель на локальную переменную (представьте себе ее как список python), которая отслеживает прошлые запросы.

Это значение по умолчанию в Flask-limiter, потому что оно достаточно просто для разработки. Но это так не похоже на производство!

В производстве у вас обычно есть несколько контейнеров, обслуживающих один и тот же API, и все они должны быть подключены к одному и тому же хранилищу ограничителей. В противном случае запрос может быть получен контейнером, который не может проверить хранилище “в памяти” другого контейнера…и это сделало бы всю систему бесполезной.

Колба-ограничитель хорошо спроектирована и поддерживает готовые к производству типы хранения. В частности, Memcached и Redis .

Итак, давайте посмотрим как использовать Memcached для производственной настройки .

Давайте на минутку все обдумаем. В производстве у нас будет много контейнеров (мы можем даже не знать их фактического количества в любой данный момент), и все они должны использовать один и тот же сервис memcached-иначе счетчик для каждого запроса будет работать неправильно.

Это означает, что мы должны создать небольшую микросервисную архитектуру. Одна служба будет самим API, другая-хранилищем memcached. Ограничитель в первом сервисе (API) будет подключаться к хранилищу memcached во втором сервисе.

Если это звучит сложно, не волнуйтесь! Докер снова здесь, чтобы помочь нам.

На самом деле Memcached как сервис легко доступен в образе docker. На практике вам просто нужно запустить две линии!

docker pull memcached
docker run --rm -it --network host memcached

и у вас будет Memcached, работающий на вашем компьютере и прослушивающий порт 11211. Это очень просто…просто потрясающе!

Теперь мы можем вернуться к проекту Python и сказать ограничителю, что он должен использовать memcached вместо стандартного “in-memory”.

Прежде всего, нам нужны привязки python для memcached:

pip install pymemcache
pip freeze > requirements.txt

Затем нам нужно изменить ограниченные параметры так, чтобы он подключался к сервису memcached. Верно? Mmmm…be приготовились к повороту!

Вот как вы должны изменить flaskr/core.py файл, согласно документации по колбе-ограничителю.

# flaskr/core.py

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

port = '11211'  # <---- New line
host = ...      # <---- New line, but what to use as host address?
memcached_uri = f'memcached://{host}:{port}'  # <--- New line
limiter = Limiter(storage_uri=memcached_uri,  # <---- Line changed
          key_func=get_remote_address)

Возможно , вы думаете, что host должен быть localhost , или 0.0.0.0 , так как служба memcached работает на той же машине (ваш компьютер).

В этом есть смысл, но все не так просто. В зависимости от того, какую операционную систему вы используете, параметр --network host Docker может работать не всегда. В частности:

  • Если вы используете Mac, то вам нужно будет использовать host.docker.internal .
  • Если вы находитесь в Linux, вы можете использовать localhost .

Я использовал Linux, когда писал этот код, поэтому host . Обязательно измените его, если вы работаете на Mac.

После этого вы можете снова запустить API с двумя строками:

docker build -t prod-api .
docker run --rm --network host prod-api

И снова проверить:

➜  prod-api git:(master) ✗ curl localhost:5000/resource/test
{"message":"This was a GET"}
➜  prod-api git:(master) ✗ curl localhost:5000/resource/test


429 Too Many Requests

Too Many Requests

1 per 10 second

Затем подождите 10 (или более) секунд.

➜  prod-api git:(master) ✗ curl localhost:5000/resource/test
{"message":"This was a GET"}

Это работает!

Мы уже закончили?

Ну, я бы сказал, что нет. Еще не совсем!

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

  1. Войдите на свой сервер и запустите службу memcached вручную, используя docker pull memcached и docker run -itd --network host memcached
  2. Извлеките свой код из какого-нибудь репозитория.
  3. Отредактируйте код так, чтобы в flaskr/core.py переменная host установлена правильно. Как я уже объяснял, это зависит от операционной системы, используемой вашим сервером.
  4. Затем снова запустите службу API вручную с помощью docker build-t prod-api . и docker run --rm -itd --network host prod-api .

Это прекрасно сработает. Но, на мой взгляд, это не “производственная” система. Есть слишком много ручных шагов и слишком много вещей, которые могут пойти не так.

Что делать, если вы забыли запустить службу memcached?

Что делать, если вам нужно запустить новый сервер, а это другая ОС?

Что, если…слишком много проблем.

Нам нужен еще один инструмент, чтобы решить эту последнюю постоянную проблему и иметь полностью воспроизводимое решение, готовое к производству.

Введите Docker Compose

Docker Compose автоматизирует задачу создания сети служб docker, которые могут общаться друг с другом.

Чтобы создать сеть с помощью программы docker-compose , вам понадобится файл docker-compose.yml , который определяет конфигурацию сети. Вот он для наших API и memcached сервисов.

services:
  api:
    build:
      dockerfile: Dockerfile
      context: .
    ports:
      - "5000:5000"
    restart: "always"
  memcached:
    container_name: memcached
    image: memcached:latest
    ports:
      - "11211:11211"
    restart: "always"

Этот файл очень легко понять:

  • В сети есть две службы, названные api и memcached .
  • Служба api построена с помощью Dockerfile , который находится в . папка (с именем “контекст”).
  • Сервис api доступен в сети на порту 5000 .
  • Служба memcached не использует Dockerfile и вместо этого извлекается из Интернета через образ memcached:latest .
  • Он доступен в сети на порту 11211 .
  • Предполагается, что обе службы всегда перезапускаются, если одна из них или обе выходят из строя.

Основным преимуществом этого является то, что в сети docker каждой службе присваивается уникальное имя хоста . И это название-то же самое название службы.

Это означает, что служба memcached будет доступна в сети с хостом memcached (то же имя службы) и на порту 11211 .

Поэтому строка URI для подключения к нему будет memcached:11211 , независимо от того, какую ОС использует ваш компьютер (или сервер)|/.

Вот это полная воспроизводимость!

Давайте вернемся к flaskr/core.py файл и изменить его в последний раз.

# flaskr/core.py

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

port = '11211'
host = 'memcached'   # <--------------- Changed line
memcached_uri = f'memcached://{host}:{port}'
limiter = Limiter(storage_uri=memcached_uri,
          key_func=get_remote_address)

И, наконец, мы можем раскрутить весь бэкэнд (API с ограниченным + кэшированным хранилищем) с 1 строкой:

docker-compose up

Теперь мы закончили!

Если у вас возникнут какие-то проблемы с воспроизведением и развертыванием этого проекта, дайте мне знать!