
В 2026 году асинхронный Python уже никого не удивляет. Мы привыкли разворачивать FastAPI в Kubernetes, накидывать автоскейлинг в облаке и не особо задумываться о том, сколько тактов CPU съедает сериализация одного JSON. Но что делать, если ваш бюджет на инфраструктуру равен нулю, а в распоряжении есть только «печка» из 2012 года и шумный жесткий диск?
Цель эксперимента: доказать, что Resource-Constrained Engineering (проектирование в условиях о��раничений) актуально и сегодня. Можно построить бэкенд, который не просто "работает", а выдает тысячи RPS и предсказуемый P99 на задачах, где обычно принято просто докупать ресурсы в облаке.
Тестовый стенд:
Процессор: AMD FX-8320 (Vishera).
Память: 12 ГБ DDR3.
Диск: HDD 7200 RPM.
Примечание: AMD FX-8320 - архитектура Piledriver подразумевает 4 модуля (всего 8 потоков) по 2 целочисленных ядра, разделяющих общие ресурсы, такие как FPU и L2-кэш.
Технологический стек
Runtime: Python 3.12.6.
Servers: Granian (на базе Rust) и Uvicorn (uvloop).
Infrastructure:
Angie — форк Nginx с расширенной функциональностью (используется как фронтенд и SSL-терминатор).
Valkey — полностью open-source форк Redis (наш слой кэширования).
PgBouncer — для менеджмента соединений с БД, чтобы не плодить тяжелые процессы PostgreSQL.
PostgreSQL 18 — актуальная на момент эксперимента.

