A cloud native brew with Oracle Database, Helidon and Kubernetes — Part 1

Ali Mukadam
Oracle Developers
Published in
10 min readDec 5, 2023
Photo by Elevate on Unsplash

Something cool is brewing within Oracle for application development, especially if you are of a cloud native persuasion. First, Kehinde Otubamowo announced Raft Replication support in Oracle Database 23c. Then, after growing tired of my whining about the lack of an official Prometheus Exporter for the Oracle Database, my buddy Mark Nelson released the Unified Observability tool for Oracle Database, which addresses that. That was after the Oracle Database Operator reached v1.0 and is now supported in production.

“Hold my beer”, said the OCI team and then promptly released managed services for Redis and PostgreSQL in quick succession. While all this hullabaloo is happening, we’ve also been working to add OKE Workload Identity support to a number of CNCF projects, including Thanos.

Not to be left out, Helidon 4 (codename Níma) finally went GA too and boasts the use of Java Virtual Threads for enhanced performance and for good measure, it runs on GraalVM too. And we finally released v5 of Terraform OKE module.

If you are still trying to pinch yourself awake, I won’t blame you. But we are really doing all these things at Oracle and then some more. What does this all mean? How can you take advantage of these development to run your workload on Oracle Cloud?

As each of these are quite momentous announcements in their own right, they each deserve their own article and I’ll digest them all for you and give you my take in this new multi-part series.

I’ll start with Helidon in this post, simply because I’ve been meaning to take a look at it for a while and see what the fuss Virtual Threads is all about.

Helidon — a quick introduction

Helidon is a lightweight framework for writing micro-services in Java. You can read more about it on its official blog. It comes in 2 flavors:

  • MP: which provides an Eclipse MicroProfile runtime.
  • SE: uses Java virtual threads and is crazy fast.

How fast? It’s very fast. But why build another framework when there are so many excellent ones around? You can read the Helidon white paper for a longer and deeper explanation but let’s just say that we wanted speed and in the pursuit of speed, we don’t necessarily mean start up time only but also build time, deployment time and taking advantage of container-based deployment, particularly on Kubernetes, while neither sacrificing functionality, robustness nor security.

Why the focus on speed you ask me? Speed, or lack thereof, costs money. The more bloated a framework is, the more time it takes to build, and therefore the longer a deployment iteration. Similarly, bigger frameworks also tend to result in bigger container images which take more time to ship to your container registries, download to your Kubernetes worker nodes etc.

I said flavors of Helidon but MP is really more of an extension that runs on top of Helidon SE but allows for a different programmatic style. If you’re used to Spring Boot, like to mutter annotations voodoo and the dependency injection is strong with you, the MP flavor is for you. It has a small footprint and supports JAX-RS, JPA and most of the usual suspects.

@Path("hello")
public class HelloWorld {

@GET
public String hello() {
return "Hello World";
}
}

On the other hand, if you are more inclined towards the functional style and prefer a transparent way of doing things, then the SE flavor is for you. SE originally stands for Standard Edition, as a nod to Java’s Standard Edition and meant to convey a sense of simplicity without the extra baggage that usually happens with frameworks over time. But it’s more than that. Personally, I think the name “SE” does not really do it justice and would have preferred something like Helidon Lambda Edition (in reference to λ-calculus) to convey its functional programming style capability but the “SE” moniker stuck and here we are. The SE flavor also has the added advantage of a tiny footprint compared to MP’s already small footprint. All of this is relative of course and you may want to consider your choice of Helidon flavor based on other factors e.g. your developers’ familiarity (or lack thereof) with the functional style and so on.

    static void routing(HttpRouting.Builder routing) {
routing
.register("/greet", new GreetService())
.get("/simple-greet", (req, res) -> res.send("Hello World!"));
}

The neat thing is that both supports GraalVM natively and the combination of GraalVM and Helidon makes them very, very fast. Let’s take Helidon for a spin.

Setting up our Helidon application

Our application for this article is based on the Coherence Helidon Sockshop sample application.

Let’s start with the catalog micro-service and we’ll create the project using the Helidon cli:

$ helidon init                                                                                                                          130 

Helidon versions
(1) 4.0.1
(2) 3.2.3
(3) 2.6.4
(4) Show all versions
Enter selection (default: 1):

Helidon version: 4.0.1

| Helidon Flavor

Select a Flavor
(1) se | Helidon SE
(2) mp | Helidon MP
Enter selection (default: 1):

| Application Type

Select an Application Type
(1) quickstart | Quickstart
(2) database | Database
(3) custom | Custom
Enter selection (default: 1):

| Media Support

Select a JSON library
(1) jsonp | JSON-P
(2) jackson | Jackson
(3) jsonb | JSON-B
Enter selection (default: 1): 1

| Customize Project

Project groupId (default: vzlabs.helidon): vzlabs.sockshop
Project artifactId (default: database-se): catalog
Project version (default: 1.0-SNAPSHOT):
Java package name (default: vzlabs.se.database): vzlabs.sockshop.catalog

