Choose your programming language
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 codeThe 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