Frontend: Angie (Прием HTTPS, отдача статики, сглаживание всплесков).
Transport: Unix Domain Socket (Минимизация сетевого оверхеда между прокси и бэкендом).
Application: FastAPI на базе Granian (Rust-runtime) или Uvicorn.
DB Proxy: PgBouncer (Пул соединений для экономии ресурсов CPU/RAM).
Storage: PostgreSQL 18 + Valkey (L2 кэш).
Что пошло не так? (Observed problems)
Многие считают, что переход на FastAPI автоматически решает проблемы производительности. Но на старом железе асинхронность без тюнинга быстро превращается в тыкву. В ходе нагрузочных тестов я выделил 4 критических узких места:
Узел | Проблема | Последствие |
|---|---|---|
Crypto | Тяжелый RSA-2048 | CPU-bound лимит RPS |
Runtime | Синхронный Argon2id | Блокировка Event Loop, таймауты |
JSON | jsonable_encoder | Оверхед на аллокации и GC |
Storage | random_page_cost | Ошибки планировщика БД на HDD |
Детализация проблем:
Высокая стоимость RSA-подписей: Вычислительная сложность RSA-2048 на архитектуре Piledriver приводила к быстрой утилизации ресурсов. На каждый запрос тратилось слишком много тактов процессора, что ограничивало общий предел RPS.
Блокировка Event Loop: Синхронное хэширование Argon2id буквально останавливало событийный цикл. Пока поток был занят вычислением хэша, приложение не могло ответить даже на «легкие» запросы вроде
/ping, что вызывало каскадные таймауты.Избыточность jsonable_encoder: Стандартный механизм FastAPI выполнял рекурсивный обход объектов для приведения типов (UUID, datetime) к примитивам. Это создавало лишние аллокации, нагружало Garbage Collector и неоправданно долго удерживало цикл.
Неверная оценка I/O в PostgreSQL: Из-за заниженного параметра
random_page_costпланировщик считал случайный доступ к данным на диске дешевым. В результате он выбирал индексное сканирование там, где медленный HDD физически не успевал перемещать считывающую головку, вызывая деградацию при росте базы.
Архитектурные решения (ADR) и тюнинг системы
Главный враг асинхронности — блокировка событийного цикла. Если одна задача «повесила» поток, всё приложение замирает.
Ряд внесенных мной изменений:
1. Миграция на Ed25519: Криптография без боли
Проблема: RSA-2048 (RS256) требует тяжелых вычислений, которые надолго занимали вычислительные блоки FX-8320.
Решение: Миграция со связки pyjwt (алгоритм RSA) на более современную библиотеку joserfc (алгоритм Ed25519).
Результат: Нагрузка на CPU снизилась, а Ed25519 обеспечивает сопоставимый уровень криптостойкости при значительно меньших затратах CPU. Бонусом упростился CI/CD: работа с ключами теперь не требует вороха .pem файлов, всё нативно поддерживается через стандарт JWK.
2. Гибридное кэширование: L1 + L2
Проблема: Использование только Redis (L2) создавало лишние сетевые задержки (network round-trips) даже на «горячих» данных профиля пользователя.
Решение: Замена fastapi-cache2 на библиотеку cashews с включенным client_side=True.
Архитектура (Hybrid Cache (Client-side + L2)):
Client-side (L1): Использование локального кэша приложения позволило избежать лишних сетевых вызовов (round-trips) к Redis для самых «горячих» данных.
Redis/Valkey (L2): Общее хранилище состояния для синхронизации между воркерами.
Результат: Переход на библиотеку cashews ускорил чтение, из коробки есть защита от «Cache Stampede» (одновременное обновление кэша), а также ги��кая инвалидация, которой не хватало в библиотеке fastapi-cache2.
3. Асинхронный Argon2: Укрощение шторма
Проблема: Хэширование паролей — самая тяжелая задача для CPU.
Решение: Чтобы не «вешать» основной поток, я вынес Argon2id в выделенный ThreadPoolExecutor.
Конфигурация:
Жесткое ограничение нагрузки —
max_workers = 4(ThreadPoolExecutor).Запретили Argon2 плодить внутренние потоки - параметр
parallelism=1.
Примечание: Ограничение max_workers = 4 продиктовано топологией FX-8320: процессор состоит из 4 модулей. Выделение одного потока под тяжелую криптографию на каждый модуль позволяет избежать борьбы за общий FPU и кэш L2, сохраняя ресурсы для планировщика и сетевого стека.
Результат: Ограничение позволило системе оставаться отзывчивой и обрабатывать новые запросы, пока часть ядер занята криптографией.
4. Разделение DTO: Pydantic на вход, msgspec.Struct на выход
Проблема: Стандартный механизм FastAPI (jsonable_encoder) выполнял рекурсивный обход объектов для приведения типов (UUID, datetime) к примитивам. Это создавало лишние аллокации, нагружало Garbage Collector и неоправданно долго удерживало цикл.
Решение: Я разделил слои данных: валидация входящих запросов осталась на Pydantic (из-за удобства и интеграции с FastAPI), но сериализация ответов теперь идет через msgspec.Struct.
Результат: Полностью исключили стадию промежуточного кодирования. P99 latency на эндпоинте /health упала с 859 мс до 128 мс (в 6.5 раз). На слабом железе это один из наиболее эффективных способов избавиться от оверхеда при упаковке JSON.
5. Тюнинг «под капотом»: Linux и PostgreSQL
Когда Python-код в порядке, бутылочное горлышко смещается в сторону дисковых операций на HDD 7200 RPM, для которого случайное чтение — это «физическая боль» из-за задержек позиционирования головки (seek time).
PostgreSQL: Дружим с «вращающимися блинами»
Проблема: Изначально заниженное мной значение (random_page_cost=1.1) заставляет планировщик верить, что чтение из произвольного места диска стоит столько же, сколько последовательное. Для HDD это ложь: головке диска нужно время на позиционирование.
Решение: После коррекции (изменил параметр random_page_cost с 1.1 до 3.0) планировщик начал строить адекватные планы запросов, и производительность перестала падать при росте базы.
Оптимизация:
Partial Attribute Loading: В ORM используем
load_only, забирая только необходимые поля. Меньше данных с диска — меньше нагрузка на шину и кэш.Экономия на INSERT: Отключил автоматическое обновление объектов (
auto_refresh=False) средствамиadvanced-alchemy, избавившись от избыточных SELECT после инсертов.
Ядро Debian: Performance Mode
Чтобы система не «отфутболивала» соединения в моменты пиковой нагрузки, подкрутил параметры ядра:
net.core.somaxconn = 2048: Увеличили очередь на прослушивание портов. Это позволяет ОС удерживать входящие подключения в очереди, пока воркеры заняты вычислением Argon2id.
vm.overcommit_memory = 1: Критично для систем с 12 ГБ RAM при использовании Valkey/Redis. Разрешили ядру выделять память более гибко, предотвращая внезапные OOM-киллеры при всплесках нагрузки.
Синхронизация очередей (Backlog): Чтобы избежать потерь соединений на стыке компонентов, привел лимиты к единому знаменателю. Параметр ядра
net.core.somaxconn = 2048был синхронизирован с настройками запуска Valkey (--tcp-backlog 2048) и флагом Granian--backlog 2048. Это гарантирует, что ни одно звено цепи не начнет дропать пакеты раньше времени при резких всплесках нагрузки.
«Точка А» (Baseline)
Прежде чем приступать к оптимизации, нужно было понять масштаб катастрофы.
Я развернул проект IronTrack на «голом железе» под управлением Debian 12.
Методология и воспроизведение (Repro)
Для чистоты эксперимента все замеры проводились с использованием утилиты wrk. Нагрузка подавалась с внешнего узла в той же локальной сети (1 Гбит), чтобы исключить влияние самого бенчмарка на ресурсы CPU.
Основные сценарии тестирования:
# 1. Проверка «пропускной способности» (Throughput) # Цель: замерить чистый оверхед ASGI-адаптера и мидлварей без нагрузки на БД. wrk -t8 -c100 -d30s --latency http://127.0.0.1:8000/ping # 2. Интеграционный тест (App + DB + Cache) # Цель: убедиться в отсутствии задержек при опросе всех зависимостей. wrk -t8 -c100 -d30s --latency http://127.0.0.1:8000/health # 3. Авторизованный доступ (JWT + Valkey) # Цель: замерить производительность валидации JWT и скорость извлечения сессии из кэша. wrk -t8 -c100 -d30s --latency -H 'Cookie: access_token="TOKEN"' http://127.0.0.1:8000/api/v1/access/me # 4. Регистрация (Heavy CPU-bound + HDD) # Цель: замер Argon2id и скорости записи на HDD. # Используем 20 соединений, чтобы минимизировать context switching на 8 ядрах. wrk -t8 -c20 -d30s --latency -s benchmarks/scripts/signup.lua http://127.0.0.1:8000/api/v1/access/signup # 5. Тест через прокси (HTTPS + UDS) # Цель: оценка оверхеда TLS и эффективности транспорта через Unix-сокеты (Angie). wrk -t8 -c20 -d30s --latency -s benchmarks/scripts/signup.lua https://app.localhost/api/v1/access/signup
Каждый тест запускался после 30-секундного прогрева, результаты усреднялись по 5 прогонам.
Примечание: Ограничение до 20 соединений для регистрации позволяет ядрам реально доделывать работу, а не только переключаться между задачами.
Условия запуска granian/uvicorn
Uvicorn: запускался с 8 воркерами и циклом событий
--loop uvloop.Granian: запускался с 8 воркерами, работал в многопоточном режиме
--runtime-mode mt(--runtime-threads 2), циклом событий--loop uvloopи количеством соединений--backlog 2048.
Подготовка «взлетной полосы»
Стандартные настройки Linux не рассчитаны на агрессивный бенчмаркин��. Чтобы стек TCP/IP не стал бутылочным горлышком раньше времени, я подкрутил лимиты:
ulimit -n 65535 # Расширяем лимит открытых файлов sudo sysctl -w net.core.rmem_max=2500000 # Буферы приема sudo sysctl -w net.core.wmem_max=2500000 # Буферы передачи
Результаты Baseline: Замеры до оптимизации
На этом этапе в коде еще жил тяжелый jsonable_encoder, RSA-2048 и PostgreSQL (random_page_cost=1.1).
Эндпоинт | Сценарий | Метрика | Granian (mt) | Uvicorn (uvloop) | Вердикт |
|---|---|---|---|---|---|
/ping | ASGI & Middlewares | RPS | 5720 | 4703 | Granian на 21% быстрее |
/health | DB + Redis Check | P99 Latency | 459 ms | 859 ms | Uvicorn стабильнее (0 ошибок), но медленнее |
/access/me | JWT + L2 Cache | RPS | 1404 | 1613 | Uvicorn быстрее на чтении |
/access/signup | Argon2 + HDD | Avg Latency | 863 ms | 970 ms | Granian лучше под нагрузкой |
Результаты Baseline: Визуализация проблем
Сценарий «Средняя нагрузка» (GET /access/me, 100 соединений)
Здесь мы проверяем чистую работу с кэшем (Valkey) и валидацию JWT. На графике отчетливо видна разница в поведении серверов:

