Рэалізацыя каманд docker pull і docker push без docker кліента з дапамогай HTTP запытаў

У нас было 2 мяшка травы, 75 таблетак мескаліну unix environment, docker рэпазітар і задача рэалізаваць каманды docker pull і docker push без докер кліента.

Рэалізацыя каманд docker pull і docker push без docker кліента з дапамогай HTTP запытаў

UPD:
Пытанне: Для чаго ўсё гэта?
Адказ: Нагрузачнае тэставанне прадукта (НЕ сродкамі баша, скрыпты прыведзены ў адукацыйных мэтах). Не выкарыстоўваць докер кліент было вырашана для памяншэння дадатковых праслоек (у разумных межах) і адпаведна эмулявання больш высокай нагрузкі. У выніку прыбралі ўсе сістэмныя затрымкі докер кліента. Атрымалі параўнальна чыстую нагрузку непасрэдна на прадукт.
У артыкуле выкарыстоўваліся тулы GNU версій.

Для пачатку разбяромся што робяць гэтыя каманды.

Такім чынам навошта выкарыстоўваецца docker pull? Згодна дакументацыі:

"Pull an image або repository з registry".

Тамсама знаходзім спасылку на understand images, containers, and storage drivers.

Рэалізацыя каманд docker pull і docker push без docker кліента з дапамогай HTTP запытаў

Адгэтуль мы можам зразумець што docker image гэта набор нейкіх layers, якія ўтрымоўваюць у сабе інфармацыю аб апошніх зменах у имедже, якія відавочна нам і патрэбныя. Далей глядзім у registry API.

Тут гаворыцца наступнае:

"An "image" з'яўляецца combination of JSON manifest і асабовыя літары. Працэс pulling an > image centers are retrieving these two components."

Такім чынам першы крок згодна з дакументацыяй гэта "Pulling an Image Manifest».

Пуліць мы яго, вядома, не будзем, але дадзеныя з яго нам патрэбныя. Далей прыводзіцца прыклад запыту: GET /v2/{name}/manifests/{reference}

"Ваша назва і аналітык вызначае малюнак і патрабуецца. Рэферэнцыя можа быць складзена з tag або digest."

Наш докер рэпазітар разгорнуць лакальна, паспрабуем выканаць запыт:

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

Рэалізацыя каманд docker pull і docker push без docker кліента з дапамогай HTTP запытаў

У адказ атрымліваем json з якога нам цікавыя на дадзены момант толькі лееры, дакладней іх хэшы. Атрымаўшы іх, можам па кожным прайсціся і выканаць наступны запыт: "GET /v2/{name}/blobs/{digest}"

“Access to layer will be gated by the name of the repository but is identified uniquely in the registry by digest.”

digest у дадзеным выпадку і есць хэш, які мы атрымалі.

Спрабуем

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

Рэалізацыя каманд docker pull і docker push без docker кліента з дапамогай HTTP запытаў

паглядзім, што за файл мы ў выніку атрымалі ў якасці першага леера.

file firstLayer

Рэалізацыя каманд docker pull і docker push без docker кліента з дапамогай HTTP запытаў

г.зн. лееры ўяўляюць з сябе tar архівы, распакаваўшы якія ў які адпавядае парадку мы атрымаем змесціва меджу.

Напішам невялікі баш скрыпт каб усё гэта можна было аўтаматызаваць

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

Цяпер можам запусціць яго з жаданымі параметрамі і атрымаць змесціва неабходнага меджу

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

Частка 2 - docker push

Тут будзе крыху больш складана.

Пачнем зноў з дакументацыі. Такім чынам нам трэба загрузіць кожны леер, сабраць які адпавядае маніфест і загрузіць яго таксама. Накшталт гучыць проста.

