I am using Docker to isolate a specific process. This process is run repeatedly a number of times on a multi-core virtual machine.
Each execution time is measured by its wall clock time and recorded. I'm looking to get time differences smaller than 200 ms. Unfortunately, I get about 1 second difference between the best and worst executions in Docker. I don't understand why. I want to bring it down to < 200 ms.
Here is a chart to illustrate my problem:
Here the blue columns represent the native time executions in ms, which are pretty consistent, while the orange columns show the execution times when the same code is run as a Docker process.
My goals is to get consistent execution times in Docker.
Here is my minimal reproducible example:
mem.cpp
This program performs memory expensive operations to take time.
#include <bits/stdc++.h>
#include <vector>
using namespace std;
string CustomString(int len)
{
string result = "";
for (int i = 0; i<len; i++)
result = result + 'm';
return result;
}
int main()
{
int len = 320;
std::vector< string > arr;
for (int i = 0; i < 100000; i++) {
string s = CustomString(len);
arr.push_back(s);
}
cout<<arr[10] <<"\n";
return 0;
}
script.sh
This script is the starting point for the Docker containers and it compiles and runs the above C++ program and records its wall time.
#!/bin/bash
# compile the file
g++ -O2 -std=c++17 -Wall -o _sol mem.cpp
# execute file and record execution time (wall clock)
ts=$(date +%s%N)
./_sol
echo $((($(date +%s%N) - $ts)/1000000)) ms
python program. It uses ProcessPoolExecutor
for parallelism. It copies the files into the Docker containers and executes script.sh
.
import docker
import logging
import os
import tarfile
import tempfile
from concurrent.futures import ProcessPoolExecutor
log_format = '%(asctime)s %(threadName)s %(levelname)s: %(message)s'
dkr = docker.from_env()
def task():
ctr = dkr.containers.create("gcc:12-bullseye", command="/home/script.sh", working_dir="/home")
# copy files into container
cp_to_container(ctr, "./mem.cpp", "/home/mem.cpp")
cp_to_container(ctr, "./script.sh", "/home/script.sh")
# run container and capture logs
ctr.start()
ec = ctr.wait()
logs = ctr.logs().decode()
ctr.stop()
ctr.remove()
# handle error
if (code := ec['StatusCode']) != 0:
logging.error(f"Error occurred during execution with exit code {code}")
logging.info(logs)
def file_to_tar(src: str, fname: str):
f = tempfile.NamedTemporaryFile()
abs_src = os.path.abspath(src)
with tarfile.open(fileobj=f, mode='w') as tar:
tar.add(abs_src, arcname=fname, recursive=False)
f.seek(0)
return f
def cp_to_container(ctr, src: str, dst: str):
(dir, fname) = os.path.split(os.path.abspath(dst))
with file_to_tar(src, fname) as tar:
ctr.put_archive(dir, tar)
if __name__ == "__main__":
# set logging level
logging.basicConfig(level=logging.INFO, format=log_format)
# start ProcessPoolExecutor
ppex = ProcessPoolExecutor(max_workers=max(os.cpu_count()-1,1))
for _ in range(21):
ppex.submit(task)
I have tried to use much fewer of the available CPU cores (4 or less out of 8) to make sure that the OS can utilize 4 or more for its own purposes, but that doesn't help. This makes me think the reason lies within Docker Engine most likely.
EDIT:
I tried using the newly released gcc:13-bookworm
image and it performs better than native and much better than gcc:12-bullseye
. Also, the times are a lot more consistent. This makes me think it has to do something with the image?