вторник, 29 декабря 2015 г.

Spring Framework vs EJB vs CDI. Небольшой бенчмарк с использованием JMH

На днях Суровый выложил на GitHub исходники и некоторые результаты небольшого бенчмарка, проверяющего гипотезу о том, что Spring Framework быстрее этих ваших EJB.

Как оказалось - нет, не быстрее.

Описание эксперимента


Для тестирования был выбран кейс, представленный Адамом Бином в его вебкасте What Is Faster--EJBs Or CDI? A JMH Benchmark: были разработаны три реализации простейшего RESTful веб-сервиса, с использованием Spring Framework, CDI и EJB, соответственно. Конструкция сервисов в общем случае напоминает архитектуру корпоративного приложения: в контроллер инжектируется сервис, в который в свою очередь инжектируются два ресурса (этакие "DAO").

Пример кода с использованием EJB:

@Stateless
public class EJBResourceA {

    public String message() {
        return "A#" + System.currentTimeMillis();
    }
}

@Stateless
public class MessageService {

    @EJB
    private EJBResourceA aresource;

    @EJB
    private EJBResourceB bresource;

    public String message() {
        return aresource.message() + bresource.message();
    }
}

@Stateless
@Path("/")
public class MessageController {

    @EJB
    private MessageService service;

    @GET
    @Path("/message")
    @Produces({"text/plain"})
    public String message() {
        return service.message();
    }
}

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

  • Для Spring Framework в качестве основы реализации RESTful веб-сервиса был взят Spring MVC: запросы обрабатываются с помощью DispatcherServlet, к которому подключается MessageController, аннотированный RestController, что говорит фреймворку возвратить результат метода MessageController#message() в теле ответа сервиса. В данный контроллер с помощью аннотации Autowired инжектируется сервис, в который, в свою очередь, аналогичным образом инжектируются два компонента-ресурса. Конфигурация контекста приложения (Spring Framework ApplicationContext) осуществляется с помощью аннотаций:

    @Configuration
    @ComponentScan(basePackageClasses = ApplicationConfig.class)
    @EnableWebMvc
    public class ApplicationConfig {
    }
    

    Регистрация слушателей, осуществляющих загрузку контекста, а так же настройка DispatcherServlet'а осуществляется в классе, реализующем интерфейс org.springframework.web.WebApplicationInitializer:

    public class ApplicationInitializer implements WebApplicationInitializer {
    
        @Override
        public void onStartup(ServletContext servletContext) throws ServletException {
            AnnotationConfigWebApplicationContext applicationContext = 
                    new AnnotationConfigWebApplicationContext();
            applicationContext.register(ApplicationConfig.class);       
            
            servletContext.addListener(new ContextLoaderListener(applicationContext));
            
            ServletRegistration.Dynamic dispatcher = 
                    servletContext.addServlet("spring-mvc-dispatcher", 
                            new DispatcherServlet(applicationContext));
            dispatcher.setLoadOnStartup(1);
            dispatcher.addMapping("/api/*");        
        }
    }
    

  • Для CDI: все компоненты создаются в области видимости по-умолчанию - Dependent.

  • Для EJB: все классы аннотированы @Stateless, что соответствует наиболее популярному типу - сессионным компонентам без сохранения состояния. Для хранения экземпляров компонентов используется пул, предоставляемый контейнером EJB сервера приложений. Так же очень важный момент - управление транзакциями. По-умолчанию, EJB-контейнер берет на себя управление транзакциями при обращении к компоненту, при этом если вызов бизнес-метода компонента осуществляется вне контекста глобальной транзакции, то такая транзакция будет создана. Можно было отключить использование данного механизма, тогда условия тестирования больше бы соответствовали режимам, в которых работают конкуренты, но для следования озвученному выше критерию - технология используется наиболее распространенным способом - этого сделано не было.



Результаты тестирования


Результаты тестирования - пропускная способность - представлены на диаграмме. Чем больше значение - тем лучше.


Описание тестовых сред и вывод JMH для каждого запуска приведены на официальной странице бенчмарка (eng).

Анализ


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

В экспериментах, выполненных Суровым, сервером приложений выступал WebSphere Application Server 8.5.5, причем запуски осуществлялись как на разных аппаратных платформах: x86 и zArchitecture, так и под управлением разных операционных систем: z/OS, Linux for z Systems и Windows. Все экземпляры серверов приложений настроены одинаковым образом, размеры кучи JVM подобраны таким образом, чтобы избежать циклов полной сборки мусора во время тестирования. С этой же целью перед каждым запуском бенчмарка вручную инициировался цикл полной сборки мусора на сервере.

Почему результаты CDI хуже чем у конкурентов?

Если запустить тесты под профайлером (для z/OS и Linux for z Systems можно использовать, например, Jinsight), то отчетливо видно, что значительную долю времени выполнения программы занимает поиск и инжекция экземпляра класса MessageService в контроллер MessageController. Дело в том, что контроллер MessageController, аннотированный @Path, по-умолчанию создается в области видимости запроса (Request Scope), т.е. для каждого запроса контейнер будет создавать новый экземпляр данного класса и заново осуществлять инжекцию в него компонента MessageService.

