Анализ разницы в производительности между средами JVM с помощью тестов

На вопрос Почему Spring быстрее Vert.x? в разных его вариациях спрашивается на StackOverflow в среднем раз в месяц. В конце концов, Spring по-прежнему остается самой популярной средой JVM, поэтому многие компании используют ее. Но Spring Framework не славится своей производительностью. С другой стороны, Vert.x считается одной из самых эффективных сред JVM. Поэтому ожидается, что Vert.x превзойдет Spring в любом тесте. Но это не так.

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

Во-первых, что мы имеем в виду, когда говорим о «быстром» фреймворке или языке? Что касается веб-сервисов, мы не говорим о скорости получения ответа, также известной как задержка запроса. Обычно мы имеем в виду другую метрику, называемую пропускная способность. Задержка показывает, сколько времени требуется для возврата ответа на один запрос. Пропускная способность — это количество запросов, которые сервер может обработать за заданный период времени. Обычно: в течение секунды.

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

Например, в 20-м раунде Vert.x занимает 10-е место с 572 тыс. запросов в секунду, а Spring — 219-е место с 102 тыс. запросов в секунду. Это действительно очень впечатляет.

Но попытка воспроизвести эти впечатляющие результаты иногда оказывается сложной задачей, отсюда и вопрос из заголовка.

Давайте попробуем понять, каковы основные недостатки стратегии бенчмаркинга.

Говоря о Spring, я имею в виду именно Spring Framework, а не Spring WebFlux/Project Reactor, которые работают по-другому. Я также предполагаю, что приложение Spring работает в контейнере Tomcat.

Vert.x ориентирован на ввод-вывод

Изобретательность платформы Vert.x позволила с самого начала распознать, что узким местом большинства реальных приложений является ожидание операций ввода-вывода. Это означает, что не имеет значения, насколько хорошо написано ваше приложение, насколько умна JIT-оптимизация и насколько передовой JVM GC. Большую часть времени ваше приложение будет ожидать ответа от базы данных или от службы, которую кто-то написал на Python или PHP, возможно, 10 лет назад.

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

Это, конечно, очень упрощенное объяснение. Есть несколько очередей, и переключатели контекста, и реактивные драйверы, и куча других интересных вещей, которые я не буду здесь описывать. Однако я хочу, чтобы вы помнили, что Vert.x оптимизирован для ввода-вывода.

Теперь давайте посмотрим, как обычно тестируется производительность Vert.x:

app.get("/json").handler(ctx -> {      
    ctx.response().end("Hello, World!"); 
});

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

app.get("/json").handler(ctx -> {      
    ctx.response()
        .putHeader(HttpHeaders.SERVER, SERVER)
        .putHeader(HttpHeaders.DATE, date)
        .putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
        .end(Json.encodeToBuffer(new Message("Hello, World!"))); 
    }
);


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

Таким образом, преимущество, которое предоставляет вам реактивная среда, такая как Vert.x, сводится к минимуму этим тестом.

Если вы хотите увидеть реальные преимущества реактивной платформы, такой как Vert.x, напишите тестовое приложение, которое выполняет некоторые операции ввода-вывода, например запись в базу данных или чтение из удаленной службы.

Запуск тестов с низким параллелизмом

Способ, которым Spring Framework обрабатывает параллелизм, заключается в выделении пула потоков, предназначенного для обслуживания входящих запросов. Это также называется моделью «поток на запрос». Как только у вас закончатся потоки, пропускная способность вашего приложения Spring начнет снижаться.

ab -c 100 http://localhost:8080/

Здесь мы используем инструмент под названием Apache HTTP Benchmark, чтобы бомбардировать наш сервис запросами. Флаг -c указывает на выполнение 100 одновременных запросов одновременно.

Вы запускаете этот тест на двух сервисах, один написан на Spring, а другой на Vert.x, и не видите никакой разницы в производительности. Почему это?

В отличие от Vert.x, Spring Framework не контролирует количество используемых потоков напрямую. Вместо этого количество потоков контролируется контейнером, в нашем случае — Tomcat. Максимальное количество потоков, устанавливаемое Tomcat по умолчанию, равно 200. Это означает, что пока у вас не будет хотя бы 200 одновременных запросов, вы не увидите большой разницы между приложением Spring и Vert.x. Вы просто недостаточно подчеркнули свое приложение.

Если вы хотите нагрузить приложение Spring, установите количество одновременных запросов выше, чем максимальный размер вашего пула потоков.

Запуск тестов на той же машине

Вернемся к тому, как работает Vert.x. Я уже упоминал, что Vert.x оптимизирует свою работу, помещая все входящие запросы в очередь. Как только приходит ответ, он также ставится в ту же очередь. Существует очень ограниченное количество потоков, называемых потоками EventLoop, занятых обработкой этой очереди. Чем больше у вас запросов, тем более загруженными становятся потоки EventLoop и тем больше ресурсов ЦП они потребляют.

Что происходит теперь, когда вы запускаете тест на своей машине? Например:

ab -c 1000 http://localhost:8080/

Дальше будет следующее. Тестовый инструмент попытается создать как можно больше запросов, используя все ресурсы ЦП вашего компьютера. Служба Vert.x попытается обслужить все эти запросы, а также попытается использовать все ресурсы.

Чтобы максимизировать производительность приложения Vert.x во время теста, обязательно запускайте его на отдельном компьютере, который не использует общий процессор с компьютерами, на которых выполняется тест.

Это подводит нас к следующему пункту.

Производительность Spring Framework в порядке

Я был большим поклонником Vert.x, по крайней мере, последние 5 лет. Но давайте посмотрим на пропускную способность приложения Spring в тестах, которые мы упоминали ранее.

  • Открытый текст: 28K
  • Сериализация JSON: 20 КБ
  • Один запрос: 14 КБ
  • Состояния: 6K
  • Множественные запросы: 1,8K
  • Обновления данных: 0,8K

Глядя на эти цифры и принимая во внимание, что мы обычно запускаем наши сервисы в кластерах по крайней мере из 3 экземпляров, вы должны спросить себя: нужно ли моему приложению обрабатывать 2 КБ обновлений в секунду?

Если ответ «да», вам может потребоваться запустить тесты на нескольких машинах, чтобы нагрузить даже приложение Spring до предела.

Заключение

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

И при этом важно использовать объективные показатели. Измерение пропускной способности сервиса с помощью бенчмарка — хорошее начало, но делать это нужно правильно.

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

Кроме того, убедитесь, что вы запускаете свои тесты на разных машинах, а не на тех, на которых выполняется код вашего приложения. В противном случае результаты могут вас не впечатлить.

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