Дата публикации
ai_products

Как масштабировать MCP‑серверы в Azure App Service без боли с сессиями

Что нового

Microsoft показала, как запускать и масштабировать MCP‑серверы (Model Context Protocol) в Azure App Service как обычные веб‑API — без «липких» сессий и ручной возни с балансировкой.

Ключевые новшества и факты:

  • MCP‑спецификация в ревизии 2025-06-18 формализовала статичный HTTP‑транспорт, текущая ревизия 2025-11-25 это сохраняет.
  • Каждый запрос теперь самодостаточен: серверу не нужно хранить состояние клиента между запросами.
  • Это позволяет ставить MCP‑сервер за встроенный load balancer App Service и горизонтально масштабировать его как любой REST API.
  • Есть готовый рабочий пример: github.com/seligj95/app-service-mcp-stateless-scale-python.
  • Один azd up поднимает:
    • MCP‑сервер на FastAPI;
    • 3 инстанса App Service (P0v3) за балансировщиком;
    • staging‑слот для деплоя без простоя;
    • Application Insights для телеметрии;
    • скрипт k6 для нагрузки и визуализации распределения трафика по инстансам.
  • MCP‑инструмент whoami показывает ID инстанса (WEBSITE_INSTANCE_ID), что позволяет проверить, как балансировщик раскидывает запросы.
  • В Bicep‑шаблоне критичный параметр: clientAffinityEnabled: false — он отключает ARRAffinity‑cookie и делает балансировку по‑настоящему равномерной.
  • План Premium v3 (P0v3) — минимальный уровень, который даёт:
    • Always On (процесс не выгружается);
    • deployment slots (staging/production) для zero‑downtime деплоя.

Как это работает

Статичный HTTP‑транспорт MCP

Раньше MCP часто использовал постоянные соединения: SSE или WebSocket‑сессии, где сервер держал в памяти состояние клиента — открытые инструменты, подписки, частичные стримы. Это удобно для локального процесса рядом с IDE, но плохо масштабируется: если следующий запрос прилетает на другой инстанс, сессия рвётся.

Новый статичный HTTP‑транспорт решает это:

  • Каждый запрос — полноценный JSON‑RPC‑конверт (initialize, tools/list, tools/call).
  • Каждый ответ самодостаточен.
  • Сервер может забыть клиента между запросами.
  • Любой инстанс может обслужить любой вызов.

Это именно то поведение, которое нужен L4/L7‑балансировщику: нет привязки к конкретной машине.

В примере все инструменты реализованы как чистые функции от аргументов:

  • whoami — сообщает, какой инстанс обслужил запрос;
  • lookup_fact — читает данные из статического словаря;
  • compute_primes — считает простые числа через решето.

Они не трогают память, привязанную к конкретному клиенту. Это не требование протокола, а дисциплина, которая сохраняет статичность.

Почему именно App Service, а не Functions или AKS

Microsoft делает ставку на App Service для MCP‑серверов по нескольким причинам:

  1. Always On

    • MCP‑инструменты часто вызывают LLM и внешние API.
    • Латентность легко улетает в несколько секунд и больше.
    • Azure Functions по умолчанию ограничивает выполнение одним запросом до 10 минут и агрессивно скейлит воркеры до нуля между всплесками.
    • App Service держит процесс в памяти, не убивает его между запросами и не ломает кэши.
  2. Горизонтальный скейлинг одной настройкой

    • Выбираете Premium SKU, ставите capacity = N — получаете N инстансов за управляемым балансировщиком.
    • Не нужно настраивать VM Scale Set, Ingress‑контроллер и Kubernetes Service.
  3. Deployment slots

    • Готовите staging‑слот, прогреваете его и делаете swap в production без простоя.
    • Это критично, когда MCP‑endpoint — это «поверхность инструментов» для агента, который сейчас работает.
  4. Easy Auth

    • Включаете аутентификацию в App Service, настраиваете Entra ID.
    • Получаете OAuth 2.1 перед MCP‑endpoint без ручной реализации flow.
    • В примере авторизацию отключили, чтобы развернуть всё одной командой, но включается она парой кликов.

