Controle e monitore seus containers Docker com Python

Apr 14 2023
Use Python para controlar seus contêineres docker! Aqui está um passo a passo de como começar. É baseado em um teste de três pacotes populares para lidar com dados tabulares.

Use Python para controlar seus contêineres docker! Aqui está um passo a passo de como começar. É baseado em um teste de três pacotes populares para lidar com dados tabulares.

Repositório Git:https://github.com/martinkarlssonio/polars-pandas-spark

Se você quiser ler sobre o aspecto dos dados deste teste, há uma história separada para isso!
/@martinkarlsson.io/pandas-spark-and-polars-when-to-use-which-f4e85d909c6f

Percorra o código

A imagem abaixo ilustra a ideia geral com o código. A imagem de compilação do código main.py inicia, controla e monitora 12 contêineres do Docker.

Controle suas imagens e contêineres

A estrela desta solução é o pacote Docker Python. Em um requirements.txt (instalado com pip) especificamos o pacote docker.

docker==6.0.1

import docker
dockerClient = docker.DockerClient()

Obtenha uma lista de todos os contêineres. Aqui, o código também filtra os contêineres criados com um determinado nome de imagem.

def getContainers():
    containersReturn = []
    containers = dockerClient.containers.list(all=True)
    for container in containers:
        if imageName in str(container.image):
            containersReturn.append(container)
    return containersReturn

Exclua uma lista de contêineres (sempre uma boa ideia antes de iniciar uma nova execução do teste).

def removeContainers():
    containers = getContainers()
    for container in containers:
        if imageName in str(container.image):
            print("################################################### Deleting old container {}".format(container.name))
            try:
                container.stop()
                container.remove()
            except Exception as e:
                print("################################################### Error deleting old container {}".format(container.name))
                print(e)

Inicie um novo contêiner a partir de uma imagem existente — ou crie a imagem se ela ainda não existir. Também passamos um conjunto de variáveis ​​de ambiente ao iniciar o contêiner.

def runContainer(testType,dataframeN):
    images = dockerClient.images.list(all=True)
    if imageName in ' '.join(map(str, images)):
        print("################################################### Image exist, starting container..")
        dockerClient.containers.run(imageName+":latest", environment = {"TEST_TYPE":testType,"DATAFRAME_N":dataframeN,"CALC_N":calcN})
    else:
        print("################################################### Image doesn't exist, need to create it!")
        dockerClient.images.build(path = "./", tag = imageName)
        dockerClient.containers.run(imageName+":latest", environment = {"TEST_TYPE":testType,"DATAFRAME_N":dataframeN,"CALC_N":calcN})

#UBUNTU AS BASE
FROM ubuntu:focal

##UPDATE
RUN apt-get update -y
RUN apt-get install nano
RUN apt-get update -y

##PYTHON
RUN apt-get install -y python3-pip python3-dev
RUN pip3 install --upgrade pip
COPY requirements-container.txt /requirements.txt
RUN pip3 install -r requirements.txt

##JAVA
RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata
RUN apt install -y openjdk-8-jdk

##COPY IN FILES
COPY test.py /test.py
CMD python3 /test.py

Aqui está uma função para monitorar o tempo de CPU, memória e execução usando o comando 'stats' do Dockers.

O cálculo do uso da CPU pode precisar de alguma explicação extra. Ele compara o uso da CPU da última leitura com o atual e o compara com o uso da CPU do sistema multiplicado pela quantidade de núcleos.

Aqui está a lógica escrita em palavras:

cpuDelta = Total CPU Usage (now) - Total Usage (last read)
systemDelta = System CPU Usage (now) - System CPU Usage (last read)
cpuPercent = (cpuDelta/systemDelta) * CPU Cores * 100

