Реализация команд docker pull и docker push без docker клиента посредством HTTP запросов

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



UPD:


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


В статье использовались тулы GNU-версий.


Для начала разберемся что делают эти команды.


Итак для чего используется docker pull? Согласно документации:


"Pull an image or a repository from a registry".

Там же находим ссылку на understand images, containers, and storage drivers.



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


Здесь говорится следующее:


"An “image” is a combination of a JSON manifest and individual layer files. The process of pulling an > image centers around retrieving these two components."

Итак первый шаг согласно документации это “Pulling an Image Manifest”.


Пулить мы его конечно не будем, но данные из него нам нужны. Дальше приводится пример запроса: GET /v2/{name}/manifests/{reference}


"The name and reference parameter identify the image and are required. The reference may include a tag or digest."

Наш докер репозиторий развернут локально, попробуем выполнить запрос:


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


В ответ получаем json из которого нам интересны на данный момент только лееры, точнее их хэши. Получив их, можем по каждому пройтись и выполнить следующий запрос: "GET /v2/{name}/blobs/{digest}"


“Access to a 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


посмотрим что за файл мы в итоге получили в качестве первого леера.


file firstLayer


т.е. лееры представляют из себя 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 save c24fe13d37b9 -o savedArch


Распакуем полученный архив в текущую директорию


tar xvf savedArch


Как видим каждый леер лежит в отдельной папке. Теперь посмотрим на струткуру манифеста, который мы получили


cat manifest.json | json_pp


Не густо. Посмотрим какой манифест нужен для загрузки, согласно документации.



Очевидно существующий манифест нам не подходит, значит сделаем свой с блэкджеком и куртизанками леерами и конфигами.


У нас всегда будет минмум один 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 "\n\t]\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%.


Спасибо за внимание.

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 16

    0
    А можете поведать, сколько хим. средств понадобилось, что бы прийти к мысле, делать это самому без docker? :D

    Я не против — мотивация интересна просто.
      0

      Был запрос от клиента на реальном проекте. Есть задача — надо делать. Статья уже постфактум для того чтобы попытаться структурировать то что получилось и возможно кому-то помочь, поскольку часть информации пришлось добывать опытным путем

        0
        Так а чем обусловленна необходимость двигать докер-образы без докера? :)
          +1

          Нагрузочное тестирование продукта (НЕ средствами баша, скрипты приведены в образовательных целях), который реализует хранение и распространение докер образов. Не использовать докер клиент было решено для уменьшения дополнительных прослоек (в разумных пределах) и соответственно эмулирования более высокой нагрузки. В результате убрали все системные задержки докер клиента. Получили сравнительно чистую нагрузку непосредственно на продукт

            +1
            спасибо за ответ.
            Думаю эта информация, достаточно одного предложения, в начале статьи будет не лишней ;)
              +1

              Согласен, добавил. Спасибо за вопрос :)

        0

        Я примерно так же pullил образы из интернета через корпоративный прокси на свою Windows машину.

        –1

        image

          0
            0

            Спасибо, не знал об этих тулах и видимо не так искал. Они бы могли упростить часть задачи, но не решить её полностью

            0
            Ура, теперь ваш клиент делает то же что и докер клиент :) А есть какие-то цифры для сравнения до и после?
              +1

              Ну практически тоже самое (без лишних для нас шагов, т.к. исключили все локальные операции не относящиеся к продукту непосредственно). Во-первых мы смогли получить реальные данные для анализа, посколькоу тесты запускаются в blazemeter и данные по запросам докер клиента весьма не информативные в отличии от чистых HTTP запросов. Во-вторых, переход позволил нам увеличить колличество виртуальных пользователей для docker upload примерно на 150% и получить при этом avg response time на 20-25% быстрее. Для docker download получилось увеличить количество пользователей на 500%, avg response time при этом снизился примерно на 60%.
              Вероятно стоит эту информацию добавить в выводы

                0

                По подробнее пожалуйста. Вы тестировали Что? Ваш самописный сервер докер репозиториев? Чтобы он быстро отдавал и принимал докер образы от докер-клиентов?


                Потому как если вам было мало информации от докер-клиента и вам нужны были чистые запросы/ответы — то не проще ли было на серверной стороне снимать логи всех запросов и посчитать потом статистику?!

                  0
                  Ваш самописный сервер докер репозиториев? Чтобы он быстро отдавал и принимал докер образы от докер-клиентов?

                  Можно сказать да.


                  не проще ли было на серверной стороне снимать логи всех запросов и посчитать потом статистику?!

                  Не проще, т.к. уже есть готовая инфраструктура для тестов и проще было сделать свой велосипед, но который бы подходил под готовую структуру чем наоборот

                    0

                    А проценты увеличения производительности (эти 500% и 150%) это сравнение чего с чем? Вашего нагрузочного скрипта с ванилла-докер-клиентом? Или/и вашего сервера с ванилла-докер-сервером на одном и том же железе без AutoScaling?

                      0

                      Это в сравнении с чистым докер клиентом

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое