LIVE

Языки программирования для бэкенда: бенчмарк скорости и памяти на миллион запросов

График, на котором Rust уходит вверх, а Python или Node.js остаются внизу, легко принять за готовый ответ на вопрос о выборе бэкенда. Но без методики он говорит мало. Миллион запросов за всё время теста и миллион запросов в секунду — разные нагрузки.

Ратмир Чеботарев·Обновлено: 26 октября 2026 г.·11 мин

Языки программирования для бэкенда: бенчмарк скорости и памяти на миллион запросов

А отдача короткого текста и обработка запроса с базой данных, авторизацией и несколькими внешними вызовами — разные задачи.

Бенчмарк полезен, если понимать, что именно он измеряет. Он показывает поведение конкретной связки языка, фреймворка, рантайма и конфигурации на конкретной нагрузке. Это не универсальный рейтинг и не прогноз для произвольного продакшен-сервиса.

Поэтому сравнивать производительность серверных языков программирования стоит не по одной цифре, а по нескольким вопросам: как устроен тест, что ограничивает скорость, сколько памяти занимает процесс и насколько сценарий похож на рабочую нагрузку. Без этих оговорок красивое сравнение легко превращается в спор о языках вместо разговора об архитектуре.

Методология стресс-тестирования: от синтетики TechEmpower до wrk

TechEmpower Framework Benchmarks сравнивает веб-фреймворки на нескольких типах задач. Среди них есть выдача простого текста, сериализация JSON, запросы к базе данных и сценарии, где база данных сочетается с шаблонизацией. Такой набор полезен именно разнообразием: он позволяет увидеть, как меняется результат, когда к обработке HTTP добавляются новые операции.

При этом тесты не сравнивают абстрактный Rust с абстрактным Go. В результат входят конкретные фреймворк, версия рантайма, настройки сервера, драйвер базы данных и способ выполнения запроса. Важны также аппаратная конфигурация и условия запуска. Если у двух участников разные настройки или тесты выполнялись на разных стендах, число из таблицы нельзя трактовать как чистую разницу между языками.

Особенно осторожно стоит читать результаты сценария Plaintext. Сервер получает запрос и быстро отдаёт короткий ответ. Это хороший способ оценить верхний предел пропускной способности на простой операции. Но такой тест почти ничего не рассказывает о работе с бизнес-правилами, внешними сервисами или сложной схемой данных.

Инструмент wrk часто используют для генерации HTTP-нагрузки. Он позволяет задавать параметры теста, например число соединений и длительность запуска, а затем смотреть на пропускную способность и распределение задержек. Но само упоминание wrk ещё не делает результат реалистичным. Для сравнения нужно знать, где работали генератор и сервер, как были заданы соединения, какой запрос отправлялся и не стал ли ограничением сам генератор нагрузки.

Слово «конкурентный» здесь тоже требует точности. Число активных соединений не равно числу запросов в секунду. А миллион запросов может означать общее количество обработанных запросов за тест или целевой темп в секунду. Пока эти условия не названы, сравнение цифр будет выглядеть точнее, чем оно есть.

Бенчмарк отвечает не на вопрос «какой язык быстрее», а на вопрос «как эта конкретная реализация справилась с этой конкретной нагрузкой».

Чтобы результат можно было интерпретировать, полезно фиксировать хотя бы такие параметры:

  • сценарий запроса: простая выдача, JSON, база данных или несколько операций;
  • аппаратную конфигурацию и лимиты ресурсов;
  • версии языка, рантайма, фреймворка и зависимостей;
  • настройки генератора нагрузки и длительность теста;
  • пропускную способность, задержки и их распределение;
  • потребление памяти в одинаковых условиях.

Если этих сведений нет, цифру RPS можно использовать как повод изучить тест, но не как основу для расчёта числа серверов.

Пропускная способность на миллион запросов: что говорит RPS

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

