As a whale, eat the Java application and do not choke

Hello, dear people of Khabarovsk! Today I would like to talk about how to "feed" a Java application to a docker, how to do it better, and what not to do. I've been working on Java for more than 10 years, and I spent the last three years in very close contact with Docker, so I got a definite idea of ​​what it can and can not do. But after all, the hypothesis must be tested in practice, is not it?
 
 
I presented the whole process as an old good computer game with a warm tube pixel art.
 
 
We will start, as befits any game, with some briefing. As an introductory note, take a little Docker advertising.
 
 
On the site docker it is possible to familiarize with a number of advertising promises - namely, with the promise to increase the speed of development and deployment already in 13 time and increase portability in the development (in particular, get rid of the sacramental "works on my machine"). But does this correspond to reality?
 
 
Now we will try to prove /disprove these statements.
 
Dockerfile . The syntax is very simple - use as the base image. java: 8 , we add our Java class (command ADD ), compile it (using the command RUN ) and specify the command that will be executed when the container is started (command CMD ).
 
 
HelloWorld.java
 

 
public class HelloWorld {
public static void main (String[].args) {
System.out.println ("Hello World!");
}
}

 
Dockerfile
 
 
FROM java: 8
ADD HelloWorld.java.
RUN javac HelloWorld.java
CMD["java", "HelloWorld"]

 
Docker commands:
 

 
    $ docker build -t java-app: demo.
$ docker images
$ docker run java-app: demo

 
As a whale, eat the Java application and do not choke

 
To collect all this, we need, in fact, one team - this is docker build . When assembling, we specify the name of our image and the tag that we assign to it (so we can version various assemblies of our application). Next, make sure that we compiled the image by running the command docker images . In order to run our application, execute the command docker run .
 

 
Hooray, everything went fine, and we are good fellows Or not?
 

 

 

 
Yes, we fulfilled the mission. But we have to remove points for that. For what, you ask, and how to avoid such blunders next time?
 

 
  •  
  • The basic image of the docker that we used is declared as deprecated and is not supported by community of the docker . Even at DockerCon17 many of the world of Java EE, the familiar Arun Gupta recommended using as the base image of openjdk (which we are also hinted at describing and updating the images of ) https://hub.docker.com/_/openjdk/ )  
  • To reduce the size it is better to use images based on Alpine - images based on this distribution are the lightest.  
  • We compile with the help of the image jdk, run with jre (we save the disk space, we still need it).  

 
Now, we can assume that the first level is passed successfully. We rise to the second.
 

 
Useful reference for passing the first level
 

 

Level 2


 

 

 
When dealing with Java, we will most likely use Maven or Gradle. Therefore, it would be convenient to somehow integrate our assembly systems with Docker in order to have a single environment for building the project and the images of the docker.
 

 
Fortunately for us, most of the plugins are already written - for both Maven and Gradle.
 

 

 

 
The most popular plugins are Maven for Docker fabric8io and spotify . For Gradle, we can use the plugin of Benjamin Mushko - one of the developers of Gradle and the author of the book "Gradle in Action".
 

 
To connect the docker to the application build system, in the gradle configuration it is enough to create several tasks that will collect and launch our containers, as well as specify some general information from the category - what image to use as the base and what name to give to the image.
 

 
We will not be verbose: take the plug bmuschko /gradle-docker-plugin and Gradle (fans of Maven and XML lovers, wait a bit!).
 

 
We will perform our first task, but now with the help of this plugin. The main parts of the build.gradle, which we will need:
 

 
    docker {
javaApplication {
baseImage = 'openjdk: latest'
tag = 'java-app: gradle'
}
}
task createContainer (type: DockerCreateContainer) {
dependsOn dockerBuildImage
targetImageId {dockerBuildImage.getImageId ()}
}
task startContainer (type: DockerStartContainer) {
dependsOn createContainer
targetContainerId {createContainer.getContainerId ()}
}

 
Run the command gradle startContainer and see the assembly of our image and even the launch of the container. But instead of the welcome message "Hello JavaMeetup!" We get a notification of a successful build!
 

 

 

 
We somewhere were mistaken? Not really, just redirect the output of our container to the console:
 

 
    task logContainer (type: DockerLogsContainer, dependsOn: startContainer) {
targetContainerId {startContainer.getContainerId ()}
follow = true
tailAll = true
onNext {
message -> logger.quiet message.toString ()
}
}

 
Run the command gradle logContainer and Hooray, the treasured message and the passed level.
 

 

 

 
That, strictly speaking, that's all. We do not even need a Dockerfile (but it will not be superfluous - it's not enough, Gradle will not be at hand).
 

 
We move on!
 

 

