Serverless Java: Reduce Infrastructure Overhead

Light and fast ...
14
Sep

Serverless Java: Reduce Infrastructure Overhead

Java is still the first choice when it comes to software development for business use [1]. However, the development of Java software alone is not enough: machines, operating systems, JREs, application servers, etc. are required for productive use - and large frameworks and libraries are also required as the basis for code functionality. This overhead hurts more the simpler the required functionality is, because it makes development, testing, and operation more difficult. The alternative concept: Serverless.

In this alternative concept, software is deployed as a callable function in a suitable environment instead of as a standalone executable binary. Instead of an application server and all infrastructure below, it takes care of the connection to the outside world. Up until now, these functions are written in JavaScript, Python and Go, among other things because of long startup times and high memory requirements of JVM-based applications.

You can also benefit from existing Java experience and code, as well as from reduced deployment and operation effort: Improvements of the JVM, new lean web frameworks like Quarkus and Micronaut, and techniques like native image compilation with GraalVM make Java interesting again for serverless applications. This article presents some approaches to using Java in serverless environments.

Application Scenarios

In general, a serverless approach is well suited for tasks that fit the following usage profiles:

  • Asynchronous, easily parallelized into independent execution units
  • Irregular or sporadic demand with a large, unpredictable variance in usage
  • Stateless, short-lived, tolerates high latency
  • Rapidly changing business requirements with the need for high development speed

Some concrete examples are:

  • File processing after upload
  • WebHooks to connect logic to HTTP calls
  • Background jobs – time-controlled processing
  • Simple backend for web frontends

Basics

This article wants to illuminate two views on Serverless: Development and operation. For the developer this means implementing the application logic in the form of simple functions. For the admin, it is a simple way to deploy and operate software, namely in appropriate local or cloud environments that take care of the business-critical stuff.

Once deployed, a function can then be called via HTTP, messaging or other events, for example. An example is the generation of a preview for uploaded photos: Once a user has uploaded a new image, this event triggers the execution of a function for creating a preview. The function itself can trigger other functions.

How a serverless platform works internally is ultimately irrelevant to the developer as long as it only takes away boilerplate code, e.g. the HTTP layer. Once they have uploaded their software, the platform takes care of the operation and scaling of the function. Cloud-based offerings charge only for actual resource usage, such as CPU time, memory, or number of calls. In addition to efficient and economical operation, the focus is on developer productivity. A good summary on this topic is provided in the Serverless Whitepaper of the CNCF Serverless Working Group [2].

When a request is received to operate the software, it is first started in an execution environment and an end point is set up that calls the function, passes arguments and returns the result. The latency time from the request to the response is decisive for the performance, which the developer can influence, on the one hand, by choosing the platform and, on the other hand, by using suitable runtime environments and program optimizations (Fig. 1). The platform can then destroy the environment or, if necessary, reuse it. Platforms usually limit the maximum execution time of a function, for example to 15 minutes, after which the execution is aborted.

Functions must be stateless, because the runtime environment can be terminated or restarted at any time. Multiple environments are also possible, so that the same function can run several times in parallel. Within an environment, for example, static fields can be used as caches so that time-consuming initializations can be amortized over several calls as long as the environment exists.

Fig. 1: Ideal-typical life cycle of request processing of a serverless function

Fig. 1: Ideal-typical life cycle of request processing of a serverless function

Up to now, Java has not played a major role in the serverless environment, since the start of a JVM in cloud environments alone can take several seconds, which adds to the latency. This is particularly unfavorable if the actual function execution only takes a few milliseconds and is frequently requested. Furthermore, the libraries and frameworks used may extend the startup time with their initialization. In the meantime, however, it is now possible to restrict oneself to dependencies that are required for execution, while there are also alternatives to virtually eliminate the JVM start-up latency.

In addition, serverless platforms can minimize deployment latency by reusing execution environments for multiple requests. This way, the same execution environment handles requests that occur in quick succession. This eliminates the need for re-provisioning.

Platforms

Serverless platforms are also referred to as FaaS (Function as a Service) in the cloud nomenclature and thus, move the execution of functions to the center of attention (Fig. 2), analogous to virtual machines in IaaS and complete applications in PaaS. BaaS (Backend as a Service), i.e. infrastructure services such as databases, messaging systems, and storage, are not in the picture because no separate application code is usually introduced.

Fig. 2: Classification of FaaS in the context of cloud models [3]

