Почему изоляция критична при сравнении
Сравнение производительности веб‑фреймворков часто оказывается некорректным из‑за множества скрытых факторов. Разные уровни загрузки процессора, количество выделенной памяти, количество воркеров, работа фоновых сервисов или разная конфигурация сервера могут существенно влиять на результаты. Если тестировать один фреймворк в «чистом» окружении, а другой — в уже нагруженной системе, полученные цифры будут отражать не свойства кода, а различия в инфраструктуре. Поэтому каждый тестовый стенд должен быть полностью изолирован, иметь фиксированные ресурсы и одинаковую конфигурацию для всех сравниваемых решений.
Требования к окружению
- Контейнеризация – Docker позволяет задать точный лимит CPU и RAM, гарантируя, что каждый фреймворк будет работать в одинаковых условиях.
- Инструмент нагрузки – k6 (или любой совместимый HTTP‑генератор) обеспечивает контролируемый поток запросов, фиксированную длительность и повторяемость сценариев.
- Однородные маршруты – все сравниваемые сервисы должны обслуживать идентичные эндпоинты, чтобы измерения касались только различий в обработке запросов, а не в бизнес‑логике.
- Сохранение результатов – выводы тестов следует записывать в файл (JSON/CSV) для последующего анализа и построения графиков.
Структура проекта
Организовать репозиторий удобно по следующей схеме:
web_framework_benchmarks/
│
├─ framework_fastapi/
│ ├─ Dockerfile
│ └─ app.py
│
├─ framework_flask/
│ ├─ Dockerfile
│ └─ app.py
│
├─ k6-tests/
│ └─ load_test.js
│
└─ results/
└─ <timestamp>.json
Папки framework_fastapi и framework_flask можно заменить на любые другие фреймворки (NestJS, Spring Boot, Gin и т.д.). Главное — сохранить одинаковую вложенность, чтобы скрипты сборки и тестирования работали без изменений.
Пример простого API‑эндпоинта
Для демонстрации берём элементарный маршрут /hello, который возвращает небольшое JSON‑сообщение. Реализация в FastAPI:
# app.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello")
def hello():
return {"message": "hello world"}
И аналог в Flask:
# app.py
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/hello")
def hello():
return jsonify({"message": "hello world"})
Эти два сервиса полностью эквивалентны с точки зрения бизнес‑логики, поэтому любые различия в показателях будут обусловлены только особенностями фреймворка и его серверного стека.
Docker‑контейнеры с одинаковыми настройками
Важно сравнивать одинаковые серверные решения. Например, запуск FastAPI только через uvicorn и Flask через gunicorn создаст неравные условия (одно приложение будет обслуживаться асинхронным сервером, другое — синхронным). Чтобы устранить дисбаланс, оба фреймворка помещаем за один и тот же процесс‑менеджер. В случае Python удобно использовать gunicorn с воркером uvicorn.workers.UvicornWorker:
# Dockerfile (для FastAPI и Flask)
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Один воркер, 2 CPU, 256 MiB RAM
CMD ["gunicorn", "app:app", "-w", "1", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]
Для Flask меняем только импортируемый объект (app:app остаётся тем же). При сборке контейнеров указываем ограничения:
docker build -t fastapi-bench ./framework_fastapi
docker run -d --name fastapi_test --cpus="2.0" --memory="256m" -p 8001:8000 fastapi-bench
docker build -t flask-bench ./framework_flask
docker run -d --name flask_test --cpus="2.0" --memory="256m" -p 8002:8000 flask-bench
Таким образом оба сервиса получают ровно два виртуальных ядра и 256 MiB ОЗУ, а также одинаковый набор воркеров.
Нагрузка с помощью k6
Сценарий нагрузки в k6-tests/load_test.js может выглядеть так:
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend } from 'k6/metrics';
export let options = {
stages: [
{ duration: '30s', target: 50 }, // разгон до 50 RPS
{ duration: '1m', target: 50 }, // стабильная нагрузка
{ duration: '30s', target: 0 }, // спуск
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95‑й процентиль < 500 мс
},
};
let latency = new Trend('latency');
export default function () {
const res = http.get('http://localhost:8000/hello');
check(res, { 'status is 200': (r) => r.status === 200 });
latency.add(res.timings.duration);
sleep(1);
}
Запуск теста для FastAPI:
docker exec -i fastapi_test k6 run /tests/load_test.js --out json=../results/fastapi_$(date +%s).json
Аналогично для Flask, меняя только имя контейнера. Параметры stages позволяют моделировать постепенный рост нагрузки, а пороги (thresholds) фиксируют требуемый уровень отклика.
Сбор и анализ результатов
k6 генерирует JSON‑файл со всеми метриками: среднее время ответа, процентильные значения, количество ошибок, количество запросов в секунду и пр. Для сравнения удобно импортировать файлы в pandas или любой BI‑инструмент:
import pandas as pd
fastapi = pd.read_json('results/fastapi_1700000000.json')
flask = pd.read_json('results/flask_1700000000.json')
summary = pd.concat([
fastapi['metrics'].apply(lambda x: x['value']).rename('fastapi'),
flask['metrics'].apply(lambda x: x['value']).rename('flask')
], axis=1)
print(summary.describe())
Полученные таблицы позволяют увидеть, где один стек выигрывает по латентности, а где другой показывает лучшую пропускную способность. При необходимости добавляем дополнительные метрики (CPU‑использование, память) через docker stats и включаем их в итоговый отчет.
Расширение подхода на другие технологии
Описанный workflow универсален. Для Node.js достаточно написать Dockerfile с npm install и запустить приложение через pm2 или node с фиксированным числом воркеров. Для Go‑сервисов компилируем бинарник, упаковываем в scratch‑образ и задаём те же ограничения CPU/Memory. Java‑приложения можно запускать в контейнере с ограничением JVM‑heap (-Xmx256m). Rust‑серверы собираются в статический бинарник и тоже помещаются в минимальный образ.
Главное правило остаётся неизменным: одинаковые ресурсы, одинаковый набор запросов, одинаковая конфигурация сервера. При соблюдении этих условий сравнение становится репрезентативным, а полученные цифры — надёжным ориентиром при выборе стеков для продакшна.