Wir hatten 2 Säcke Gras, 75 Meskalin-Tabletten in einer Unix-Umgebung, ein Docker-Repository und die Aufgabe, die Befehle Docker Pull und Docker Push ohne Docker-Client zu implementieren.
UPD:
Frage: Wozu dient das alles?
Antwort: Lasttest des Produkts (NICHT mit Bash, die Skripte werden zu Bildungszwecken bereitgestellt). Es wurde beschlossen, den Docker-Client nicht zu verwenden, um zusätzliche Schichten (innerhalb angemessener Grenzen) zu reduzieren und dementsprechend eine höhere Last zu emulieren. Dadurch wurden alle Systemverzögerungen des Docker-Clients entfernt. Wir haben eine relativ saubere Ladung direkt auf das Produkt erhalten.
Der Artikel verwendete GNU-Versionen von Tools.
Lassen Sie uns zunächst herausfinden, was diese Befehle bewirken.
Wofür wird Docker Pull verwendet? Entsprechend
„Ein Image oder ein Repository aus einer Registrierung abrufen“.
Dort finden wir auch einen Link zu
Von hier aus können wir verstehen, dass ein Docker-Image eine Reihe bestimmter Ebenen ist, die Informationen über die letzten Änderungen im Image enthalten, was offensichtlich das ist, was wir brauchen. Als nächstes schauen wir uns an
Darin heißt es:
„Ein „Bild“ ist eine Kombination aus einem JSON-Manifest und einzelnen Ebenendateien. Der Prozess des Ziehens eines > Bildes konzentriert sich auf das Abrufen dieser beiden Komponenten.“
Der erste Schritt laut Dokumentation ist also „Ein Bildmanifest abrufen".
Natürlich werden wir es nicht drehen, aber wir brauchen die Daten davon. Das Folgende ist eine Beispielanfrage: GET /v2/{name}/manifests/{reference}
„Der Name und der Referenzparameter identifizieren das Bild und sind erforderlich. Die Referenz kann ein Tag oder einen Digest enthalten.“
Unser Docker-Repository wird lokal bereitgestellt. Versuchen wir, die Anfrage auszuführen:
curl -s -X GET "http://localhost:8081/link/to/docker/registry/v2/centos-11-10/manifests/1.1.1" -H "header_if_needed"
Als Antwort erhalten wir JSON, von dem uns derzeit nur die Lebenslinien bzw. deren Hashes interessieren. Nachdem wir sie erhalten haben, können wir sie durchgehen und die folgende Anfrage ausführen: „GET /v2/{name}/blobs/{digest}“
„Der Zugriff auf eine Ebene wird durch den Namen des Repositorys gesteuert, wird aber in der Registrierung durch Digest eindeutig identifiziert.“
Digest ist in diesem Fall der Hash, den wir erhalten haben.
Lass es uns versuchen
curl -s -X GET "http://localhost:8081/link/to/docker/registry/v2/centos-11-10/blobs/sha256:f972d139738dfcd1519fd2461815651336ee25a8b54c358834c50af094bb262f" -H "header_if_needed" --output firstLayer
Mal sehen, was für eine Datei wir schließlich als erste Lebensader erhalten haben.
file firstLayer
diese. Schienen sind Tar-Archive. Wenn wir sie in der richtigen Reihenfolge entpacken, erhalten wir den Inhalt des Bildes.
Schreiben wir ein kleines Bash-Skript, damit das alles automatisiert werden kann
#!/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 0
Jetzt können wir es mit den gewünschten Parametern ausführen und den Inhalt des erforderlichen Bildes erhalten
./script.sh dirName “http://localhost:8081/link/to/docker/registry” myAwesomeImage 1.0
Teil 2 – Docker-Push
Das wird etwas komplizierter.
Beginnen wir noch einmal mit
Nach dem Studium der Dokumentation können wir den Download-Prozess in mehrere Schritte unterteilen:
- Prozessinitialisierung – „POST /v2/{repoName}/blobs/uploads/“
- Hochladen einer Lebenslinie (wir verwenden einen monolithischen Upload, d. h. wir senden jede Lebenslinie in ihrer Gesamtheit) – „PUT /v2/{repoName}/blobs/uploads/{uuid}?digest={digest}“
Inhaltslänge: {Größe der Ebene}
Inhaltstyp: Anwendung/Oktett-Stream
Layer-Binärdaten". - Laden des Manifests – „PUT /v2/{repoName}/manifests/{reference}“.
Doch in der Dokumentation fehlt ein Schritt, ohne den nichts geht. Für monolithisches Laden sowie für teilweises (chunkiertes) Laden müssen Sie vor dem Laden der Schiene eine PATCH-Anfrage durchführen:
„PATCH /v2/{repoName}/blobs/uploads/{uuid}
Inhaltslänge: {Größe des Blocks}
Inhaltstyp: Anwendung/Oktett-Stream
{Layer Chunk Binary Data}".
Andernfalls kommen Sie nicht über den ersten Punkt hinaus, denn... Anstelle des erwarteten Antwortcodes 202 erhalten Sie 4xx.
Nun sieht der Algorithmus so aus:
- Initialisierung
- Patchschiene
- Laden des Handlaufs
- Laden des Manifests
Die Punkte 2 und 3 werden jeweils so oft wiederholt, wie Zeilen geladen werden müssen.
Zuerst brauchen wir ein beliebiges Bild. Ich werde archlinux:latest verwenden
docker pull archlinux
Speichern wir es nun lokal zur weiteren Analyse
docker save c24fe13d37b9 -o savedArch
Entpacken Sie das resultierende Archiv in das aktuelle Verzeichnis
tar xvf savedArch
Wie Sie sehen, befindet sich jede Lebenslinie in einem separaten Ordner. Schauen wir uns nun die Struktur des Manifests an, das wir erhalten haben
cat manifest.json | json_pp
Nicht viel. Mal sehen, welches Manifest zum Laden benötigt wird
Offensichtlich passt das bestehende Manifest nicht zu uns, also machen wir unser eigenes mit Blackjack und Kurtisanen, Lebenslinien und Konfigurationen.
Wir werden immer mindestens eine Konfigurationsdatei und eine Reihe von Lebenslinien haben. Schemaversion 2 (aktuell zum Zeitpunkt des Schreibens), mediaType bleibt unverändert:
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.json
Nachdem Sie das Basismanifest erstellt haben, müssen Sie es mit gültigen Daten füllen. Dazu verwenden wir das JSON-Template des Rail-Objekts:
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": ${layersSizes[$i]},
"digest": "sha256:${layersNames[$i]}"
},
Wir werden es dem Manifest für jede Schiene hinzufügen.
Als nächstes müssen wir die Größe der Konfigurationsdatei ermitteln und die Stubs im Manifest durch echte Daten ersetzen
sed -i "s/config_size/$configSize/g; s/config_hash/$configName/g" $manifestFile
Jetzt können Sie den Downloadvorgang starten und sich eine UUID speichern, die allen nachfolgenden Anfragen beiliegen sollte.
Das komplette Skript sieht in etwa so aus:
#!/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
Wir können ein vorgefertigtes Skript verwenden:
./uploadImage.sh "~/path/to/saved/image" "http://localhost:8081/link/to/docker/registry" myRepoName 1.0
UPD:
Was haben wir als Ergebnis erhalten?
Erstens echte Daten zur Analyse, da die Tests in Blazemeter ausgeführt werden und die Daten zu Docker-Client-Anfragen im Gegensatz zu reinen HTTP-Anfragen nicht sehr aussagekräftig sind.
Zweitens konnten wir durch die Umstellung die Anzahl der virtuellen Benutzer für den Docker-Upload um etwa 150 % erhöhen und die durchschnittliche Antwortzeit um 20–25 % verkürzen. Beim Docker-Download ist es uns gelungen, die Anzahl der Benutzer um 500 % zu steigern, während die durchschnittliche Antwortzeit um etwa 60 % sank.
Vielen Dank für Ihre Aufmerksamkeit.
Source: habr.com