Avevamo 2 sacchi di erba, 75 compresse di mescalina in ambiente unix, un repository docker e l'obiettivo di implementare i comandi docker pull e docker push senza il client docker.

UPD:
Domanda: A cosa serve tutto ciò?
Risposta: Test di carico del prodotto (NON tramite bash, gli script sono forniti a scopo educativo). È stata presa la decisione di non utilizzare il client docker per ridurre le ulteriori layer (in limiti ragionevoli) e quindi emulare un carico maggiore. Di conseguenza, abbiamo rimosso tutti i ritardi sistemici del client docker. Abbiamo ottenuto un carico relativamente pulito direttamente sul prodotto.
Nell'articolo sono stati utilizzati strumenti GNU.
Iniziamo a capire cosa fanno questi comandi.
Dunque, a cosa serve docker pull? Secondo :
"Scarica un'immagine o un repository da un registry".
Là troviamo il link su .

Da qui possiamo capire che un'immagine docker è un insieme di alcuni layer che contengono informazioni sulle ultime modifiche nell'immagine, che evidentemente ci servono. Proseguiamo con .
Qui si dice quanto segue:
"Un'immagine è una combinazione di un manifesto JSON e file di layer individuali. Il processo di scaricamento di un'immagine si concentra sul recupero di questi due componenti."
Quindi il primo passo secondo la documentazione è "Scaricare un manifesto dell'immagine”.
Non lo scaricheremo ovviamente, ma ci servono i dati. Qui viene fornito un esempio di richiesta: GET /v2/{name}/manifests/{reference}
"Il parametro name e reference identificano l'immagine e sono richiesti. Il riferimento può includere un tag o un digest."
Il nostro repository docker è distribuito localmente, proviamo a eseguire la richiesta:
curl -s -X GET "http://localhost:8081/link/to/docker/registry/v2/centos-11-10/manifests/1.1.1" -H "header_if_needed"
In risposta otteniamo un json da cui ci interessano al momento solo i layer, precisamente i loro hash. Ottenendoli, possiamo procedere con la seguente richiesta: "GET /v2/{name}/blobs/{digest}"
“L'accesso a un layer sarà limitato dal nome del repository ma è identificato univocamente nel registry dal digest.”
Il digest in questo caso è l'hash che abbiamo ottenuto.
Proviamo
curl -s -X GET "http://localhost:8081/link/to/docker/registry/v2/centos-11-10/blobs/sha256:f972d139738dfcd1519fd2461815651336ee25a8b54c358834c50af094bb262f" -H "header_if_needed" --output firstLayer![]()
vediamo che file abbiamo ottenuto come primo layer.
file firstLayer
ossia, i layer sono archivi tar, scompattandoli nell'ordine corretto otteniamo il contenuto dell'immagine.
Scriviamo un piccolo script bash per automatizzare tutto ciò.
#!/bin/bash -eu
downloadDir=$1
# url as http://localhost:8081/link/to/docker/registry
url=$2
imageName=$3
tag=$4
# array of layers
layers=($(curl -s -X GET "$url/v2/$imageName/manifests/$tag" | grep -oP '(?<=blobSum" : ").+(?=")'))
# download each layer from array
for layer in "${layers[@]}"; do
echo "Downloading ${layer}"
curl -v -X GET "$url/v2/$imageName/blobs/$layer" --output "$downloadDir/$layer.tar"
done
# find all layers, untar them and remove source .tar files
cd "$downloadDir" && find . -name "sha256:*" -exec tar xvf {} ;
rm sha256:*.tar
exit 0Ora possiamo avviarlo con i parametri desiderati e ottenere il contenuto dell'immagine necessaria
.\/script.sh dirName “http:\/\/localhost:8081\/link\/to\/docker\/registry” myAwesomeImage 1.0Parte 2 — docker push
Qui sarà un po' più complicato.
Iniziamo di nuovo con . Quindi dobbiamo caricare ogni layer, assemblare il manifesto corrispondente e caricarlo anche. Sembra semplice.
Dopo aver studiato la documentazione possiamo suddividere il processo di caricamento in diversi passaggi:
- Inizializzazione del processo — "POST \/v2\/{repoName}\/blobs\/uploads\/"
- Caricamento del layer (useremo il caricamento monolitico, ovvero ogni layer viene inviato per intero) — "PUT \/v2\/{repoName}\/blobs\/uploads\/{uuid}?digest=\{digest}
Content-Length: {size of layer}
Content-Type: application\/octet-stream
Layer Binary Data". - Caricamento del manifesto — "PUT \/v2\/{repoName}\/manifests\/{reference}".
Ma nella documentazione è stato omesso un passaggio, senza il quale non si può andare avanti. Per il caricamento monolitico, così come per quello parziale (chunked), prima di caricare il layer è necessario eseguire una richiesta PATCH:
"PATCH \/v2\/{repoName}\/blobs\/uploads\/{uuid}
Content-Length: {size of chunk}
Content-Type: application\/octet-stream
{Layer Chunk Binary Data}".
Altrimenti non riuscirai a procedere oltre il primo punto, poiché invece del codice di risposta 202 previsto riceverai 4xx.
Ora l'algoritmo appare come:
- Inizializzazione
- Patch del layer
- Caricamento del layer
- Caricamento del manifesto
I punti 2 e 3 si ripeteranno rispettivamente tante volte quanti sono i layer da caricare.
Per iniziare, abbiamo bisogno di qualsiasi immagine. Utilizzerò archlinux:latest
docker pull archlinux
Ora salviamolo localmente per ulteriori analisi
docker save c24fe13d37b9 -o savedArch
Estrarremo l'archivio ottenuto nella directory corrente
tar xvf savedArch
Come vediamo, ogni layer si trova in una cartella separata. Ora diamo un'occhiata alla struttura del manifesto che abbiamo ottenuto
cat manifest.json | json_pp
Non c'è molto. Vediamo quale manifesto è necessario caricare, secondo .

