Spring Boot中嵌入式Servlet容器的比较

2018/08/08

1. 介绍


随着云原生应用和微服务的流行也催生了对嵌入式Servlet容器需求的增长。为更加简单的构建应用和服务,Spring Boot为开发者提供了三种成熟的容器:Tomcat,Undertow和Jetty。

在本文中,我们会演示了一种方法:测量启动和增加负载时获取的指标来快速的比较不同容器实现的性能差异。

2. 依赖


首先我们在pom.xml中指定了_spring-boot-starter-web_ 这个依赖,这是我们为我们每一个容器实现进行测量前所必须的具备。

通常的,我们会指定使用 spring-boot-starter-parent 作为我们的父依赖,然后接着加入我们需要的starter:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
        <relativePath/>
    </parent>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

2.1 Tomcat

因为在我们_spring-boot-starter-web_在我们的依赖中,默认采用的是Tomcat容器,因此我们不需要再做更多的配置。

2.2 Jetty

为了使用Jetty,我们首先需要从_spring-boot-starter-web_中去掉_spring-boot-starter-tomcat_ 这个依赖。

然后,我们只需要简单引入_spring-boot-starter-jetty_的依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>

2.3 Undertow

设置Undertow的方式和Jetty类似,不过去除依赖后,我们会使用_spring-boot-starter-undertow_ 作为我们的依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>

2.4 Actuator

我们使用Spring Boot的Actuator组件来对系统进行压力测试和查询应用指标。

你可以通过阅读这篇文章来更加详细的了解Actuator。本文中,我们只需要在pom中添加这个依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

2.5 Apache Beach

Apache Bench是一个开源的负载测试工具,但它通常会和Apache Web 服务器捆绑在一起。

Windows用户可以点击此处进行下载。如果你的Windows电脑上已经有了这个工具,你应该可以在你的_apache/bin_ 目录下找到_ab.exe_ 。

如果你是Linux的用户,你可以通过_apt-get_命令来安装_ab_

$ apt-get install apache2-utils

3. 启动指标


3.1 搜集

为了搜集我们的启动指标,我们会在Spring Boot的_ApplicationReadyEvent_ 注册我们关注的指标。

我们直接使用Actuator组件提供的_MeterRegistry_工具,通过编程的方式来直接获取我们所关注的指标:

    @Component
    public class StartupEventHandler {
    
        // logger, constructor
    
        private String[] METRICS = {
          "jvm.memory.used", 
          "jvm.classes.loaded", 
          "jvm.threads.live"};
        private String METRIC_MSG_FORMAT = "Startup Metric >> {}={}";
    
        private MeterRegistry meterRegistry;
    
        @EventListener
        public void getAndLogStartupMetrics(
          ApplicationReadyEvent event) {
            Arrays.asList(METRICS)
              .forEach(this::getAndLogActuatorMetric);
        }
    
        private void processMetric(String metric) {
            Meter meter = meterRegistry.find(metric).meter();
            Map<Statistic, Double> stats = getSamples(meter);
    
            logger.info(METRIC_MSG_FORMAT, metric, stats.get(Statistic.VALUE).longValue());
        }
    
        // other methods
    }

为了避免人为的通过Actuator的REST端点进行性能指标的查询,我们启动一个独立的JMX进程来记录应用启动应用时我们所关注的指标数据。

3.2 选择

Actuator可以为我们提供了大量的指标数据。在应用启动后,我们选择了三个具有代表性的指标,他们可以展现系统运行时的关键点的概况。

  • jvm.memory.used JVM在启动后总共使用的内存量
  • jvm.classes.loaded JVM中总共加载的class文件的数量
  • jvm.threads.live JVM中存活的线程数量。在我们的测试中,这个值可以展现为处于”休息”状态的线程的数量。

4. 运行时指标


4.1 搜集

除了提供启动指标, 当我们启动Apache Bench后,使用Actuator组件提供的_/metrics_端点作为目标url进行请求,以使我们的应用处于负载阶段。

为了测试一个真实的处于负载的应用,我们可能更需要使用我们的应用系统所提供的端点进行测试。

一旦我们的应用启动完成,我们使用以下命令来启动并执行的_ab_:

ab -n 10000 -c 10 http://localhost:8080/actuator/metrics

4.2 选择

Apache Bench能够快速的给予我们一些有用的信息:包括连接时间,在某一段时间的请求的占比等。

为了我们的目的,我们通常更加关注每秒请求个数和每个请求的处理时间的均值。

5. 结果


在启动阶段,我们通过对Tomcat,Jetty和Undertow所占用内存的比较,我们发现Jetty占用的内存最小,Undertow次之,Tomcat最多。

在我们的测量中,我么还能发现Tomcat,Jetty和Undertow的性能比较:我们可以清楚的看到Undertow很明显是最快的,而Jetty则相对于慢一些。

Metric Tomcat Jetty Undertow
jvm.memory.used (MB) 168 155 164
jvm.classes.loaded 9869 9784 9787
jvm.threads.live 25 17 19
Requests per second 1542 1627 1650
Average time per request (ms) 6.483 6.148 6.059

请注意,我们的这些指标都是在裸项目(没有添加任何业务代码的项目)下进行的测量。如果是自己的项目,那么测量指标大概率会有不同。

6. 基准测试讨论


开发适当的基准测试以充分测试容器实现的性能可能是相当复杂的。为了提取其中最关键的信息,能够清晰认识到要针对每个具体问题所编写出正确的测试案例是非常重要的。

值得注意的是,示例中使用了Actutor的HTTP GET请求作为负载,以此来收集的所需要指标的测量值。

可预见的是,不同的工作负载会导致对容器实现的进行不同指标的测量和搜集。如果需要更加健壮和精确的测量,建立一个更接近生产用例的测试计划是一个非常好的办法。

此外,更复杂的基准测试解决方案(如JMeter或Gatling)可能会得到更有价值的测试结论。

7. 选择容器


选择一个合适的容器实现应当要基于多方面的考量,而不能仅仅是基于一些硬性指标的概况就做出仓促的选择。适用性,特性,可配置性和策略通常也起到相当大的考量。

8. 结论


在本文中,我们看了Tomcat,Jetty和Undertow的嵌入式的Servlet容器的实现。我们通过Actuator暴露的metrics端点来测试了每一个Servlet容器在使用默认配置时的启动后的运行时指标。 我们通过使用Apache Bench来对应用进行压力测试,同时收集各项性能指标。 最后,我们谈论了这个策略的优势,并提及了当比较各个实现的基准时应当要熟记于心的几个建议。和往常一样,你可以从Github上获取项目中的所有源码

(转载本站文章请注明作者和出处 zhanghaichang-think in java

Post Directory