Choose your programming language

Share on:

Overview

We know, Micro-services are a technology agnostic, basically you can use any available technology to create and deploy your bounded context, we know also that micro-services give us choices, and those choices should be made to ensure that the goal of your bounded context is met.

In this post I’m going to talk about programming languages and how to choose the right one to develop your micro-services

The History of programing languages

Before diving in our comparison, let’s go back to the history of programming language.

The evolution of programming languages started with compiled programming languages like C and C++ witch are typed programing languages, to interpreted programming languages like Python and PHP witch are dynamic programming languages, and in the middle of 90s a new kind of programing show up, Java was a new concept with two steps compilation and cross platform support. Microsoft catches up years later with .net framework and .net Core framework.

The only exception in this evolution came at the beginning of the 2010s, when google introduced a new modern programming language, which is compiled to machine code, Golang the new multi paradigms programming language.

Compiled vs. Interpreted

Compiled languages are compiled to target platform machine code, so the program can be executed without any requirement.

Interpreted languages are not complied, but interpreted and translated to machine code by an anther application at runtime.

Two-setps compilation combines both processes, first the program is compiled to an intermediate language, then the intermediate language is translated to machine code by a virtual machine at run time

Static typed vs. Dynamic typed

Static typed means that types are checked before run-time, usually at compilation time.

Dynamic typed means that Types are checked on the fly, during execution


The battle

We are going to develop a small micro-service with three modern programming languages:

  • Golang: Statically typed & compiled
  • Python: Dynamically typed & interpreted
  • C# : Statically typed & two steps compilation

The Three micro-service implement the same algorithm: Fibonacci numbers

Fibonacci with Dotnet Core 6

First, we need to add some code to configure and register a web server

await Host.CreateDefaultBuilder(args)
  .ConfigureWebHostDefaults(config => {
    config
    .ConfigureKestrel(options => {
        options.Listen(IPAddress.Any, 8082, listenOptions =>
        {
          listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
        });
      })
    .Configure(app => {
        app.UseRouting()
          .UseEndpoints(endpoint => {
              endpoint.MapGet("/fibonacci" ,GetFibonacci);
              endpoint.MapGet("/healthz", GetHealthz);
      });
    });
})
.Build()
.RunAsync();

Then we implement the algorithm to calculate the first Fibonacci number with 10,000 digits

private BigInteger Fibonacci(){
  
    var a = new BigInteger(0);
    var b = new BigInteger(1);
    BigInteger tmp;

    var limit = BigInteger.Pow(10,9999);
    while(BigInteger.Compare(a,limit) < 0)
    {
      a = BigInteger.Add(a,b);
      tmp = a;
      a = b;
      b = tmp;
    }
    return a;
}

Dotnet Core allows two types of completion, JIT (just in time) and AOT (ahead of time) JIT is two steps-compilation process

AOT is more optimized than JIT, the code is compiled to target platform machine code, and all the requisite to execute the application are packaged in the same package. The package will be deployed to standard Alpin Linux docker image, which is more optimized than Microsoft standard .net core runtime image

dockerfile with just in time compilation

# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env
WORKDIR /app

COPY *.csproj ./
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -r linux-x64  \
        -p:PublishSingleFile=true --self-contained false -o out

# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY --from=build-env /app/out .
EXPOSE 8082
ENTRYPOINT ["./dotnet-fibonnaci-webapp"]

dockerfile with ahead of time compilation

# syntax=docker/dockerfile:1
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build-env
WORKDIR /app

COPY *.csproj ./
RUN dotnet restore

COPY . .
RUN dotnet publish -c Release -r alpine-x64 \
        -p:PublishSingleFile=true  \
        -p:PublishTrimmed=true \
        --self-contained true \
        -o out-self

# Build runtime image
FROM amd64/alpine:3.14

RUN apk add --no-cache \
        ca-certificates \
        krb5-libs \
        libgcc \      
        libintl \
        libssl1.1 \
        libstdc++ \
        zlib

ENV ASPNETCORE_URLS=http://+:8082 \
    DOTNET_RUNNING_IN_CONTAINER=true \
    DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true
  
WORKDIR /app
COPY --from=build-env /app/out-self .
EXPOSE 8082
ENTRYPOINT ["./dotnet-fibonnaci-webapp","--urls", "http://0.0.0.0:8082"]

The second docker file is more complicated than the first one, because we need to prepare the runtime image to host the AOT package

Fibonacci with Python

Same algorithm, but with Python

To start a web server with python we use flask and waitress libraries

if __name__ == "__main__":
    from waitress import serve
    serve(app, host="0.0.0.0", port=8080)

Then we implement Fibonacci numbers algorithm

def getFibonacci():
    a = 0
    b = 1
    limit = 10 ** 9999
    while a < limit:
        a = a + b
        a,b = b,a

    return a

And finally we package the micro-service on docker image

FROM python:3.8-alpine
RUN mkdir /app
ADD . /app
WORKDIR /app
RUN pip install flask
RUN pip install waitress
EXPOSE 8080
CMD ["python", "main.py"]

Fibonacci with Golang

Register and configure a web server

func main() {

	http.HandleFunc("/fibonacci", getFibonacci)
	http.HandleFunc("/healthz", getHealthzt)

	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		log.Fatal(err)
	}

}

Then implement Fibonacci numbers algorithm

func fibonacci() *big.Int {

	a := big.NewInt(0)
	b := big.NewInt(1)

	var limit big.Int
	limit.Exp(big.NewInt(10), big.NewInt(9999), nil)
	for a.Cmp(&limit) < 0 {
		a.Add(a, b)
		a, b = b, a
	}

	return a
}

Now let’s package the micro-service on docker image, we are going to use golang standard image as build image and then we deploy the artifact in to debian distroless image

# syntax=docker/dockerfile:1
## Build
FROM golang:1.17.3-buster AS build
WORKDIR /app

COPY *.go ./
RUN go mod init go-container
RUN go build -o  /go-container -ldflags="-s -w" main.go 

## Deploy
FROM gcr.io/distroless/base-debian11
WORKDIR /

COPY --from=build /go-container /go-container
USER nonroot:nonroot
EXPOSE 8088
ENTRYPOINT ["/go-container"]

Once I have the four docker images, I am going to deploy them on kubernetes cluster using the same pod resource, health check, readiness and hpa configuration.

Note that hpa is configured to scale out up to 8 replicas

To deploy everything with one click you can run the command line ./deploy/deploy.sh

Please note that you need to add new entries to your hosts file

127.0.0.1 dotnet.fibonacci.local
127.0.0.1 dotnet.aot.fibonacci.local
127.0.0.1 go.fibonacci.local
127.0.0.1 python.fibonacci.local

Now we have our services deployed on Kubernetes cluster let’s compare the performance of each one, to do that you will find a jmeter script to simulate traffic and workload.

source code

The result

Perfomance

It’s obvious that Golang is the winner, it has a small docker image size, small memory footprint, and a great response time :).

Part of the result can be explained by the ability of go micro-sevice to scale out and in very quickly, you can check in the dashboard below the behaviour of the four micro service with the same hpa configuration

SlideShare