Если бы строители строили здания так, как программисты пишут программы, то первый же появившийся дятел уничтожил бы цивилизацию.
Джеральд Вайнберг, ученый в области IT
У вас есть приложение, работающее на локальной машине, и теперь вы хотите поделиться им. В этой главе представлено множество сценариев того, как перенести приложение в среду эксплуатации и поддерживать его правильную и эффективную работу. Поскольку часть описаний очень подробные, в некоторых случаях я буду ссылаться на полезные сторонние документы, а не выкладывать их здесь.
До сих пор во всех примерах кода в этой книге использовался один экземпляр uvicorn, запущенный на адресе localhost на порте 8000. Для обработки большого количества трафика вам потребуется несколько серверов, работающих на нескольких ядрах, предоставляемых современным оборудованием. Кроме того, понадобится что-то поверх этих серверов для выполнения следующих действий:
• поддержания их в рабочем состоянии (супервайзер);
• сбора и отправки внешних запросов (обратный прокси);
• возврата ответов;
• обеспечения HTTPS-терминации (расшифровка SSL).
Вы наверняка видели сервер Python под названием Gunicorn (https://gunicorn.org). Он может контролировать несколько процессов, но это сервер WSGI, а FastAPI основан на ASGI. К счастью, существует специальный класс процессов Uvicorn, которым может управлять Gunicorn.
В примере 13.1 рассматриваются эти процессы Uvicorn на localhost, порт 8000 (взято из официальной документации, https://oreil.ly/Svdhx). Кавычки защищают оболочку от любой специальной интерпретации.
Пример 13.1. Использование Gunicorn с процессами Uvicorn
$ pip install "uvicorn[standard]" gunicorn
$ gunicorn main:app --workers 4 --worker-class \
uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000
Вы увидите множество строк, когда Gunicorn будет выполнять ваши запросы. Будет запущен процесс Gunicorn верхнего уровня, который станет общаться с четырьмя рабочими подпроцессами Uvicorn, совместно использующими порт 8000 на local host (0.0.0.0). Измените хост, порт или количество процессов, если вам нужно что-то другое. Выражение main:app ссылается на файл main.py и объект FastAPI с именем переменной app. В документации (https://oreil.ly/TxYIy) Gunicorn утверждается: «Для обработки сотен или тысяч запросов в секунду Gunicorn потребуется всего 4–12 рабочих процессов».
Оказывается, сам Uvicorn также может запускать несколько процессов Uvicorn, как показано в примере 13.2.
Пример 13.2. Использование Uvicorn с рабочими процессами Uvicorn
$ uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
Но этот метод не позволяет управлять процессами, поэтому обычно предпочтение отдается методу Gunicorn. Для Uvicorn существуют и другие менеджеры процессов — см. официальную документацию (https://www.uvicorn.org/deployment).
Это позволяет выполнять три из четырех задач, упомянутых в предыдущем разделе, но не шифрование HTTPS.
Официальная документация FastAPI по HTTPS (https://oreil.ly/HYRW7), как и вся остальная, чрезвычайно информативна. Я рекомендую прочитать ее, а затем описание (https://oreil.ly/zcUWS) Рамиресом того, как добавить поддержку HTTPS в FastAPI с помощью обратного прокси под названием Traefik (https://traefik.io). Он располагается над вашими веб-серверами, подобно nginx в качестве обратного прокси и балансировщика нагрузки, но включает в себя магию HTTPS.
Хотя этот процесс состоит из множества этапов, он все же намного проще, чем прежде. В частности, раньше вам приходилось регулярно платить большие деньги центру сертификации за цифровой сертификат, который можно было использовать для поддержки протокола HTTPS на своем сайте. К счастью, на смену этим органам пришел бесплатный сервис Let’s Encrypt (https://letsencrypt.org).
Когда Docker появился на сцене (он был упомянут в молниеносном пятиминутном докладе (https://oreil.ly/25oef) Соломона Хайкса из dotCloud на PyCon 2013), большинство из нас впервые услышали о контейнерах для Linux. Со временем мы поняли, что Docker быстрее и легче виртуальных машин. Вместо эмуляции полноценной операционной системы все контейнеры совместно используют ядро Linux сервера, а процессы и сети изолируются в собственных пространствах имен. Внезапно у вас появилась возможность с помощью бесплатного программного обеспечения Docker разместить несколько независимых сервисов на одной машине, не беспокоясь о том, что они будут пересекаться.
Десять лет спустя Docker получил всеобщее признание и поддержку. Если вы хотите разместить свое приложение FastAPI на облачном сервисе, обычно нужно сначала создать его образ в Docker. В официальной документации FastAPI (https://oreil.ly/QnwOW) содержится подробное описание того, как сделать Docker-версию вашего FastAPI-приложения. Одним из этапов будет написание Dockerfile — текстового файла, содержащего информацию о конфигурации Docker, например, какой код приложения использовать и какие процессы запускать. Чтобы доказать, что по уровню сложности это не операция на мозге во время запуска космической ракеты, приведу Dockerfile с этой страницы:
FROM python:3.9
WORKDIR /code
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY ./app /code/app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
Я рекомендую прочитать официальную документацию или другие ссылки, выдаваемые поисковой системой Google по запросу fastapi docker, например The Ultimate FastAPI Tutorial Part 13 — Using Docker to Deploy Your App (https://oreil.ly/7TUpR) Кристофера Самиуллы.
В Сети можно найти множество источников платного или бесплатного хостинга. Вот некоторые примеры сведений о том, как разместить FastAPI с их помощью:
• статья FastAPI — Deployment на сайте Tutorials Point (https://oreil.ly/DBZcm);
• материалы The Ultimate FastAPI Tutorial Part 6b — Basic Deployment on Linode, сформированные инженером Кристофером Самиуллой (https://oreil.ly/s8iar);
• статья How to Deploy a FastAPI App on Heroku for Free Шиничи Окады (https://oreil.ly/A6gij).
Платформа Kubernetes выросла из внутреннего кода Google для управления внутренними системами, которые становились просто ужасающе сложными. Системные администраторы (так их тогда называли) вручную настраивали такие инструменты, как балансировщики нагрузки, обратные прокси, хьюмидоры и т.д. Kubernetes стремился взять бо́льшую часть этих знаний и автоматизировать их — не говорите мне, как это сделать, а скажите, чего вы хотите. Сюда входят такие задачи, как поддержание работоспособности сервиса или запуск дополнительных серверов при резком увеличении трафика.
Существует множество описаний того, как развернуть FastAPI на Kubernetes, в том числе статья Суманты Мукхопадхья Deploying a FastAPI Application on Kubernetes (https://oreil.ly/ktTNu).
В настоящее время производительность FastAPI одна из самых высоких (https://oreil.ly/mxabf) среди всех веб-фреймворков на Python и даже сравнима с производительностью фреймворков на более быстрых языках, таких как Go. Но во многом это связано с ASGI, позволяющим избежать ожидания ввода-вывода с помощью асинхронности. Сам по себе Python — довольно медленный язык. Далее приведены некоторые советы и рекомендации по улучшению общей производительности.
Часто веб-серверу не нужно быть очень быстрым. Бо́льшую часть своего времени он тратит на получение сетевых HTTP-запросов и возврат результатов (в этой книге он представлен веб-уровнем). Между ними веб-сервис выполняет бизнес-логику (сервисный уровень), получает доступ к источникам данных (уровень данных) и снова тратит бо́льшую часть своего времени на сетевой ввод-вывод.
Когда код в веб-сервисе должен ждать ответа, лучше всего использовать асинхронную функцию (async def, а не def). Это позволяет FastAPI и Starlette планировать работу асинхронной функции и выполнять другие действия в ожидании ее ответа. Это одна из причин того, почему бенчмарки FastAPI лучше, чем фреймворки на базе WSGI, такие как Flask и Django. У производительности есть два аспекта:
• время обработки одного запроса;
• количество одновременно обрабатываемых запросов.
Если у вас есть конечная точка веб-приложения, получающая данные из статичного источника (например, записи в базе данных, которые меняются редко или не меняются никогда), можно кэшировать данные в функции. Это может быть на любом уровне. В Python представлен стандартный модуль functools (https://oreil.ly/8Kg4V), а также функции cache() и lru_cache().
Одна из самых распространенных причин медленной работы веб-сайта — отсутствие индекса для таблицы базы данных достаточного размера. Часто вы не замечаете проблемы до тех пор, пока ваша таблица не вырастет до определенного размера, и тогда запросы внезапно становятся намного медленнее. В SQL любой столбец в операторе WHERE должен быть проиндексирован.
Во многих примерах, приведенных в книге, первичный ключ таблиц creature и explorer был представлен текстовым полем name. При создании таблиц поле name было объявлено в качестве первичного ключа — primary key. Для крошечных таблиц, приведенных ранее, SQLite в любом случае проигнорирует этот ключ, поскольку быстрее будет просто просканировать таблицу. Но как только она достигает приличного размера — скажем, миллиона строк, отсутствие индекса становится заметным. Решением может стать запуск оптимизатора запросов (https://oreil.ly/YPR3Q).
Даже если у вас небольшая таблица, можете провести нагрузочное тестирование базы данных с помощью скриптов Python или инструментов с открытым исходным кодом. Если выполняется множество последовательных запросов к базе данных, возможно, их стоит объединить в один пакет. Если вы выгружаете или скачиваете большой файл, используйте потоковые версии, а не гигантский фрагмент данных.
Если вы выполняете какую-либо задачу, занимающую больше доли секунды, например отправку письма с подтверждением или уменьшение изображения, возможно, стоит передать ее в очередь заданий, например в Celery (https://docs.celeryq.dev).
Если веб-сервис кажется медленным, потому что выполняет значительные вычисления с помощью Python, вам может понадобиться «более быстрый Python». Альтернативные варианты:
• использовать PyPy (https://www.pypy.org) вместо стандартной реализации CPython;
• написать расширение (https://oreil.ly/BElJa) для Python на C, C++ или Rust;
• преобразовать медленный код Python в язык Cython (https://cython.org), используемый Pydantic и Uvicorn.
Недавно был сделан очень интригующий анонс языка Mojo (https://oreil.ly/C96kx). Он стремится стать полным супернабором Python с новыми возможностями (применяя тот же дружественный синтаксис Python), способными ускорить примеры на Python в тысячи раз. Основной автор Крис Латтнер ранее работал над такими инструментами компиляции, как LLVM (https://llvm.org), Clang (https://clang.llvm.org) и MLIR (https://mlir.llvm.org), а также языком Swift (https://www.swift.org) для Apple.
Mojo стремится стать одноязычным решением для разработки ИИ, для чего сейчас (в PyTorch и TensorFlow) требуются сборки Python/C/C++, сложные в разработке, управлении и отладке. Но Mojo был бы хорошим языком общего назначения не только в сфере ИИ.
Я много лет писал на C и все ждал преемника, обладающего производительностью и простотой применения языка Python. Возможными вариантами были D, Go, Julia, Zig и Rust, но если Mojo сможет оправдать свои цели (https://oreil.ly/EojvA), я бы активно его использовал.
Смотрите снизу вверх с того момента и места, где вы столкнулись с проблемой. К ним относятся проблемы производительности во времени и пространстве, а также логические и асинхронные ловушки.
Какой код ответа HTTP вы получили в первую очередь?
• 404 — ошибка аутентификации или авторизации.
• 422 — обычно это жалоба Pydantic на использование модели.
• 500 — отказ сервиса, расположенного за вашим FastAPI.
Uvicorn и другие веб-серверы обычно пишут журналы в файл stdout. Вы можете проверить журнал, чтобы узнать, какой вызов был сделан на самом деле, включая HTTP-глагол и URL-адрес, но не данные в теле, заголовках или файлах cookies.
Если определенная конечная точка возвращает код состояния семейства 400, можно попробовать подать те же данные еще раз и посмотреть, не повторится ли ошибка. Если да, то у меня срабатывает первый пещерный инстинкт отладчика — добавить операторы print() в соответствующие функции веб, сервисного и уровня данных.
Кроме того, везде, где вы инициируете выброс исключения, добавляйте подробное описание. Если поиск в базе данных не удался, укажите входные значения и конкретную ошибку, например попытку дублировать уникальное ключевое поле.
Может показаться, что значения терминов «метрика», «мониторинг», «наблюдаемость» и «телеметрия» частично совпадают. В стране Python принято использовать:
• Prometheus (https://prometheus.io) — для получения метрик;
• Grafana (https://grafana.com) — для их отображения;
• OpenTelemetry (https://opentelemetry.io) — для измерения времени.
Вы можете применить их ко всем уровням своего сайта: веб-уровню, сервисному и уровню данных. Сервисные уровни могут быть более бизнес-ориентированными, а другие — более техническими, полезными при разработке и сопровождении сайтов.
Вот несколько ссылок для сбора метрик FastAPI:
• Prometheus FastAPI Instrumentator (https://oreil.ly/EYJwR);
• Getting Started: Monitoring a FastAPI App with Grafana and Prometheus — A Step-by-Step Guide, автор Зу Кодес (https://oreil.ly/Gs90t);
• страница FastAPI Observability на сайте Grafana Labs (https://oreil.ly/spKwe);
• OpenTelemetry FastAPI Instrumentation (https://oreil.ly/wDSNv);
• OpenTelemetry FastAPI Tutorial — Complete Implementation Guide, автор Анкит Ананд (https://oreil.ly/ZpSXs);
• документация OpenTelemetry Python (https://oreil.ly/nSD4G).
Понятно, что производство — дело непростое. Среди проблем — сама веб-техника, перегрузка сети и дисков, а также проблемы с базой данных. В этой главе вы найдете подсказки о том, как получить нужную информацию и где начать искать, если возникли проблемы.
Погодите, они же сохраняют сигары свежими.