Site icon Vinsguru

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.

Project Setup:

Create a Spring application with the dependencies as shown here.

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

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

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

}
@Repository
public interface ProductRepository extends ReactiveMongoRepository<Product, Integer> {
}
@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);
    }

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

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

}
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
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).

./mvnw spring-boot:build-image
docker run -p 8080:8080 -e SPRING_DATA_MONGODB_HOST=10.11.12.13 product-service-native:0.0.1-SNAPSHOT
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)

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.

FROM openjdk:11.0.6-jre-slim

WORKDIR /usr/app

ADD target/*jar app.jar

CMD java -jar app.jar
docker build -t product-service:standard .
docker run -p 8080:8080 -e SPRING_DATA_MONGODB_HOST=10.11.12.13 product-service:standard

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

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:

Exit mobile version