Realizimi i komandave docker pull dhe docker push pa klientin docker përmes kërkesave HTTP

Kishim 2 thesë bari, 75 tableta meskaline, një ambient unix, një depo docker dhe detyra për të realizuar komandat docker pull dhe docker push pa klientin docker.

Realizimi i komandave docker pull dhe docker push pa klientin docker përmes kërkesave HTTP

UPD:
Pyetje: Për çfarë është e gjithë kjo?
Përgjigje: Testimi i ngarkesës së produktit (JO me mjete bashi, skenarët janë dhënë për qëllime edukative). U vendos të mos përdoret klienti docker për të reduktuar shtresat e panevojshme (në kufij të arsyeshëm) dhe për të emuluar ngarkesë më të lartë. Si rezultat, u hoqën të gjitha vonesat sistemore të klientit docker. Festa një ngarkesë relativisht të pastër direkt në produkt.
Në artikull janë përdorur mjete GNU versioni.

Së pari, le të sqarojmë çfarë bëjnë këto komanda.

Pra, për çfarë përdoret docker pull? Sipas dokumentacion:

"Tërheq një imazh ose një depo nga një regjistër".

Atje gjejmë gjithashtu lidhjen në kuptoni imazhet, kontejnerët dhe drejtorët e ruajtjes.

Realizimi i komandave docker pull dhe docker push pa klientin docker përmes kërkesave HTTP

Nga këtu mund të kuptojmë që imazhi docker është një grup layer-ash, që përmbajnë informacion mbi ndryshimet e fundit në imazh, të cilat janë të nevojshme. Pastaj shohim në API-në e regjistrit.

Këtu thuhet si vijon:

"Një "imazh" është një kombinim i një manifesti JSON dhe skedave individuale të shtresave. Procesi i marrëjes së një imazhi përqendrohet në rikuperimin e këtyre dy komponenteve."

Pra, hapi i parĂ« sipas dokumentacionit Ă«shtĂ« "Marrja e njĂ« Manifesti Imazhi”.

Sigurisht që nuk do ta marrim, por të dhënat nga ai janë të nevojshme për ne. Më poshtë është një shembull i kërkesës: GET /v2/{name}/manifests/{reference}

"Parametrat emri dhe referenca identifikojnë imazhin dhe janë të nevojshme. Referenca mund të përfshijë një etiketë ose digjest."

Repoja jonë Docker është e instaluar lokalisht, le të tentojmë të bëjmë kërkesën:

curl -s -X GET "http://localhost:8081/link/to/docker/registry/v2/centos-11-10/manifests/1.1.1" -H "header_if_needed"

Realizimi i komandave docker pull dhe docker push pa klientin docker përmes kërkesave HTTP

Në përgjigje, marrim një JSON nga i cili për momentin na interesojnë vetëm shtresat, më saktësisht hash-et e tyre. Pas marrëjes së tyre, mund të kalojmë për secilin dhe të kryejmë kërkesën e mëposhtme: "GET /v2/{name}/blobs/{digest}"

“Qasja nĂ« njĂ« shtresĂ« do tĂ« jetĂ« e kufizuar nga emri i depozitĂ«s, por identifikohet unikisht nĂ« regjistrin me digjest.”

Digesti në këtë rast është hash, që ne kemi marrë.

Le ta provojmë

curl -s -X GET "http://localhost:8081/link/to/docker/registry/v2/centos-11-10/blobs/sha256:f972d139738dfcd1519fd2461815651336ee25a8b54c358834c50af094bb262f" -H "header_if_needed" --output firstLayer

Realizimi i komandave docker pull dhe docker push pa klientin docker përmes kërkesave HTTP

Le të shohim se çfarë skedari kemi marrë si shtresën e parë.

file firstLayer

Realizimi i komandave docker pull dhe docker push pa klientin docker përmes kërkesave HTTP

Pra, shtresat përbëjnë arkiva tar, duke i shpërbërë ato në rendin e duhur do të marrim përmbajtjen e imazhit.

Të shkruajmë një skript të vogël në bash për ta automatizuar këtë proces.

#!/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

Tani mund ta startojmë me parametrat e dëshiruar dhe të marrim përmbajtjen e imazhit të nevojshëm.

./script.sh dirName “http://localhost:8081/link/to/docker/registry” myAwesomeImage 1.0

Pjesa 2 — docker push

Këtu do të jetë pak më e komplikuar.

Le të fillojmë sërish me dokumentacion. Pra, na duhet të ngarkojmë çdo shtresë, të krijojmë manifestin përkatës dhe ta ngarkojmë atë gjithashtu. Duket e thjeshtë.

