# Концепции развёртывания { #deployments-concepts } При развёртывании приложения **FastAPI** (и вообще любого веб‑API) есть несколько концепций, о которых стоит думать — с их помощью можно выбрать **наиболее подходящий** способ **развёртывания вашего приложения**. Некоторые из важных концепций: * Безопасность — HTTPS * Запуск при старте * Перезапуски * Репликация (количество запущенных процессов) * Память * Предварительные шаги перед запуском Посмотрим, как они влияют на **развёртывания**. В конечном итоге цель — **обслуживать клиентов вашего API** безопасно, **избегать перебоев** и максимально эффективно использовать **вычислительные ресурсы** (например, удалённые серверы/виртуальные машины). 🚀 Здесь я немного расскажу о этих **концепциях**, чтобы у вас появилась **интуиция**, как развёртывать ваш API в разных окружениях, возможно даже в **будущих**, которых ещё не существует. Учитывая эти концепции, вы сможете **оценить и спроектировать** лучший способ развёртывания **своих API**. В следующих главах я дам более **конкретные рецепты** по развёртыванию приложений FastAPI. А пока давайте разберём важные **идеи**. Эти концепции применимы и к другим типам веб‑API. 💡 ## Безопасность — HTTPS { #security-https } В [предыдущей главе про HTTPS](https.md){.internal-link target=_blank} мы разобрались, как HTTPS обеспечивает шифрование для вашего API. Также мы увидели, что HTTPS обычно обеспечивает компонент, **внешний** по отношению к серверу вашего приложения — **TLS Termination Proxy**. И должен быть компонент, отвечающий за **обновление HTTPS‑сертификатов** — это может быть тот же самый компонент или отдельный. ### Примеры инструментов для HTTPS { #example-tools-for-https } Некоторые инструменты, которые можно использовать как TLS Termination Proxy: * Traefik * Автоматически обновляет сертификаты ✨ * Caddy * Автоматически обновляет сертификаты ✨ * Nginx * С внешним компонентом (например, Certbot) для обновления сертификатов * HAProxy * С внешним компонентом (например, Certbot) для обновления сертификатов * Kubernetes с Ingress Controller (например, Nginx) * С внешним компонентом (например, cert-manager) для обновления сертификатов * Обрабатывается внутри облачного провайдера как часть его услуг (см. ниже 👇) Другой вариант — использовать **облачный сервис**, который возьмёт на себя больше задач, включая настройку HTTPS. Там могут быть ограничения или дополнительная стоимость и т.п., но в таком случае вам не придётся самим настраивать TLS Termination Proxy. В следующих главах я покажу конкретные примеры. --- Далее рассмотрим концепции, связанные с программой, которая запускает ваш реальный API (например, Uvicorn). ## Программа и процесс { #program-and-process } Мы часто будем говорить о работающем "**процессе**", поэтому полезно чётко понимать, что это значит и чем отличается от "**программы**". ### Что такое программа { #what-is-a-program } Словом **программа** обычно называют разные вещи: * **Код**, который вы пишете, то есть **Python‑файлы**. * **Файл**, который может быть **запущен** операционной системой, например: `python`, `python.exe` или `uvicorn`. * Конкретную программу в момент, когда она **работает** в операционной системе, используя CPU и память. Это также называют **процессом**. ### Что такое процесс { #what-is-a-process } Слово **процесс** обычно используют более конкретно — только для того, что реально выполняется в операционной системе (как в последнем пункте выше): * Конкретная программа в момент, когда она **запущена** в операционной системе. * Речь не о файле и не о коде, а **конкретно** о том, что **исполняется** и управляется операционной системой. * Любая программа, любой код **могут что‑то делать** только когда **исполняются**, то есть когда есть **работающий процесс**. * Процесс можно **завершить** (или «убить») вами или операционной системой. В этот момент он перестаёт выполняться и **больше ничего делать не может**. * У каждого запущенного приложения на вашем компьютере есть свой процесс; у каждой программы, у каждого окна и т.д. Обычно одновременно **работает много процессов**, пока компьютер включён. * Могут **одновременно** работать **несколько процессов** одной и той же **программы**. Если вы посмотрите «диспетчер задач» или «системный монитор» (или аналогичные инструменты) в вашей операционной системе, то увидите множество работающих процессов. Например, вы, скорее всего, увидите несколько процессов одного и того же браузера (Firefox, Chrome, Edge и т.д.). Обычно браузеры запускают один процесс на вкладку плюс дополнительные процессы. --- Теперь, когда мы понимаем разницу между **процессом** и **программой**, продолжим разговор о развёртываниях. ## Запуск при старте { #running-on-startup } В большинстве случаев, создавая веб‑API, вы хотите, чтобы он **работал постоянно**, без перерывов, чтобы клиенты всегда могли к нему обратиться. Разве что у вас есть особые причины запускать его только при определённых условиях, но обычно вы хотите, чтобы он был постоянно запущен и **доступен**. ### На удалённом сервере { #in-a-remote-server } Когда вы настраиваете удалённый сервер (облачный сервер, виртуальную машину и т.п.), самый простой вариант — вручную использовать `fastapi run` (он использует Uvicorn) или что‑то похожее, как вы делаете при локальной разработке. Это будет работать и полезно **во время разработки**. Но если соединение с сервером прервётся, **запущенный процесс**, скорее всего, завершится. А если сервер перезагрузится (например, после обновлений или миграций у облачного провайдера), вы, вероятно, **даже не заметите этого**. Из‑за этого вы не узнаете, что нужно вручную перезапустить процесс — и ваш API просто будет «мёртв». 😱 ### Автоматический запуск при старте { #run-automatically-on-startup } Как правило, вы захотите, чтобы серверная программа (например, Uvicorn) запускалась автоматически при старте сервера и без **участия человека**, чтобы всегда был процесс, запущенный с вашим API (например, Uvicorn, запускающий ваше приложение FastAPI). ### Отдельная программа { #separate-program } Чтобы этого добиться, обычно используют **отдельную программу**, которая гарантирует запуск вашего приложения при старте. Во многих случаях она также запускает и другие компоненты/приложения, например базу данных. ### Примеры инструментов для запуска при старте { #example-tools-to-run-at-startup } Примеры инструментов, которые могут с этим справиться: * Docker * Kubernetes * Docker Compose * Docker в режиме Swarm (Swarm Mode) * Systemd * Supervisor * Обработка внутри облачного провайдера как часть его услуг * Прочие... Более конкретные примеры будут в следующих главах. ## Перезапуски { #restarts } Подобно тому как вы обеспечиваете запуск приложения при старте, вы, вероятно, захотите обеспечить его **перезапуск** после сбоев. ### Мы ошибаемся { #we-make-mistakes } Мы, люди, постоянно совершаем **ошибки**. В программном обеспечении почти всегда есть **баги**, скрытые в разных местах. 🐛 И мы, как разработчики, продолжаем улучшать код — находим баги и добавляем новые возможности (иногда добавляя новые баги 😅). ### Небольшие ошибки обрабатываются автоматически { #small-errors-automatically-handled } Создавая веб‑API с FastAPI, если в нашем коде возникает ошибка, FastAPI обычно «локализует» её в пределах одного запроса, который эту ошибку вызвал. 🛡 Клиент получит **500 Internal Server Error** для этого запроса, но приложение продолжит работать для последующих запросов, а не «упадёт» целиком. ### Большие ошибки — падения { #bigger-errors-crashes } Тем не менее возможны случаи, когда код **роняет всё приложение**, приводя к сбою Uvicorn и Python. 💥 И вы, скорее всего, не захотите, чтобы приложение оставалось «мёртвым» из‑за ошибки в одном месте — вы захотите, чтобы оно **продолжало работать** хотя бы для *операций пути*, которые не сломаны. ### Перезапуск после падения { #restart-after-crash } В случаях действительно серьёзных ошибок, которые роняют работающий **процесс**, вам понадобится внешний компонент, отвечающий за **перезапуск** процесса, как минимум пару раз... /// tip | Совет ...Хотя если приложение **падает сразу же**, вероятно, нет смысла перезапускать его бесконечно. Но такие случаи вы, скорее всего, заметите во время разработки или как минимум сразу после развёртывания. Давайте сосредоточимся на основных сценариях, когда в каких‑то конкретных ситуациях **в будущем** приложение может падать целиком, и при этом имеет смысл его перезапускать. /// Скорее всего, вы захотите, чтобы перезапуском вашего приложения занимался **внешний компонент**, потому что к тому моменту Uvicorn и Python уже упали, и внутри того же кода вашего приложения сделать уже ничего нельзя. ### Примеры инструментов для автоматического перезапуска { #example-tools-to-restart-automatically } В большинстве случаев тот же инструмент, который **запускает программу при старте**, умеет обрабатывать и автоматические **перезапуски**. Например, это может быть: * Docker * Kubernetes * Docker Compose * Docker в режиме Swarm (Swarm Mode) * Systemd * Supervisor * Обработка внутри облачного провайдера как часть его услуг * Прочие... ## Репликация — процессы и память { #replication-processes-and-memory } В приложении FastAPI, используя серверную программу (например, команду `fastapi`, которая запускает Uvicorn), запуск в **одном процессе** уже позволяет обслуживать нескольких клиентов одновременно. Но во многих случаях вы захотите одновременно запустить несколько процессов‑воркеров. ### Несколько процессов — Воркеры { #multiple-processes-workers } Если клиентов больше, чем способен обслужить один процесс (например, если виртуальная машина не слишком мощная), и на сервере есть **несколько ядер CPU**, вы можете запустить **несколько процессов** одного и того же приложения параллельно и распределять запросы между ними. Когда вы запускаете **несколько процессов** одной и той же программы API, их обычно называют **воркерами**. ### Процессы‑воркеры и порты { #worker-processes-and-ports } Помните из раздела [Об HTTPS](https.md){.internal-link target=_blank}, что на сервере только один процесс может слушать конкретную комбинацию порта и IP‑адреса? Это по‑прежнему так. Поэтому, чтобы одновременно работало **несколько процессов**, должен быть **один процесс, слушающий порт**, который затем каким‑то образом передаёт коммуникацию каждому воркер‑процессу. ### Память на процесс { #memory-per-process } Когда программа загружает что‑то в память (например, модель машинного обучения в переменную или содержимое большого файла в переменную), всё это **потребляет часть памяти (RAM)** сервера. И разные процессы обычно **не делят память**. Это значит, что у каждого процесса свои переменные и своя память. Если ваш код потребляет много памяти, то **каждый процесс** будет потреблять сопоставимый объём памяти. ### Память сервера { #server-memory } Например, если ваш код загружает модель Машинного обучения размером **1 ГБ**, то при запуске одного процесса с вашим API он будет использовать как минимум 1 ГБ RAM. А если вы запустите **4 процесса** (4 воркера), каждый процесс будет использовать 1 ГБ RAM. Всего ваш API будет потреблять **4 ГБ RAM**. И если у вашего удалённого сервера или виртуальной машины только 3 ГБ RAM, попытка загрузить более 4 ГБ вызовет проблемы. 🚨 ### Несколько процессов — пример { #multiple-processes-an-example } В этом примере есть **процесс‑менеджер**, который запускает и контролирует два **процесса‑воркера**. Процесс‑менеджер, вероятно, будет тем, кто слушает **порт** на IP. И он будет передавать всю коммуникацию воркер‑процессам. Эти воркеры будут запускать ваше приложение, выполнять основные вычисления для получения **запроса** и возврата **ответа**, и загружать всё, что вы кладёте в переменные, в RAM. Конечно, на той же машине помимо вашего приложения, скорее всего, будут работать и **другие процессы**. Интересная деталь: процент **использования CPU** каждым процессом со временем может сильно **меняться**, но **память (RAM)** обычно остаётся более‑менее **стабильной**. Если у вас API, который каждый раз выполняет сопоставимый объём вычислений, и у вас много клиентов, то **загрузка процессора**, вероятно, *тоже будет стабильной* (вместо того, чтобы быстро и постоянно «скакать»). ### Примеры инструментов и стратегий репликации { #examples-of-replication-tools-and-strategies } Есть несколько подходов для достижения этого, и я расскажу больше о конкретных стратегиях в следующих главах, например, говоря о Docker и контейнерах. Главное ограничение: должен быть **один** компонент, который обрабатывает **порт** на **публичном IP**. И у него должен быть способ **передавать** коммуникацию реплицированным **процессам/воркерам**. Некоторые возможные комбинации и стратегии: * **Uvicorn** с `--workers` * Один **процесс‑менеджер** Uvicorn будет слушать **IP** и **порт** и запускать **несколько процессов‑воркеров Uvicorn**. * **Kubernetes** и другие распределённые **контейнерные системы** * Некий компонент на уровне **Kubernetes** будет слушать **IP** и **порт**. Репликация достигается с помощью **нескольких контейнеров**, в каждом из которых работает **один процесс Uvicorn**. * **Облачные сервисы**, которые берут это на себя * Облачный сервис, скорее всего, **возьмёт репликацию на себя**. Он, возможно, позволит указать **процесс для запуска** или **образ контейнера**. В любом случае это, скорее всего, будет **один процесс Uvicorn**, а сервис займётся его репликацией. /// tip | Совет Не беспокойтесь, если некоторые пункты про **контейнеры**, Docker или Kubernetes пока кажутся неочевидными. Я расскажу больше про образы контейнеров, Docker, Kubernetes и т.п. в следующей главе: [FastAPI внутри контейнеров — Docker](docker.md){.internal-link target=_blank}. /// ## Предварительные шаги перед запуском { #previous-steps-before-starting } Во многих случаях вы захотите выполнить некоторые шаги **перед запуском** приложения. Например, запустить **миграции базы данных**. Но чаще всего эти шаги нужно выполнять только **один раз**. Поэтому вы захотите иметь **один процесс**, который выполнит эти **предварительные шаги**, прежде чем запускать приложение. И вам нужно будет убедиться, что это делает один процесс **даже** если потом вы запускаете **несколько процессов** (несколько воркеров) самого приложения. Если эти шаги выполнят **несколько процессов**, они **дублируют** работу, запустив её **параллельно**, и, если речь о чём‑то деликатном (например, миграции БД), это может вызвать конфликты. Конечно, бывают случаи, когда нет проблем, если предварительные шаги выполняются несколько раз — тогда всё проще. /// tip | Совет Также учтите, что в зависимости от вашей схемы развёртывания в некоторых случаях **предварительные шаги могут вовсе не требоваться**. Тогда об этом можно не беспокоиться. 🤷 /// ### Примеры стратегий для предварительных шагов { #examples-of-previous-steps-strategies } Это будет **сильно зависеть** от того, как вы **развёртываете систему**, как запускаете программы, обрабатываете перезапуски и т.д. Некоторые возможные идеи: * «Init Container» в Kubernetes, который запускается перед контейнером с приложением * Bash‑скрипт, который выполняет предварительные шаги, а затем запускает приложение * При этом всё равно нужен способ запускать/перезапускать *этот* bash‑скрипт, обнаруживать ошибки и т.п. /// tip | Совет Я приведу более конкретные примеры с контейнерами в следующей главе: [FastAPI внутри контейнеров — Docker](docker.md){.internal-link target=_blank}. /// ## Использование ресурсов { #resource-utilization } Ваш сервер(а) — это **ресурс**, который ваши программы могут потреблять или **использовать**: время вычислений на CPU и доступную оперативную память (RAM). Какую долю системных ресурсов вы хотите потреблять/использовать? Можно подумать «немного», но на практике вы, скорее всего, захотите потреблять **максимум без падений**. Если вы платите за 3 сервера, но используете лишь малую часть их RAM и CPU, вы, вероятно, **тратите деньги впустую** 💸 и **электроэнергию серверов** 🌎 и т.п. В таком случае лучше иметь 2 сервера и использовать более высокий процент их ресурсов (CPU, память, диск, сетевую полосу и т.д.). С другой стороны, если у вас 2 сервера и вы используете **100% их CPU и RAM**, в какой‑то момент один процесс попросит больше памяти, и сервер начнёт использовать диск как «память» (что в тысячи раз медленнее) или даже **упадёт**. Или процессу понадобятся вычисления, но ему придётся ждать освобождения CPU. В таком случае лучше добавить **ещё один сервер** и запустить часть процессов на нём, чтобы у всех было **достаточно RAM и времени CPU**. Также возможен **всплеск** использования вашего API: он мог «взорваться» по популярности, или какие‑то сервисы/боты начали его активно использовать. На такие случаи стоит иметь запас ресурсов. Можно задать **целевое значение**, например **между 50% и 90%** использования ресурсов. Скорее всего, именно эти вещи вы будете измерять и на их основе настраивать развёртывание. Можно использовать простые инструменты вроде `htop`, чтобы смотреть загрузку CPU и RAM на сервере или по процессам. Или более сложные распределённые системы мониторинга. ## Резюме { #recap } Здесь вы прочитали о некоторых основных концепциях, которые, вероятно, стоит учитывать при выборе способа развёртывания приложения: * Безопасность — HTTPS * Запуск при старте * Перезапуски * Репликация (количество запущенных процессов) * Память * Предварительные шаги перед запуском Понимание этих идей и того, как их применять, даст вам интуицию, необходимую для принятия решений при настройке и доработке ваших развёртываний. 🤓 В следующих разделах я приведу более конкретные примеры возможных стратегий. 🚀