How Docker affects your Go app performance on Mac OS

Wacław The Developer
3 min readMay 2, 2022

Introduction

Unlike PHP, there is no “classical” way to deploy your Go app. In 2022 Docker / Kubernetes is a gold standard to deploy non-script-based web apps. Also, some people prefer to build and deploy Go apps on bare metal (for example by using the Supervisor). Let’s figure out, is there a big difference between the performance of the app that runs on bare metal and the same app that runs as a Docker container.

Writing the benchmark

I think, that the best way to simulate the activity of the app is to execute something that uses memory, cpu, and filesystem. I decided to write a piece of code that: reads the big file (around 1GB) of JSON with complex structure, and decode this JSON. Let the structure will be:

type Country struct {
Name string `json:"name"`
Code string `json:"code"`
States []State `json:"states"`
}

type State struct {
Name string `json:"name"`
Code string `json:"code"`
Population int `json:"population"`
Cities []City `json:"cities"`
}

type City struct {
Name string `json:"name"`
Population int `json:"population"`
Companies []Company `json:"companies"`
}

type Company struct {
Name string `json:"name"`
Description string `json:"description"`
Employees []Employee `json:"employees"`
}

type Employee struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
PhoneNumber string `json:"phone_number"`
BirthYear int `json:"birth_year"`
BirthMonth int `json:"birth_month"`
BirthDay int `json:"birth_day"`
PostalCode string `json:"postal_code"`
Street string `json:"street"`
Building int `json:"building"`
Resume string `json:"resume"`
}

And the code that will decode the file:

startTime := time.Now()
jsonData, err := os.ReadFile("data.bin")
if err != nil {
panic(err)
}

var countries []Country
err = json.Unmarshal(jsonData, &countries)
if err != nil {
panic(err)
}
fmt.Printf("Parsing complete in: %v\n", time.Since(startTime))

var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Alloc = %v MiB\n", m.Alloc/1024/1024)
fmt.Printf("TotalAlloc = %v MiB\n", m.TotalAlloc/1024/1024)
fmt.Printf("Sys = %v MiB\n", m.Sys/1024/1024)
fmt.Printf("NumGC = %v\n", m.NumGC)

Let’s run!

I will use the go 1.18.1, docker engine 20.10.14, Mac OS 12.3.1 (Intel)

First of all, i will build the main.go to test the bare-metal version. In the next step, I will dockerize the app by adding the Dockerfile and building the image.

So, I am aware of caching of file with json. To prevent this, I will run my app in the next order:

  • bare metal version
  • dockerized version
  • bare metal version
  • dockerized version
  • bare metal version
  • dockerized version
  • ….

One start of the app consumes around 2.5GB of RAM, so every start will prevent the caching in long term. Let’s run every version of the app 30 times to ensure that the results can be interpreted correctly.

The results

After the loop is completed, we can see the result of 30 runs. On Y-axis is the time in second spent to parse ~1GB JSON. The unstable results at the start were caused by parallel activity on the laptop. After the start, I left my laptop alone to prevent the background activity.

Time spend (in seconds) to parse 1GB JSON. App was run 30 times for every version.

The difference you can see on the chart. Next, I will get just the second part of the measures (15–30th iteration) and calculate the difference (in percents) between performance of the bare-metal app and dockerized version:

The difference between performance of dockerized and bare-metal app (percentage)

The text version:

The conclusion:

After the calculation of average difference in performance, I got the result: the dockerized Go app is 25.09% slower than the app run on bare metal. I think that Docker provides us a universal and convenient way to run and manage apps. More than that, most of apps spend most of time for waiting for DB and I/O operations. For these things, the Docker fits very well. Choose your way to deploy the app 😉

--

--