# Концепции развёртывания { #deployments-concepts }

При развёртывании приложения **FastAPI** (и вообще любого веб‑API) есть несколько концепций, о которых стоит думать — с их помощью можно выбрать **наиболее подходящий** способ **развёртывания вашего приложения**.

Некоторые из важных концепций:

* Безопасность — HTTPS
* Запуск при старте
* Перезапуски
* Репликация (количество запущенных процессов)
* Память
* Предварительные шаги перед запуском

Посмотрим, как они влияют на **развёртывания**.

В конечном итоге цель — **обслуживать клиентов вашего API** безопасно, **избегать перебоев** и максимально эффективно использовать **вычислительные ресурсы** (например, удалённые серверы/виртуальные машины). 🚀

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

Учитывая эти концепции, вы сможете **оценить и спроектировать** лучший способ развёртывания **своих API**.

В следующих главах я дам более **конкретные рецепты** по развёртыванию приложений FastAPI.

А пока давайте разберём важные **идеи**. Эти концепции применимы и к другим типам веб‑API. 💡

## Безопасность — HTTPS { #security-https }

В [предыдущей главе про HTTPS](https.md) мы разобрались, как HTTPS обеспечивает шифрование для вашего API.

Также мы увидели, что HTTPS обычно обеспечивает компонент, **внешний** по отношению к серверу вашего приложения — **прокси-сервер TSL-терминации**.

И должен быть компонент, отвечающий за **обновление HTTPS‑сертификатов** — это может быть тот же самый компонент или отдельный.

### Примеры инструментов для HTTPS { #example-tools-for-https }

Некоторые инструменты, которые можно использовать как прокси-сервер TSL-терминации:

* Traefik
    * Автоматически обновляет сертификаты ✨
* Caddy
    * Автоматически обновляет сертификаты ✨
* Nginx
    * С внешним компонентом (например, Certbot) для обновления сертификатов
* HAProxy
    * С внешним компонентом (например, Certbot) для обновления сертификатов
* Kubernetes с Ingress Controller (например, Nginx)
    * С внешним компонентом (например, cert-manager) для обновления сертификатов
* Обрабатывается внутри облачного провайдера как часть его услуг (см. ниже 👇)

Другой вариант — использовать **облачный сервис**, который возьмёт на себя больше задач, включая настройку HTTPS. Там могут быть ограничения или дополнительная стоимость и т.п., но в таком случае вам не придётся самим настраивать прокси-сервер TSL-терминации.

В следующих главах я покажу конкретные примеры.

---

Далее рассмотрим концепции, связанные с программой, которая запускает ваш реальный API (например, Uvicorn).

## Программа и процесс { #program-and-process }

Мы часто будем говорить о работающем "**процессе**", поэтому полезно чётко понимать, что это значит и чем отличается от "**программы**".

### Что такое программа { #what-is-a-program }

Словом **программа** обычно называют разные вещи:

* **Код**, который вы пишете, то есть **Python‑файлы**.
* **Файл**, который может быть **запущен** операционной системой, например: `python`, `python.exe` или `uvicorn`.
* Конкретную программу в момент, когда она **работает** в операционной системе, используя CPU и память. Это также называют **процессом**.

### Что такое процесс { #what-is-a-process }

Слово **процесс** обычно используют более конкретно — только для того, что реально выполняется в операционной системе (как в последнем пункте выше):

* Конкретная программа в момент, когда она **запущена** в операционной системе.
    * Речь не о файле и не о коде, а **конкретно** о том, что **исполняется** и управляется операционной системой.
* Любая программа, любой код **могут что‑то делать** только когда **исполняются**, то есть когда есть **работающий процесс**.
* Процесс можно **завершить** (или «убить») вами или операционной системой. В этот момент он перестаёт выполняться и **больше ничего делать не может**.
* У каждого запущенного приложения на вашем компьютере есть свой процесс; у каждой программы, у каждого окна и т.д. Обычно одновременно **работает много процессов**, пока компьютер включён.
* Могут **одновременно** работать **несколько процессов** одной и той же **программы**.

Если вы посмотрите «диспетчер задач» или «системный монитор» (или аналогичные инструменты) в вашей операционной системе, то увидите множество работающих процессов.