В итоге App Service ведёт себя как PaaS, который умеет держать долго живущий процесс и масштабировать его по горизонтали. MCP‑сервер как раз такой процесс.

Статичный MCP‑сервер на FastAPI: один endpoint

Весь транспорт MCP укладывается в один POST‑обработчик:

@app.post("/mcp")
async def mcp_endpoint(request: Request):
    body = await request.json()
    method = body.get("method", "")
    msg_id = body.get("id")

    if method == "initialize":
        return {
            "jsonrpc": "2.0",
            "id": msg_id,
            "result": _server_info(),
        }

    if method == "tools/list":
        return {
            "jsonrpc": "2.0",
            "id": msg_id,
            "result": {"tools": [
                # ...
            ]},
        }

    if method == "tools/call":
        params = body.get("params", {})
        result = await MCP_TOOLS[params["name"]]["function"](
            **params.get("arguments", {})
        )
        return {
            "jsonrpc": "2.0",
            "id": msg_id,
            "result": {
                "content": [
                    {
                        "type": "text",
                        "text": json.dumps(result),
                    }
                ]
            },
        }
``

Что важно:

- Нет таблицы сессий.
- Нет `client_id` в cookie.
- Нет `AsyncIterator`, который живёт между запросами.
- `initialize`, `tools/list` и `tools/call` — **один запрос — один ответ**.

Это ровно тот паттерн, на который рассчитывает балансировщик App Service.

### Диагностика: инструмент `whoami`

Самый полезный инструмент в примере — `whoami`:

```python
async def tool_whoami() -> Dict[str, Any]:
    return {
        "instance_id": os.environ.get("WEBSITE_INSTANCE_ID", "local"),
        "hostname": socket.gethostname(),
        # ...
    }
``

`WEBSITE_INSTANCE_ID` — уникальный идентификатор для каждого воркера App Service.

Если вы несколько раз вызываете `whoami` из MCP‑клиента и видите, как `instance_id` меняется, — балансировщик распределяет трафик по инстансам. Если не меняется, трафик что‑то «приклеило» к одному инстансу. В App Service это почти всегда **ARRAffinity‑cookie**.

### Инфраструктура на Bicep: что именно даёт масштабирование

Пример использует план **P0v3** с тремя инстансами, веб‑приложение с отключённой аффинностью и staging‑слот:

```bicep
resource appServicePlan 'Microsoft.Web/serverfarms@2024-04-01' = {
  name: name
  sku: {
    name: 'P0v3'
    capacity: instanceCount // 3 by default
  }
  properties: {
    reserved: true
  }
}

resource web 'Microsoft.Web/sites@2024-04-01' = {
  name: name
  properties: {
    serverFarmId: appServicePlanId
    httpsOnly: true
    clientAffinityEnabled: false // ← the one line that matters
    siteConfig: {
      linuxFxVersion: 'PYTHON|3.11'
      alwaysOn: true
      healthCheckPath: '/health'
      appCommandLine: 'python -m uvicorn main:app --host 0.0.0.0 --port 8000'
    }
  }
}

resource staging 'Microsoft.Web/sites/slots@2024-04-01' = {
  parent: web
  name: 'staging'
  properties: {
    /* same shape — separate hostname, same plan */
  }
}

Ключевые детали:

  • clientAffinityEnabled: false — самая важная строка.

    • По умолчанию App Service включает её и ставит ARRAffinity‑cookie.
    • Cookie привязывает клиента к инстансу, который обработал первый запрос.
    • Это нужно старым ASP.NET‑приложениям с сессиями в памяти.
    • Для статичного MCP это вредно: балансировщик перестаёт равномерно раскидывать трафик.
  • alwaysOn: true — процесс не выгружается между запросами.

  • linuxFxVersion: 'PYTHON|3.11' + appCommandLine — запуск FastAPI через Uvicorn.

  • Premium v3 (P0v3) нужен, чтобы получить Always On и deployment slots. Ниже этого тарифа их нет.

Application Insights без ручной телеметрии

В main.py добавлена одна строка для подключения Azure Monitor OpenTelemetry:

from azure.monitor.opentelemetry import configure_azure_monitor