def dockerLog(event,testType,dataframeN):
    cpuLog = []
    memLog = []
    print("################################################### dockerLog started")
    startFlag = True
    while True:
        try:
            containers = getContainers()
            for container in containers:
                status = container.stats(decode=None, stream = False)
                try:
                    # Calculate the change for the cpu usage of the container in between readings
                    # Taking in to account the amount of cores the CPU has
                    cpuDelta = status["cpu_stats"]["cpu_usage"]["total_usage"] - status["precpu_stats"]["cpu_usage"]["total_usage"]
                    systemDelta = status["cpu_stats"]["system_cpu_usage"] - status["precpu_stats"]["system_cpu_usage"]
                    #print("systemDelta: "+str(systemDelta)+" cpuDelta: "+str(cpuDelta))
                    cpuPercent = (cpuDelta / systemDelta) * (status["cpu_stats"]["online_cpus"]) * 100
                    cpuPercent = int(cpuPercent)
                    #print("cpuPercent: "+str(cpuPercent)+"%")
                    #Fetch the memory consumption for the container
                    mem = status["memory_stats"]["usage"]
                    mem = int(mem/1000000)
                    if startFlag == True and cpuPercent == 0: #Not logging CPU increase during startup of container, before test code executes.
                        startFlag = False
                        startEpoch = int(time.time()*1000)
                        print("Startflag set to False - let's go!")
                    if startFlag == False:
                        cpuLog.append(cpuPercent)
                        memLog.append(mem)
                except Exception as e:
                    print("Error: "+str(e))
                    status = container.stats(decode=None, stream = False)
                    break
        except Exception as e:
            print("Error: "+str(e))
            pass
        if event.is_set():
            break
    # Write the logging to a file
    endEpoch = int(time.time()*1000)
    with open("output/"+str(dataframeN)+"_"+testType+"_"+containerStatsName, "w") as f:
        json.dump({"mem":memLog,"cpu":cpuLog,"timeSpent":float((endEpoch-startEpoch)/1000)}, f)
    print("################################################### dockerLog ended")

Gostaríamos de executar a função dockerLog em uma thread separada, então iniciamos a função usando o pacote 'threading'. Isso significa que não bloquearemos a execução do código e poderemos iniciar o contêiner em paralelo.

from threading import Thread
from threading import Event
event = Event()
dockerLogThread = Thread(target=dockerLog, args=(event,testType,dataframeN,))
dockerLogThread.start()

runContainer(testType,dataframeN)
event.set()
dockerLogThread.join()

Vamos visualizar os logs do docker com o pacote 'matplotlib'!

##Visualize Mem & CPU
def visMemCpu(testType,timeSpent,dataframeN,visAll=False):
    if visAll==True:
        from os import listdir
        from os.path import isfile, join
        outputFiles = [f for f in listdir("output/") if isfile(join("output/", f))]
        combDict = {}
        for file in outputFiles:
            if "containerStats.json" in file:
                with open("output/"+file, "r") as fp:
                    data = json.load(fp)
                fileSplit = file.split("_")
                rows = fileSplit[0]
                lib = fileSplit[1]
                try:
                    combDict[rows][lib] = {}
                    combDict[rows][lib]["mem"] = data["mem"]
                    combDict[rows][lib]["cpu"] = data["cpu"]
                    combDict[rows][lib]["timeSpent"] = int(data["timeSpent"])
                except:
                    combDict[rows] = {}
                    combDict[rows][lib] = {}
                    combDict[rows][lib]["mem"] = data["mem"]
                    combDict[rows][lib]["cpu"] = data["cpu"]
                    combDict[rows][lib]["timeSpent"] = int(data["timeSpent"])
        with open("output/all.json", "w") as f:
            json.dump(combDict, f)

        for rows in combDict.keys():
            textString = " | "
            plt.clf()
            for lib in combDict[rows].keys():
                plt.plot(combDict[rows][lib]["mem"], label=lib)
                textString += lib + " " + str(combDict[rows][lib]["timeSpent"]) + "s | "
            plt.xticks([], [])
            plt.ylabel("Memory MB")
            plt.title(textString)
            plt.xlabel("Dataframe rows : " + rows)
            plt.legend(framealpha=1, frameon=True)
            plt.savefig("output/"+"{}_mem.png".format(str(rows)), dpi=300)
            plt.clf()
            for lib in combDict[rows].keys():
                plt.plot(combDict[rows][lib]["cpu"], label=lib)
            plt.xticks([], [])
            plt.ylabel("CPU %")
            plt.xlabel("Dataframe rows : " + rows)
            plt.title(textString)
            plt.legend(framealpha=1, frameon=True)
            plt.savefig("output/"+"{}_cpu.png".format(str(rows)), dpi=300)
        plt.yticks([], [])

25.000 linhas

250.000 linhas

2.500.000 linhas

Conclusão

É isso! Agora você pode usar o Python para controlar suas imagens e contêineres do Docker — junto com um registrador de métricas.

Codificação feliz!