Автор оригинала: 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.
Колба-ограничитель-это очень простой в использовании пакет для достижения этой цели. Очень четкая документация, хороший питонический код. Однако, на мой взгляд, у него есть две небольшие проблемы:
- Документы не объясняют, как использовать ограничитель в каждой отдельной конечной точке blueprints (но, к счастью, решение есть, просто в другом месте).
- Документы не очень помогают в производственном развертывании.
Я решил первую проблему с помощью старого трюка: я зашел на страницу проблем проекта (в 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/test429 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/test429 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"}
Это работает!
Мы уже закончили?
Ну, я бы сказал, что нет. Еще не совсем!
Теоретически вы можете сохранить вещи такими, какие они есть сейчас, и использовать следующие шаги для производственного развертывания.
- Войдите на свой сервер и запустите службу memcached вручную, используя
docker pull memcached
иdocker run -itd --network host memcached
- Извлеките свой код из какого-нибудь репозитория.
- Отредактируйте код так, чтобы в
flaskr/core.py
переменнаяhost
установлена правильно. Как я уже объяснял, это зависит от операционной системы, используемой вашим сервером. - Затем снова запустите службу 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
Теперь мы закончили!
Если у вас возникнут какие-то проблемы с воспроизведением и развертыванием этого проекта, дайте мне знать!