Level 3


 

 

 
Most likely, in real life, our application will do something more subtle than the output of the "hello world" screen. Therefore, at the next level, we will learn how to launch a complex application - the Spring Web application, which will output some records from the database.
 

 
In order to raise the database and the application itself, we will use Docker Compose . First, create a new file (another new configuration file, you will breathe, but it will not stop us?) - docker-compose.yml . In it, we just assign the services to raise the image of the database and the image of the application. Docker Compose itself will find in the current directory yml-file and will pick up or collect the necessary containers and images.
 

 

 

 
Whatever the whole thing has started, we will first collect the image. In this example a maven-plugin for Docker (hurray, XML!) from fabric8io - so to begin with, execute the command mvn install :
 

 
      io.fabric8  
http: //localhost: 8080 / and see the desired data.
 

 
All this may seem complicated, but in fact it is extremely simple. The third level is passed. Almost.
 

 
We still have a bonus level. On it, we play a little (quite a bit) in the Docker Swarm - and to be precise, in Docker Swarm Mode .
 

 

Bonus Level


 

 

 
Docker Swarm Mode is not particularly complicated - it's just a cluster of machines on which the Docker stands. For the user, this cluster looks like one machine, and all the commands work much the same as if this Docker Swarm were not there.
 

 
In swarm mode, you can run several instances of our application - for load balancing, for example. Also, there is such an abstraction as a stack: with Docker Swarm we can unite a whole bunch of applications as a whole. And, similar to the usual scaling, we can deploy several replicas of stack .
 

 
Docker commands in swarm mode:
 

 
    $ docker service create --name japp --publish 8080: 80 --replicas 3 java-app: demo
$ docker stack deploy -c docker-compose.yml javahelloworld

 
In fact, the syntax of commands is extremely simple and resembles the creation of conventional containers.
 
We can also use the docker compose:
 

 
    $ docker-compose scale jm-app = 3  
 
Well, for the last three levels we seem to have achieved the portability of java-applications. It's time to move to the last level and try to confirm or disprove the statement that Docker makes the phrase "running on my machine" is no longer relevant.
 
 

Final Level 


 

 
 
Imagine that we have a heavy application. Or a large number of microservices, which can be on the same machine. In this case, the Java application (to be precise, the JVM) will surely grab our little blue whale in the struggle for the resources of the host machine. About this, by the way, is well told here in this article .
 
 

 
 
At this level, there will be fewer code examples, but there will be different configurations for launching the docker container. The primary means of isolating processes and resources used by Docker are cgroups and namespaces . But the main problem is that the rust for all this is a little bit over the drum. We have a voracious and a little greedy, we see that there are more resources, even if we set memory limits when creating a container with a java application using the flag-memory. You can verify this by simply running the free command inside the container. Hence, a fairly general recommendation for Java 8 is to specify the -Xmx parameter, and the -memory parameter must be at least twice as large as -Xmx. Nice news from the Java 9 fields - there support for the cgroups was added.
 
 

 
 
To simulate a memory leak in rst is quite simple. We just take the already finished image of valentinomiazzo /jvm-memory-test and will run with different heap size parameters and --memory for the docker.
 
 

 
In the first case of memory, the container is given out less than the java application, and we get an incomprehensible error. And I would like to receive OutOfMemoryException. If you inspect the "killed" container, you can see that he was killed OOMKiller , and this can lead to unpredictable consequences, java-process hangup, improper resource shutdown and all sorts of offensive things (I've even seen a kernel-panic). Not the most pleasant thing that can happen.
 
 
Raising rates, give more memory to the container. This time we can catch OutOfMemoryException and after the inspection we make sure that OOMKiller has not touched our container, and we are spared all the above troubles.
 
 

 
The last level is passed, let's try to sum up.
 
 

Resume


 
So, what did we get as a result, having passed all the levels of our game? What about the promises of Docker to roll us the mountains?
 
 
Portability is not as good as we would like, but Java 9 seems to promise to solve these problems. With the increase in flexibility, everything is already more pleasant: with the docker we get a reproducible environment configuration in the code, and near the main code. It's easier to follow such things than for who did what, when and how to correct it, replaced it, or spoiled it somewhere under the root. And in general, you can achieve a good reduction in resources due to the ability to run many containers on one machine - this can be critical in testing.
 
 
That is, I would say that the docker is ideal for testing and /or development. But when working in production, you need to be more careful, since the load in this case can be much higher. And get a fall through the fault of the docker - it's really unpleasant.
 
 
And finally - the very flags that are needed to make Java 9 a docker!
 
 

 
 

Game Over


 
Useful links for the last level:
 
 
https://hackernoon.com/crafting-perfect-Java-docker-build-flow-740f71638d63
 
https://jaxenter.com/nobody-puts-Java-container-139373.html
 
https://github.com/valentinomiazzo/docker-jvm-memory-test
 
 
P.S. All the above mentioned examples and the examples above can be found here:
 
github.com/alexff91/Java-meetup-2018
Do you use Java 9 in production?
yes
no
55 people have already voted. Abstained 12 users.
Do you use Docker in production?
yes
no
55 people have already voted. Abstained 11 users.
Only registered users can participate in the survey. Enter , you are welcome.
+ 0 -

Add comment