Abhishek Chowdhury
Dev Genius
Published in
14 min readApr 17, 2022

--

Quarkus Vs Golang APIs in AWS Lambda — A Comparative Study

Backdrop

Hello everyone. All of us loves some history and let us start with that.

Promise it won’t be long 😊

The world wide web has evolved magnanimously over the last three decades. The concept of a Web was first introduced in the late 80s (1989 to be more precise) by Tim Burners-Lee, a Professorial Fellow of Computer Science at Oxford University and a professor at MIT (Massachusetts Institute of Technology) where he perceived its capabilities to be expressed in the form of innovations associated with three phases:

1.Web of Documents (Web 1.0)

2.Web of People (Web 2.0) and

3.Web of Data (Web 3.0)

Through the trend of constant evolution over the years, web is now slowly transitioning to the more data centric Web 3.0 phase (As I write this, architectural foundations of a next-gen Web 4.0 framework are getting laid, but that is a story for some other day).

In parallel, there has been an exponential upsurge in the users of internet and world wide web over the years. This era has seen introduction of mobile devices, transactional POS systems, chatbots, desktops, tablets, electronic sensors and other different analytical and IOT channels for data streaming and analysis coupled with different modes of static and dynamic content in the form of push notifications, AI driven recommendations, video streaming, gaming solutions and the list goes on and on.

This has resulted in web to be more accessible to the general mass and become an integral player in all facets of business, finance, retail, entertainment and other commercial and non-commercial activities. Governments and non-government entities are investing heavily in digital platforms to build more resilient and secure systems for process agility and seamless and transparent data and transaction(s) traceability as well as accountability.

In the current day world as I am writing this, there are approximately 4.66 billion active users of world wide web which comprises roughly about 60% of the whole world population.

The Initial Problem

Now there is always a catch, isn’t it!! This one though is a bit more obvious.

As the demand increased, what about the infrastructure and end-user experience?

