Microservices Memory Footprint: Spring Boot vs Async Light Microservice



In this post we'll compare only the most recent version of Spring Boot (1.5.2-RELEASE) and the custom JEE microservice based on reference implementations we built two years ago. This microservice was created as a demo to compare synchronous vs several types of asynchronous REST clients. However, this service was created bearing in mind it should be as light as possible, using JEE specifications implementations. See this post for more details.
Spring Boot is right now a very extended option to compose any kind of microservices. There are other options that will be handled in future posts.

The conditions of the comparative are:
  1. The microservice does not contain any business logic. It is just as it out of the box.
  2. The type of service is basic. Just some REST services + do something + REST client. I mean, a specific and common type that are actually intermmediators between us and an external service. The microservice receives a message, processes it sends some message somewhere and returns the response to the requester.
Firstly, we have used Spring Boot gs rest service for our comparative. It does not contain any logic and is not consuming any other service. It is basically a REST service saying Hello to a request. The dependencies are the Spring Boot parent on, json-path for tests and 2 starters: Spring Boot Test and Web.

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.2.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.jayway.jsonpath</groupId>
            <artifactId>json-path</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
Moreover, we have used Tomcat and Undertow in order to detect any difference.

Our old Asynchronous Microservice contains the Grizzly http server, Jersey implementation of JAX-RS (implementation of the JEE specification JSR 339: JAX-RS 2.0: The Java API for RESTful Web Services) and HK2 as an implementation of the JSR-330, Dependency Injection for Java. This service is actually connecting to Github REST API for demo purposes but is not relevant for the comparative.

The machine for tests was Linux Ubuntu 16, Kernel 4.4.0-71-generic, 16GB Memory, SSD Ext4 FS.


Spring Boot
In this case we started with  minimal settings:

 -Xms16M -Xmx32M -Xss4M -XX:+CMSClassUnloadingEnabled

These settings were not valid as the application had not enough memory to start:

 .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.2.RELEASE)

[WARNING] 
java.lang.OutOfMemoryError: GC overhead limit exceeded
...

We noticed Spring Boot is unable to start with such as memory parameters for the JVM
So, the minimum required was:

-Xms32M -Xmx64M -Xss8M -XX:+CMSClassUnloadingEnabled


Spring Boot Tomcat. 40-48 MB Heap memory allocated just for starting.


Spring Boot Undertow. 40-48 MB Heap memory allocated just for starting. The situation was similar. Honestly, I expected something else from Undertow at this stage. Probably its beehavior is better with some load.

Regarding threads:

Spring Boot Tomcat. 18 threads and 16 daemons in NIO mode.


Spring Boot Undertow. 22 trheads and 12 daemons in XNIO mode.


Asynchronous Light Microservice
It started with these settings:

-Xms8M -Xmx32M -Xss4M -XX:+CMSClassUnloadingEnabled

That means that the service can start with only 50% of allocated memory needed for a plain Spring Boot service. This service can start with even less memory (e.g. 16 MB) but I prefer to allocate some memory for concurrency. The configured threadpool is 4 threads with queueing.


Asynchronous service. 19-20 MB Heap memory.


The Grizzly http server sets 16 threads listening requests and 9 NIO threads for consumers. Anyway, the threads are just listening (totally event oriented) and the needed memory allocation is minimal.


Coud we force the Asynchronous Light Microservice to start with only 16MB of Xmx? Here we go:

-Xms8M -Xmx16M -Xss4M -XX:+CMSClassUnloadingEnabled


Asynchronous service. 9-10 MB Heap memory. Please not it is the 25% of the minimum amount of memory required by Spring Boot.


This is only the first part of the comparative. We have seen that Spring Boot plain applications allocates certain amount of memory. From my experience, added dependencies and starters will increase dramatically the allocation of memory. The scalability of the microservice and of the system using many of these microservices will be impacted.

Instead, the Asynchronous Light Microservice is event oriented and highly scalable. By a light increase of the number of threads in the pool (8 is enough for most of cases) and memory (64 MB is enough) this service can perform perfectly the role of proxy microservice to consume 3rd Party APIs drastically lowing your needs for memory.

If you are working on Cloud, paying for allocated memory in your instances or containers, you exactly know what I am talking about, right? ;)

Imagine you have 10 different microservices in a cluster in Cloud and the cluster has 6 nodes. We will assign an increased ratio (4x) for both types of microservices, 256 MB for Spring Boot services and 64 MB for the Asynchronous light microservices . Do the Maths and you'll see the ROI of our light services:



We can save 11.5 GB of memory from our Cloud Provider's bill. Could you translate this to real money?


(partial) Conclusions
  • The JVM is not efficient for Microservices architectures as it scales badly when the number of instances increases. The JVM needs memory and each instance needs the same amount of memory. It is clear that if we use the JVM we will need very light footprint applications that can be used as real microservices.
  • Indeed, Spring Boot repeats the same approach seen in the Spring framework: reflection, complex dependencies tree, heavy starters, etc. That is why a plain Hello World application is so heavy and slow. asynchronous starters, access to database via JPA and added dependencies will provide us small monoliths, not real microservices, with all of the drawbacks of monoliths in each case.
  • Spring Boot applications are less useful in Cloud native IT Platforms. Most of dependencies and functionalities from the framework are useless. On the opposite, a Cloud native IT Platform provides needed services allowing the development of lighter and more real microservices. And cheaper!
  • Our Asynchronous Light Microservice prototype is useful for this type of microservices (communication to external services). It is just an example. It is pointing a way based on using the JEE specs instead of heavyweigth frameworks. If engineers know the JEE specs nothing in these services will be unknown for them and their productivity and maintenance capability will not be affected.
  • The low conssumption of memory increases the resiliency features of the services, increasing the robustness of the system.


See you in the next post!

Comments