17 May 2020

Summary

There's a strong trend in modern IT to migrate on-premises installations to cloud platforms. Independent of the migration strategy, either Lift-and-Shift or re-implement applications, this blog casts an eye on the popular Java platform deployed to the cloud.

 

Migrating to the cloud by just replacing machines of the own data-center by virtual machines in the cloud, does not provide much benefit, neither regarding costs nor in terms of administration effort. Therefor, the utilization of container orchestration platforms like Kubernetes or OpenShift is assumed in the following - if not even talking about FaaS or Serverless, the latest hype in cloud technologies.

Container orchestration platforms works best for MicroService architectures, ie. the shift to the cloud may have impact on the application architecture. Because this blog post wants to focus on the Java platform for cloud deployments, applications are assumed to be designed in a service-oriented way already.

Competitors

When making a technology decision for cloud-native applications, it’s worth to start with a look at the first-class citizen of cloud application, the Golang platform. The features that make Golang stand out are speed and size - speed in terms of start-up and execution, size in terms of image size and memory requirements at runtime.

The classical Java EE environments are typically based on an application server and optimized for long-running applications, running on large servers. For such deployments start-up time and memory requirements are not that important.

Building Images

One advice for building production-ready container images for Golang applications is to create the images FROM scratch, ie. don’t base the image of a Linux distribution. The main reason for this is not to reduce the image size, but to reduce the attack surface, which greatly improves security.

Can we do the same for Java applications? Unfortunately not, because the JDK isn’t statically linked like Golang applications are, but dynamically linked against glibc or musl library. These libraries are typically part of a Linux distribution.

For a small demo application, I used the Thorntail generator and selected the technologies JAX-RS + CDI + JSON-B. After extracting the downloaded ZIP file to a folder the application can be started locally with

mvn thorntail:run

When the application server is ready the application can be used:

gunther@gunther-K501UQ:~/Downloads$ curl localhost:8080/hello -v
*   Trying 127.0.0.1:8080...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /hello HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 21
< Date: Sat, 02 May 2020 13:36:50 GMT
<
* Connection #0 to host localhost left intact
Hello from Thorntail!gunther@gunther-K501UQ:~/Downloads$
gunther@gunther-K501UQ:~/Downloads$

The best you can do to keep the image lean is to use the OpenJDK linked against the musl library. That’s the case for Alpine Linux, a distribution tailored for cloud environments. After building the application with mvn clean install, the demo application can be packaged into an image by using the following Dockerfile:

FROM openjdk:14-jdk-alpine

COPY target/demo-thorntail.jar /app/demo-thorntail.jar

EXPOSE 8080

CMD ["java", "-Djava.net.preferIPv4Stack=true", "-Djava.net.preferIPv4Addresses=true", "-jar", "/app/demo-thorntail.jar"]

The resulting image size is about 450 MB. The same application packaged into an image with a Wildfly application server gives a size of about 750 MB. That’s clearly shows, that application servers are not appropriate for containerized Java payload. However, compared to about 20 MB, which is the size of a Golang image with the same functionality, the Thorntail image cannot compete.

Additionally, the start-up time of the Java EE application takes - mostly because of classpath scanning and reflection to process JEE annotations - several seconds, while the Golang application is ready to use in some milliseconds.

Because classical Java EE applications are not a good fit for cloud-native platforms, several other options arise in the Java world at this time: Eclipse MicroProfile, alternative Java EE packaging like Hammock, Micro Frameworks like Micronaut, Helidon, Spring Fu, Javalin, Spark, Ratpack, Dropwizard or Akka, native images with GraalVM, etc. The approaches to evolve the Java platform to the future could fill books. It’s almost comparable to the situation of the JavaScript world a few years ago: Lots of different frameworks with unclear future. If you go for a framework today, you might end up with a unsupported platform in a few years. Eg. the Thorntail framework, although just about 4 years old (including the life-time of its successor Wildfly Swarm), is pretty much dead (see Thorntail Community Announcement on Quarkus).

Conclusion

The situation today is a bit ambivalent: Java is the undisputed number one programming language and platform, but not well suited for cloud native runtime environments. On the other hand everybody moves from classical on-premises servers to cloud platforms.

Because of the diversity of Java frameworks future-proof technical decisions are hard today. The best you can do from my point of view is to stay as close to JEE standards as possible. That’s the reason I’ll surely have a look at Quarkus in the near future.

Tags: cloud-native docker java jakarta-ee microservices container