In the initial days where the web had to deal with maybe only 10% of the current consumption, traditional IT system infrastructure comprising of in-house(on-premises) hardware components with extensive capacity and resource planning/purchase/adjustments (cost included as part of the CAPEX (Capital Expenditure), was sufficient to cater to this.

But alarm bells had started ringing gradually and the business organizations were aware that this was not enough to support the exponentially growing consumer base.

Along came the Cloud!!!

With the introduction of Cloud based PAAS, SAAS, DAAS and other offerings along with storage and integration solutions, enterprises now had an alternative to think about shifting from a CAPEX to an OPEX paradigm. Cloud computing created a revolution which substantially decreased the operational overhead. This provided the perfect platform for bringing agility in transforming business ideations and innovations to subsequent technical implementations and product releases.

Ok all issues solved now, hopefully!!!!

Now there is More

Am afraid not, my friend.

You cannot ever afford to relax, even one bit!!!! 😊

Cloud solutions provided the IT enterprises highly available, scalable, resilient and redundant systems which resulted in increased customer interactions. Now, this might be a dream of any IT organization as it implies enhanced customer curiosity and subsequent website popularity in the long run, but this brings about another two very important aspects that the website needs to address:

1. Performance and

2. Concurrency which promotes performance

From a generic performance standpoint, Cloud based applications can support auto-scaling which can either be manually provisioned or there can be serverless implementations where the Cloud providers (AWS, GCP, etc.) can handle auto-scaling based on the input load.

For either of the two implementations, there is a cold-start period which determines how much of an interval a new application instance takes to come up and be fully ready to serve additional request(s). The more time it takes to be serviceable, requests can get queued up and this will have an adverse effect on the performance.

Now coming to the concurrency part, with increased customer interactions, chances of blocking executions increase substantially as an application might need to cater to multiple customer requests at the same time.

If I go back and try to define concurrency in a layman’s term, Concurrency means that an application is making progress on more than one task at the same time (concurrently). It is a bit confusing with parallelism which tells that a system is said to be parallel if it can support two or more actions executing simultaneously. More details can be found at this link which I found be very helpful (https://medium.com/@itIsMadhavan/concurrency-vs-parallelism-a-brief-review-b337c8dac350 ) and I am not going into details around the difference, as this will sway away from the primary topic here.

If the applications, rather the programing languages (now getting a bit technical) support concurrency either implicitly or through customizations, this has a huge bearing on the overall latency and performance as this will lessen queued up requests and enable non-blocking executions.

Both these aspects play a significant role in determining the overall website acceptability and popularity as both are linked with application performance which is intrinsically hinged with the end-user experience.

Whenever there is a problem, there is also a solution.

Luckily, we have two good ones.

The below section(s) deal with Golang and Quarkus as the programming language implementations that cater to these two problems and does a comparative analysis on the resulting performance benchmark figures for each. Tools and setup related instructions are also provided in detail. We will be dealing with a serverless solution here with AWS Lambda.

So, let us start with the fun.

Along comes Golang

Ok, some info about Go.

Go was designed at Google in 2007 to improve programming productivity in an era of multicore, networked machines and large codebases. It is a statically typed, natively compiled, garbage-collected, concurrent programming language that belongs primarily to the C family of languages in terms of basic syntax. Go is expressive, concise, clean, and efficient. It compiles quickly to machine code yet has the convenience of garbage collection and the power of run-time reflection.

As of Feb 2021 overall, there are about 1,1 million professional Go developers who use Go as a primary language. But that number is possibly closer to ~2.7 million if we include professional developers who mainly use other programming languages but also do a bit of Go on the side.

According to the Developer Ecosystem Survey 2021, Go is among the top 10 primary languages of professional developers, with a share of 8%.

In the recent decades with vast improvements in machine computational and processing power with the evolution of multi-core processors, most of the programming languages are not able to optimize or harness the full potential of the multi-cores.

Golang was able to make huge inroads in this area with its unique concurrency design. Its concurrency mechanisms make it easy to write programs that get the most out of multi core and networked machines. It does that using the power of goroutines. When a function is created as a goroutine, it is treated as an independent unit of work that gets scheduled and then executed on an available logical processor.

The goroutines are managed by a runtime scheduler which is a complicated piece of software that sits on top of the operating system, binding operating system’s threads to logical processors which, in turn, execute goroutines.

It controls which goroutines are running on which logical processor at any given time. This ensure that all the cores of the CPU are utilized as the goroutines get distributed and executed in the different cores independently in a non-blocking fashion.

The below figure illustrates this.

I say non-blocking because when the Go-Scheduler finds a blocking routine (e.g., a HTTP call), instead of awaiting the execution of that routine and thereby bocking the CPU thread, it will be swapped out for another goroutine that will execute on that thread instead.

For details on Go history and concurrency, please visit the links mentioned in the Additional Links section below.

Also, Go Scheduler ensures predominantly lesser thread context switching times as the switching is happening not between OS threads but goroutines which are much lightweight and hence more performant. We can see the final runtime figures and then reinforce on this further.

Regarding application start-up times, GO programs have significantly lesser start up times in comparison to Java and other programming languages. Specifically in java, where all the runtime bindings, autowirings and classloader hierarchy is formulated and generated during application start-up, Go programs are compiled ahead of time to native machine code and hence has significantly lesser startup times.

Quarkus joins hands

A big round of applause for Quarkus into the podium!!!

Quarkus, the Supersonic Subatomic Java, made its debut in March 2019 and is a java framework that is tailor made for Kubernetes deployment. It promises to deliver small artifacts, extremely fast boot time, and lower first-byte latency.

The current Github repo for Quarkus has around 6k downloads across all releases and 1.8k forks and it is vastly gathering popularity. This Google Trends link verifies the popularity that has grown over the last few years.

Quarkus has a lot of extensions which enable it to support a variety of distributed technology components like Kafka, Hibernate, Openshift, Kubernetes and reactive and imperative programming tools like Vert.x among others.

The primary goal of Quarkus was to enable Java to make some inroads in Kubernetes and other serverless hyperscalers where it is rapidly losing ground to NodeJS, Go and other programming language paradigms. The inbuilt support of Quarkus for the hyperscalers along with reactive extensions was the first step towards attempting acceptability of Java in the serverless world.

But this is just one of the problems.

The primary reason for the loss of popularity of Java in this discipline was the cold start issue that we have already discussed. Java programs have substantially longer start-up times owing to the different reflective bindings and classloading challenges. If this can be mitigated, Java might again come into the forefront.

Now there is GraalVM.

A significant ally of Quarkus to this campaign came in the form of GraalVM which is a JVM for compiling and running applications written in different programming languages to native machine binary.

With GraalVM as the runtime, all Quarkus programs can compile ahead-of-time(AOT). Although this results in an increase of the overall build process, as the generation of native executable is a bit time consuming, it substantially improves the start-up time and the first-byte latency as the native image contains all the resources and information to run, including a minimal JVM needed to run the application. Also GraalVM runs with a minimal memory footprint which ensures efficient program executions.

What about concurrency? Is there anything similar to goroutines in Quarkus?

Not exactly. But it can be addressed to some extent using Mutiny and reactive extension support. Using reactive extensions, programs are executed using few numbers of I/O threads in a non-blocking paradigm. A few number of I/O threads can handle many concurrent requests and also improve response times by minimizing thread switches.

The non-blocking feature is accomplished by introducing the concept of a continuation or a callback during every request execution.

Here the threads do not wait for a request to be fully executed specially if it involves a blocking call, say like a database invocation. Instead, it schedules an I/O operation with a continuation i.e., a request to process the remaining code. This continuation is then passed as a callback (a function to be invoked with the I/O outcome).

Thus, it becomes ready to serve new requests till the response of the earlier program(s) are returned in the callback. This way, there is no idle time and CPU resources of the machine are optimally utilized promoting high concurrency.

The below diagram illustrates the I/O threads and continuation model.

For details on Quarkus and Mutiny, please refer to the Additional Links section below.

Hence, combining the hyperscaler support for Quarkus with the native image generation capability of GraalVM along with the Mutiny reactive support for concurrency, Java has given itself a very strong case in the serverless world.

Let us now jump to our sample application which we will use for benchmarking.

Mock Application Overview

The diagram above depicts the overall application overview. It is a simple application exposing REST endpoints via API Gateway, one endpoint each for Golang and Quarkus based Lambda functions. The Lambda functions invoke a Postgres database server running in an EC2 instance.

The service endpoint accepts a customer_id path parameter and subsequently runs a set of data specific validations by referring to pre-loaded database tables in the Postgres instance.

The entire code base is available in the Additional Links section below.

For the AWS environment, I have used the default VPC with default security groups and Network access control lists and the corresponding roles assigned to the Lambda function for Cloudwatch access and access to the EC2 instance running Postgres.

You can turn on the X-ray feature for better traceability and latency visualization at a service-to-service level, but this is optional and only if you are interested to explore in depth.

Let us know look at the local system setup.

System Setup and Steps

Below are the software and their corresponding versions used for the benchmarking.

Go Runtime Version: 1.17.6

Postgres Version: 12

JDK Version: OpenJDK Runtime Environment GraalVM CE 21.3.0 (build 11.0.13+7-jvmci-21.3-b05)

GRAALVM Version: Java 11 based Graalvm version(graalvm-ce-java11–21.3.0)

Maven version: 3.6.3

SAM CLI Version: 1.40.1

Docker Version: 20.10.0

IDEs: IntelliJ Idea ( Or any similar) for Quarkus, Microsoft VS Code for Go

PostGres

Create an EC2 instance with the suitable instance type (I selected t2.micro for this demo).

Install postgres. This link provides with the details.

Launched the instance into a private subnet in the default VPC with relevant security group and NACL configuration, allowing access from the Golang and Quarkus Lambda functions (to be created shortly)

Golang

Build the Docker image with the Dockerfile provided in the codebase.

Create an AWS ECR Repo

Push the image to the created repo.

Create a Lambda function via the AWS console/SAM CLI/AWS CLI with the ECR image and provide outbound access to the created postgres instance.

Quarkus

Generate the Native Linux executable using the Docker runtime and Graalvm.

Deploy the executable as a Lambda function using Custom Runtime. The deployment can be done by using SAM CLI. The link here provides details.

P.S: Both the Lambda functions were deployed into the private subnet and the VPC of the Postgres instance. This was done to minimize the network latency aspects. Also, both the functions are using 128MB memory.

API Gateway

Create two REST Endpoints and expose the two Lambda functions using lambda proxy integration.

Apache Benchmark

This was installed in an AWS Cloud 9 instance that was launched again into the same private subnet and VPC.

The below two steps in sequence will install the same in an Ubuntu instance:

  1. apt-get update

2. apt-get install apache2-utils

This was then to used to invoke multitude of requests to the Lambda functions via the API Gateway REST Endpoints.

Ok, setup DONE. Time for the results guys!!!!Fingers crossed!!!!!

Benchmark Execution results

The below tables summarize the results that were obtained after running the benchmark tests.

A sample benchmark command to run 100 requests with 60 of them concurrent, is given below:

ab -n 100 -c 60 -p dummy.txt <API_GATEWAY_REST_ENDPOINT>/1

[dummy.txt was some sample file added since this is a POST request. The file can either be empty or can have any text]

The two tables below show the execution time and the percentage of execution within the recorded time.

Comparison Snapshot

Time for the reality check!!!

Let us start with the size of the deployment.

The size of the Quarkus native image package in Lambda was around 13 MB. This is a bit higher than the corresponding one for Golang, which was around 5 MB.

Now, time to focus on the results obtained above.

P.S: Spoiler alert, the results might have some effect of initialized Lambda containers getting re-utilized across different requests, but wholistically this factor should even out across all the scenarios.

If we combine the four runs, we can see that there is not much to choose in between the minimum execution times. But the mean execution times across all the transactions are lesser in Golang in comparison to Quarkus.

For Golang, it is roughly around 110 ms while for Quarkus native, it is roughly around 400 ms. This includes the start up times of the Lambda containers in addition to the processing times.

This to some context makes sense as even if thread context switching is minimized in Quarkus,

it is not totally absent, while in case of Golang, the context switching is much more efficient and managed by the Go Runtime.

Now since we have the figures handy, it is now time to wrap up the discussion with a few additional pointers.

Wrap up and a few Pointers

With respect to the above test results, it might be apparent Golang is more performant than Quarkus in concurrent scenarios and it might be a better fit in high concurrency and very low latency requirement scenarios.

But having said that, Quarkus has come a long way in breaking the “cold-start myth” around Java and serverless combination. I feel the following factors are the primary reasons:

  • Efficient memory management and AOT compilation features provided by Graalvm along with the native image generation capability
  • Reactive flavor provided by Mutiny
  • Rich integration library support for different distributed components like Kafka, Kubernetes, RabbitMQ
  • Again it is very easy to adopt if you are from a Java background (based on Eclipse Microprofile)

With Go, one of the primary challenges might be the absence of stable open-source integration libraries for distributed components like Kafka, Kubernetes, etc. The Go community is still evolving and will surely cater to these in the long run.

Also, from a programing language perspective, below are some of the drawbacks that people talk about:

  • Lack of functional overloading and default argument values
  • Lack of generics
  • Absence of a proper package manager like npm, RubyGems, etc for dependency management

Inspite of these setbacks, Go is still one of the most interesting languages currently and you can rest assured that the Go developer community is already working on these improvements.

Ok, enough talk. Time for the final wrap up.

Summary

I tried to mention all the points that I thought might help in this comparison.

All in all, it boils down to the individual program requirements on what needs to be prioritized for delivery and execution.

Serverless applications have indeed been a revolution in the areas of infrastructure auto-provisioning, scalability and resiliency aspects and researchers and analysts had long put forward their thinking caps on, trying to get a slice of the cake.

With the advent of Quarkus and previously Golang and Javascript, us, application consultants, architects and developers now have an option to choose what is best suited for our capabilities and requirements. It is indeed an exciting scenario to be in and maybe the coming days will reveal some more enthralling and innovative journeys.

Geared up for that!

Additional Links

Github links

Go: https://github.com/chowju1983/go-sample-application

Quarkus: https://github.com/chowju1983/quarkus-sample-application

Go History and Concurrency:

https://en.wikipedia.org/wiki/Go_(programming_language)

https://medium.com/rungo/achieving-concurrency-in-go-3f84cbf870ca

https://www.golangprograms.com/go-language/concurrency.html

https://www.golang-book.com/books/intro/10

Quarkus and Mutiny links:

https://en.wikipedia.org/wiki/Quarkus#:~:text=Design%20pillars-,Container%20first,to%20building%20and%20running%20applications.

https://www.baeldung.com/quarkus-io

https://quarkus.io/guides/getting-started-reactive

https://quarkus.io/guides/mutiny-primer

Photo by Markus Spiske on Unsplash

--

--