В тестах простой выдачи быстрые реализации на Rust и Go способны показывать очень высокие результаты. Однако отдельное значение нельзя переносить на сервис с другой логикой. Добавление сериализации, чтения из базы, проверки доступа или обращения к внешнему API меняет профиль нагрузки. И тогда производительность веб-сервера зависит уже не только от того, как быстро фреймворк принимает соединение.

В таблицах бенчмарков нужно внимательно смотреть на название метрики задержки. Это может быть среднее значение, медиана или один из перцентилей. Если тест сообщает просто latency, нельзя самостоятельно переименовать её в p99. Перцентиль показывает положение значения в распределении: например, p99 означает, что 99% измеренных запросов укладываются в указанное время, а оставшаяся часть отвечает дольше. Среднее и p99 отвечают на разные вопросы, поэтому их нельзя подменять друг другом.

Сравнение скорости языков программирования для бэкенда становится содержательнее, когда в нём рядом стоят пропускная способность и задержка. Ещё лучше, если показано, как обе метрики меняются при росте нагрузки. Один удачный результат на одном режиме не раскрывает, где начинается насыщение и как система ведёт себя после него.

Что измеряетсяЧто помогает понятьЧего показатель сам по себе не объясняет
RPSСколько запросов обработано за секунду в условиях тестаКак поведёт себя сервис с другой логикой и зависимостями
Задержка ответаСколько времени заняла обработка запросаКак распределены быстрые и медленные ответы, если не указан тип метрики
Потребление RAMСколько памяти занял процесс в выбранном сценарииКак расход изменится при другой нагрузке, настройках и объёме данных
Результат теста с базойКак реализация справляется с конкретной операцией чтения или записиКак она поведёт себя с другой схемой, запросом, драйвером или конфигурацией

Есть и отдельный класс CPU-bound задач: обработка изображений, сложная трансформация данных, сжатие или вычисления внутри приложения. В таком тесте нагрузка действительно дольше удерживает процессор занятым. Разница между реализациями здесь может проявиться сильнее, чем в сценарии, где основное время уходит на ожидание базы или сети. Но результат всё равно нельзя приписывать одному языку: влияют алгоритм, параллелизм, компилятор и конкретный фреймворк.

Цена масштабирования: память и влияние сборщика мусора

Потребление памяти веб-серверами важно не только как строка в сравнительной таблице. Оно влияет на то, сколько процессов можно разместить в контейнере или на узле, как часто система будет упираться в лимит и останется ли запас на всплеск нагрузки. При этом у памяти нет одного универсального «расхода на язык»: итог зависит от приложения, библиотек, объёма данных, числа соединений и настроек среды выполнения.

У Rust нет сборщика мусора, который автоматически освобождает объекты во время работы программы. Управление временем жизни данных устроено иначе, и компилятор помогает контролировать владение памятью. Это может уменьшить накладные расходы, но не означает, что любой сервис на Rust обязательно будет занимать меньше любого сервиса на Go или Node.js. Неэффективная структура данных или большой буфер останутся большими независимо от языка.

Go использует сборщик мусора. Он упрощает работу с памятью и помогает писать конкурентные серверные приложения, но его поведение входит в профиль нагрузки. На результат влияют объём создаваемых объектов, частота выделения памяти и настройки рантайма. Поэтому фраза «у Go есть GC» сама по себе не объясняет задержки сервиса. Смотреть нужно на реальные графики памяти, время работы сборщика и задержки приложения при репрезентативной нагрузке.

В Node.js за выполнение JavaScript и управление памятью отвечает V8. Для I/O-bound-сервиса, который большую часть времени ждёт сеть или базу, такая модель может быть вполне уместной. Но длительная CPU-bound-операция способна занять цикл событий и задержать обработку других задач. Это уже вопрос не столько общей скорости языка, сколько организации работы: тяжёлые вычисления можно выносить в отдельные процессы или сервисы, если это оправдано архитектурой.

При сравнении памяти важно уточнять, что именно записано в отчёте. Это может быть пиковое использование, среднее значение за запуск или объём памяти процесса в момент измерения. Нельзя напрямую сравнивать числа, полученные в разных условиях или при разной нагрузке. Имеет значение также то, включены ли в показатель накладные расходы рантайма и сколько запросов сервер обрабатывает одновременно.