Duke studiuar dokumentacionin, mund të ndajmë procesin e ngarkimit në disa hapa:

  • Inicimi i procesit — "POST /v2/{repoName}/blobs/uploads/"
  • Ngarkimi i shtresĂ«s (ne do tĂ« pĂ«rdorim ngarkimin monolitik, domethĂ«nĂ« çdo shtresĂ« e dĂ«rgohet nĂ« tĂ«rĂ«si) — "PUT /v2/{repoName}/blobs/uploads/{uuid}?digest={digest}
    Content-Length: {size of layer}
    Content-Type: application/octet-stream
    Të dhënat binare të shtresës".
  • Ngarkimi i manifestit — "PUT /v2/{repoName}/manifests/{reference}".

Por në dokumentacion mungon një hap, pa të cilin asgjë nuk do të funksionojë. Për ngarkimin monolitik, siç është për ngarkimin e pjesshëm (chunked), para se të ngarkojmë një shtresë është e nevojshme të kryejmë një kërkesë PATCH:

"PATCH /v2/{repoName}/blobs/uploads/{uuid}
Content-Length: {size of chunk}
Content-Type: application/octet-stream
{Layer Chunk Binary Data}".

Në të kundërt, nuk do të jeni në gjendje të kaloni më tej pikën e parë, sepse në vend të kodit të pritur të përgjigjes 202, do të merrni 4xx.

Tani algoritmi duket kështu:

  • Inicializimi
  • Patch i layers
  • Duke ngarkuar layerin
  • Duke ngarkuar manifen
    Pikat 2 dhe 3 do të përsëriten për aq herë sa nevojiten layers për t'u ngarkuar.

Për fillim, do të na nevojitet ndonjë imazh. Unë do të përdor archlinux:latest

docker pull archlinux

Realizimi i komandave docker pull dhe docker push pa klientin docker përmes kërkesave HTTP

Tani ta ruajmë atë lokal për analizë të mëtejshme

docker save c24fe13d37b9 -o savedArch

Realizimi i komandave docker pull dhe docker push pa klientin docker përmes kërkesave HTTP

Të shpërndajmë arkivin e marrë në direktorinë aktuale

tar xvf savedArch

Realizimi i komandave docker pull dhe docker push pa klientin docker përmes kërkesave HTTP

Siç e shohim, çdo layer është në një dosje të veçantë. Tani le të shikojmë strukturën e maniftes, që kemi marrë

cat manifest.json | json_pp

Realizimi i komandave docker pull dhe docker push pa klientin docker përmes kërkesave HTTP

Nuk është shumë. Le të shohim se cili manifer është i nevojshëm për ngarkim, sipas dokumentacion.

Realizimi i komandave docker pull dhe docker push pa klientin docker përmes kërkesave HTTP

Evidentisht, manifesti ekzistues nuk na përshtatet, kështu që ne do të bëjmë tonin tonë me blackjack dhe layers dhe konfigurimet.

Ne gjithmonë do të kemi të paktën një skedë konfigurimi dhe një array të layers. Versioni i skemës 2 (aktualisht në momentin e shkruarjes së artikullit), mediaType do ta lëmë pa ndryshime:

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

Pas krijimit të manifestit bazë, është e nevojshme ta mbushim atë me të dhëna valide. Për këtë, përdorim template-in e objektit JSON për layer-in:

{
         "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
         "size": ${layersSizes[$i]},
         "digest": "sha256:${layersNames[$i]}"
      },

Ky objekt do të shtohet në manifest për çdo layer.

Më pas, na nevojitet të dimë madhësinë e skedarit të konfigurimit dhe të zëvendësojmë vendet rezervë në manifest me të dhënat reale.

sed -i "s/config_size/$configSize/g; s/config_hash/$configName/g" $manifestFile

Tani mund të initijojmë procesin e ngarkimit dhe të ruajmë UUID-në, e cila duhet të shoqërojë të gjitha kërkesat e mëvonshme.

Skripti i plotë duket më shumë kështu:

#!/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

mund të përdorim një skript të gatshëm:

./uploadImage.sh "~/path/to/saved/image" "http://localhost:8081/link/to/docker/registry" myRepoName 1.0

UPD:
ÇfarĂ« morĂ«m si rezultat?
Së pari, të dhëna reale për analizë, pasi testet zhvillohen në blazemeter dhe të dhënat nga kërkesat e klientit Docker janë shumë pak informuese në krahasim me kërkesat e pastra HTTP.

Së dyti, kalimi na ndihmoi të rrisim numrin e përdoruesve virtualë për docker upload rreth 150% dhe për këtë morëm një avg response time rreth 20-25% më të shpejtë. Për docker download arritëm të rrisim numrin e përdoruesve me 500%, ndërsa avg response time ra rreth 60%.

Faleminderit për vëmendjen.

Burimi: habr.com

Bli njĂ« hosting tĂ« besueshĂ«m pĂ«r faqet me mbrojtje DDoS, VPS VDS serverĂ« đŸ”„ Bli njĂ« hosting tĂ« besueshĂ«m pĂ«r faqet me mbrojtje DDoS, VPS VDS serverĂ« | ProHoster