Mieliśmy 2 worki trawy, 75 tabletek meskaliny, środowisko unix, repozytorium docker i zadanie zrealizowania poleceń docker pull i docker push bez klienta docker.

UPD:
Pytanie: Po co to wszystko?
Odpowiedź: Testowanie obciążeniowe produktu (NIE za pomocą basha, skrypty podane w celach edukacyjnych). Zdecydowano się nie używać klienta docker, aby zmniejszyć dodatkowe warstwy (w rozsądnych granicach) i tym samym emulować wyższe obciążenie. W wyniku usunięcia wszystkich opóźnień systemowych klienta docker, uzyskaliśmy stosunkowo czyste obciążenie bezpośrednio na produkcie.
W artykule użyto narzędzi GNU version.
Na początku zrozumiemy, co robią te polecenia.
Więc do czego służy docker pull? Zgodnie z :
"Pobierz obraz lub repozytorium z rejestru".
Tam również znajdujemy link do .

Stąd możemy zrozumieć, że obraz docker to zestaw pewnych warstw, które zawierają informacje o ostatnich zmianach w obrazie, które są nam oczywiście potrzebne. Następnie patrzymy w .
Mówi się tu następująco:
"Obraz" to połączenie manifestu JSON i indywidualnych plików warstw. Proces pobierania obrazu koncentruje się na pobraniu tych dwóch komponentów.
Więc pierwszy krok według dokumentacji to “Pobieranie manifestu obrazu”.
Pobierać go oczywiście nie będziemy, ale potrzebne nam są z niego dane. Dalej podano przykład zapytania: GET /v2/{name}/manifests/{reference}
"Parametry name i reference identyfikują obraz i są wymagane. Referencja może zawierać tag lub skrót."
Nasze repozytorium docker jest uruchomione lokalnie, spróbujemy wykonać zapytanie:
curl -s -X GET "http://localhost:8081/link/to/docker/registry/v2/centos-11-10/manifests/1.1.1" -H "header_if_needed"
W odpowiedzi otrzymujemy json, z którego aktualnie interesują nas tylko warstwy, a dokładniej ich hashe. Otrzymawszy je, możemy przejść do każdego z nich i wykonać następne zapytanie: "GET /v2/{name}/blobs/{digest}"
„Dostęp do warstwy będzie kontrolowany przez nazwę repozytorium, ale jest identyfikowany unikalnie w rejestrze przez skrót.”
Skrót w tym przypadku to hash, który otrzymaliśmy.
Próbujemy
curl -s -X GET "http://localhost:8081/link/to/docker/registry/v2/centos-11-10/blobs/sha256:f972d139738dfcd1519fd2461815651336ee25a8b54c358834c50af094bb262f" -H "header_if_needed" --output firstLayer![]()
Zobaczymy, jaki plik otrzymaliśmy jako pierwszy layer.
file firstLayer
to znaczy, że warstwy są tar archiwami, po ich rozpakowaniu w odpowiedniej kolejności otrzymamy zawartość obrazu.
Napiszemy mały skrypt bash, aby wszystko to zautomatyzować.
#!/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 0Teraz możemy uruchomić go z pożądanymi parametrami i uzyskać zawartość wymaganego obrazu.
.\/script.sh dirName "http:\/\/localhost:8081\/link\/to\/docker\/registry" myAwesomeImage 1.0Część 2 — docker push
Tutaj będzie trochę trudniej.
Zacznijmy ponownie od . A zatem musimy załadować każdą warstwę, zebrać odpowiedni manifest i załadować go również. Wydaje się proste.
Po zapoznaniu się z dokumentacją możemy podzielić proces ładowania na kilka kroków:
- Inicjalizacja procesu — "POST \/v2\/{repoName}\/blobs\/uploads\/
- Ładowanie warstwy (będziemy używać monolitycznego ładowania, tzn. każdą warstwę wysyłamy w całości) — "PUT \/v2\/{repoName}\/blobs\/uploads\/{uuid}?digest={digest}
Content-Length: {size of layer}
Content-Type: application\/octet-stream
Dane binarne warstwy". - Ładowanie manifestu — "PUT \/v2\/{repoName}\/manifests\/{reference}".
Jednak w dokumentacji pominięto jeden krok, bez którego nic się nie uda. Dla monolitycznego ładowania, tak jak dla częściowego (chunked), przed załadunkiem warstwy należy wykonać zapytanie PATCH:
"PATCH \/v2\/{repoName}\/blobs\/uploads\/{uuid}
Content-Length: {size of chunk}
Content-Type: application\/octet-stream
{Layer Chunk Binary Data}".
W przeciwnym razie nie będziesz mógł przejść dalej niż pierwszy punkt, ponieważ zamiast oczekiwanego kodu odpowiedzi 202 otrzymasz 4xx.
Teraz algorytm wygląda następująco:
- Inicjalizacja
- PATCH warstwy
- Ładowanie warstwy
- Ładowanie manifestu
Punkty 2 i 3 będą się powtarzać tyle razy, ile warstw musimy załadować.
Na początek potrzebujemy dowolnego obrazu. Będę używać archlinux:latest
docker pull archlinux
Teraz zapiszemy go lokalnie do dalszej analizy.
docker save c24fe13d37b9 -o savedArch
Rozpakujemy otrzymany archiwum w bieżącym katalogu.
tar xvf savedArch
Jak widzimy, każda warstwa leży w oddzielnym folderze. Teraz przyjrzymy się strukturze manifestu, który otrzymaliśmy.
cat manifest.json | json_pp
Niewiele. Zobaczmy, jaki manifest jest potrzebny do załadunku, zgodnie z .

Oczywiście istniejący manifest nam nie odpowiada, więc zrobimy swój z warstwami i konfiguracjami.
Zawsze będziemy mieli przynajmniej jeden plik konfiguracyjny i tablicę warstw. Wersja schematu 2 (aktualna w momencie pisania artykułu), mediaType pozostawimy bez zmian:
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.jsonPo stworzeniu podstawowego manifestu należy go wypełnić poprawnymi danymi. W tym celu użyjemy szablonu obiektu json warstwy:
{
"mediaType": "application\/vnd.docker.image.rootfs.diff.tar.gzip",
"size": ${layersSizes[$i]},
"digest": "sha256:${layersNames[$i]}"
},którego będziemy dodawać do manifestu dla każdej warstwy.
Następnie musimy poznać rozmiar pliku konfiguracyjnego i zastąpić miejsca w manifeście rzeczywistymi danymi.
sed -i "s/config_size/$configSize/g; s/config_hash/$configName/g" $manifestFileTeraz możemy zainicjować proces przesyłania i zachować uuid, który powinien towarzyszyć wszystkim kolejnym żądaniom.
Cały skrypt wygląda mniej więcej tak:
#!/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
możemy użyć gotowego skryptu:
./uploadImage.sh "~\/path\/to\/saved\/image" "http:\/\/localhost:8081\/link\/to\/docker\/registry" myRepoName 1.0UPD:
Co otrzymaliśmy w wyniku?
Po pierwsze, rzeczywiste dane do analizy, ponieważ testy są uruchamiane w blazemeter, a dane otrzymywane z klienta docker są znacznie mniej informacyjne w porównaniu do czystych żądań HTTP.
Po drugie, przejście to pozwoliło nam zwiększyć ilość wirtualnych użytkowników dla przesyłania docker o około 150% i uzyskać przy tym średni czas odpowiedzi o 20-25% szybciej. Dla pobierania docker udało się zwiększyć liczbę użytkowników o 500%, a średni czas odpowiedzi spadł o około 60%.
Dziękujemy za uwagę.
Źródło: habr.com