Практичный способ читать такие результаты прост: смотреть не на ярлык языка, а на график памяти в течение теста. Если процесс постепенно растёт, это повод искать причину в приложении или конфигурации. Если использование памяти стабильно, но резко повышается при росте числа соединений, нужно понять, что именно удерживается: буферы, контексты запросов, кэш или данные, ожидающие завершения внешней операции.

У сборщика мусора есть и обратная сторона. Ручное управление памятью не исчезает бесследно: в системах без GC больше ответственности лежит на разработчике и выбранной модели владения данными. Язык может дать контроль, но команде нужно уметь им пользоваться. В одном проекте предсказуемое управление ресурсами будет решающим преимуществом, в другом затраты на разработку и поддержку окажутся важнее.

Процессорные ограничения и задержки в CPU-bound-сценариях

Когда обработка запроса почти не ждёт сеть, тест становится чувствительнее к скорости вычислений и работе рантайма. В такой ситуации полезно сравнивать несколько уровней: время самой операции, загрузку CPU, пропускную способность при росте параллелизма и распределение задержек. Одного результата RPS недостаточно, чтобы понять, почему одна реализация оказалась впереди.

Rust компилируется в машинный код и не использует сборщик мусора. Это позволяет избегать ряда накладных расходов рантайма, но не гарантирует автоматического выигрыша на любой задаче. Алгоритм, способ параллельной обработки, обращения к памяти и конкретные библиотеки могут иметь большее значение, чем выбор языка.

Go предлагает модель конкурентности с горутинами и каналами. Для серверов с большим количеством одновременно выполняемых операций это удобная основа. Но в CPU-bound-сценарии важны количество доступных ядер, планирование задач и то, сколько работы выполняет каждая горутина. Если приложение создаёт много временных объектов, влияние сборщика мусора также стоит проверять измерениями, а не предполагать заранее.

Node.js обычно особенно удобен там, где сервер координирует асинхронные операции: принимает запросы, ждёт ответы от базы и других сервисов, формирует результат. Если же один обработчик надолго занимает основной поток вычислениями, запросы в очереди могут ждать его завершения. Для такой нагрузки нужны отдельное исполнение тяжёлой работы или другая архитектурная схема.

Показательно, что «задержка» под нагрузкой не всегда означает время выполнения кода. В неё могут входить ожидание свободного соединения, очередь в приложении, задержка базы и передача ответа. Поэтому при расследовании медленных ответов полезно разложить путь запроса на этапы. Иначе можно оптимизировать язык там, где время теряется на очереди или внешней системе.

В CPU-bound-тесте язык виден лучше. В рабочем сервисе результат часто определяет вся цепочка обработки, от очереди до ответа базы.

Реальные сценарии против синтетики: почему Hello World не отражает нагруженную систему

Синтетический бенчмарк полезен как контрольная точка. Он помогает понять, на что способны фреймворк и рантайм при заданных условиях. Ошибка начинается, когда результат теста простой выдачи принимают за прогноз для приложения с авторизацией, бизнес-правилами и несколькими зависимостями.

Обычный запрос может включать проверку токена, чтение нескольких сущностей из базы, обращение к внешнему API и сериализацию ответа. Каждая операция добавляет собственную задержку и вероятность ошибки. Если запрос к базе выполняется медленно или внешний сервис отвечает нестабильно, повышение скорости HTTP-слоя не устранит это ограничение. Сначала нужно измерить, на каком участке тратится время.

Как построить тест ближе к своему сервису

Начать стоит с профиля реальной нагрузки. Какие запросы преобладают? Какой объём данных проходит через обработчик? Сколько операций приходится на один запрос и какие из них параллельны? Без этих ответов нагрузочный тест рискует проверить удобный сценарий, которого в продакшене почти нет.

Затем полезно повторить не весь сервис целиком, а его критичные пути. Например, один сценарий может проверять чтение из базы и формирование JSON, другой — взаимодействие с внешним API, третий — CPU-bound-преобразование. Это помогает отделить ограничения приложения от ограничений его зависимостей.

