Проектирование API на 10k RPS с современной JDK: ёмкость и контроль перегрузки
На этой странице
HTTP API на 10 000 запросов в секунду прежде всего нуждается в явных бюджетах ёмкости запросов и зависимостей и поведении при перегрузке, а уже затем — в новой JDK. Начните с реплик API без состояния в нескольких зонах доступности, ограничений допуска запросов и конкурентности для каждой зависимости. Добавляйте кеши, очереди и реплики чтения согласно измеренным узким местам и требованиям согласованности. Это руководство по проектированию с иллюстративными расчётами, а не отчёт об успешном нагрузочном тесте на 10k RPS.
По состоянию на 31 октября 2026 года последний общедоступный выпуск — JDK 27. Код ниже проверялся на Temurin 27.0.1.0.2+1. Разделяйте новизну возможностей и срок поддержки: план поддержки Oracle указывает JDK 25 как LTS, а 27 — как non-LTS. Политики поддержки других поставщиков и совместимость фреймворков проверяйте в их собственной документации.
Определите границу 10k и критерии успеха
Здесь 10k означает запросы в секунду, достигающие балансировщика API, без обращений к статике CDN. Это не количество одновременных соединений или виртуальных потоков. Учитывайте отдельно предлагаемые RPS, RPS успешно завершённых бизнес-операций, отказы в допуске и ошибки. Одни лишь быстрые ответы с ошибками не подтверждают ёмкость.
Эти предположения иллюстрируют расчёты. Они не являются измерениями реального сервиса или рекомендуемыми настройками.
| Параметр | Иллюстративное предположение |
|---|---|
| Целевой входящий поток | 10 000 RPS |
| Доля чтения/записи | 90% / 10% |
| Доля попаданий в кеш чтения | 80% |
| Среднее время пребывания в API в устойчивом режиме | 50ms |
| Цель ответа | p99 ниже 200ms; частота ошибок ниже 0.1% |
| Использование базы данных | Одно обращение на промах чтения/запись; среднее время удержания соединения 10ms |
Закон Литтла, L = λ × W, применяется к средним значениям в одной границе стабильной системы. Если нормальная пропускная способность равна 10 000/s, а среднее время пребывания — 0.05 секунды, то в среднем во всей системе одновременно обрабатывается около 500 запросов. Не подставляйте p99 200ms для расчёта средней конкурентности и не превращайте 500 напрямую в жёсткий предел. Всплески, распределения задержек и запас при отказах требуют отдельной проверки.
Обращения к базе данных составляют 9,000 × (1 − 0.8) + 1,000 = 2,800/s; среднее количество занятых соединений — 2,800 × 0.01 = 28. Это не рекомендация размера пула 28. Несколько SQL-операторов, блокировки, транзакции и отказ кеша меняют предположения. Включите в бюджет пропускной способности сети также байты ответов, TLS, репликацию и журналирование.
Опорная архитектура: горизонтальное тиражирование небольшого сервиса
Это предлагаемый вариант размещения. Пример Java ниже реализует только принцип контроля допуска, а не всю инфраструктуру. Одна цель 10k RPS не устанавливает необходимость микросервисов или шардинга. Тиражируйте модульное приложение без состояния, затем отделяйте узкие места, выявленные измерениями.
- Пограничный слой и балансировщик: ограничивайте частоту запросов каждого клиента, соединения, размеры тел запросов и медленные соединения. Кешируйте в CDN только подходящие публичные ответы; ключи и область кеширования аутентифицированных данных проектируйте отдельно. Используйте проверки состояния и завершение обслуживания с отводом трафика, чтобы новые запросы не попадали на неготовые реплики.
- Реплики API: ограничивайте обрабатываемую работу на границе запросов и используйте виртуальные потоки для понятного кода с блокирующим вводом-выводом. Виртуальные потоки не увеличивают ёмкость БД и не ускоряют вычисления CPU. Как объясняет руководство JDK 27, дефицитные ресурсы нужно ограничивать отдельно, например семафорами.
- Кеш и база данных: кеш чтения необязателен. Проверяйте распределение TTL, объединение одновременных промахов и трафик БД при отказе кеша. Сначала изучите индексы, планы запросов и длительность транзакций. Используйте реплики чтения только там, где допустимо отставание репликации.
- Внешние вызовы и асинхронная работа: выделите каждой зависимости отдельный бюджет конкурентности. Ставьте в очередь только работу, которую необязательно завершать до ответа. Если нужно согласовать записи БД и доставку сообщений, спроектируйте транзакционный outbox с повторяемой доставкой и идемпотентными потребителями. Очереди тоже имеют ограниченные ресурсы: наблюдайте накопившиеся задания, возраст самого старого сообщения и ошибки обработки.
Проектируйте быстрый отказ в допуске и защиту ресурсов как нормальное эксплуатационное поведение, описанное в рекомендациях Google SRE по перегрузке. Масштабирование только по CPU может пропустить остановки ввода-вывода; совместно наблюдайте выполняемые запросы, частоту отказов в допуске, ожидание получения соединений и насыщение зависимостей.
Сколько нужно реплик и соединений с БД?
При фиксированных оборудовании, куче, лимитах CPU контейнера и составе запросов измерьте q_safe: пропускную способность одной реплики, соответствующую SLO и согласованному эксплуатационному запасу. В этой статье она не измерялась.
Если тесты действительно установят q_safe = 2,500 RPS, четыре реплики арифметически покроют обычную цель. Пять дают отправную точку для сохранения 10k после отказа одной реплики. Это не гарантирует ёмкости при отказе зоны. Шесть реплик, по две в каждой из трёх зон, оставляют четыре после отказа одной зоны и таким образом покрывают 10k на бумаге, но перераспределение балансировщиком, перекос зон и переключение БД/кеша тоже нужно проверить. Сохраняйте запас на время до завершения автоматического масштабирования.
Не увеличивайте пулы БД независимо для каждой реплики. Для каждой конечной точки БД удерживайте maximum simultaneously existing API replicas × per-replica pool limit + worker connections + operational reserve в пределах доступного бюджета соединений. Учитывайте перекрывающиеся реплики при увеличении масштаба, постепенном развёртывании и льготном периоде завершения. Документация PostgreSQL объясняет, что увеличение max_connections увеличивает выделение ресурсов, включая общую память. Дополнительные соединения не исправляют медленные запросы.
Минимальный пример допуска на виртуальных потоках
Схема соответствует Gate.call(). Метод main() — демонстрация семантики допуска без HTTP-сервера, создающая две операции и один отказ. Ограничение в две операции предназначено для теста, а не для настройки 10k RPS. В настоящем сервере применяйте допуск до дорогой работы и также ограничивайте соединения, серверные очереди и создание виртуальных потоков до шлюза средствами пограничного слоя и сервера.
AdmissionDemo.java
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;
public class AdmissionDemo {
static final class Gate {
private final Semaphore permits;
Gate(int limit) {
if (limit <= 0) throw new IllegalArgumentException("limit must be positive");
permits = new Semaphore(limit);
}
<T> T call(Callable<T> work) throws Exception {
if (!permits.tryAcquire()) throw new RejectedExecutionException("busy");
try {
return work.call();
} finally {
permits.release();
}
}
int available() { return permits.availablePermits(); }
}
public static void main(String[] args) throws Exception {
Gate gate = new Gate(2);
CountDownLatch entered = new CountDownLatch(2);
CountDownLatch release = new CountDownLatch(1);
Callable<String> request = () -> gate.call(() -> {
if (!Thread.currentThread().isVirtual()) throw new AssertionError("virtual thread required");
entered.countDown();
release.await();
return "ok";
});
try (var tasks = Executors.newVirtualThreadPerTaskExecutor()) {
var first = tasks.submit(request);
var second = tasks.submit(request);
try {
entered.await();
System.out.println("admitted=2");
try {
gate.call(() -> "unexpected");
throw new AssertionError("capacity exceeded");
} catch (RejectedExecutionException expected) {
System.out.println("overload=rejected");
}
} finally {
release.countDown();
}
System.out.println("completed=" + first.get() + "," + second.get());
}
try {
gate.call(() -> { throw new IllegalStateException("demonstration"); });
} catch (IllegalStateException expected) {
System.out.println("failure=propagated");
}
System.out.println("available=" + gate.available());
}
}
javac --release 27 AdmissionDemo.java
java AdmissionDemo
Этот вывод наблюдался с javac и java из одной установки JDK 27. Флаги предварительных возможностей не нужны.
admitted=2
overload=rejected
completed=ok,ok
failure=propagated
available=2
Неудача tryAcquire() отклоняет работу вместо постановки в очередь. finally возвращает разрешение по фактическому завершению операции, а исключения передаются вызывающему коду. Это ограничение конкурентности, а не ограничитель запросов в секунду. HTTP-адаптер может сопоставить отказ из-за ёмкости сервиса с ответом вроде 503 отдельно от квот клиентов. Этот код не реализует сопоставление HTTP-статусов, аутентификацию, сроки выполнения или пулы БД.
Не допускайте усиления перегрузки сроками и повторами
Задайте сквозной срок выполнения и распределяйте оставшийся бюджет между получением соединения БД, запросами, HTTP-соединением и чтением ответа. Сумма p99 этапов не даёт сквозной p99. Истечение времени ожидания вызывающей стороны не останавливает мгновенно настоящую работу БД или HTTP; проверяйте отмену драйвера/клиента и возврат ресурсов. Возврат разрешения конкурентности при продолжающейся нижележащей операции нарушает ограничение.
Повторяйте на одном уровне с ограниченным числом попыток, общим сроком, экспоненциальной задержкой и случайным разбросом, разрешая повторы только для подходящих ошибок и операций с установленной идемпотентностью. Как объясняют рекомендации Amazon о тайм-аутах и повторах, повторы могут добавлять нагрузку. Для платежей и заказов сначала определите предотвращение дубликатов и поведение поиска результата.
Что должно пройти проверку перед заявлением о ёмкости 10k?
Подготовьте настоящий HTTP-сервер, данные, близкие к рабочим, TLS, аутентификацию и размеры ответов, затем проверяйте в таком порядке.
- Повышайте нагрузку на одну реплику, чтобы установить
q_safe. Совместно записывайте CPU, GC, выделение памяти, сокеты, блокировки, ожидания БД, успешные RPS и сквозную p99. - После подготовки и прогрева повышайте предлагаемый трафик через всю архитектуру ступенями 1k→3k→5k→10k и удерживайте цель согласованное время. Включите длительность и распределение данных в результаты.
- При открытой модели нагрузки, например k6 constant-arrival-rate, связывайте 10 000 итераций/с с 10k HTTP RPS только после подтверждения одного HTTP-запроса на итерацию с отключёнными автоматическими перенаправлениями и повторами. Проверяйте CPU генератора, сеть, запас VU и dropped_iterations. Тест, который не создал целевой входящий поток, не доказывает ёмкость сервера.
- Проверьте потерю реплики/зоны, холодные или отказавшие кеши, медленные БД/зависимости, постепенное развёртывание и всплески. Исключайте отклонённые ответы из бизнес-успехов и проверяйте восстановление, опустошение очередей и утечки соединений после насыщения.
Для этой статьи выполнялись только отказ шлюза в допуске и возврат разрешений после нормальных и аварийных операций. Полная архитектура не проходила нагрузочный тест. Рассматривайте сравнение виртуальных потоков JDK 21/25 и границы контекста запроса ScopedValue отдельно от проверки рабочей архитектуры.
Обсуждение