if os.environ.get("APPLICATIONINSIGHTS_CONNECTION_STRING"):
    configure_azure_monitor(logger_name="mcp")

Дальше всё делает дистрибутив OpenTelemetry для Azure:

  • Автоматически инструментирует FastAPI и исходящие HTTP‑запросы.
  • Каждый span запроса помечается тегом cloud_RoleInstance.
  • Application Insights заполняет этот тег из WEBSITE_INSTANCE_ID.

Проверка распределения трафика по инстансам превращается в один запрос в Logs:

requests
| where timestamp > ago(15m)
| where name contains "/mcp"
| summarize count() by cloud_RoleInstance
| order by count_ desc

Если видите три строки с примерно одинаковым числом запросов — балансировка работает. Если одна строка, клиент шлёт ARRAffinity‑cookie, и нужно отключить affinity и переразвернуть приложение.

Что это значит для вас

Для кого это полезно

  • Команды, которые строят продакшен‑агентов на MCP.

    • Вы можете запускать MCP‑инструменты в App Service, не думая о сессиях и липких подключениях.
    • Масштабирование по горизонтали превращается в настройку capacity и правил автоскейла.
  • Разработчики IDE‑плагинов и внутренних тулов.

    • Если MCP‑сервер перестал помещаться на одной машине, можно перенести его в App Service и плавно увеличивать число инстансов.
  • Техлиды, которые не хотят заводить Kubernetes ради одного сервиса.

    • App Service закрывает сценарий MCP‑серверов без управления кластерами, ingress и манифестами.

Для каких задач это хорошо

  • MCP‑инструменты, которые:

    • не зависят от состояния сессии;
    • могут восстановить контекст из запроса (например, передаёте нужные ID и параметры);
    • делают тяжёлые вычисления (compute_primes) или ходят в медленные внешние API.
  • Агентные системы, которые вызывают MCP через HTTP и готовы жить с моделью «один запрос — один ответ».

  • Сценарии, где важны:

    • безотказность при деплое — staging‑слот + swap;
    • масштабирование во время пиков — автоскейл по CPU/задержкам;
    • наблюдаемость — Application Insights и распределение трафика по инстансам.

Когда это не подойдёт

  • Вам нужен стриминг или долгие двусторонние сессии поверх WebSocket/SSE, где сервер держит в памяти состояние диалога.

    • В этом случае придётся либо проектировать state store вне процесса, либо идти в более низкоуровневые решения (AKS, собственный Nginx/Envoy и т.п.).
  • MCP‑сервер жёстко завязан на in‑memory сессии и вы не готовы их выносить наружу.

  • Вы не можете использовать Azure (регуляторика, политика компании) или сервис недоступен из вашей инфраструктуры без VPN.

Ограничения и нюансы для России

  • Azure App Service и сопутствующие сервисы (Entra ID, Application Insights) официально относятся к облачной инфраструктуре Microsoft.
  • Для доступа из России могут потребоваться VPN, корпоративные каналы или инфраструктура вне юрисдикции РФ.
  • Если вы строите продукт для локального рынка и у вас нет стабильного доступа к Azure, архитектура останется концептуальным ориентиром, но придётся искать аналог в других облаках.

Место на рынке

По сравнению с Azure Functions

Плюсы App Service для MCP:

  • Постоянный процесс (Always On) против агрессивного scale‑to‑zero в Functions.
  • Нет лимита исполнения в 10 минут, который может мешать сложным MCP‑инструментам.
  • Проще контролировать версию рантайма и команду запуска (appCommandLine).

Минусы:

  • Потенциально дороже, чем безсерверная модель при редких запросах.
  • Нужно самим думать о масштабировании (или настраивать автоскейл), а не полагаться на «магический» скейл Functions.

Конкретных чисел по цене в материале нет, но по модели биллинга App Service Premium v3 обычно дороже «холодных» функций при низкой нагрузке и выгоднее при постоянном трафике.

По сравнению с AKS и кастомным Kubernetes

Где выигрывает App Service:

  • Меньше инфраструктурной сложности: нет кластера, ingress, сервис‑манифестов.
  • Быстрый старт: один azd up вместо развёртывания и поддержки Kubernetes.