Switch directory to /opt/demos/sockshop/catalog to use CLI

Start development loop? (default: n): n

This will create a basic Helidon application with the Greet (aka Hello World) service. Alternatively, you can also use the Helidon starter to generate your project. Build the application and run it to make a basic sanity test:

$ helidon build
...

[INFO] --- jar:3.3.0:jar (default-jar) @ catalog ---
[INFO] Building jar: /opt/demos/sockshop/catalog/target/catalog.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.834 s
[INFO] Finished at: 2023-12-05T22:19:29+11:00
[INFO] ------------------------------------------------------------------------

$ java -jar target/catalog.jar
2023.12.05 22:20:11.919 Helidon SE 4.0.1 features: [Config, Encoding, Health, Media, Metrics, Observe, WebServer]
2023.12.05 22:20:11.925 [0x457ce5fb] http://0.0.0.0:8080 bound for socket '@default'
2023.12.05 22:20:11.934 Started all channels in 17 milliseconds. 501 milliseconds since JVM startup. Java 21.0.1+12-jvmci-23.1-b19
WEB server is up! http://localhost:8080/simple-greet

You’ll notice that the whole thing starts in less than 1s, including the JVM startup. That’s because we are using GraalVM’s JIT compiler, which is simply fantastic.

OK, that’s just a simple REST service that doesn’t do much other than saying “Hello” back. Still being up and running in that amount of time is insanely fast. Let’s take it for quick spin:

curl http://localhost:8080/simple-greet                                                                                         7 

Hello World!

curl http://localhost:8080/greet/LedZeppelin 6

{"message":"Hello LedZeppelin!","greeting":null}

Continuous Development with Helidon

Now, we can keep coding and rebuilding but why do that when you can run in live mode:

helidon dev

| source file changed
| rebuilding (incremental)
| rebuild completed (0.0 seconds)
| catalog starting

and let Helidon automatically pick up code changes and automatically rebuild as they are detected?

Building images

When you want to run your application, you can always run:

helidon build

as we did above or:

mvn package

Either will build the jar file which you can then run:

java -jar target/catalog.jar

However, you can also build native images. My colleague Ewan Slater likens native images to turbines vs pistons (traditional methods): turbines have less moving parts, output greater power and deliver a higher performance with greater efficiency. For us, native images compile and package your application into an executable and as such, have a few advantages:

  • native images start faster
  • native images have lower memory requirements
  • native images deliver more compact packaging making applications smaller and easier to distribute
  • native images have a reduced attack surface: no new code can be loaded at runtime

There are 2 types of images you can build:

  • Java Custom Runtime Image (JCRI)
  • GraalVM native image

Building in both formats is very easy. Let’s build one with the Java Custom Runtime Image:

mvn package -Pjlink-image -DskipTests

For the catalog micro-service above, the build took an average of 33s. You can then start it as follows:

./target/catalog-jri/bin/start
2023.12.05 22:31:01.262 Helidon SE 4.0.1 features: [Config, Encoding, Health, Media, Metrics, Observe, WebServer]
2023.12.05 22:31:01.263 [0x776e1146] http://0.0.0.0:8080 bound for socket '@default'
2023.12.05 22:31:01.273 Started all channels in 14 milliseconds. 305 milliseconds since JVM startup. Java 21.0.1+12-jvmci-23.1-b19
WEB server is up! http://localhost:8080/simple-greet

The startup time is pretty good too at around 300ms, including the JVM.

If you want to build the GraalVM native image, then specify its profile:

mvn package -Pnative-image -DskipTests

The GraalVM native image build will take slightly longer. In the same environment, this took 3 mins compared with JCRI’s average of 33s. Once it’s built, you can run it like so:

$ target/catalog
2023.12.06 10:55:27.435 Logging at runtime configured using classpath: /logging.properties
2023.12.06 10:55:27.445 Helidon SE 4.0.1 features: [Config, Encoding, Health, Media, Metrics, Observe, WebServer]
2023.12.06 10:55:27.445 [0x1a9b6889] http://0.0.0.0:8080 bound for socket '@default'
2023.12.06 10:55:27.446 Started all channels in 2 milliseconds. 13 milliseconds since JVM startup. Java 21.0.1+12-jvmci-23.1-b19
WEB server is up! http://localhost:8080/simple-greet

Yep, you read that right. Only 1ms to start the application and 11ms for the JVM.

From 1 perspective, there’s a tradeoff between build time vs startup time and which is more important to you will vary depending on your application and use case. However, there are other perspectives to consider which I’ll return to in a future post.

Since developers mostly use containers these days, Helidon also very helpfully include Dockerfiles for:

  • OpenJDK i.e. builds a jar and packages it into a container image aka the Piston method as Ewan would call it. It’s a useful transition for developers who are grappling with moving to Kubernetes, cloud, containers and this is 1 less thing to worry about, at least for now anyway.
  • JCRI: builds a JCRI native image using OpenJDK and packages it in a container image
  • GraalVM: builds a GraalVM native image and packages into a container image

You can now include use these as part of your CI pipelines.