Слева: RPS (Uvicorn лидирует: 1613 vs 1404). Справа: P99 Latency (у Granian «хвост» задержек почти в 2 раза выше: 279 мс).
Инсайт: Uvicorn показывает себя стабильнее на простых операциях чтения. P99 в 279 мс у Granian — сигнал о том, что на архитектуре FX накладные расходы на синхронизацию потоков (mt) превышают пользу от Rust-движка.
Сценарий «Тяжелая нагрузка» (POST /access/signup, 20 соединений)
Здесь в игру вступают Argon2id и запись на медленный HDD:

Слева: Успешные инсерты (Granian: 409, Uvicorn: 384). Справа: Средняя задержка (Granian быстрее: 863 мс против 970 мс).
Инсайт: Rust-планировщик Granian эффективнее распределяет задачи. Однако абсолютные цифры (задержка почти в 1 секунду) — это то, с чем мы будем бороться.
Эффект «Сглаживания» (Direct vs Angie + UDS)
Сравнение прямой работы с бэкендом и через прокси Angie:

Снижение задержек на 12% при использовании связки Angie + UDS на эндпоинте /signup.
Инсайт: Это визуальное доказательство эффекта Request Smoothing. Несмотря на оверхед от TLS, Angie выступает демпфером: она буферизирует соединения и отдает их бэкенду ровным потоком через Unix-сокеты, минимизируя накладные расходы на сетевой стек внутри хоста. Это снижает вероятность нелинейного роста задержек при переполнении очереди задач планировщика Python.
Итоговые результаты и Режим Proxy
После внедрения всех оптимизаций и глубокой настройки сетевого стека, мы получили полную картину производительности. Сравнение проводилось в трех состояниях: «голый» Baseline, оптимизированный код и финальная сборка под прокси-сервером.
1. Сравнение серверов: Легкость vs Мощность
На графике ниже представлено лобовое столкновение Uvicorn (uvloop) и Granian (mt) на разных типах задач.