È ovvio che il manifesto esistente non è adatto, quindi creeremo il nostro con layer e configurazioni di blackjack e prostitute.
Avremo sempre almeno un file di config e un array di layer. La versione dello schema è 2 (attuale al momento della scrittura dell'articolo), lasciamo il mediaType invariato:
echo ‘{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": config_size,
"digest": "config_hash"
},
"layers": [
’ > manifest.jsonDopo aver creato il manifesto di base, è necessario riempirlo con dati validi. Per fare ciò, utilizziamo il modello JSON dell'oggetto layer:
{
"mediaType": "application\/vnd.docker.image.rootfs.diff.tar.gzip",
"size": ${layersSizes[$i]},
"digest": "sha256:${layersNames[$i]}"
},lo aggiungeremo nel manifesto per ogni layer.
Dobbiamo innanzitutto scoprire la dimensione del file di configurazione e sostituire i segnaposto nel manifesto con dati reali.
sed -i "s\/config_size\/$configSize\/g; s\/config_hash\/$configName\/g" $manifestFileOra possiamo avviare il processo di caricamento e salvare il uuid che dovrà accompagnare tutte le richieste successive.
Lo script completo appare più o meno così:
#!/bin/bash -eux
imageDir=$1
# url as http://localhost:8081/link/to/docker/registry
url=$2
repoName=$3
tag=$4
manifestFile=$(readlink -f ${imageDir}/manifestCopy)
configFile=$(readlink -f $(find $imageDir -name "*.json" ! -name "manifest.json"))
# calc layers sha 256 sum, rename them accordingly, and add info about each to manifest file
function prepareLayersForUpload() {
info_file=$imageDir/info
# lets calculate layers sha256 and use it as layers names further
layersNames=($(find $imageDir -name "layer.tar" -exec shasum -a 256 {} ; | cut -d" " -f1))
# rename layers according to shasums. !!!Set required amount of fields for cut command!!!
# this part definitely can be done easier but i didn't found another way, sry
find $imageDir -name "layer.tar" -exec bash -c 'mv {} "$(echo {} | cut -d"/" -f1,2)/$(shasum -a 256 {} | cut -d" " -f1)"' ;
layersSizes=($(find $imageDir -name "*.tar" -exec ls -l {} ; | awk '{print $5}'))
for i in "${!layersNames[@]}"; do
echo "{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": ${layersSizes[$i]},
"digest": "sha256:${layersNames[$i]}"
}," >> $manifestFile
done
# remove last ','
truncate -s-2 $manifestFile
# add closing brakets to keep json consistent
printf "nt]n}" >> $manifestFile
}
# calc config sha 256 sum and add info about it to manifest
function setConfigProps() {
configSize=$(ls -l $configFile | awk '{print $5}')
configName=$(basename $configFile | cut -d"." -f1)
sed -i "s/config_size/$configSize/g; s/config_hash/$configName/g" $manifestFile
}
#prepare manifest file
prepareLayersForUpload
setConfigProps
cat $manifestFile
# initiate upload and get uuid
uuid=$(curl -s -X POST -I "$url/v2/$repoName/blobs/uploads/" | grep -oP "(?<=Docker-Upload-Uuid: ).+")
# patch layers
# in data-binary we're getting absolute path to layer file
for l in "${!layersNames[@]}"; do
pathToLayer=$(find $imageDir -name ${layersNames[$l]} -exec readlink -f {} ;)
curl -v -X PATCH "$url/v2/$repoName/blobs/uploads/$uuid"
-H "Content-Length: ${layersSizes[$i]}"
-H "Content-Type: application/octet-stream"
--data-binary "@$pathToLayer"
# put layer
curl -v -X PUT "$url/v2/$repoName/blobs/uploads/$uuid?digest=sha256:${layersNames[$i]}"
-H 'Content-Type: application/octet-stream'
-H "Content-Length: ${layersSizes[$i]}"
--data-binary "@$pathToLayer"
done
# patch and put config after all layers
curl -v -X PATCH "$url/v2/$repoName/blobs/uploads/$uuid"
-H "Content-Length: $configSize"
-H "Content-Type: application/octet-stream"
--data-binary "@$configFile"
curl -v -X PUT "$url/v2/$repoName/blobs/uploads/$uuid?digest=sha256:$configName"
-H 'Content-Type: application/octet-stream'
-H "Content-Length: $configSize"
--data-binary "@$configFile"
# put manifest
curl -v -X PUT "$url/v2/$repoName/manifests/$tag"
-H 'Content-Type: application/vnd.docker.distribution.manifest.v2+json'
--data-binary "@$manifestFile"
exit 0
Possiamo utilizzare uno script già pronto:
.\/uploadImage.sh "~\/path\/to\/saved\/image" "http:\/\/localhost:8081\/link\/to\/docker\/registry" myRepoName 1.0UPD:
Cosa abbiamo ottenuto in risultato?
In primo luogo, dati reali per l'analisi, poiché i test vengono eseguiti in blazemeter e i dati delle richieste del client Docker sono piuttosto poco informativi rispetto alle pure richieste HTTP.
In secondo luogo, il passaggio ci ha permesso di aumentare il numero di utenti virtuali per il caricamento di Docker di circa il 150% ottenendo nel contempo un tempo medio di risposta più veloce del 20-25%. Per il download di Docker, siamo riusciti ad aumentare il numero di utenti del 500%, mentre il tempo medio di risposta è diminuito di circa il 60%.
Grazie per l'attenzione.
Fonte: habr.com