Health Check & Observability

The default Helidon SE template will also add health checks. However, by default they are configured to return no content (HTTP 204). To get the health check data, add the “details” line as below:

        ObserveFeature observe = ObserveFeature.builder()
.config(config.get("server.features.observe"))
.addObserver(HealthObserver.builder()
.addCheck(DbClientHealthCheck.create(dbClient, config.get("db.health-check")))
.details(true)
.build())
.build();

You can now retrieve them:

curl http://localhost:8080/observe/health | json_pp
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 443 100 443 0 0 63213 0 --:--:-- --:--:-- --:--:-- 73833
{
"checks" : [
{
"name" : "jdbc:h2",
"status" : "UP"
},
{
"data" : {
"free" : "943.89 GB",
"freeBytes" : 1013489332224,
"percentFree" : "93.75%",
"total" : "1006.85 GB",
"totalBytes" : 1081101176832
},
"name" : "diskSpace",
"status" : "UP"
},
{
"data" : {
"free" : "230.76 MB",
"freeBytes" : 241965472,
"max" : "3.87 GB",
"maxBytes" : 4158652416,
"percentFree" : "99.51%",
"total" : "250.00 MB",
"totalBytes" : 262144000
},
"name" : "heapMemory",
"status" : "UP"
},
{
"name" : "deadlock",
"status" : "UP"
}
],
"status" : "UP"
}

Since we plan to eventually deploy to Kubernetes, you’ll be glad to know there are started, readiness and liveness checks too that you can then use as part of Kubernetes probes:

curl http://localhost:8080/observe/health/ready | json_pp
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 27 100 27 0 0 15526 0 --:--:-- --:--:-- --:--:-- 27000
{
"checks" : [],
"status" : "UP"
}

Similarly, Helidon exposes metrics in both Prometheus and OpenMetrics format and this allows you to retrieve interesting metrics such as garbage collection time, the size of your database connection pool and so on:

 curl http://localhost:8080/observe/metrics
# HELP cpu_systemLoadAverage Displays the system load average for the last minute. The system load average is the sum of the number of runnable entities queued to the available processors and the number of runnable entities running on the available processors averaged over a period of time. The way in which the load average is calculated is operating system specific but is typically a damped timedependent average. If the load average is not available, a negative value is displayed. This attribute is designed to provide a hint about the system load and may be queried frequently. The load average may be unavailable on some platforms where it is expensive to implement this method.
# TYPE cpu_systemLoadAverage gauge
cpu_systemLoadAverage{scope="base",} 0.2314453125
# HELP gc_total Displays the total number of collections that have occurred. This attribute lists -1 if the collection count is undefined for this collector.
# TYPE gc_total counter
gc_total{name="complete scavenger",scope="base",} 0.0
gc_total{name="young generation scavenger",scope="base",} 0.0
# HELP requests_count_total Each request (regardless of HTTP method) will increase this counter
# TYPE requests_count_total counter
requests_count_total{scope="vendor",} 1.0
# HELP thread_count Displays the current number of live threads including both daemon and nondaemon threads
# TYPE thread_count gauge
thread_count{scope="base",} 7.0
# HELP classloader_unloadedClasses_total Displays the total number of classes unloaded since the Java virtual machine has started execution.
# TYPE classloader_unloadedClasses_total counter
classloader_unloadedClasses_total{scope="base",} 0.0
...

You can also enable tracing and logging is included as well. Let’s monitor this with Prometheus by providing it with the scrape target:

  - job_name: "helidon"

# metrics_path defaults to '/metrics'
# scheme defaults to 'http'.
metrics_path: '/observe/metrics'
static_configs:
- targets: ["localhost:8080"]

And start a local Prometheus instance:

./prometheus
ts=2023-12-05T12:43:00.526Z caller=main.go:539 level=info msg="No time or size retention was set so using the default time retention" duration=15d
ts=2023-12-05T12:43:00.527Z caller=main.go:583 level=info msg="Starting Prometheus Server" mode=server version="(version=2.48.0, branch=HEAD, revision=6d80b30990bc297d95b5c844e118c4011fad8054)"

...

We can access Prometheus and check the status:

Scraping Helidon’s metrics in Prometheus

We can see that Prometheus is able to scrape Helidon’s metrics.

Let’s run a local Grafana instance and add our local Prometheus as a datasource. We can then create a dashboard:

Helidon metrics in Grafana

Summary

With this article, we begin a new series of articles to look at recent cloud native enhancements, products and services in and around Oracle Cloud as well as Oracle’s stack of products and services. We start the series with Helidon, a fast, very lightweight framework for writing your microservices in Java and designed from the ground up to take advantage of cloud native, and in particular, Kubernetes-based deployment. We looked at some of its productivity features including database integration, health checks and observability as well its performance capabilities, especially when running with GraalVM.

Throughout this series, we’ll also explore the capabilities and purpose behind their additions, how you can combine them and how you can use them in modernizing and deploying your applications in a cloud native way. Roll your sleeves up, this will be fun.

--

--