Метод org.apache.wink.server.internal.handlers.InvokeMethodHandler.handleRequest() отвечает за построение контроллера и вызов метода MessageController#message, при этом построение контроллера (метод org.apache.wink.server.internal.registry.ResourceInstance.getInstance()) занимает до 98% времени выполнения метода InvokeMethodHandler.handleRequest(), в то время как "бизнес-логика" приложения - меньше 1%.

Spring MVC

Примерно две трети времени обработки запроса занимает метод org.springframework.web.servlet.DispatcherServlet.doDispath(). Логика работы данного метода раскладывается на две части: найти обработчик для запроса и, соответственно, - вызвать его. Поиск обработчика запроса занимает до 35% времени выполнения метода DispatcherServlet.doDispatch().

Метод DispatcherServlet.getHandler(). Большую часть поиска обработчика занимает анализ заголовка ACCEPT (до 20% времени исполнения метода DispatcherServler.doDispatch()), заключающаяся в извлечении данного заголовка из запроса, его разбора с помощью регулярного выражения (метод org.springframework.http.MediaType.parseMediaTypes()) ",\\s*" и сортировки полученных значений по "конкретности" и "качеству" (см. код класса MediaType).

Вызов найденного обработчика занимает примерно 60% времени исполнения метода DispatcherServler.doDispatch(). Около 12% из них - это запись ответа в буфер, которой в основном занимается WebSphere Application Server. Еще до 15% занимает вызов метода org.springframework.web.accept.HeaderContentNegotiationManager.resolveMediaTypes(), который уже вызывался внутри метода DispatcherServler.getHandlers().

При этом слой "бизнес-логики", реализованной с использованием Spring Framework, довольно "узкий": все классы приложения - одиночки, представленные в контексте приложения единственными экземплярами. Соответственно все инжекции выполняются при запуске. Управление транзакциями в данном кейсе не используется, поэтому объекты-прокси не генерируются. Бизнес-логика вызывается через метод org.springframework.web.method.support.InvokableHandlerMethod.invokeForRequest(), время выполнения которого составляет лишь доли процента от времени выполнения метода DispatcherServlet.doDispatch().

Enterprise Java Beans

Как было упомянуто выше, в тестовом приложении используются управляемые контейнером транзакции (CMT). Если вызов бизнес-метода компонента осуществляется вне контекста глобальной транзакции, то такая транзакция будет создана. В сервере приложений WebSphere Application Server данное поведение реализуется следующим образом: для каждого сессионного компонента создается обертка, например psamolysov.demo.restws.controller.EJSLocalNSLMessageController_89d4e104. Если рассмотреть вызов метода message() данного класса, то под профайлером видно, что треть времени его выполнения занимает операция EJBPreInvoke(), в которой:

а) создается глобальная транзакция;

б) проверяется наличие у пользователя прав доступа к вызываемому методу компонента.

Еще порядка 10-15% берет на себя метод postInvoke(), в котором выполняется, в частности, коммит транзакции.

Во вложенных методах message() создания транзакции не происходит, т.к. данные методы уже выполняются в контексте ранее созданной транзакции.

Для производительности реализации на EJB очень важна аннотация javax.ws.rs.Produces, которой отмечен метод MessageController#message(). Без этой аннотации время выполнения метода org.apache.wink.server.internal.handlers.PopulateResponseMediaTypeHandler.handleResponse() увеличивается за счет необходимости вызвать org.apache.wink.common.internal.registry.ProvidersRegistry.getMessageBodyWriterMediaTypes() для поиска типа ответа, соответствующего классу возвращенного методом message() объекта. Время выполнения метода ProviderRegistry.getMessageBodyWriterMediaTypes() может занимать до 45% времени выполнения метода PopulateResponseMediaTypeHandler.handleResponse().

P.S. Результаты полученные на ноутбуке под управлением Windows самые волатильные, в том смысле, что на пользовательском ноутбуке очень трудно обеспечить постоянство параметров тестовой среды, в зависимости от времени запуска результаты могут варьироваться: иногда Spring Framework показывает лучшую пропускную способность, иногда - EJB, в любом случае производительность, обеспечиваемая любой из данных технологий, примерно одинакова. На данном кейсе не наблюдается поведения, при котором Spring Framework показывает в разы лучшие результаты, нежели EJB! На этой оптимистической ноте позвольте мне и закончить.

Всех читателей и подписчиков Суровый поздравляет с наступающими Новым годом и Рождеством!

Понравилось сообщение - подпишитесь на блог

7 комментариев:

alxt комментирует...

Так суть Spring в том, что можно запуститься на tomcat/jetty и т.п. Как на них это работает?
Помнится Кекс утверждал, что томкат уделывает ВебСферу (или ВебЛоджик) раза в два...

Pavel Samolisov комментирует...

На какой платформе и под какую задачу "уделывает"?

spyroid комментирует...

В тот же Spring Boot можно запихнуть Undertow. Какие на нём будут результаты?

Pavel Samolisov комментирует...

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

Sergey комментирует...

Прочитав заголовок, подумал "интересно, какая реакция была бы у Шипилева". И действительно, есть реакция %)

ZeroSetup комментирует...

а какие JVM использовались?

Pavel Samolysov комментирует...

Поскольку тестировалось на вебсфере, то IBM JVM. Так же делал для себя сравнение спринг на томкат под оракл JVM vs спринг на вебсфере под IBM JVM, на винде. Повторить может каждый, результат получился очень интересный, многим хейтерам кровавого ынтырпрайза понравится.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!