Производительность серверов в зависимости от сложности запроса (RPS, логарифмическая шкала).
Инсайт:
Uvicorn остается королем на «пустых» запросах (
/ping) и простых JSON-ответах. Его модель идеально ложится на архитектуру AMD FX, не создавая лишней нагрузки на общую шину данных и L3-кэш.Granian вырывается вперед в тяжелом сценарии (
/signup). Когда ядра CPU плотно заняты Argon2id, Rust-планировщик Granian эффективнее распределяет задачи, обеспечивая +10% RPS и более быстрый отклик.
Примечание: После тюнинга очередей в ядре Linux (somaxconn) преимущество Uvicorn на легких запросах стало еще заметнее. Это связано с тем, что на архитектуре Piledriver затраты на синхронизацию потоков в Granian (mt) начинают превышать время обработки самого запроса.
2. Эволюция авторизации (/access/me)
Путь от базовой настройки до финальной наглядно показывает эффективность принятых архитектурных решений.

Сравнение RPS и задержки (Latency) в процессе оптимизации системы.
Мы видим характерный «горб» производительности:
Baseline: Система работает на пределе возможностей RSA.
Optimized: Переход на Ed25519 и Hybrid L1-кэш поднял RPS с 1613 до 2384 (+48%).
Proxy Mode: Падение RPS в режиме Proxy обусловлено не только затратами на TLS-рукопожатия. На старом CPU суммируются задержки на копирование данных между сокетами (Userspace <-> Kernel), переключение контекста между процессами Angie и Granian, а также буферизацию пакетов. Однако Angie работает как демпфер (Request Smoothing), сглаживая распределение задержек (Latency distribution) и предотвращая переполнение очередей бэкенда.
3. Итоговая интеграция: Angie + UDS
Режим Proxy через Unix-сокеты доказал, что безопасность может приносить пользу стабильности бэкенда.

Сравнение прямой работы по HTTP и через связку Angie + HTTPS + UDS.
Анализ интеграции:
SSL Overhead: Падение RPS на легких запросах почти в два раза (3170 → 1556) было неизбежным. Старый FX-8320 тратит слишком много циклов на симметричное шифрование трафика.
Request Smoothing: На «тяжелой» регистрации (
/signup) мы получили стабильные 10.2 RPS. Хотя это число ниже прямого HTTP, использование Angie позволило полностью избавиться от ошибок тайм-аута.Надежность: Прокси выступает в роли демпфера: он принимает входящие соединения, буферизирует их и отдает бэкенду ровным потоком. Это позволило системе выдержать наполнение базы свыше 10,000 записей без деградации времени отклика.
Заключение
Эксперимент показал, что «старое» железо часто списывают со счетов преждевременно. Проблема медленных ответов в 90% случаев крылась не в возрасте транзисторов, а в архитектурных решениях, которые мы принимаем по умолчанию, надеясь на мощь современных облаков.
Основные выводы для тех, кто решит повторить:
Криптография — это дорого. Если у вас старый CPU, замена RSA на Ed25519 — это самый дешевый способ поднять RPS.
Блокировка цикла — это фатально.
ThreadPoolExecutorс жестким лимитом воркеров для Argon2id спасает отзывчивость системы.Прокси — это стабилизатор. Angie (или Nginx) перед FastAPI на старом железе нужна не только для SSL, но и для того, чтобы бэкенд не захлебнулся от неравномерного потока TCP-пакетов.
В конечном итоге оптимизация под AMD FX и HDD — это отличный тренажер для инженера. Она заставляет вспомнить о том, как работают кэши, как перемещается головка диска и почему каждый лишний системный вызов имеет значение.
Материалы и ресурсы
Все этапы эксперимента задокументированы и доступны для воспроизведения:
Репозиторий проекта: github.com/bizoxe/iron-track — здесь находится код приложения, Docker-файлы и конфигурации Angie/PostgreSQL.
Результаты тестов: BENCHMARKS.md — полная история замеров «До» и «После» оптимизации, команды запуска, логи.
