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.
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.
- RSocket + WebSocket + Spring Boot = Real Time Application
- Kafka Stream With Spring Boot
- Server Sent Events With Spring WebFlux
Happy Coding 🙂