Can Java be fast? Performance comparison between Helidon Níma and Spring

Introduction

The main trend in the IT world now is saving resources and lightweight solutions. In the cloud, we only pay for the resources we actually use. And the more efficiently we use them, the less money is wasted: by reducing costs, we increase margins.

Java has long been and remains a favorite in enterprise solutions, but increasingly in high-load projects, preference is given to more “productive” languages ​​such as Go, and sometimes even C++. But what if Java can be fast too?

Eternal spring

The standard and most commonly used framework in the Java community is Spring. Once upon a time, Spring was a breath of fresh air compared to monstrous enterprise servers. Spring provides a huge number of libraries and integrations combined into a single ecosystem.

However, despite its convenience, Spring has a number of disadvantages:

  1. Dive speed. It is not enough for a developer to simply know Java to understand and write code in Spring. It is necessary to learn the features of the framework, its abstractions and the rules for working with them. This increases the onboarding time for new developers unfamiliar with the framework.

  2. Isolating developers from low-level APIs. When working with databases, message queues or other systems, Spring provides its own ready-made abstractions. This allows developers not to delve into the nuances of how specific APIs work. Often Spring developers do not know how to work with databases or queues apart from Spring.

  3. Difficulty in debugging. Abstractions are convenient during development, but if problems arise at runtime, the developer is required to have a deep understanding of how the framework works. Sometimes even experienced developers spend a lot of time trying to figure out what went wrong.

  4. Performance. We pay for convenience with performance. Spring applications are quite heavy – they take a long time to launch and consume a lot of resources. Isolating developers from the API is also bad for performance because… does not allow you to use the API to its full potential.

Otherwise, Spring was, is, and will remain the standard solution for most projects for a long time. Thanks to a powerful ecosystem and a huge community, choosing Spring as the basis for your project is unlikely to be a mistake. But if you want something fashionable, youthful and fast…

What are the alternatives?

Although Spring is the leader in popularity in the Java community, there are a number of alternative solutions. In my opinion, the most famous and promising:

  • Microprofile and its various implementations – lighter than Spring, but at the core Java EE. Essentially the same “magic”, but less.

  • Ktor – microframework for Kotlin. Nothing extra, good performance, but only suitable for teams who love and practice Kotlin.

But in this article I would like to talk about a young and not so well-known solution – Helidon SE And Helidon Níma.

Swallow flight

Helidon (Swallow) is a young Java framework with an eye toward maximum lightness and operation in the cloud. It has 2 versions:

  • MP – implementation of the same Microprofile. Add-on on top of the SE version;

  • S.E.cloud-native microframework.

Exactly S.E. interests us within the framework of this article.

The philosophy of Helidon SE is completely opposite to Spring and Java EE:

  1. Transparency and full developer control over the framework;

  2. “No magic” an approach without Inversion of Control and autoconfigurations.

This is what creating a simple web server application looks like:

public final class Main {

    public static void main(final String[] args) {
        LogConfig.configureRuntime();

        //Конфигурация автоматически подтягивается из ENV и папки resources.
        //Поддерживаются YML, properties, json и других форматы.
        Config config = Config.create();

        //Веб-сервер - просто Java-объект. Мы имеем над ним полный контроль.
        WebServer server = WebServer.builder()
                //Конфигурация сервера - хост, порт и т.д. 
                .config(config.get("server"))
                //Правила маршрутизации запросов
                .routing(routing -> {
                    routing.get("/greeting", (req, res) -> res.send("Hello World!"));
                    routing.post("/greeting/{name}", (req, res) -> {
                        String name = req.path().pathParameters().get("name");
                        res.send("Hello %s!".formatted(name));
                    });
                })
                .build()
                .start();
    }
}

Helidon provides the developer with a set of libraries that cover…Omost of the needs of modern microservice applications. Health-checks, metrics, and tracing are available out of the box. There is also support for gRPC, WebSocket, OpenAPI, MQ.

Helidon Níma – virtual flows are very real

Helidon Nima – the first web framework completely built around Project Loom and virtual threads. But to understand what advantages this gives, you will have to dive a little into the theory.

There are 2 models of web servers:

  1. Blocking – for each HTTP request a thread is created that is blocked for any blocking I/O (any network interaction). The code is written in standard imperative paradigm. Among blocking servers, embedded servers are the most commonly used Tomcat, Jetty.

  2. Non-blocking – there is a small shared pool of threads that processes incoming requests. Threads process many requests simultaneously, receiving and sending events. Moreover, any network interaction must use non-blocking I/O. For this you have to use special libraries (Netty Client instead of HttpClient, R2DBC instead of JDBC). The code is written in reactive paradigm using ProjectReactor. The most popular non-blocking Java web server is Netty.

