使用 HTTP 请求在没有 docker 客户端的情况下实现 docker pull 和 docker push 命令

我们有 2 袋草、75 片麦斯卡林片剂、unix 环境、一个 docker 存储库,以及在没有 docker 客户端的情况下实现 docker pull 和 docker push 命令的任务。

使用 HTTP 请求在没有 docker 客户端的情况下实现 docker pull 和 docker push 命令

UPD:
问:这一切是为了什么?
答案:产品的负载测试(不使用 bash,提供的脚本用于教育目的)。 决定不使用 docker 客户端来减少额外的层(在合理的限制内),并相应地模拟更高的负载。 这样一来,Docker 客户端的所有系统延迟都被消除了。 我们直接在产品上收到了相对干净的负载。
本文使用了 GNU 版本的工具。

首先,让我们弄清楚这些命令的作用。

那么 docker pull 是用来做什么的呢? 根据 文件资料:

“从注册表中提取图像或存储库”。

在那里我们还找到了一个链接 了解镜像、容器和存储驱动程序.

使用 HTTP 请求在没有 docker 客户端的情况下实现 docker pull 和 docker push 命令

从这里我们可以理解,docker镜像是一组某些层,其中包含镜像最新变化的信息,这显然是我们所需要的。 接下来我们看看 注册表API.

它说如下:

“‘镜像’是 JSON 清单和各个层文件的组合。拉取 > 镜像的过程主要围绕检索这两个组件进行。”

因此,根据文档,第一步是“拉取镜像清单“。

当然,我们不会拍摄它,但我们需要其中的数据。 以下是请求示例: GET /v2/{name}/manifests/{reference}

“名称和参考参数可识别图像,并且是必需的。参考可以包括标签或摘要。”

我们的docker存储库部署在本地,让我们尝试执行请求:

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

使用 HTTP 请求在没有 docker 客户端的情况下实现 docker pull 和 docker push 命令

作为响应,我们收到 json,我们目前只对生命线感兴趣,或者更确切地说对它们的哈希值感兴趣。 收到它们后,我们可以检查每一个并执行以下请求:“GET /v2/{name}/blobs/{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

使用 HTTP 请求在没有 docker 客户端的情况下实现 docker pull 和 docker push 命令

让我们看看我们最终收到什么样的文件作为第一条生命线。

file firstLayer

使用 HTTP 请求在没有 docker 客户端的情况下实现 docker pull 和 docker push 命令

那些。 Rails 是 tar 档案,按照适当的顺序解压它们,我们将获得图像的内容。

让我们编写一个小的 bash 脚本,以便所有这些都可以自动化

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

这会稍微复杂一些。

让我们重新开始 文件资料。 所以我们需要下载每个leader,收集相应的manifest并下载。 看起来很简单。

研究完文档后,我们可以将下载过程分为几个步骤:

  • 进程初始化 - “POST /v2/{repoName}/blob/uploads/”
  • 上传生命线(我们将使用整体上传,即我们完整地发送每个生命线)-“PUT /v2/{repoName}/blobs/uploads/{uuid}?digest={digest}
    内容长度:{层的大小}
    Content-Type:application / octet-stream
    层二进制数据”。
  • 加载清单 - “PUT /v2/{repoName}/manifests/{reference}”。

但该文档遗漏了一步,没有这一步骤,一切都将不起作用。 对于整体加载以及部分(分块)加载,在加载导轨之前,您必须执行 PATCH 请求:

“补丁/v2/{repoName}/blob/uploads/{uuid}
内容长度:{块的大小}
Content-Type:application / octet-stream
{层块二进制数据}”。

否则,你将无法超越第一点,因为…… 您将收到 202xx,而不是预期的响应代码 4。

现在算法看起来像:

  • 初始化
  • 补轨
  • 装载扶手
  • 加载清单
    第 2 点和第 3 点将分别根据需要加载的行数重复多次。

首先,我们需要任何图像。 我将使用 archlinux:latest

docker pull archlinux

使用 HTTP 请求在没有 docker 客户端的情况下实现 docker pull 和 docker push 命令

现在我们将其保存在本地以供进一步分析

docker save c24fe13d37b9 -o savedArch

使用 HTTP 请求在没有 docker 客户端的情况下实现 docker pull 和 docker push 命令

将生成的存档解压到当前目录

tar xvf savedArch

使用 HTTP 请求在没有 docker 客户端的情况下实现 docker pull 和 docker push 命令

如您所见,每条生命线都位于单独的文件夹中。 现在让我们看看我们收到的清单的结构

cat manifest.json | json_pp

使用 HTTP 请求在没有 docker 客户端的情况下实现 docker pull 和 docker push 命令

不多。 让我们看看需要加载什么清单,根据 文件资料.

使用 HTTP 请求在没有 docker 客户端的情况下实现 docker pull 和 docker push 命令

显然,现有的宣言不适合我们,所以我们将用二十一点和妓女、生命线和配置来制作我们自己的宣言。

我们将始终拥有至少一个配置文件和一组生命线。 方案版本 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

创建基本清单后,您需要填充有效数据。 为此,我们使用 Rail 对象的 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,该 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 客户端请求的数据信息量不大。

其次,这一转变使我们能够将 docker upload 的虚拟用户数量增加约 150%,并将平均响应时间加快 20-25%。 对于 docker download,我们成功地将用户数量增加了 500%,而平均响应时间减少了约 60%。

感谢您的关注。

来源: habr.com

添加评论