При запуске теста нужно смотреть не только на максимальную пропускную способность. Важно, как сервер ведёт себя при постепенном увеличении нагрузки: растёт ли задержка, образуется ли очередь, увеличивается ли потребление памяти, остаётся ли запас по CPU. Если система быстро отвечает на коротком прогоне, но ухудшает задержки при длительной работе, одной итоговой цифры это не покажет.

Для повторяемого сравнения нужно сохранять конфигурацию и менять по возможности один параметр за раз. Если одновременно поменять язык, фреймворк, настройки базы и размер машины, итоговый результат будет трудно объяснить. Такой эксперимент может подсказать, что один вариант в целом работает лучше, но не покажет, почему.

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

Выбор языка начинается с нагрузки и команды

Rust имеет смысл рассматривать, когда важны контроль над ресурсами и производительность, а команда готова работать с особенностями языка. Он особенно интересен для CPU-bound-компонентов и сервисов с жёсткими ограничениями по памяти. Но потенциал языка не отменяет стоимости разработки и сопровождения.

Go часто выбирают для серверных приложений, где нужны понятная конкурентная модель и практичный баланс между производительностью и простотой развёртывания. Его GC не следует считать автоматическим источником проблем: поведение нужно проверять на нагрузке, похожей на рабочую.

Node.js подходит для сервисов, где много асинхронного взаимодействия и команда уверенно работает с JavaScript. Для тяжёлых вычислений важно заранее решить, как не блокировать обработку остальных запросов. Это может быть отдельный процесс, очередь или специализированный компонент, в зависимости от задачи.

Bun и другие развивающиеся рантаймы тоже стоит оценивать не по одной таблице. Помимо скорости, проверяют совместимость с нужными пакетами, стабильность обновлений, поддержку инструментов и поведение в собственном сценарии. Для критичного сервиса важна не только пропускная способность, но и возможность надёжно диагностировать проблему и выпустить исправление.

Сначала профиль нагрузки, потом бенчмарк

Бенчмарк языков программирования для веб-разработки полезен как инструмент постановки вопросов. Он показывает, где искать различия между фреймворками и рантаймами, но не выбирает технологию за команду. Синтетическая скорость, задержки и потребление памяти имеют смысл только вместе с условиями измерения.

Сравнение скорости языков программирования бэкенда стоит начинать с профиля запросов, а затем собирать тест, похожий на реальные пути приложения. Если сервис большую часть времени ждёт базу или сеть, победа в выдаче plaintext может почти ничего не изменить. Если он выполняет тяжёлые вычисления или работает в жёстком лимите памяти, свойства языка и рантайма становятся заметнее.

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

Частые вопросы

Что измеряет бенчмарк серверных языков программирования?
Бенчмарк показывает поведение конкретной связки языка, фреймворка, рантайма и конфигурации на определенной нагрузке, но он не является универсальным рейтингом или прогнозом для произвольного продакшен-сервиса.
Можно ли использовать тест простой выдачи текста (Plaintext) для оценки реальной работы бэкенда?
Тест Plaintext оценивает верхний предел пропускной способности на простой операции, однако он практически ничего не рассказывает о работе с бизнес-правилами, внешними сервисами или сложной схемой данных.
Что означает метрика RPS в тестах производительности?
RPS показывает, сколько запросов сервер обработал за секунду в условиях конкретного теста, но сама по себе эта метрика не описывает качество обслуживания и не учитывает возможные задержки.
Как наличие сборщика мусора в Go влияет на работу бэкенда?
Сборщик мусора упрощает работу с памятью, но его поведение входит в профиль нагрузки, поэтому на задержки и работу сервиса влияют объем создаваемых объектов, частота выделения памяти и настройки рантайма.
Какие факторы важно учитывать при выборе языка программирования для бэкенда?
Выбор технологии зависит от профиля запросов, допустимых задержек, лимитов ресурсов, особенностей архитектуры, а также от опыта команды и стоимости поддержки.