Fig. 2: Classification of FaaS in the context of cloud models [3]

Serverless platforms offer the following features, among others: no installation or maintenance effort for infrastructure, flexible scalability, consumption-dependent costs (i.e. no usage = no costs), event-driven, self-scaling within given limits, easy integration with other services of the platform.

The major FaaS cloud providers with native Java support are currently Azure Functions, AWS Lambda and IBM Cloud Functions. Google Cloud Functions does not currently provide direct support for Java-based features.

In addition to these cloud services, there are a number of platforms that can be run locally, including Riff, Fn Project, OpenWhisk, and OpenFaaS.

Programming models

A simple Java class is often sufficient to define a function. The metadata required for provision is specified declaratively using annotations or external configuration files. However, some FaaS services, such as Azure Functions, require the use of special APIs, which also allow platform specifics to be used. If you want to use logic across multiple FaaS services, the Spring Cloud Functions project [4] is a good place to start. This allows you to define function logic that is independent of the FaaS vendor, yet can be called in different FaaS environments. In addition, it is also possible to containerize Java apps with HTTP endpoints and run them on serverless container platforms like Google Cloud Run or AWS Fargate.

Fn Project

For the first steps in the serverless environment, the Fn Project [5] is well suited. It is a serverless platform written in Go, that runs on your own computer and supports Java as a runtime environment in addition to Go and JavaScript. Fn provides a server component that receives the client requests and uses dynamically started docker containers for function execution. In addition, there is a UI component [6], which provides further information about the applications and functions provided. A function in Fn lives in the context of a logical application.

A brief guide to setting up the Fn Tooling is provided in the Quick Start on the project’s website [7]. Using the fn command line tool, we will create a new Maven project called hello-fn:

fn init --runtime java hello-fn

This project contains a sample function in HelloFunction.java (Listing 1) and the configuration file func.yaml (Listing 2) to run the function in the Fn-Server.

The project and the class HelloFunction are simple and use no further dependencies. The logic is mapped in the method String handleRequest(String).

Listing 1: Example function in HelloFunction.java

package demo;
 
public class HelloFunction {
 
  public String handleRequest(String input) {
    String name = (input == null || input.isEmpty()) ? "world" : input;
    return "Hello, " + name;
  }
}

The most important information in func.yaml includes name, runtime, and cmd. The name attribute specifies the logical name of the exposed function. With runtime: java, we instruct the Fn server to execute our function in a Java runtime environment. Using the attribute cmd, the Fn runtime knows that a new instance of the class HelloFunction is created and that the handleRequest method must be called by reflection.

Listing 2: func.yaml

schema_version: 20180708
name: hello-fn
version: 0.0.1
runtime: java
cmd: demo.HelloFunction::handleRequest

The logical application containing the function is created with the command fn create app:

fn create app hello-fn-app

The function is then made available via the Fn server using the fn deploy command:

fn deploy --app hello-fn-app --local

The function can be called with the command fn invoke:

fn invoke hello-fn-app hello-fn

The Fn User Interface can be hosted as a docker container:

docker run --rm -it --link fnserver:api -p 4000:4000 \
  --name ui -e "FN_API_URL=http://api:8080" fnproject/ui

At http://localhost:4000 you will find an overview of the applications and functions provided, which can also be tested directly via the interface.

Azure Functions

Azure Functions is a FaaS service that also supports Java. For this purpose, a corresponding Java class must be created with the Azure Functions Java SDK. A good guide for creating a Java-based Azure Function is provided [8].

Listing 3 shows a sample function using annotations from the Azure Functions Java SDK. The external name of the function is defined using @FunctionName. The @HttpTrigger defines how the function can be called via HTTP. HttpRequestMessage encapsulates information about the request, and information about the execution environment, such as configuration and logging, can be accessed via the ExecutionContext.

Listing 3: Example function using annotations from the Azure Functions Java SDK

public class Fun {
 
  @FunctionName("greet")
  public HttpResponseMessage<String> httpHandler(
    @HttpTrigger(name = "req", methods = "post",
                 authLevel = AuthorizationLevel.ANONYMOUS)
    HttpRequestMessage<optional<String>> request, 
    ExecutionContext context) {
 
    context.getLogger().info("processed a request.");
 
    String name = request.getBody().orElse(null);
 
    if (name == null)
      return request.createResponse(400, "Missing name");
 
    return request.createResponse(200, "Hello, " + name);
  }
}

