Spring Boot和Micronaut之间的性能比较
今天,我们将比较用于在JVM上构建微服务的两个框架:Spring Boot和Micronaut。首先,Spring Boot当前是JVM世界中最流行,最受关注的框架。另一面是Micronaut,它也是流行的框架,特别是为构建无服务器功能或低内存占用的微服务而设计的。我们将比较Spring Boot的2.1.4版本和Micronaut的1.0.0.RC1版本。比较标准为:
· 内存使用情况(堆和非堆)
· 生成的JAR文件的大小(MB)
· 应用程序启动时间
· 应用程序的性能,即样本负载测试期间来自REST端点的平均响应时间
为了使我们的测试具有相关性,我们将收集两个几乎相同的应用程序的统计信息。当然,两者之间的唯一区别将是我们用于构建它的框架。我们的示例应用程序非常简单。它通过单个实体的内存CRUD操作公开了一些终结点。它还公开info和health端点,以及带有所有端点自动生成的文档的Swagger API。
示例应用程序性能将在JDK 11上进行测试。在启动后和负载测试期间,我们将使用Yourkit来分析和监视内存使用情况,在构建性能API测试时将使用Gatling。首先,让我们对示例应用程序进行简短概述。
源代码
我实现了非常简单的内存存储库bean,该存储库bean将新对象添加到列表中,并提供了find方法,用于通过add方法期间生成的id搜索对象。
public class PersonRepository {
List<Person> persons = new ArrayList<>();
public Person add(Person person) {
person.setId(persons.size()+1);
persons.add(person);
return person;
}
public Person findById(Long id) {
Optional<Person> person = persons.stream().filter(a -> a.getId().equals(id)).findFirst();
if (person.isPresent())
return person.get();
else
return null;
}
public List<Person> findAll() {
return persons;
}
}
仓库Bean被注入到控制器中。控制器公开了两个HTTP方法。其中第一个(POST)用于添加新对象,第二个(GET)用于按ID搜索对象。这是Spring Boot应用程序内部的控制器实现:
@RestController
@RequestMapping("/persons")
public class PersonsController {
private static final Logger LOGGER = LoggerFactory.getLogger(PersonsController.class);
@Autowired
PersonRepository repository;
@PostMapping
public Person add(@RequestBody Person person) {
LOGGER.info("Person add: {}", person);
return repository.add(person);
}
@GetMapping("/{id}")
public Person findById(@PathVariable("id") Long id) {
LOGGER.info("Person find: id={}", id);
return repository.findById(id);
}
@GetMapping
public List<Person> findAll() {
LOGGER.info("Person find");
return repository.findAll();
}
}
这是Micronaut的类似实现:
要实现REST端点,运行状况检查和Swagger API,我们需要包括一些依赖项。这是Spring Boot的依赖项列表:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
</parent>
<groupId>pl.piomin.services</groupId>
<artifactId>sample-app</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>11</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
</dependencies>
这是Micronaut所需的类似依赖项列表:
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-runtime</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-management</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject-java</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>runtime</scope>
</dependency>
我还必须提供一些其他配置application.yml以启用Swagger和运行状况检查:
micronaut:
router:
static-resources:
swagger:
paths: classpath:META-INF/swagger
mapping: /swagger/**
endpoints:
info:
enabled: true
sensitive: false
开始测试
首先,让我们开始我们的应用程序。我为此使用Intellij。基于Spring Boot构建的示例应用程序的启动时间约为6-7秒。以下启动正好占用6.344。
建立在Micronaut之上的类似应用程序的启动时间约为3-4秒。以下启动正好占用3.463,如下所示。但是,当我通过设置VM option在公司代理后面启动应用程序时,必须禁用环境扣除-Dmicronaut.cloud.platform=BARE_METAL。我认为两个应用程序的启动时间都还不错。
这是说明Spring Boot和Micronaut之间启动时间差异的图表。
build应用
我们还将检查应用程序JAR的大小。为此,您应该使用构建应用程序mvn clean install command。对于Spring Boot,我们使用了两个标准启动器:Web,Actuator和库Swagger SpringFox。因此,其中包含了50多个库。当然,我们可以做一些排除或不使用入门程序,但是我选择了构建应用程序的最简单方法。胖JAR的大小为24.2 MB。基于Micronaut的类似应用程序要小得多。胖JAR的大小为12.1 MB。我在中包含了更多库pom.xml,最后包含了37个库。Spring Boot在标准配置中包含更多的库,但另一方面,它比Micronaut具有更多的功能和自动配置。
以下图表说明了Spring Boot和Micronaut之间目标JAR大小的差异。
内存管理
刚启动后,Spring Boot应用程序已为堆分配305 MB,为非堆分配81 MB。我没有使用Xmx或任何其他选项设置任何内存限制。在堆中,旧一代消耗了8 MB,伊甸园空间消耗了60 MB,幸存者消耗了15 MB。大多数非堆都由元空间消耗-52 MB。运行性能负载测试后,堆分配增加到369 MB,非堆增加到87 MB。这是说明性能测试之前和期间的CPU和RAM使用情况的屏幕。
刚启动后,Micronaut应用程序已为堆分配254 MB,为非堆分配51 MB。我没有使用Xmx或任何其他选项设置任何内存限制-与Spring Boot应用程序相同。在堆中,旧一代消耗了2.5 MB,伊甸园空间消耗了20 MB,幸存者消耗了7 MB。大多数非堆空间都由元空间消耗-35 MB。运行性能负载测试后,堆分配没有更改,并且非堆增加到63 MB。这是说明性能测试之前和期间的CPU和RAM使用情况的屏幕。
这是刚启动后Spring Boot和Micronaut之间的堆内存使用情况比较。
性能测试
我将加特林(Gatling)用于构建性能负载测试。该工具允许您在Scala中创建测试方案。我们正在生成由20个线程同时发送的40k个样本请求。这是为POST方法实现的测试。
class SimpleTest extends Simulation {
val scn = scenario("AddPerson").repeat(2000, "n") {
exec(http("Persons-POST")
.post("")
.header("Content-Type", "application/json")
.body(StringBody("""{"name":"Test${n}","gender":"MALE","age":100}"""))
.check(status.is(200)))
}
setUp(scn.inject(atOnceUsers(20))).maxDuration(FiniteDuration.apply(10, TimeUnit.MINUTES))
}
这是为GET方法实现的测试。
class SimpleTest2 extends Simulation {
val scn = scenario("GetPerson").repeat(2000, "n") {
exec(http("Persons-GET")
.get("${n}")
.check(status.is(200)))
}
setUp(scn.inject(atOnceUsers(20))).maxDuration(FiniteDuration.apply(10, TimeUnit.MINUTES))
}
POST /persons方法的性能测试结果如下图所示。一秒钟内处理的平均请求数为1176。
以下屏幕显示了随时间变化的响应时间百分比的直方图。
GET /persons/{id}方法的性能测试结果如下图所示。一秒钟内处理的平均请求数为1428。
以下屏幕显示了随时间变化的响应时间百分比的直方图。
现在,我们正在为Micronaut应用程序运行相同的加特林负载测试。POST /persons方法的性能测试结果如下图所示。一秒钟内处理的平均请求数为1290。
以下屏幕显示了随时间变化的响应时间百分比的直方图。
GET /persons/{id}方法的性能测试结果如下图所示。一秒钟内处理的平均请求数为1538。
以下屏幕显示了随时间变化的响应时间百分比的直方图。
Spring Boot和Micronaut之间的处理时间没有太大差异。时间上的微小差异可能与框架无关,而与基础服务器有关。默认情况下,Spring Boot使用Tomcat,而Micronaut使用Netty。