Например, вы, скорее всего, увидите несколько процессов одного и того же браузера (Firefox, Chrome, Edge и т.д.). Обычно браузеры запускают один процесс на вкладку плюс дополнительные процессы.

<img class="shadow" src="/img/deployment/concepts/image01.png">

---

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

## Запуск при старте { #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), что на сервере только один процесс может слушать конкретную комбинацию порта и 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. И он будет передавать всю коммуникацию воркер‑процессам.

Эти воркеры будут запускать ваше приложение, выполнять основные вычисления для получения **HTTP‑запроса** и возврата **HTTP‑ответа**, и загружать всё, что вы кладёте в переменные, в RAM.

<img src="/img/deployment/concepts/process-ram.drawio.svg">

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

Интересная деталь: процент **использования 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).

///

## Предварительные шаги перед запуском { #previous-steps-before-starting }

Во многих случаях вы захотите выполнить некоторые шаги **перед запуском** приложения.

Например, запустить **миграции базы данных**.

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

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

И вам нужно будет убедиться, что это делает один процесс **даже** если потом вы запускаете **несколько процессов** (несколько воркеров) самого приложения. Если эти шаги выполнят **несколько процессов**, они **дублируют** работу, запустив её **параллельно**, и, если речь о чём‑то деликатном (например, миграции БД), это может вызвать конфликты.

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

/// tip | Совет

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

Тогда об этом можно не беспокоиться. 🤷

///

### Примеры стратегий для предварительных шагов { #examples-of-previous-steps-strategies }

Это будет **сильно зависеть** от того, как вы **развёртываете систему**, как запускаете программы, обрабатываете перезапуски и т.д.

Некоторые возможные идеи:

* «Init Container» в Kubernetes, который запускается перед контейнером с приложением
* Bash‑скрипт, который выполняет предварительные шаги, а затем запускает приложение
    * При этом всё равно нужен способ запускать/перезапускать *этот* bash‑скрипт, обнаруживать ошибки и т.п.

/// tip | Совет

Я приведу более конкретные примеры с контейнерами в следующей главе: [FastAPI внутри контейнеров — Docker](docker.md).

///

## Использование ресурсов { #resource-utilization }

Ваш сервер(а) — это **ресурс**, который ваши программы могут потреблять или **использовать**: время вычислений на CPU и доступную оперативную память (RAM).

Какую долю системных ресурсов вы хотите потреблять/использовать? Можно подумать «немного», но на практике вы, вероятно, захотите потреблять **максимум без падений**.

Если вы платите за 3 сервера, но используете лишь малую часть их RAM и CPU, вы, вероятно, **тратите деньги впустую** 💸 и **электроэнергию серверов** 🌎 и т.п.

В таком случае лучше иметь 2 сервера и использовать более высокий процент их ресурсов (CPU, память, диск, сетевую полосу и т.д.).

С другой стороны, если у вас 2 сервера и вы используете **100% их CPU и RAM**, в какой‑то момент один процесс попросит больше памяти, и сервер начнёт использовать диск как «память» (что в тысячи раз медленнее) или даже **упадёт**. Или процессу понадобятся вычисления, но ему придётся ждать освобождения CPU.

В таком случае лучше добавить **ещё один сервер** и запустить часть процессов на нём, чтобы у всех было **достаточно RAM и времени CPU**.

Также возможен **всплеск** использования вашего API: он мог «взорваться» по популярности, или какие‑то сервисы/боты начали его активно использовать. На такие случаи стоит иметь запас ресурсов.

Можно задать **целевое значение**, например **между 50% и 90%** использования ресурсов. Скорее всего, именно эти вещи вы будете измерять и на их основе настраивать развёртывание.

Можно использовать простые инструменты вроде `htop`, чтобы смотреть загрузку CPU и RAM на сервере или по процессам. Или более сложные распределённые системы мониторинга.

## Резюме { #recap }

Здесь вы прочитали о некоторых основных концепциях, которые, вероятно, стоит учитывать при выборе способа развёртывания приложения:

* Безопасность — HTTPS
* Запуск при старте
* Перезапуски
* Репликация (количество запущенных процессов)
* Память
* Предварительные шаги перед запуском

Понимание этих идей и того, как их применять, даст вам интуицию, необходимую для принятия решений при настройке и доработке ваших развёртываний. 🤓

В следующих разделах я приведу более конкретные примеры возможных стратегий. 🚀