Где Kubernetes сильнее:

  • Тонкий контроль над сетевой топологией, балансировкой и sidecar‑паттернами.
  • Легче объединять десятки микросервисов с разными требованиями в один кластер.

Если у вас уже есть AKS и сильная DevOps‑команда, MCP можно вписать туда. Если нет — App Service закрывает задачу MCP‑серверов с гораздо меньшей ценой входа.

По сравнению с локальными MCP‑серверми

Локальный процесс рядом с IDE или агентом:

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

App Service‑подход даёт:

  • Масштабирование по горизонтали.
  • Балансировку без «липких» сессий.
  • Наблюдаемость и логирование на уровне платформы.

Это логичный шаг, когда MCP переходит из «игрушки для локальной разработки» в продакшен‑сервис.

Установка

Для развёртывания примера Microsoft предлагает использовать Azure Developer CLI (azd).

Базовые шаги:

  1. Установите azd и залогиньтесь в Azure.

  2. Клонируйте репозиторий:

    git clone https://github.com/seligj95/app-service-mcp-stateless-scale-python
    cd app-service-mcp-stateless-scale-python
    
  3. Аутентифицируйтесь в Azure Developer CLI:

    azd auth login
    
  4. Разверните инфраструктуру и код одной командой:

    azd up
    

Команда создаст:

  • ресурсную группу;
  • план App Service (P0v3) с тремя инстансами;
  • веб‑приложение с отключённой аффинностью;
  • staging‑слот;
  • Log Analytics workspace;
  • ресурс Application Insights;
  • деплой Python‑приложения через Oryx.

В выводе вы увидите WEB_URI и WEB_STAGING_URI.

Откройте WEB_URI в браузере — на главной странице отобразится instance_id инстанса, который обслужил запрос. Обновляйте страницу и смотрите, как ID меняется.

Как запустить и проверить масштабирование

Swap staging → production без простоя

Чтобы поменять местами staging и production‑слоты:

az webapp deployment slot swap \
  --resource-group <rg> --name <app> \
  --slot staging --target-slot production

App Service прогреет staging‑инстансы, переключит трафик, а старый production станет новым staging. Это классический blue‑green‑деплой без ручной настройки.

Нагрузочное тестирование с k6

В репозитории есть скрипт k6, который шлёт запросы на /mcp с методом tools/call и помечает каждый ответ по instance_id, который вернул сервер.

Пример запуска:

BASE_URL=https://<your-app>.azurewebsites.net \
  k6 run --summary-export=summary.json loadtest/k6-mcp.js

jq '.metrics.mcp_instance_hits.values' summary.json

Ожидаемый результат на плане с тремя инстансами и 60‑секундной нагрузкой:

{
  "count": 1842,
  "instance0d3e2f...": 614,
  "instance7a91bc...": 612,
  "instance19f0c4...": 616
}

Каждый инстанс получает примерно 33% запросов. Балансировщик App Service равномерно раздаёт новые подключения, без участия приложения.

Что сделать дальше

Microsoft предлагает два очевидных шага развития архитектуры:

  1. Подключить Easy Auth

    • Включить аутентификацию в App Service.
    • Выбрать Entra ID.
    • Требовать авторизацию на /mcp.
    • Токен прилетит в заголовках, а обработчики инструментов смогут распознавать вызывающего агента без собственной реализации OAuth.
  2. Включить автоскейл по CPU

    • Текущий instanceCount: 3 — стартовая точка.
    • Добавить ресурс Microsoft.Insights/autoscalesettings для плана.
    • Разрешить масштабирование, например, с 3 до 10 инстансов при высоком CPU на инструменте compute_primes.
    • Архитектура уже это поддерживает — в этом и смысл статичности.

Полезные ссылки

  • Пример: github.com/seligj95/app-service-mcp-stateless-scale-python
  • Спецификация MCP: modelcontextprotocol.io/specification/2025-11-25
  • Документация App Service: learn.microsoft.com/azure/app-service/overview

Если вы строите MCP‑инструменты для агентов и устали от ручной балансировки и падений при деплое, этот паттерн с App Service даёт понятную дорожную карту: статичные инструменты, отключённая аффинность, Always On и автоскейл по метрикам.


Читайте также