spring boot graalvm native image

Spring Boot GraalVM Native Image

Overview:

In this tutorial, I would like to show you building Spring Boot GraalVM Native Image and Its performance.

GraalVM Native Image:

GraalVM is an universal VM for running applications written in Java, JavaScript, Python, Ruby..etc. It compiles the Java and bytecode into native binary executable which can run without a JVM. This can provide a quick startup time.

In this tutorial, Lets see how to build a GraalVM image of our Spring WebFlux application.

Sample Application:

Just to keep things simple, I am going to create a simple product-service with CRUD operations. MongoDB will be our database.

Once the app is built, we will be building 2 images as shown below and run a performance test.

  • GraalVM Native Image
  • JVM image

Project Setup:

Create a Spring application with the dependencies as shown here.

spring boot graalvm native image

You can build some REST API which works fine for this demo. I am going to create a simple Product service with CRUD operations.

  • Entity
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor(staticName = "create")
public class Product {

    @Id
    private Integer id;
    private String description;
    private Integer price;

}
  • Repository
@Repository
public interface ProductRepository extends ReactiveMongoRepository<Product, Integer> {
}
  • Service
@Service
public class ProductService {

    @Autowired
    private ProductRepository repository;

    public Flux<Product> getAllProducts(){
        return this.repository.findAll();
    }

    public Mono<Product> getProductById(int productId){
        return this.repository.findById(productId);
    }

    public Mono<Product> createProduct(final Product product){
        return this.repository.save(product);
    }

    public Mono<Product> updateProduct(int productId, final Mono<Product> productMono){
        return this.repository.findById(productId)
                .flatMap(p -> productMono.map(u -> {
                    p.setDescription(u.getDescription());
                    p.setPrice(u.getPrice());
                    return p;
                }))
                .flatMap(p -> this.repository.save(p));
    }

    public Mono<Void> deleteProduct(final int id){
        return this.repository.deleteById(id);
    }

}
  • Data setup  – I insert 100 random products when the server starts up.
@Service
public class DataSetupService implements CommandLineRunner {

    @Autowired
    private ProductRepository repository;

    @Override
    public void run(String... args) throws Exception {
        Flux<Product> productFlux = Flux.range(1, 100)
                .map(i -> Product.create(i, "product " + i, ThreadLocalRandom.current().nextInt(1, 1000)))
                .flatMap(this.repository::save);

        this.repository.deleteAll()
                .thenMany(productFlux)
                .doFinally(s -> System.out.println("Data setup done : " + s))
                .subscribe();
    }

}
  • Controller
@RestController
@RequestMapping("product")
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("all")
    public Flux<Product> getAll(){
        return this.productService.getAllProducts();
    }

    @GetMapping("{productId}")
    public Mono<ResponseEntity<Product>> getProductById(@PathVariable int productId){
        return this.productService.getProductById(productId)
                .map(ResponseEntity::ok)
                .defaultIfEmpty(ResponseEntity.notFound().build());
    }

    @PostMapping
    public Mono<Product> createProduct(@RequestBody Mono<Product> productMono){
        return productMono.flatMap(this.productService::createProduct);
    }

    @PutMapping("{productId}")
    public Mono<Product> updateProduct(@PathVariable int productId,
                                       @RequestBody Mono<Product> productMono){
        return this.productService.updateProduct(productId, productMono);
    }

    @DeleteMapping("/{id}")
    public Mono<Void> deleteProduct(@PathVariable int id){
        return this.productService.deleteProduct(id);
    }

}
  • MongoDB using docker-compose file.
version: "3"
services:
  mongo:
    image: mongo
    container_name: mongo
    ports:
      - 27017:27017
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: password
  mongo-express:
    image: mongo-express
    ports:
      - 8081:8081
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: admin
      ME_CONFIG_MONGODB_ADMINPASSWORD: password
  • application properties
spring.data.mongodb.database=admin
spring.data.mongodb.username=admin
spring.data.mongodb.password=password

At this point, we can bring our application up and running. Make a note of the application start up time. It took 3.86 seconds for me.

2021-08-01 15:25:23.232  INFO 5137 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Reactive MongoDB repositories in DEFAULT mode.
2021-08-01 15:25:23.456  INFO 5137 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 219 ms. Found 1 Reactive MongoDB repository interfaces.
2021-08-01 15:25:24.058  INFO 5137 --- [           main] org.mongodb.driver.cluster               : Cluster created with settings {hosts=[localhost:27017], mode=SINGLE, requiredClusterType=UNKNOWN, serverSelectionTimeout='30000 ms'}
2021-08-01 15:25:24.333  INFO 5137 --- [localhost:27017] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:2, serverValue:4}] to localhost:27017
2021-08-01 15:25:24.334  INFO 5137 --- [localhost:27017] org.mongodb.driver.cluster               : Monitor thread successfully connected to server with description ServerDescription{address=localhost:27017, type=STANDALONE, state=CONNECTED, ok=true, minWireVersion=0, maxWireVersion=8, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=77438439}
2021-08-01 15:25:24.335  INFO 5137 --- [localhost:27017] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:1, serverValue:5}] to localhost:27017
2021-08-01 15:25:25.004  INFO 5137 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port 8080
2021-08-01 15:25:25.019  INFO 5137 --- [           main] c.v.p.ProductServiceNativeApplication    : Started ProductServiceNativeApplication in 3.84 seconds (JVM running for 4.539)

