Business applications are becoming more and more complex, they are expected to be reliable and fully accessible (the so-called Zero Downtime Deployment). In order to support business, very frequently we need to apply complex solutions, e.g. several databases (including relational and non-relational ones). We require low costs and high availability, if necessary, i.e. auto-scaling. One of the tools we have to use is containerisation with the use of Docker tool, which integrates very well with modern cloud solutions. In this article, we are going to focus on containerisation of an application core, the so-called ‘backend’, which is most often written in Java or a language operating on virtual Java machine (JVM).
Java – memory and processor requirements
When developing an application in Java and other languages, we have to take into consideration processor and memory requirements. A wide range of frameworks to choose from does not make the task easier. Starting from the simplest applications, where we responsible for manage on our own and take care of the right life cycle. Finally, we pay attention to write a code that may have not some memory leaks. Starting from the simplest frameworks, such as ‘javalin’, Dropwizard, or RESTX, as well as heavy solutions such as ‘enterprise’ that uses standard Java EE or Spring Framework, which are able to use different servers, e.g. web Apache Tomcat, Netty, as well as enterprise: WildFly or IBM WebSphere.
By choosing one of the following solutions, we are able to configure a server and add parameters for JVM, such as the maximum number of created t and potential memory to be used for stack and heap in Java. Here, we use parameters such as ‘Xmx’, ‘Xms’ and ‘Xss’. Of course, we need to take into consideration a particular version of Java which our application is run on.
The most optimal setting of a Java virtual machine is not such a trivial matter after all. If we use containerisation, another layer of complexity arises. Let’s assume that an application run in a container sees ‘host’s’ resources, and not those assigned to a given container, which can be observed in older versions of Java. Application resources are able to go beyond the framework set for the container – so how will our application react in such a case? What will happen to the application? Unfortunately, there is no clear answer. Each case is individual due to different version of Java or a different problem in a system.
One thing is certain – it is an inconvenient situation both for the server and programmers. Very frequently, one minor problem may do much damage. Let’s assume that we have a container that has come across such a problem. It is set for restart in any problematic situation. How is it going to affect the whole system, other microservices and the host or cloud that our application is currently on?
I will give some examples based on the following testing environment ‘http://play-with-docker.com’. The results may differ if you use your own environments with Docker installed, or even testing environment presented in the article – I’m not familiar with the exact rules of assigning resources to a particular instance.
Testing old Java version vs newer that includes updates for Docker
Below, you can find the Dockerfile which will be useful during the tests. I will present two tests, one of them within a newer version – Java 11, and the second one in an older version – Java 7, in order to depict the problem that concerned Java not such a long time ago.
The sourcecode in the Java language, which enters the number of available processors and the maximum reserved memory, visible to a given runtime environment
The sourcecode building a container by means of a simple Java application (as above) operating on a particular 7th version of a Java virtual machine.
The sourcecode building a container by means of a simple Java application (as above), operating on a particular 13th version of Java virtual machine.
The commands which we need to run in order to build a container based on `Dockerfile` that can be found in the current folder. Starting a container and, as the last step, reading logs of our simple program. (for <version> use the Java version that is currently being tested to prevent mixing up the container names)
The results for the selected Java versions:
|Number of processors||8||2|
|Max memory||7494172672 bytes
~ 7147 MB
~ 62 MB
To sum up the exercise that we have done and the results we have obtained, one can notice that we come across the problem described before.
The best method is to verify and test a particular version of Java virtual machine that is being used at the moment. The main version of Java, e.g. 9, which does not run the parameters controlled by Docker, can obtain the so-called Patches, which will fix the problem in the next versions with ‘minor’ label.
Java efficiency based on Spring Framework
In order to reflect and measure the efficiency in a better and more reliable way, we will discuss another example. We will build a web application using Spring Framework. We will use REST endpoint, which will create a list of randomly integer numbers for a request parameter then it will select all the prime numbers for each element from the list.
The above-mentioned functionality of calculating prime numbers and REST endpoint. Based on Spring Boot.
For this service, we will prepare ‘Dockerfile’, which will create ‘maven’ project with the required Spring Boot dependencies. It will copy the previously prepared Java file to run everything in the last step.
Dockerfile preparing REST application.
In order to measure the efficiency, I’m going to use ‘ab’ tool – Apache Bench. Apache Bench makes it possible to fulfill many HTTP demands [–n option] by means of many several threads [–c option]. We build the right Docker image with the presented tool.
Dockerfile with Apache Bench and Curl tool.
We need to connect everything to the network so that the images could see one another. We also have to inform the images that they belong to the same network.
Commands necessary to run our example.
In the next step, it’s worth making sure that all the containers are able to communicate with one another.
A set of commands testing the network between the containers.
Let’s discuss the results that can be found below. We run tests (precisely 1000 requests to the server), regarding our method of calculating prime numbers – maximum 50 demands parallelly. Both containers were tested in this way, i.e. one of them limited to one thread, and the other one with a bigger number of threads. We can easily observe that the limited container is nearly half slower when it comes to responding. Of course, there can be more indices, e.g. how Tomcat server handles a bigger number of threads, which is used by default by Spring Boot Framework. Obviously, each approach needs to be assessed individually and it’s necessary to run efficiency tests – but it may very often turn out to be a better solution, i.e. increasing the resources of a given container rather than creating several instances.
Test results of the container limited to one thread.
Test results of the container that can „nearly” no limits, 8 threads.
What next – Java, Docker, …
Obviously, the topic is very broad and that’s why I will only outline the directions that can be chosen. The example is very simple – it is to depict the problem. The situation which is quite simple with monolithic applications – one container, one virtual version of Java machine.
In the microservice approach, we can have many additional applications, each of them operating on a different version of a virtual machine, or even written in different languages operating on JVM, e.g. Java, Scala, Kotlin, Groovy, … The complexity level keeps rising.
Another element of the puzzle is the orchestrator. We have a few to choose from, being in Docker environment firstly we probably think about Swarm and Docker Enterprise. However, the biggest player is Kubernetes. Recently, it’s been gaining many shares in the market, and is supported by the biggest platforms such as AWS, Azure or Google Cloud Platform. Orchestrator provides, among other things, management, implementation and auto-scaling of an application.