У нас было 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%.
Спасибо за внимание.
akdes
А можете поведать, сколько хим. средств понадобилось, что бы прийти к мысле, делать это самому без docker? :D
Я не против — мотивация интересна просто.
b-i-b Автор
Был запрос от клиента на реальном проекте. Есть задача — надо делать. Статья уже постфактум для того чтобы
попытатьсяструктурировать то что получилось и возможно кому-то помочь, поскольку часть информации пришлось добывать опытным путемakdes
Так а чем обусловленна необходимость двигать докер-образы без докера? :)
b-i-b Автор
Нагрузочное тестирование продукта (НЕ средствами баша, скрипты приведены в образовательных целях), который реализует хранение и распространение докер образов. Не использовать докер клиент было решено для уменьшения дополнительных прослоек (в разумных пределах) и соответственно эмулирования более высокой нагрузки. В результате убрали все системные задержки докер клиента. Получили сравнительно чистую нагрузку непосредственно на продукт
akdes
спасибо за ответ.
Думаю эта информация, достаточно одного предложения, в начале статьи будет не лишней ;)
b-i-b Автор
Согласен, добавил. Спасибо за вопрос :)
Relecto
Я примерно так же pullил образы из интернета через корпоративный прокси на свою Windows машину.