Вывучыўшы дакументацыю можам падзяліць працэс загрузкі на некалькі крокаў:

  • Ініцыялізацыя працэсу - "POST /v2/{repoName}/blobs/uploads/"
  • Загрузка леера (мы будзем выкарыстоўваць маналітную загрузку, г.зн. кожны леер адпраўляем цалкам) - "PUT /v2/{repoName}/blobs/uploads/{uuid}?digest={digest}
    Content-Length: {size of layer}
    Content-Type: application/octet-stream
    Layer Binary Data".
  • Загрузка маніфесту - "PUT /v2/{repoName}/manifests/{reference}".

Але ў дакументацыі ўпушчаны адзін крок, без якога нічога не атрымаецца. Для маналітнай загрузкі гэтак жа як і для частковай (chunked) перад тым як грузіць леер неабходна выканаць PATCH запыт:

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

У адваротным выпадку вы не зможаце прасунуцца далей за першы пункт, т.я. замест чаканага кода адказу 202 будзеце атрымліваць 4хх.

Цяпер алгарытм выглядае як:

  • ініцыялізацыя
  • Патч леера
  • Загрузка леера
  • Загрузка маніфеста
    Пункты 2 і 3 адпаведна будуць паўтарацца столькі разоў, колькі леераў неабходна загрузіць.

Для пачатку нам спатрэбіцца любы медж. Я буду выкарыстоўваць archlinux:latest

docker pull archlinux

Рэалізацыя каманд docker pull і docker push без docker кліента з дапамогай HTTP запытаў

Цяпер захаваем яго сабе лакальна для далейшага разбору

docker save c24fe13d37b9 -o savedArch

Рэалізацыя каманд docker pull і docker push без docker кліента з дапамогай HTTP запытаў

Распакуем атрыманы архіў у бягучую дырэкторыю

tar xvf savedArch

Рэалізацыя каманд docker pull і docker push без docker кліента з дапамогай HTTP запытаў

Як бачым кожны леер ляжыць у асобнай тэчцы. Цяпер паглядзім на струткуру маніфэсту, які мы атрымалі

cat manifest.json | json_pp

Рэалізацыя каманд docker pull і docker push без docker кліента з дапамогай HTTP запытаў

Ня густа. Паглядзім які маніфест патрэбен для загрузкі, паводле дакументацыі.

Рэалізацыя каманд docker pull і docker push без docker кліента з дапамогай HTTP запытаў

Відавочна існуючы маніфест нам не падыходзіць, значыць зробім свой з блэкджэкам і куртызанкамі леерамі і канфігамі.

У нас заўсёды будзе мінмум адзін config файл і масіў Леер. Версія схемы 2 (актуальная на момант напісання артыкула), mediaType пакінем без змен:

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

Пасля стварэння базавага маніфеста неабходна яго напоўніць валіднымі звесткамі. Для гэтага выкарыстаем шаблон json аб'екта леера:

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

яго мы будзем дадаваць у маніфест для кожнага леера.

Далей нам трэба пазнаць памер канфіг файла і замяніць заглушкі ў маніфесце на рэальныя дадзеныя.

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

Цяпер можна ініцыяваць працэс загрузкі і захаваць сабе uuid, якім павінны суправаджацца ўсе наступныя запыты.

Поўны скрыпт выглядае прыкладна так:

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

можам выкарыстоўваць гатовы скрыпт:

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

UPD:
Што мы атрымалі ў выніку?
Па-першае, рэальныя дадзеныя для аналізу, паколькі тэсты запускаюцца ў blazemeter і дадзеныя па запытах докер кліента вельмі не інфарматыўныя у адрозненні ад чыстых HTTP запытаў.

Па-другое, пераход дазволіў нам павялічыць колькасць віртуальных карыстальнікаў для docker upload прыкладна на 150% і атрымаць пры гэтым avg response time на 20-25% хутчэй. Для docker download атрымалася павялічыць колькасць карыстачоў на 500%, avg response time пры гэтым знізіўся прыкладна на 60%.

Дзякуй за ўвагу.

Крыніца: habr.com

Дадаць каментар