The example uses the Azure Functions Maven plugin and requires installation of the azfunc tool from the Azure Function SDK [9]. The project can be built as a Maven project and tested locally. To do this, you can start the function locally via mvn azure-functions:run and call the local endpoint via curl -d ‘{“name”: “World”}’ http://localhost:7071/api/greet.

Spring Cloud Function

Spring Cloud Function provides support for serverless development with all of Spring’s capabilities such as dependency injection, integrations, auto-configuration in a unified programming model across multiple serverless providers. Currently, Spring Cloud Function provides support for AWS Lambda, Microsoft Azure, Apache OpenWhisk SDKs, Project Riff and Fn Project. The framework also provides special adapter implementations for integration with FaaS providers that abstract the provider-specific APIs. Functions can either be declared as Spring Beans or discovered automatically via component scanning. The following is a bean function definition:

@Bean // function is exposed with name "greet"
public Function<User, Greeting> greet() {
  return user -> new Greeting(String.format("Hello, %s", user.getName()));
}

Listing 4 shows an example of Spring adaptation of Azure Function specific APIs. By calling the handleRequest(…) method, the actual function call is triggered at the Bean function named greet.

Listing 4: GreetingHandler for mapping to Azure Functions

public class GreetingHandler extends AzureSpringBootRequestHandler<User, Greeting> {
 
  @FunctionName("greet")
  public Greeting execute(
    @HttpTrigger(name = "request", methods = HttpMethod.POST, 
      authLevel = AuthorizationLevel.ANONYMOUS)
    HttpRequestMessage<optional<User>> request, ExecutionContext context) {
 
    String name = request.getBody().map(User::getName).orElse("unknown");
    context.getLogger().info(String.format("Invoking greeting =: %s", name));
 
    // this invokes the greet function
    return handleRequest(request.getBody().get(), context);
  }
}

To provide the function on Azure, you have to login to Azure in the console using az login. After that you call the maven goal mvn azure-functions:deploy. Afterwards, the app is available under the generated URL.

Quarkus with GraalVM

Quarkus enables the development of lean Java microservices [10]. In combination with GraalVM and its native image extension, it is even possible to create an executable native binary from a classic JAX-RS application [11]. This binary contains the pre-compiled application as well as GraalVM’s SubstrateVM as runtime environment. SubstrateVM not only requires less resources than classic JVMs, but also starts much faster – often in a few milliseconds. This makes the combination of Quarkus and GraalVM Native Image ideal for use in serverless container platforms like Google Cloud Run [12]. The platform offers pay per use, flexible scaling, and the ability to completely remove containers that are no longer needed [13].

Tips

A few tips for using Java in serverless environments:

  • Minimize start-up times
  • Pay attention to efficient processing
  • Use batch processing of messages
  • Use minimal dependencies

Conclusion

Suitable use cases can benefit enormously from the serverless paradigm: If the task is manageable and can be easily parallelized, the infrastructure overhead is practically reduced to zero. Where previously only JavaScript, Python, and Go were possible, fast starting application frameworks such as Quarkus and Micronaut (especially in combination with GraalVM Native) have recently put Java on the short list for serverless implementation [14].

 


Links & Literature

[1] https://www.tiobe.com/tiobe-index/java/

[2] https://github.com/cncf/wg-serverless/tree/master/whitepapers/serverless-overview

[3] https://serverless.zone/abstracting-the-back-end-with-faas-e5e80e837362

[4] https://spring.io/projects/spring-cloud-function

[5] https://fnproject.io/

[6] https://github.com/fnproject/ui

[7] https://github.com/fnproject/fn#quickstart

[8] https://code.visualstudio.com/docs/java/java-azurefunctions

[9] https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local

[10] https://quarkus.io/guides/getting-started

[11] https://quarkus.io/guides/building-native-image

[12] https://github.com/cncf/wg-serverless/tree/master/whitepapers/serverless-overview

[13] https://cloud.google.com/run/

[14] https://github.com/thomasdarimont/serverless-javamagazin-article

Stay tuned!
Learn more about Serverless
Architecture Conference 2020

Behind the Tracks

Software Architecture & Design
Software innovation & more
Microservices
Architecture structure & more
Agile & Communication
Methodologies & more
Emerging Technologies
Everything about the latest technologies
DevOps & Continuous Delivery
Delivery Pipelines, Testing & more
Cloud & Modern Infrastructure
Everything about new tools and platforms
Big Data & Machine Learning
Saving, processing & more