We can test our APIs. I am able to get all the 100 products (It is not sorted. But it is ok). I also ensured that POST / PUT / DELETE operations work.

Spring Boot GraalVM Native Image:

Once our application is fine, It is time for us to build the GraalVM Native Image.  (You should have docker installed and ensure that docker service is up and running).

  • Building GraalVM Native Image
    • Run this below command and be patient. It might take up to 10 mins to build the image.
    • After ~10 mins,  a docker image is built with the name – product-service-native:0.0.1-SNAPSHOT
./mvnw spring-boot:build-image
  • Running GraalVM Image
    • Once the image is built successfully & assuming MongoDB up and running, lets run the Spring Boot GraalVM Native Image.
    • Issue this command. (Here I pass the MongoDB host is because our app is running inside the container. You need to tell exactly where MongoDB is running.)
docker run -p 8080:8080 -e SPRING_DATA_MONGODB_HOST=10.11.12.13 product-service-native:0.0.1-SNAPSHOT
    • Check the time it takes to start. (0.068 seconds!!) . Have you ever seen this for a Spring Boot application? 🙂 Even a simple Spring app will take couple of seconds.
2021-08-01 20:29:13.162  INFO 1 --- [           main] c.v.p.ProductServiceNativeApplication    : No active profile set, falling back to default profiles: default
2021-08-01 20:29:13.180  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Reactive MongoDB repositories in DEFAULT mode.
2021-08-01 20:29:13.181  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 0 ms. Found 1 Reactive MongoDB repository interfaces.
2021-08-01 20:29:13.192  INFO 1 --- [           main] org.mongodb.driver.cluster               : Cluster created with settings {hosts=[10.11.12.13:27017], mode=SINGLE, requiredClusterType=UNKNOWN, serverSelectionTimeout='30000 ms'}
2021-08-01 20:29:13.204  INFO 1 --- [2.168.0.9:27017] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:1, serverValue:102}] to 10.11.12.13:27017
2021-08-01 20:29:13.205  INFO 1 --- [2.168.0.9:27017] org.mongodb.driver.connection            : Opened connection [connectionId{localValue:2, serverValue:103}] to 10.11.12.13:27017
2021-08-01 20:29:13.205  INFO 1 --- [2.168.0.9:27017] org.mongodb.driver.cluster               : Monitor thread successfully connected to server with description ServerDescription{address=10.11.12.13:27017, type=STANDALONE, state=CONNECTED, ok=true, minWireVersion=0, maxWireVersion=8, maxDocumentSize=16777216, logicalSessionTimeoutMinutes=30, roundTripTimeNanos=9413630}
2021-08-01 20:29:13.224  INFO 1 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port 8080
2021-08-01 20:29:13.225  INFO 1 --- [           main] c.v.p.ProductServiceNativeApplication    : Started ProductServiceNativeApplication in 0.068 seconds (JVM running for 0.071)
    • It is ridiculously fast for a Java Spring application. All the APIs are also working fine.
  • I am also going to run a simple performance test using JMeter for 5 mins with 200 concurrent users.
    • I was able to achieve the throughput 3970 requests / second. (I ran this few times. Results were more or less same. This is the best of all)

GraalVM is the way to go!? Lets not come to any conclusion immediately. Lets also give a chance for our beloved JVM!

Spring Boot JVM Image:

Lets also try to build the docker image for our application which is going to run on JVM as usual.

  • Use this dockerfile.
FROM openjdk:11.0.6-jre-slim

WORKDIR /usr/app

ADD target/*jar app.jar

CMD java -jar app.jar
  • Issue this docker command to build the image.
docker build -t product-service:standard .
  • Run the application.
docker run -p 8080:8080 -e SPRING_DATA_MONGODB_HOST=10.11.12.13 product-service:standard
  • The application runs just fine. However the start up time is more than 3 seconds. 🙁 . This is expected for Spring Boot + JVM.

Ok..what about the 5 mins JMeter performance test.?

  • When I run the JMeter performance test for 200 concurrent users – I get much better throughput compared to GraalVM native image. 🙂

Results:

Note: Your results could vary.

Description GraalVM Native Image JVM / Regular Image
Build Time ~ 9 Minutes 10 seconds
Start up time 0.06 seconds (This is awesome) 3.9 seconds
Application Throughput 3970 requests / second 4493 requests / second

Summary:

We were able to successfully demonstrate Spring Boot GraalVM Native Image build process and its performance. GraalVM has an excellent start up time. However It does NOT seem to provide better performance for overall application. This is also mentioned here.  GraalVM might eventually fix this problem. But it is a good choice for serverless applications like AWS Lambda which requires quick startup. Otherwise you can just go with traditional way of running the application with JVM.

The source code is here.

Learn more about Spring WebFlux.

Happy Coding 🙂

 

 

Share This:

Leave a Reply

Your email address will not be published. Required fields are marked *

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