Wir hatten 2 Säcke Gras, 75 Tabletten Mescalin, eine Unix-Umgebung, ein Docker-Repository und die Aufgabe, die Befehle docker pull und docker push ohne den Docker-Client umzusetzen.

UPD:
Frage: Wozu das alles?
Antwort: Lasttests des Produkts (NICHT mit Bash, die Skripte dienen Bildungszwecken). Die Entscheidung, den Docker-Client nicht zu verwenden, wurde getroffen, um zusätzliche Schichten (im vertretbaren Rahmen) zu minimieren und entsprechend eine höhere Last zu simulieren. Dadurch wurden alle systembedingten Verzögerungen des Docker-Clients eliminiert. Wir erhielten eine vergleichsweise saubere Last direkt auf das Produkt.
In diesem Artikel wurden Tools von GNU verwendet.
Lassen Sie uns zunächst klären, was diese Befehle bewirken.
Wofür wird also docker pull verwendet? Laut :
"Ein Bild oder ein Repository aus einem Registry ziehen".
Dort finden wir auch den Link zu .

Von hier aus können wir verstehen, dass ein Docker-Image eine Ansammlung von Layers ist, die Informationen über die letzten Änderungen im Image enthalten, die uns offensichtlich wichtig sind. Weiter schauen wir in die .
Hier wird Folgendes gesagt:
"Ein 'Image' ist eine Kombination aus einem JSON-Manifest und einzelnen Schichtdateien. Der Prozess des Abrufens eines > Images konzentriert sich auf das Abrufen dieser beiden Komponenten."
Der erste Schritt gemäß der Dokumentation ist "Abrufen eines Image-Manifestes”.
Das werden wir natürlich nicht abziehen, aber die Daten daraus benötigen wir. Im Folgenden wird ein Beispiel für die Anfrage gegeben: GET /v2/{name}/manifests/{reference}
"Der Name und der Referenzparameter identifizieren das Image und sind erforderlich. Die Referenz kann ein Tag oder einen Digest enthalten."
Unser Docker-Repository ist lokal eingerichtet, 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 ein JSON, aus dem uns momentan nur die Schichten, genauer gesagt deren Hashes, interessieren. Nachdem wir diese erhalten haben, können wir für jede die folgende Anfrage durchführen: "GET /v2/{name}/blobs/{digest}"
„Der Zugriff auf eine Schicht wird durch den Namen des Repositories gesteuert, aber im Registry wird sie eindeutig durch den Digest identifiziert."
Der 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![]()
Schauen wir uns an, welche Datei wir letztendlich als erste Schicht erhalten haben.
file firstLayer
Das heißt, die Schichten stellen TAR-Archive dar, die wir in der entsprechenden Reihenfolge entpacken, um den Inhalt des Images zu erhalten.
Lassen Sie uns ein kleines Bash-Skript schreiben, um alles zu automatisieren.
#!/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 0Jetzt können wir es mit den gewünschten Parametern ausführen und den Inhalt des benötigten Images erhalten.
./script.sh dirName "http://localhost:8081/link/to/docker/registry" myAwesomeImage 1.0Teil 2 — docker push
Das wird etwas komplizierter.
Beginnen wir erneut mit . Wir müssen jeden Layer hochladen, das entsprechende Manifest erstellen und dieses ebenfalls hochladen. Klingt einfach.
Nachdem wir die Dokumentation durchgesehen haben, können wir den Upload-Prozess in mehrere Schritte unterteilen:
- Initialisierung des Prozesses — "POST /v2/{repoName}/blobs/uploads/"
- Hochladen des Layers (wir verwenden den monolithischen Upload, d.h. wir senden jeden Layer komplett) — "PUT /v2/{repoName}/blobs/uploads/{uuid}?digest={digest}"
Content-Length: {size of layer}
Content-Type: application/octet-stream
Layer Binärdaten. - Hochladen des Manifests — "PUT /v2/{repoName}/manifests/{reference}."
Aber in der Dokumentation fehlt ein Schritt, ohne den nichts funktionieren wird. Vor dem Hochladen des Layers muss auch bei einem monolithischen Upload wie bei einem teilweisen Upload ein PATCH-Request ausgeführt werden:
"PATCH /v2/{repoName}/blobs/uploads/{uuid}"
Content-Length: {size of chunk}
Content-Type: application/octet-stream
{Layer Chunk Binärdaten}."
Andernfalls können Sie nicht über den ersten Punkt hinauskommen, da Sie statt des erwarteten Antwortcodes 202 einen 4xx-Code erhalten.
Jetzt sieht der Algorithmus so aus:
- Initialisierung
- Layer-Patch
- Layer-Upload
- Manifest-Upload
Die Punkte 2 und 3 werden entsprechend so oft wiederholt, wie es nötig ist, um alle Layer zu laden.
Zunächst benötigen wir ein beliebiges Image. Ich werde archlinux:latest verwenden.
docker pull archlinux
Jetzt speichern wir es lokal für die weitere Analyse.
docker save c24fe13d37b9 -o savedArch
Entpacken wir das erhaltene Archiv im aktuellen Verzeichnis.
tar xvf savedArch
Wie wir sehen, befindet sich jeder Layer in einem eigenen Ordner. Lassen Sie uns die Struktur des erhaltenen Manifests ansehen.
cat manifest.json | json_pp
Nicht viel. Schauen wir, welches Manifest für den Upload benötigt wird, gemäß .

Offensichtlich ist das vorhandene Manifest für uns nicht geeignet, also erstellen wir unser eigenes mit Layern und Konfigurationen, mit Blackjack und Nutten.
Wir werden immer mindestens eine Konfigurationsdatei und ein Array von Layers haben. Die Schemaprimärversion ist 2 (zum Zeitpunkt des Schreibens des Artikels aktuell), 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.jsonNach der Erstellung des Basis-Manifests müssen wir es mit gültigen Daten füllen. Hierzu verwenden wir die JSON-Vorlage des Layers:
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": ${layersSizes[$i]},
"digest": "sha256:${layersNames[$i]}"
},dies werden wir für jedes Layer in das Manifest einfügen.
Als nächstes müssen wir die Größe der Konfigurationsdatei ermitteln und die Platzhalter im Manifest durch die echten Daten ersetzen.
sed -i "s/config_size/$configSize/g; s/config_hash/$configName/g" $manifestFileJetzt können wir den Upload-Prozess initiieren und uns die UUID merken, die alle nachfolgenden Anfragen begleiten sollte.
Das vollständige Skript sieht ungefähr 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 fertiges Skript verwenden:
./uploadImage.sh "~/path/to/saved/image" "http://localhost:8081/link/to/docker/registry" myRepoName 1.0UPD:
Was haben wir als Ergebnis erhalten?
Erstens echte Daten zur Analyse, denn die Tests werden in BlazeMeter durchgeführt und die Daten von Docker-Client-Anfragen sind im Vergleich zu reinen HTTP-Anfragen wenig aussagekräftig.
Zweitens hat der Übergang es uns ermöglicht, die Anzahl der virtuellen Benutzer für den Docker-Upload um etwa 150 % zu erhöhen und gleichzeitig die durchschnittliche Antwortzeit um 20-25 % zu verbessern. Für den Docker-Download konnten wir die Anzahl der Benutzer um 500 % steigern, während sich die durchschnittliche Antwortzeit um etwa 60 % verringerte.
Vielen Dank für Ihre Aufmerksamkeit.
Quelle: habr.com
