Performance Comparison Between Spring Boot and Micronaut

Today we will compare two frameworks used for building microservices on the JVM: Spring Boot and Micronaut. First of them, Spring Boot is currently the most popular and opinionated framework in the JVM world. On the other side of the barrier is staying Micronaut, quickly gaining popularity framework especially designed for building serverless functions or low memory-footprint microservices. We will be comparing version 2.1.4 of Spring Boot with 1.0.0.RC1 of Micronaut. The comparison criteria are:

  • memory usage (heap and non-heap)
  • the size in MB of generated fat JAR file
  • the application startup time
  • the performance of application, in the meaning of average response time from the REST endpoint during sample load testing

To make our test relevant we will gather the statistics for the two almost identical applications. Of course, the only difference between will be in the frameworks we used for building it. Our sample application is very simple. It exposes some endpoints with in-memory CRUD operations for a single entity. It also exposes info and health endpoints, and also Swagger API with all endpoints auto-generated documentation.

The sample application performance will be tested on JDK 11. We will use Yourkit for profiling and monitoring memory usage after startup and during load testing, and Gatling for building performance API tests. First, let’s perform a short overview of our sample application.

Source Code

I have implemented very simple in-memory repository bean that add new object into the list and provides find method for searching object by id generated during add method.

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;
    }

}

The repository bean is injected into controller. Controller exposes two HTTP methods. First of them (POST) is used for adding new object, while the second (GET) for searching it by id. Here’s controller implementation inside Spring Boot application:

@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();
    }

}

Here’s the similar implementation for Micronaut:

To implement REST endpoints, healthcheck and Swagger API we need to include some dependencies. Here’s the list of dependencies for 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>

Here’s the similar list of dependencies required for 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>

I also had to provide some additional configuration in application.yml to enable Swagger and healthchecks:

micronaut:
  router:
    static-resources:
      swagger:
        paths: classpath:META-INF/swagger
        mapping: /swagger/**
endpoints:
  info:
    enabled: true
    sensitive: false

Starting Application

First, let’s start our application. I use Intellij for that. The sample application built basing on Spring Boot starts around 6-7 seconds. The following start take up exactly 6.344.

performance-1

The similar application built on top of Micronaut starts around 3-4 seconds. The following start take up exactly 3.463 as shown below. However, I had to disable environment deduction when I started application behind corporate proxy by setting VM option -Dmicronaut.cloud.platform=BARE_METAL. I think that startup time for both applications is really ok.

performance-7

Here’s the graph that illustrates difference in startup time between Spring Boot and Micronaut.

performance-sum-1

Building Application

We will also check out the size of application fat JAR. To do that you should build the application using mvn clean install command. For Spring Boot we used two standard starters: Web, Actuator, and library Swagger SpringFox. As a result of this there are more than 50 libraries included. Of course, we could made some exclusions or do not use starters, but I have chosen the simplest way to built an application. The fat JAR has a size of 24.2 MB.
The similar application based on Micronaut is much smaller. The fat JAR has a size of 12.1 MB. I have included more libraries in pom.xml, and finally there were 37 libraries included.
Spring Boot includes more libraries on the standard configuration, but on the other hand it has more features and auto-configuration than Micronaut.

Here’s the graph that illustrates difference in size of target JAR between Spring Boot and Micronaut.

performance-sum-2

Memory Management

Just after startup Spring Boot application has allocated 305 MB for heap and 81 MB for non-heap. I haven’t set any memory limit using Xmx or any other option. In heap, 8 MB has been consumed by old gen, 60 MB by eden space, and 15 MB by survivor. Most of non-heap were consumed by metaspace – 52 MB.
After running performance load test heap allocation increased to 369 MB, and non-heap to 87 MB. Here’s the screen that illustrates CPU and RAM usage before and during performance test.

performance-2

Just after startup Micronaut application has allocated 254 MB for heap and 51 MB for non-heap. I haven’t set any memory limit using Xmx or any other option – the same as for Spring Boot application. In heap, 2.5 MB has been consumed by old gen, 20 MB by eden space, and 7 MB by survivor. Most of non-heap were consumed by metaspace – 35 MB.
After running performance load test heap allocation has not changed, and non-heap increased to 63 MB. Here’s the screen that illustrates CPU and RAM usage before and during performance test.

performance-8

Here’s heap memory usage comparison between Spring Boot and Micronaut just after startup.

performance-sum-3

And non-heap.

performance-sum-4

Performance Tests

I used Gatling for building performance load tests. This tool allows you to create test scenarios in Scala. We are generating 40k sample requests sent simultaneously by 20 threads. Here’s the test implemented for POST method.

class SimpleTest extends Simulation {

  val scn = scenario("AddPerson").repeat(2000, "n") {
    exec(http("Persons-POST")
      .post("http://localhost:8080/persons")
      .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))

}

Here’s the test implemented for GET method.

class SimpleTest2 extends Simulation {

  val scn = scenario("GetPerson").repeat(2000, "n") {
    exec(http("Persons-GET")
      .get("http://localhost:8080/persons/${n}")
      .check(status.is(200)))
  }

  setUp(scn.inject(atOnceUsers(20))).maxDuration(FiniteDuration.apply(10, TimeUnit.MINUTES))

}

The result of performance test for POST /persons method is visible on the picture below. An average number of requests processed during one second is 1176.

performance-3

The following screen shows the histogram with response time percentiles over time.

performance-5

The result of performance test for GET /persons/{id} method is visible on the picture below. An average number of requests processed during one second is 1428.

performance-4

The following screen shows the histogram with response time percentiles over time.

performance-6

Now, we are running the same Gatling load test for Micronaut application. The result of performance test for POST /persons method is visible on the picture below. An average number of requests processed during one second is 1290.

performance-9

The following screen shows the histogram with response time percentiles over time.

performance-11

The result of performance test for GET /persons/{id} method is visible on the picture below. An average number of requests processed during one second is 1538.

performance-10

The following screen shows the histogram with response time percentiles over time.

performance-12

There are no big difference in processing time between Spring Boot and Micronaut. It’s possible that small differences in time are not related with framework, but rather with base server. By default, Spring Boot uses Tomcat, while Micronaut uses Netty.

Author: Piotr Mińkowski

IT Architect, Java Software Developer

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.