The non-blocking approach generally allows for more concurrent requests to be served, but it requires a fundamental change in development approaches. The reactive model is more complex to develop, debug and test.

Helidon SE version 3 is based on a non-blocking web server Netty and uses a reactive model.

But on September 19, 2023 it was released Java 21 and brought Project Loom – virtual threads for Java. To briefly describe why virtual threads are so good – they allow you to write non-blocking code in the imperative paradigm. We can have the best of both worlds: the efficiency of a reactive approach and the simplicity of an imperative approach.

The Helidon team decided to be “on the cutting edge of technology” and even before Project Loom was released, they began making a web server that would be based entirely on virtual threads. The name of this technology is Helidon Nima and at the time of writing the current version 4.0.0-M2 – release candidate, which is not yet ready for production, but is great for experimentation. Níma, running on virtual threads, allows you to achieve the performance of a reactive web server (and even more), using a simple and understandable imperative model.

Helidon 4 will use the Níma web server in both SE and MP versions.

Performance chatter is great, but let’s see how it works in action. I want to show test results Helidon Nima 4.0.0-M2 compared to Spring WebFlux And Spring WebMVC.

Test results

Preparing for testing

To conduct testing, I took a simple example: the service must take 100 rows from a table in the Postgres database, translate them into Json and return them to the client. A database query is the most common type of blocking call, and web applications are typically burdened by multiple read queries.

For the benchmark I created 3 projects:

  • Helidon Nima, JDBI

  • Spring WebFlux, Spring Data R2DBC

  • Spring WebMVC, Spring Data JDBC/JDBI (Spring 3.1.4)

Code available in repositories.

Library versions:

  • Spring 3.1.4

  • JDBI 3.41.1

  • Helidon 4.0.0-M2

Applications were launched via docker compose on my local machine with resource limits.

2 configurations were used:

  • 1 CPU + 1Gb memory

  • 2 CPU + 2Gb memory

The image was chosen as the base container-registry.oracle.com/java/openjdk:21JVM started with
settings -XX:InitialRAMPercentage=90.0 -XX:MaxRAMPercentage=90.0. Default GC – G1GC.

Each application used a thread pool (Hikari for Helidon/Spring WebMVC and R2DBC POOL for WebFlux) with a fixed pool size of 100 connections.

Before testing, 100k requests were sent to services to warm up the JVM. The load was supplied through AutoCannon with 3 scenarios:

  • 100 user threads – according to the size of the database connection pool;

  • 200 user threads – 2 times the pool;

  • 1000 user threads is the peak abnormal load.

The number of simultaneous requests is equal to the number of threads, and the number of requests per second is limited only by the performance of the service.

I entered the data obtained into a table and visualized it in the form of diagrams:

Based on the testing results, Níma emerged as the absolute winner in both RPS and response time.
To make testing more objective, in one of the tests I replaced the persistence layer in the MVC version from Data Jdbc with a more lightweight JDBI library (the same one I used in Níma). This gave a performance boost, but did not save Web MVC from failure.

Instead of a conclusion

Let’s talk about money.

Níma was able to process 4.5 times more requests per second than Spring WebFlux using the same resources. It is clear that these results are approximate and can deviate greatly in both directions in real problems, but I will allow myself to take out a calculator and dream up.

Let’s imagine a high-load system of 10 microservices located in three availability zones. There is at least one MS in each zone. Each MS runs on a machine with 1CPU And 1GB memory. Then, in order for a Spring solution to process a number of requests comparable to a Helidon solution, we will need 4-5 times more application instances.

Taking as a basis prices to virtual machines in Yandex Cloud and having sketched out a small table, we can estimate that a Spring solution will cost more by 1.5 million rubles in year.

Price

Hour

Year

Year 10 MS

Year 10 MS x 3 copies

Year 10 MS x 15 copies

Difference

1 CPU

1.12 RUB

RUB 9,811.20

RUB 98,112.00

RUB 294,336.00

RUB 1,471,680.00

RUB 1,177,344.00

1 GB RAM

RUB 0.39

RUB 3,416.40

RUB 34,164.00

RUB 102,492.00

RUB 512,460.00

RUB 409,968.00

Total

RUB 1.51

RUB 13,227.60

RUB 132,276.00

RUB 396,828.00

RUB 1,984,140.00

RUB 1,587,312.00

I hope I was able to dispel a bit of the myth that Java is hard and slow. Java can also be quick and easy if prepared correctly.

Additional materials

Similar Posts

Leave a Reply

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