MegaThinking

better tokens, better intelligence, contributing superior tokens to models

0.16.2

horovod 启动流程

hvd.init() 发生了什么

HorovodBasics -> init -> horovod_init -> InitializeHorovodOnce

启动线程

BackgroundThreadLoop

MPI 初始化

MPI_Comm_dup

MPI_Comm_rank 获取当前进程的 RANK

MPI_Comm_size 获取 local size

两次 AllGather, 把 rank 与 size 分发到所有进程

检查是否同构,即 size 是否相同

rank 0 初始化 MessageTable

initialization_done = true 初始化结束

主线程等待

initialization_done = true

背景线程持续 RunLoopOnce

1
while (RunLoopOnce(state, is_coordinator));

从 message_queue 中获取数据

DistributedOptimizer

MXNet 分布式通信

MXnet 使用了 dmlc/ps-lite 实现其分布式通信机制

ps-lite

van.h 父类,使用 Protocol buffer 定义了数据格式

zmq_van.h 继承 van.h,使用 zmq 做具体的数据传输

当前 MXnet 只能使用 IPoIB,不能直接使用 IB verbs 来通信;有个改动 ps-lite 也能直接使用 ib 来通信的 PR 挂了一年多了 …

RDMA in kubernetes

IB 这东西嘛,看起来是没服务发现的能力的;就是使用之前,需要使用 IP 网络通信一次,然后知道对端的 IB 的地址后,才能直接基于 IB 来通信

NCCL 就是这么干的

当然参考阿里云的说法

RDMA通讯建连过程普遍分为 TCP 和 RDMA_CM 两种实现,如果应用程序使用RDMA_CM 的建连的方式,vpc网络插件中分配的pod ip 无法作为RDMA_CM 地址, 容器需要设置HostNetwork。并将bond0 ip 设置为CM的通讯地址

这得看下 mlx device plugin 的实现和容器网络的实现,比较好奇,看一下吧

业界有两种实现 SR-IOV (single root io virtualization) 另一种就是直接共享设备 (HCA: host channel adapter)

HCA

HCA 就没啥好说的了,device plugin allocate 时,给 kubelet 返回设备路径,直接整个把 /dev/infiniband 挂载入容器中即可 … 这点会比较误导,相当于可以用所有的 HCA ?

所以对于 HCA 来说,直接用原先的容器网络来初始化 IB 通信即可,即在容器网络上做 ib 设备地址的发现,随后再用 ib verbs api 来通信

当然如果实现时,不是使用 tcp 的方式来初始化 IB,使用的是 rdma_cm,会有区别,容器的 ip 不能用于通信,只能用主机上的 bond0 ip 来通信 (主网卡)

有点儿奇怪可能与 rdma_cm 的 api 有关 …

另外最好加个这个,让单机多卡的训练可以 IPC 通信

1
2
3
securityContext:
capabilities:
add: [ "IPC_LOCK" ]

SR-IOV

PF -> VF

需要 SR-IOV CNI plugin 及 device plugin 配合

device plugin 挂载 /dev/infiniband 到容器中

SR-IOV CNI plugin 负责将 VF 绑定到容器的 network namespace 中

假设 VF 事先创建好

逻辑是通的,CNI 记录每个节点哪些 VF 已被分配,每个节点记录剩余几个 VF,当有新的 pod 被调度到当前节点时,CNI 即可分配未使用的 VF 与 pod

IP over Infiniband

IPoIB 由 ipoib driver 实现,能像一般 NIC (ifconfig) 一样使用 ib 网络,当然性能差一些,在到达 NIC 之前,与 socket 通信的开销一致

  • socket api
  • ib verbs api

socket api 走 os 调用栈,CPU 参与

ib verbs 直接走 ib,相当于 bypass 了 os 与 CPU,硬件卸载

不过有个地方比较疑惑,IPoIB,infiniband 底层的传输方式是 datagram 即不可靠传输,不过上层应用(zmp)都是 tcp,应有自己的重传机制;其次 ps-lite van.h 里也有开关控制重传功能,在未收到当前消息的 ACK 时,不会处理该请求,这里展开说下

van.h 初始时,如果看到打开了重传开关,则初始化 resend.h

启动了一个线程,周期性 (Timeout) 的重传 send_buf 中的消息

van.h 在

  • 发送数据时,将数据加入 send_buf 中;
  • 接收到数据时
    • 如果是 ACK,则从 send_buf 中移除该消息,并把数据交给上层处理
    • 若当前数据仍不是 ack 数据,则查看是否已发送过 ACK
      • 是的话,则认为是重复消息,不交上层处理,并发送 ACK
      • 否则 交上层处理,记录到 ACK set 中,并发送 ACK

不过这个重传的场景比较神奇,话说 TCP 已经有 ACK 和重传机制了 … 不懂 ps-lite 是遇到了什么具体的场景,再做了一次重传的增强设计

而且这个设计有个限制,收发数据越多,似乎内存消耗越大,因为记录是否收到过该消息的 ACK 的 set,并没有清理的机会,会一直增加

所以一般也未见开启这个功能,env.md 里都没说明这两环境变量,姑且认为是个废弃特性吧 …

1
2
PS_RESEND=1
PS_RESEND_TIMEOUT=1000 # ms

厂家

Azure

只支持 RDMA on IB 不支持 IP over IB

In Azure, IP over IB is not supported. Only RDMA over IB is supported.

https://docs.microsoft.com/zh-cn/azure/virtual-machines/linux/sizes-hpc#rdma-capable-instances

Aliyun

k8s 目前只看到 RDMA on IB,IP over IB 没看到,看起来是直接主机网络之后,用 ib0 ip 就行?

AWS

sagemaker 25Bbps/s 互联,看起来不像 IB or RoCE

参考

https://community.mellanox.com/s/article/kubernetes-ipoib-ethernet-rdma-sr-iov-networking-with-connectx4-connectx5

在 Kubernetes 上使用 RDMA

最近接触了一些 HPC (高性能计算) 新玩意儿,入门首先要掌握一些基本的概念

术语

MPI

Message passing interface

消息传递接口,可以理解为分布式消息传递框架

OpenMPI

MPI 的一种开源实现

RDMA

remote direct memory access

Infiniband

IB

网络设备,支持 RDMA

OpenMPI

Terminology

MCA

  • framework
  • components
  • module

An easy example framework to discuss is the MPI framework named “btl”, or the Byte Transfer Layer. It is used to send and receive data on different kinds of networks. Hence, Open MPI has btl components for shared memory, TCP, Infiniband, Myrinet, etc.

不同的 MCA 支持不同的参数,我们可以参考如下查询支持的 MCA 参数

available-mca-params

1
2
3
4
5
ompi_info --param all all --level 9
# only btl
ompi_info --param btl all --level 9
# only tcp of btl
ompi_info --param btl tcp --level 9

可以通过 MCA 参数选择 Components

selecting-components

1
mpirun --mca btl ^tcp,openib

不使用 btl framework 中的 tcp component,使用其中的 openib

mpirun

mpirun 的常用参数

  • -H: List of hosts on which to invoke processes.
  • -np: Run this many copies of the program on the given nodes. This option indicates that the specified file is an executable program and not an application context. If no value is provided for the number of copies to execute (i.e., neither the “-np” nor its synonyms are provided on the command line), Open MPI will automatically execute a copy of the program on each process slot (see below for description of a “process slot”). This feature, however, can only be used in the SPMD model and will return an error (without beginning execution of the application) otherwise.
  • –bind-to: Bind processes to the specified object, defaults to core. Supported options include slot, hwthread, core, l1cache, l2cache, l3cache, socket, numa, board, and none.
  • -x: Export the specified environment variables to the remote nodes before executing the program. Only one environment variable can be specified per -x option. Existing environment variables can be specified or new variable names specified with corresponding values. For example: % mpirun -x DISPLAY -x OFILE=/tmp/out … The parser for the -x option is not very sophisticated; it does not even understand quoted values. Users are advised to set variables in the environment, and then use -x to export (not define) them.
  • -mca: Send arguments to various MCA modules. See the “MCA” section, below.

--mca btl self 的作用

ib-btl

self 用于本地进程通信 (可能使用 lo 设备,也可能不用,例如可以使用内存共享)

openmpi,假设多机同一个地址族的地址可通,如果主机上有多个网络,openmpi 参考如下链接进行网络选择

tcp-selection

注意到 openmpi 会使用所见的所有网络,如果你不想其使用 ip network,你可以显式的禁用之,然而

Note that Open MPI will still use TCP for control messages, such as data between mpirun and the MPI processes, rendezvous information during MPI_INIT, etc. To disable TCP altogether, you also need to disable the tcp component from the OOB framework.

这句话比较有意思,一般来说 mpirun 要求 node 间可以 ssh 免密登录,而 ssh 是应用层协议,依赖 TCP

openmpi 会选择最优的网络

tcp-routability

tcp-routability-1.3

如何查看 mpirun 连接的过程

1
mpirun --mca btl self,vader,tcp --mca btl_base_verbose 30 -np 2 -host NodeA,NodeB a.out

a.out 为可执行程序

如果有 ib 卡,tcp component 会自动下线

tcp-auto-disable

openmpi build default option

default-build

1
--with-openib(=DIR) and --with-openib-libdir=DIR

  1. 计算 Replica 及 Slot 数
  2. 创建 ConfigMap
  • hostfile
1
[mpiJobName]-worker-i slots=[8]

[mpiJobName]-worker-i i.e. ${POD_NAME} of statefulset’s pod

kubexec.sh

1
2
3
4
5
#!/bin/sh
set -x
POD_NAME=$1
shift
/opt/kube/kubectl exec ${POD_NAME} -- /bin/sh -c "$*"
  1. 创建 Statefulset

Container Command: Sleep

  1. 创建 Launcher (Job)

Statefulset ready 后,创建 Launcher (Job)

设置 env OMPI_MCA_plm_rsh_agent 为 kubexec.sh

即使用 kubectl exec ${POD_NAME} -- /bin/sh -c "$*" 作为 ssh_agent

rsh-not-ssh

所以 openmpi 是有个潜在要求的,要么是支持 IPoIB,or 要有 IP 网络,纯 IB 网络不行

当然 SDP (Socket Direct Protocol) 能加速 Socket 又是另外一个话题了

1
2
3
4
5
6
7
# server side
/etc/init.d/sshd stop
env LD_PRELOAD=/usr/lib64/libsdp.so
LIBSDP_CONFIG_FILE=/u/etc/libsdp.conf /etc/init.d/sshd start
# client side
LD_PRELOAD=/usr//lib64/libsdp.so
LIBSDP_CONFIG_FILE=/etc/libsdp.conf scp <file> <user>@<IPoIBaddr>:<dir>

Running ssh, scp over SDP

lsmod | grep sdp

sdpnetstat -S

设置 env OMPI_MCA_orte_default_hostfile 为 hostfile

Container Command: mpirun

综上,mpi-operator 使用 kube-dns 获得 pod ip,mpirun 使用 kubectl exec 远程登录 container

Docker Network

IBM 的几篇 Blog overall 的讲了一下

容器如何访问外部网络

通过 docker0 网桥的外发包经过 NAT 之后 src ip 变为主机 ip

外部网络如何访问容器

容器内的端口,可在容器启动时,通过 -p 参数映射到 host 上,这时 host 上的端口会随机分配。当然也可以通过 -p [container-port]:[host-port] 方式,指定映射到 host 的特定端口

至于实现上也较为直接,若容器有 expose 端口,则 docker 会相应启动一个 docker-proxy 监听 host 上的 host 端口 (如上述例子中的 host-port),外部流量到达 host-port 时,由 docker-proxy 转发至最终容器

当然上述只是 docker network 的原生实现,docker 原生实现的不同 host 的 container 略去

Flannel

如果在 k8s 生态中,docker container 跨 host 通信,早期版本多使用 Flannel 完成

Flannel 原理

Flannel 实现的是 overlay network,即基于已有的 underlay network,在其之上扩展报文字段,完成报文转发

原理也比较好理解

  • 在 ETCD 中设置 Flannel 网段及子网范围
  • 多个 Host 上运行 Flannel daemon
  • Flannel daemon 根据 ETCD 中记录的已分配子网,确定自己的子网,并注册至 ETCD 中
  • Docker 根据 Flannel 划分的子网启动,docker0 地址从 Flannel 子网中分配得到,一般来说 Flannel0 地址为子网的第一个地址 (10.0.2.0),docker0 地址为子网的第二个地址 (10.0.2.1)

VM1 Container 1 至 VM2 Container 2 的报文转发过程

可参看该作者的一篇详细分析

看上述链接吧,讲的非常好,图文并茂,下面我只是自我温习 😆 努力积累

VM1 Container 1

  • Container 1 报文中 src ip 为容器 ip,假设为 10.1.15.2/24,dst ip 为对端容器 ip,假设为 10.1.20.3/24
  • 报文从容器中的 veth0 发往 host 上的 veth pair (veth_XXX)
  • kernel 根据 route 表将报文转发至 Flannel0 TUN
  • Flannel0 接收到之后 overlay 的作用体现了,首先根据目的 ip 查询其所在 host 的 ip,封装一层 IP 报文,随后封装一层 UDP 报文,投递到对端 Flannel daemon 监听端口 8285。这个时候报文就能通过 underlay network 转发至对端 host 了

VM2 Container 2

  • 报文到达当前 host 后,UDP 报文交由 Flannel daemon 处理
  • Flannel daemon 交由 Flannel0 TUN 处理
  • kernel 直接根据 route 表处理,转发至 docker0
  • docker0 是网桥设备,所有 docker container 均连接在其之上,因此最后根据 container dst ip 转发至 dst container

当然这是 Flannel 早期的版本,使用了 UDP 的报文封装,这样会有一些 packet 来回拷贝的开销

Flannel 还支持 VxLan 的模式,看下它的原理,网络这块还是比较有意思

这篇也很 nice An illustrated guide to Kubernetes Networking [Part 2]

nice shot An illustrated guide to Kubernetes Networking [Part 1]

这篇非常详细 … 蛤蛤

ARP 协议

ARP

Flannel VxLan

Term

ref Han’s blog

  • TUN is a software interface implemented in linux kernel, it can pass raw ip packet between user program and the kernel

Port

VIP

查询虚拟 IP,via device_owner=neutron:VIP_PORT

虚拟 IP 绑定的 IP(网卡) allowed_address_pairs

例如 VIP 192.168.186.192

1
2
3
4
5
6
7
8
9
10
allowed_address_pairs: [
{
"ip_address": "192.168.129.104",
"mac_address": "fa:16:3e:6e:e0:d8"
},
{
"ip_address": "192.168.155.84",
"mac_address": "fa:16:3e:6e:e0:d8"
}
]

VIP 可手动配置至网卡,例如给 eth0 配置 vip(ip 别名),使得 eth0 存在多个 ip

1
ifconfig eth0:1 192.168.0.107 netmask 255.255.0.0

亦或者直接添加 ip 至 dev eth0

1
ip addr add 192.168.2.105/24 dev eth0

常见做法是外部通过 VIP 访问服务,服务使用 Keeplive 组件实现 VIP 在多个后端节点漂移,从而实现服务 HA

ECS IP

查询 ECS IP(网卡),via device_id

例如 device_id=fe6b212b-9b84-4c0a-8137-528be40f0b04,即 ECS ID

主网卡有如下字段

1
2
3
{
"primary_interface": true
}

VIP 设计为绑定在其他 IP 上,因此其不存在 port_id (网卡 IP),仅存在自身的 vip_port_id,若 VIP 被多个 IP 绑定,则其对应多个 port_id

openstack 创建 ECS 过程,首先使用 neutron 命令创建 port (主网卡),其次使用 cinder 命令创建系统盘,最后使用 nova 命令根据主网卡及系统盘创建出 ECS 实例

ALL IP

查询 ALL IP,via network_id

例如 network_id=8b8457ab-521a-4da0-9cd4-aee1688ee0f8,即 VPC ID

结果包括 ECS IP、VIP 等

Up / Down

  • up 启用 network interface
  • down 停用 network interface

VPC

VPC 访问方案

VPC Endpoint

优势

  • 发布处于 VPC 中的服务,供外部使用

限制

  • 服务发布方发布服务后,需将服务标识告知使用方
  • 使用方在本 VPC 中,通过创建 VPC Endpoint 以访问服务发布方 VPC 中提供的服务,VPC Endpoint 需消耗 本 VPC 的一个 IP 资源

VPC Peering

优势

  • VPC 全互通

限制

  • 对于 VPC Peering 来说,有网段及子网的限制 ,若冲突则无法 Peering
  • VPC Peering 仅在主网卡生效,对于多网卡的主机,需额外设置路由规则,使得与 VPC Peering 通信的报文被正确转发至主网卡

当然有点儿标题党的意思

学习到这呢,已经大概有点儿感觉了

union fs

docker container 的 root fs,本质上呢都叫 ufs 技术,union file system

docker 用它来干啥的,镜像不是分层的嘛,docker 用这玩意儿技术来把所有层 union 成一个单一的 fs,给用户使用

这就是 docker container root fs 的基础了

问题就来了,现在不依赖 dockerd 咋 union file system,于是乎在 google 中搜索了下 union file system impl in golang,发现了个项目,还挺有意思

https://github.com/spf13/afero

readme 中提到它可以干

  • Support for compositional (union) file systems by combining multiple file systems acting as one

看看能不能用吧

显然不能 … 粗略一扫,就是一些 os api 封装,文档也不友好 sigh

still docker pull

docker pull 的大概过程,pull 镜像,随后使用 graph driver union mount,最后把 image 注册到 layer store

怎么看的,在 daemon/graphdriver/aufs 往上搜就行,最后发现 docker pull 也用了它

所以回答上篇的问题 扫描镜像时,为何不把 layer union 之后,再扫描,看到这,诸位可能已经发现不好实现呀

能不能实现,当然能!

  1. 按照这里所说 loading-an-image-filesystem-changeset
    1.1 untar root layer to a dir (act as container root fs)
    1.2 untar each layer to the previous dir, and walk once, rm any file with .wh. prefix and its coresponding file
    1.3 continue this process
    1.4 … pay attention, 可能有童鞋会觉得这个细节可能因 storage driver 而异,实则不然,image tar archive 的格式是独立于 storage driver 的
  2. 熟悉 docker layer 代码的老铁,没准能把这部分代码给整出个独立的 lib 来,实现把 image layer union mount 之后,给扫描程序一个统一的 fs view, 但是显然它依赖于 storage driver 的能力,你要想在容器里面干这个事情,我就 🙄 了。要是非得在容器里这么折腾,不如直接挂 docker socket 到容器里,用宿主机的 dockerd 直接搞来的快些,废这大劲儿 sucks

https://docs.docker.com/storage/storagedriver/
Storage drivers allow you to create data in the writable layer of your container. The files won’t be persisted after the container is deleted, and both read and write speeds are low.

也是够精辟

不过我还是有个疑问,不同 storage driver 实现分层镜像的细节不同,docker save 的时候,是怎么把不同 storage driver 的 layer 能统一到 Image Tar File Archive 里面去的

手头上没有试验 devicemapper 的机器,按说 divicemapper 实现分层镜像用的是 snapshot 技术,所以删除文件的时候,当前 layer 并不会有 .wh. 文件才对

这么说来,似乎是 layer diff 是 docker 自己算出来的了,删除的文件,给标记上 .wh. ?

whatever it needs time to cover it

https://learn-docker-the-hard-way.readthedocs.io/zh_CN/latest/

最后的时候,发现 google 又为世界造轮子了

https://github.com/GoogleContainerTools/container-diff

行吧,google 大佬已经做了,而且的确有 lib,效果好不好那就再说了,这个库基本上实现了 fundamental 的 loading-an-image-filesystem-changeset 描述的过程

当然因为是 file diff,所以权限恢复不出来的

简析 crane pull image 过程

详细过程在内网写了,粗略过程罗列于此

以 docker hub registry 为例

以 crane 二进制工具为参考

https://github.com/google/go-containerregistry/blob/master/cmd/crane/doc/crane.md

google 的同事真是为世界造轮子,many thanks

Target

从 registry 下载 nginx:latest 镜像

1
docker pull nginx:latest

Auth

https://docs.docker.com/registry/spec/auth/token/#requesting-a-token

  1. ping registry
1
2
3
4
5
6
7
8
curl -i https://registry.hub.docker.com/v2/
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8
Docker-Distribution-Api-Version: registry/2.0
Www-Authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io"
Date: Sat, 17 Nov 2018 01:27:49 GMT
Content-Length: 87
Strict-Transport-Security: max-age=31536000

在 Www-Authenticate 请求头中可见 Registry 使用 Bearer 认证方式

  1. refresh docker auth
1
2
3
4
5
6
7
curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/nginx:pull" | python -mjson.tool
{
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlDK2pDQ0FwK2dBd0lCQWdJQkFEQUtCZ2dxaGtqT1BRUURBakJHTVVRd1FnWURWUVFERXpzeVYwNVpPbFZMUzFJNlJFMUVVanBTU1U5Rk9reEhOa0U2UTFWWVZEcE5SbFZNT2tZelNFVTZOVkF5VlRwTFNqTkdPa05CTmxrNlNrbEVVVEFlRncweE9EQXlNVFF5TXpBMk5EZGFGdzB4T1RBeU1UUXlNekEyTkRkYU1FWXhSREJDQmdOVkJBTVRPMVpCUTFZNk5VNWFNenBNTkZSWk9sQlFTbGc2VWsxQlZEcEdWalpQT2xZMU1sTTZRa2szV2pwU1REVk9PbGhXVDBJNlFsTmFSanBHVTFRMk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGtyTmgyZWxESnVvYjVERWd5Wi9oZ3l1ZlpxNHo0OXdvNStGRnFRK3VPTGNCMDRyc3N4cnVNdm1aSzJZQ0RSRVRERU9xNW5keEVMMHNaTE51UXRMSlNRdFY1YUhlY2dQVFRkeVJHUTl2aURPWGlqNFBocE40R0N0eFV6YTNKWlNDZC9qbm1YbmtUeDViOElUWXBCZzg2TGNUdmMyRFVUV2tHNy91UThrVjVPNFFxNlZKY05TUWRId1B2Mmp4YWRZa3hBMnhaaWNvRFNFQlpjWGRneUFCRWI2YkRnUzV3QjdtYjRRVXBuM3FXRnRqdCttKzBsdDZOR3hvenNOSFJHd3EwakpqNWtZbWFnWHpEQm5NQ3l5eDFBWFpkMHBNaUlPSjhsaDhRQ09GMStsMkVuV1U1K0thaTZKYVNEOFZJc2VrRzB3YXd4T1dER3U0YzYreE1XYUx3SURBUUFCbzRHeU1JR3ZNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVBCZ05WSFNVRUNEQUdCZ1JWSFNVQU1FUUdBMVVkRGdROUJEdFdRVU5XT2pWT1dqTTZURFJVV1RwUVVFcFlPbEpOUVZRNlJsWTJUenBXTlRKVE9rSkpOMW82VWt3MVRqcFlWazlDT2tKVFdrWTZSbE5VTmpCR0JnTlZIU01FUHpBOWdEc3lWMDVaT2xWTFMxSTZSRTFFVWpwU1NVOUZPa3hITmtFNlExVllWRHBOUmxWTU9rWXpTRVU2TlZBeVZUcExTak5HT2tOQk5sazZTa2xFVVRBS0JnZ3Foa2pPUFFRREFnTkpBREJHQWlFQWdZTWF3Si9uMXM0dDlva0VhRjh2aGVkeURzbERObWNyTHNRNldmWTFmRTRDSVFEbzNWazJXcndiSjNmU1dwZEVjT3hNazZ1ZEFwK2c1Nkd6TjlRSGFNeVZ1QT09Il19.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvbmdpbngiLCJhY3Rpb25zIjpbInB1bGwiXX1dLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuaW8iLCJleHAiOjE1NDI0MTg2MjgsImlhdCI6MTU0MjQxODMyOCwiaXNzIjoiYXV0aC5kb2NrZXIuaW8iLCJqdGkiOiI1SG5JQllqVkluZERFYlRkRlUzOSIsIm5iZiI6MTU0MjQxODAyOCwic3ViIjoiIn0.zzVk9t-govqoyzQCwHfivOAkdIG0D6r5RoMS7HRq4vOBj1bQdASOfB6YqVLGWP6G-4cf6ESCDTxdidREgZYnklpApX7dYdrAf6OpxA5HXP5MYDMTE7PEZueoUpBipz0UsPI4lzMC1j80UjjgTVHyjiIMcwgxPXpT6-zPJJFp9EjDrLsBHtj2cdmPv_54KA0j50VQLZKccUvC67z0iT5KpSRvKyFcWLEActeCnmuZjkJgySmaVduVfLiDLFbboBOw0mNLeTFIodfHoEdFqYBooBK1d_x37GFCSunYH8fFZ0XkfS7OFDyaYiOlQzafbnz0TxLIUU-jOEsJkaofOnHF2Q",
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlDK2pDQ0FwK2dBd0lCQWdJQkFEQUtCZ2dxaGtqT1BRUURBakJHTVVRd1FnWURWUVFERXpzeVYwNVpPbFZMUzFJNlJFMUVVanBTU1U5Rk9reEhOa0U2UTFWWVZEcE5SbFZNT2tZelNFVTZOVkF5VlRwTFNqTkdPa05CTmxrNlNrbEVVVEFlRncweE9EQXlNVFF5TXpBMk5EZGFGdzB4T1RBeU1UUXlNekEyTkRkYU1FWXhSREJDQmdOVkJBTVRPMVpCUTFZNk5VNWFNenBNTkZSWk9sQlFTbGc2VWsxQlZEcEdWalpQT2xZMU1sTTZRa2szV2pwU1REVk9PbGhXVDBJNlFsTmFSanBHVTFRMk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGtyTmgyZWxESnVvYjVERWd5Wi9oZ3l1ZlpxNHo0OXdvNStGRnFRK3VPTGNCMDRyc3N4cnVNdm1aSzJZQ0RSRVRERU9xNW5keEVMMHNaTE51UXRMSlNRdFY1YUhlY2dQVFRkeVJHUTl2aURPWGlqNFBocE40R0N0eFV6YTNKWlNDZC9qbm1YbmtUeDViOElUWXBCZzg2TGNUdmMyRFVUV2tHNy91UThrVjVPNFFxNlZKY05TUWRId1B2Mmp4YWRZa3hBMnhaaWNvRFNFQlpjWGRneUFCRWI2YkRnUzV3QjdtYjRRVXBuM3FXRnRqdCttKzBsdDZOR3hvenNOSFJHd3EwakpqNWtZbWFnWHpEQm5NQ3l5eDFBWFpkMHBNaUlPSjhsaDhRQ09GMStsMkVuV1U1K0thaTZKYVNEOFZJc2VrRzB3YXd4T1dER3U0YzYreE1XYUx3SURBUUFCbzRHeU1JR3ZNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVBCZ05WSFNVRUNEQUdCZ1JWSFNVQU1FUUdBMVVkRGdROUJEdFdRVU5XT2pWT1dqTTZURFJVV1RwUVVFcFlPbEpOUVZRNlJsWTJUenBXTlRKVE9rSkpOMW82VWt3MVRqcFlWazlDT2tKVFdrWTZSbE5VTmpCR0JnTlZIU01FUHpBOWdEc3lWMDVaT2xWTFMxSTZSRTFFVWpwU1NVOUZPa3hITmtFNlExVllWRHBOUmxWTU9rWXpTRVU2TlZBeVZUcExTak5HT2tOQk5sazZTa2xFVVRBS0JnZ3Foa2pPUFFRREFnTkpBREJHQWlFQWdZTWF3Si9uMXM0dDlva0VhRjh2aGVkeURzbERObWNyTHNRNldmWTFmRTRDSVFEbzNWazJXcndiSjNmU1dwZEVjT3hNazZ1ZEFwK2c1Nkd6TjlRSGFNeVZ1QT09Il19.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvbmdpbngiLCJhY3Rpb25zIjpbInB1bGwiXX1dLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuaW8iLCJleHAiOjE1NDI0MTg2MjgsImlhdCI6MTU0MjQxODMyOCwiaXNzIjoiYXV0aC5kb2NrZXIuaW8iLCJqdGkiOiI1SG5JQllqVkluZERFYlRkRlUzOSIsIm5iZiI6MTU0MjQxODAyOCwic3ViIjoiIn0.zzVk9t-govqoyzQCwHfivOAkdIG0D6r5RoMS7HRq4vOBj1bQdASOfB6YqVLGWP6G-4cf6ESCDTxdidREgZYnklpApX7dYdrAf6OpxA5HXP5MYDMTE7PEZueoUpBipz0UsPI4lzMC1j80UjjgTVHyjiIMcwgxPXpT6-zPJJFp9EjDrLsBHtj2cdmPv_54KA0j50VQLZKccUvC67z0iT5KpSRvKyFcWLEActeCnmuZjkJgySmaVduVfLiDLFbboBOw0mNLeTFIodfHoEdFqYBooBK1d_x37GFCSunYH8fFZ0XkfS7OFDyaYiOlQzafbnz0TxLIUU-jOEsJkaofOnHF2Q",
"expires_in": 300,
"issued_at": "2018-11-17T01:32:08.367366757Z"
}

设置 token var

1
export token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlDK2pDQ0FwK2dBd0lCQWdJQkFEQUtCZ2dxaGtqT1BRUURBakJHTVVRd1FnWURWUVFERXpzeVYwNVpPbFZMUzFJNlJFMUVVanBTU1U5Rk9reEhOa0U2UTFWWVZEcE5SbFZNT2tZelNFVTZOVkF5VlRwTFNqTkdPa05CTmxrNlNrbEVVVEFlRncweE9EQXlNVFF5TXpBMk5EZGFGdzB4T1RBeU1UUXlNekEyTkRkYU1FWXhSREJDQmdOVkJBTVRPMVpCUTFZNk5VNWFNenBNTkZSWk9sQlFTbGc2VWsxQlZEcEdWalpQT2xZMU1sTTZRa2szV2pwU1REVk9PbGhXVDBJNlFsTmFSanBHVTFRMk1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBMGtyTmgyZWxESnVvYjVERWd5Wi9oZ3l1ZlpxNHo0OXdvNStGRnFRK3VPTGNCMDRyc3N4cnVNdm1aSzJZQ0RSRVRERU9xNW5keEVMMHNaTE51UXRMSlNRdFY1YUhlY2dQVFRkeVJHUTl2aURPWGlqNFBocE40R0N0eFV6YTNKWlNDZC9qbm1YbmtUeDViOElUWXBCZzg2TGNUdmMyRFVUV2tHNy91UThrVjVPNFFxNlZKY05TUWRId1B2Mmp4YWRZa3hBMnhaaWNvRFNFQlpjWGRneUFCRWI2YkRnUzV3QjdtYjRRVXBuM3FXRnRqdCttKzBsdDZOR3hvenNOSFJHd3EwakpqNWtZbWFnWHpEQm5NQ3l5eDFBWFpkMHBNaUlPSjhsaDhRQ09GMStsMkVuV1U1K0thaTZKYVNEOFZJc2VrRzB3YXd4T1dER3U0YzYreE1XYUx3SURBUUFCbzRHeU1JR3ZNQTRHQTFVZER3RUIvd1FFQXdJSGdEQVBCZ05WSFNVRUNEQUdCZ1JWSFNVQU1FUUdBMVVkRGdROUJEdFdRVU5XT2pWT1dqTTZURFJVV1RwUVVFcFlPbEpOUVZRNlJsWTJUenBXTlRKVE9rSkpOMW82VWt3MVRqcFlWazlDT2tKVFdrWTZSbE5VTmpCR0JnTlZIU01FUHpBOWdEc3lWMDVaT2xWTFMxSTZSRTFFVWpwU1NVOUZPa3hITmtFNlExVllWRHBOUmxWTU9rWXpTRVU2TlZBeVZUcExTak5HT2tOQk5sazZTa2xFVVRBS0JnZ3Foa2pPUFFRREFnTkpBREJHQWlFQWdZTWF3Si9uMXM0dDlva0VhRjh2aGVkeURzbERObWNyTHNRNldmWTFmRTRDSVFEbzNWazJXcndiSjNmU1dwZEVjT3hNazZ1ZEFwK2c1Nkd6TjlRSGFNeVZ1QT09Il19.eyJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6ImxpYnJhcnkvbmdpbngiLCJhY3Rpb25zIjpbInB1bGwiXX1dLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuaW8iLCJleHAiOjE1NDI0MTg2MjgsImlhdCI6MTU0MjQxODMyOCwiaXNzIjoiYXV0aC5kb2NrZXIuaW8iLCJqdGkiOiI1SG5JQllqVkluZERFYlRkRlUzOSIsIm5iZiI6MTU0MjQxODAyOCwic3ViIjoiIn0.zzVk9t-govqoyzQCwHfivOAkdIG0D6r5RoMS7HRq4vOBj1bQdASOfB6YqVLGWP6G-4cf6ESCDTxdidREgZYnklpApX7dYdrAf6OpxA5HXP5MYDMTE7PEZueoUpBipz0UsPI4lzMC1j80UjjgTVHyjiIMcwgxPXpT6-zPJJFp9EjDrLsBHtj2cdmPv_54KA0j50VQLZKccUvC67z0iT5KpSRvKyFcWLEActeCnmuZjkJgySmaVduVfLiDLFbboBOw0mNLeTFIodfHoEdFqYBooBK1d_x37GFCSunYH8fFZ0XkfS7OFDyaYiOlQzafbnz0TxLIUU-jOEsJkaofOnHF2Q

Pull

  1. Get Image Manifest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
curl -v -H "Authorization: Bearer $token" -H "Accept: application/vnd.docker.distribution.manifest.v2+json" "https://registry.hub.docker.com/v2/library/nginx/manifests/latest"
resp

{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 6022,
"digest": "sha256:e81eb098537d6c4a75438eacc6a2ed94af74ca168076f719f3a0558bd24d646a"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 22486277,
"digest": "sha256:a5a6f2f73cd8abbdc55d0df0d8834f7262713e87d6c8800ea3851f103025e0f0"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 22204196,
"digest": "sha256:67da5fbcb7a04397eda35dccb073d8569d28de13172fbd569fbb7a3e30b5886b"
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 203,
"digest": "sha256:e82455fa5628738170735528c8db36567b5423ec59802a1e2c084ed42b082527"
}
]
}
  1. Get Image Config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
curl -i -H "Authorization: Bearer $token" "https://registry.hub.docker.com/v2/library/nginx/blobs/sha256:e81eb098537d6c4a75438eacc6a2ed94af74ca168076f719f3a0558bd24d646a"
HTTP/1.1 307 Temporary Redirect
Content-Type: text/html; charset=utf-8
Docker-Distribution-Api-Version: registry/2.0
Location: https://production.cloudflare.docker.com/registry-v2/docker/registry/v2/blobs/sha256/e8/e81eb098537d6c4a75438eacc6a2ed94af74ca168076f719f3a0558bd24d646a/data?verify=1542423249-8ogA6RSAc3PlmtNd%2FOuiIuAUo3c%3D
Date: Sat, 17 Nov 2018 02:04:09 GMT
Content-Length: 244
Strict-Transport-Security: max-age=31536000
redirect

curl https://production.cloudflare.docker.com/registry-v2/docker/registry/v2/blobs/sha256/e8/e81eb098537d6c4a75438eacc6a2ed94af74ca168076f719f3a0558bd24d646a/data?verify=1542423249-8ogA6RSAc3PlmtNd%2FOuiIuAUo3c%3D | python -mjson.tool
config json file

{
"architecture": "amd64",
"config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"80/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"NGINX_VERSION=1.15.6-1~stretch",
"NJS_VERSION=1.15.6.0.2.5-1~stretch"
],
"Cmd": [
"nginx",
"-g",
"daemon off;"
],
"ArgsEscaped": true,
"Image": "sha256:eb4657966d3e92498b450e24969a0a2808f254ab44102f31674543f642e35ed7",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": [],
"Labels": {
"maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
},
"StopSignal": "SIGTERM"
},
"container": "d4fa15093ad8ad3df60d7403c1752a379503686e32a76b70771b3ea268ec5d66",
"container_config": {
"Hostname": "d4fa15093ad8",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"80/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"NGINX_VERSION=1.15.6-1~stretch",
"NJS_VERSION=1.15.6.0.2.5-1~stretch"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"CMD [\"nginx\" \"-g\" \"daemon off;\"]"
],
"ArgsEscaped": true,
"Image": "sha256:eb4657966d3e92498b450e24969a0a2808f254ab44102f31674543f642e35ed7",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": [],
"Labels": {
"maintainer": "NGINX Docker Maintainers <docker-maint@nginx.com>"
},
"StopSignal": "SIGTERM"
},
"created": "2018-11-16T13:32:10.147294787Z",
"docker_version": "17.06.2-ce",
"history": [
{
"created": "2018-11-15T22:45:06.938205528Z",
"created_by": "/bin/sh -c #(nop) ADD file:dab9baf938799c515ddce14c02f899da5992f0b76a432fa10a2338556a3cb04f in / "
},
{
"created": "2018-11-15T22:45:07.243453424Z",
"created_by": "/bin/sh -c #(nop) CMD [\"bash\"]",
"empty_layer": true
},
{
"created": "2018-11-16T13:31:11.175776557Z",
"created_by": "/bin/sh -c #(nop) LABEL maintainer=NGINX Docker Maintainers <docker-maint@nginx.com>",
"empty_layer": true
},
{
"created": "2018-11-16T13:31:11.487598267Z",
"created_by": "/bin/sh -c #(nop) ENV NGINX_VERSION=1.15.6-1~stretch",
"empty_layer": true
},
{
"created": "2018-11-16T13:31:11.783900832Z",
"created_by": "/bin/sh -c #(nop) ENV NJS_VERSION=1.15.6.0.2.5-1~stretch",
"empty_layer": true
},
{
"created": "2018-11-16T13:32:07.382613887Z",
"created_by": "/bin/sh -c set -x \t&& apt-get update \t&& apt-get install --no-install-recommends --no-install-suggests -y gnupg1 apt-transport-https ca-certificates \t&& \tNGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \tfound=''; \tfor server in \t\tha.pool.sks-keyservers.net \t\thkp://keyserver.ubuntu.com:80 \t\thkp://p80.pool.sks-keyservers.net:80 \t\tpgp.mit.edu \t; do \t\techo \"Fetching GPG key $NGINX_GPGKEY from $server\"; \t\tapt-key adv --keyserver \"$server\" --keyserver-options timeout=10 --recv-keys \"$NGINX_GPGKEY\" && found=yes && break; \tdone; \ttest -z \"$found\" && echo >&2 \"error: failed to fetch GPG key $NGINX_GPGKEY\" && exit 1; \tapt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \t&& dpkgArch=\"$(dpkg --print-architecture)\" \t&& nginxPackages=\" \t\tnginx=${NGINX_VERSION} \t\tnginx-module-xslt=${NGINX_VERSION} \t\tnginx-module-geoip=${NGINX_VERSION} \t\tnginx-module-image-filter=${NGINX_VERSION} \t\tnginx-module-njs=${NJS_VERSION} \t\" \t&& case \"$dpkgArch\" in \t\tamd64|i386) \t\t\techo \"deb https://nginx.org/packages/mainline/debian/ stretch nginx\" >> /etc/apt/sources.list.d/nginx.list \t\t\t&& apt-get update \t\t\t;; \t\t*) \t\t\techo \"deb-src https://nginx.org/packages/mainline/debian/ stretch nginx\" >> /etc/apt/sources.list.d/nginx.list \t\t\t\t\t\t&& tempDir=\"$(mktemp -d)\" \t\t\t&& chmod 777 \"$tempDir\" \t\t\t\t\t\t&& savedAptMark=\"$(apt-mark showmanual)\" \t\t\t\t\t\t&& apt-get update \t\t\t&& apt-get build-dep -y $nginxPackages \t\t\t&& ( \t\t\t\tcd \"$tempDir\" \t\t\t\t&& DEB_BUILD_OPTIONS=\"nocheck parallel=$(nproc)\" \t\t\t\t\tapt-get source --compile $nginxPackages \t\t\t) \t\t\t\t\t\t&& apt-mark showmanual | xargs apt-mark auto > /dev/null \t\t\t&& { [ -z \"$savedAptMark\" ] || apt-mark manual $savedAptMark; } \t\t\t\t\t\t&& ls -lAFh \"$tempDir\" \t\t\t&& ( cd \"$tempDir\" && dpkg-scanpackages . > Packages ) \t\t\t&& grep '^Package: ' \"$tempDir/Packages\" \t\t\t&& echo \"deb [ trusted=yes ] file://$tempDir ./\" > /etc/apt/sources.list.d/temp.list \t\t\t&& apt-get -o Acquire::GzipIndexes=false update \t\t\t;; \tesac \t\t&& apt-get install --no-install-recommends --no-install-suggests -y \t\t\t\t\t\t$nginxPackages \t\t\t\t\t\tgettext-base \t&& apt-get remove --purge --auto-remove -y apt-transport-https ca-certificates && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list \t\t&& if [ -n \"$tempDir\" ]; then \t\tapt-get purge -y --auto-remove \t\t&& rm -rf \"$tempDir\" /etc/apt/sources.list.d/temp.list; \tfi"
},
{
"created": "2018-11-16T13:32:08.778195069Z",
"created_by": "/bin/sh -c ln -sf /dev/stdout /var/log/nginx/access.log \t&& ln -sf /dev/stderr /var/log/nginx/error.log"
},
{
"created": "2018-11-16T13:32:09.22115772Z",
"created_by": "/bin/sh -c #(nop) EXPOSE 80/tcp",
"empty_layer": true
},
{
"created": "2018-11-16T13:32:09.696803649Z",
"created_by": "/bin/sh -c #(nop) STOPSIGNAL [SIGTERM]",
"empty_layer": true
},
{
"created": "2018-11-16T13:32:10.147294787Z",
"created_by": "/bin/sh -c #(nop) CMD [\"nginx\" \"-g\" \"daemon off;\"]",
"empty_layer": true
}
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:ef68f6734aa485edf13a8509fe60e4272428deaf63f446a441b79d47fc5d17d3",
"sha256:876456b964239fb297770341ec7e4c2630e42b64b7bbad5112becb1bd2c72795",
"sha256:9a8f339aeebe1e8bcef322376e1274360653fb802abd4b94c69ea45a54f71a2b"
]
}
}
  1. Get Image Layers

根据 Image Manifest Layers 下载 Image Layers

1
2
3
4
5
6
curl -i -H "Authorization: Bearer $token" -H "Accept: application/vnd.docker.distribution.manifest.v2+json" "https://registry.hub.docker.com/v2/library/nginx/blobs/sha256:a5a6f2f73cd8abbdc55d0df0d8834f7262713e87d6c8800ea3851f103025e0f0"
curl -o a5a6f2f73cd8abbdc55d0df0d8834f7262713e87d6c8800ea3851f103025e0f0.tar.gz https://production.cloudflare.docker.com/registry-v2/docker/registry/v2/blobs/sha256/a5/a5a6f2f73cd8abbdc55d0df0d8834f7262713e87d6c8800ea3851f103025e0f0/data?verify=1542424303-e8ERUR8oG%2BoBh41TGIpCy7iFeYg%3D
curl -i -H "Authorization: Bearer $token" -H "Accept: application/vnd.docker.distribution.manifest.v2+json" "https://registry.hub.docker.com/v2/library/nginx/blobs/sha256:67da5fbcb7a04397eda35dccb073d8569d28de13172fbd569fbb7a3e30b5886b"
curl -o 67da5fbcb7a04397eda35dccb073d8569d28de13172fbd569fbb7a3e30b5886b.tar.gz https://production.cloudflare.docker.com/registry-v2/docker/registry/v2/blobs/sha256/67/67da5fbcb7a04397eda35dccb073d8569d28de13172fbd569fbb7a3e30b5886b/data\?verify\=1542424505-PVOE52Er6OY7iKDTx5QZSZxI99I%3D
curl -i -H "Authorization: Bearer $token" -H "Accept: application/vnd.docker.distribution.manifest.v2+json" "https://registry.hub.docker.com/v2/library/nginx/blobs/sha256:e82455fa5628738170735528c8db36567b5423ec59802a1e2c084ed42b082527"
curl -o e82455fa5628738170735528c8db36567b5423ec59802a1e2c084ed42b082527.tar.gz https://production.cloudflare.docker.com/registry-v2/docker/registry/v2/blobs/sha256/e8/e82455fa5628738170735528c8db36567b5423ec59802a1e2c084ed42b082527/data\?verify\=1542424575-kXRXCIAWm%2FXyHHfrhI1yOIPt4FA%3D
  1. Generated Image Tar Archive Description (manifest.json)

根据 Config file 及 Layers files 的组织目录结构,生成 Image Tar Archive Manifest

1
2
3
4
5
6
7
8
9
10
11
12
13
[
{
"Config": "sha256:e81eb098537d6c4a75438eacc6a2ed94af74ca168076f719f3a0558bd24d646a",
"RepoTags": [
"index.docker.io/library/nginx:latest"
],
"Layers": [
"a5a6f2f73cd8abbdc55d0df0d8834f7262713e87d6c8800ea3851f103025e0f0.tar.gz",
"67da5fbcb7a04397eda35dccb073d8569d28de13172fbd569fbb7a3e30b5886b.tar.gz",
"e82455fa5628738170735528c8db36567b5423ec59802a1e2c084ed42b082527.tar.gz"
]
}
]
  1. Tar Archive File Structure Summary
1
2
3
4
5
6
7
.
├── 67da5fbcb7a04397eda35dccb073d8569d28de13172fbd569fbb7a3e30b5886b.tar.gz --- Layer file
├── a5a6f2f73cd8abbdc55d0df0d8834f7262713e87d6c8800ea3851f103025e0f0.tar.gz --- Layer file
├── e82455fa5628738170735528c8db36567b5423ec59802a1e2c084ed42b082527.tar.gz --- Layer file
├── manifest.json --- Image Tar Archive Description
└── sha256:e81eb098537d6c4a75438eacc6a2ed94af74ca168076f719f3a0558bd24d646a --- Image Config
0 directories, 5 files

在上一遍文章中我们 get 到了 crane pull 的过程,然而好奇的我仍然有个疑问,那就是 tar archive 中并未定义了 layer 的 stack 关系,如此 docker load 如何能正确组织 image 的 fs changeset 呢 ?

docker load

https://docs.docker.com/engine/reference/commandline/load/

docker (moby)

https://github.com/moby/moby

Layer 是如何串联起来的

https://www.hi-linux.com/posts/44544.html

Study

按说理论上每层,应有镜像层 id,并且有指向 parent layer 的指针,当然基础镜像没有 parent layer

否则无法将 layer 组织起来

既然如此那在 image tar archive 中,是如何体现这种 layer stack 关系的 ?

docker save

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.
├── 2daafd635a629218204652bd3b10ddd23ae5e33abe1ebc3c26c01103e33369de
│ ├── VERSION
│ ├── json
│ └── layer.tar
├── 9fbc75679caed833594370d2effbdbba4e09eb6ee7a87f9d2e94b41627d56831
│ ├── VERSION
│ ├── json
│ └── layer.tar
├── e81eb098537d6c4a75438eacc6a2ed94af74ca168076f719f3a0558bd24d646a.json
├── ecfed8505473c79ab6831d6272a1adff68a42cd102e9992e60b2f8925df01927
│ ├── VERSION
│ ├── json
│ └── layer.tar
├── manifest.json
└── repositories
3 directories, 12 files

可见 docker save 时会将 layer 的 metadata 文件输出,查看 json 文件后发现其的确有 parent 的定义

1
2
3
4
5
{
"id": "2daafd635a629218204652bd3b10ddd23ae5e33abe1ebc3c26c01103e33369de",
"parent": "9fbc75679caed833594370d2effbdbba4e09eb6ee7a87f9d2e94b41627d56831",
"created": "2018-11-16T13:32:10.147294787Z"
}

我还做了个实验,将每层的 json, VERSION 及 repositories 文件删除,检查是否仍然能继续 docker load,呃当然是肯定的

1
2
3
4
5
6
7
8
9
10
.
├── 2daafd635a629218204652bd3b10ddd23ae5e33abe1ebc3c26c01103e33369de
│ └── layer.tar --- Layer File
├── 9fbc75679caed833594370d2effbdbba4e09eb6ee7a87f9d2e94b41627d56831
│ └── layer.tar --- Layer File
├── e81eb098537d6c4a75438eacc6a2ed94af74ca168076f719f3a0558bd24d646a.json --- Config file
├── ecfed8505473c79ab6831d6272a1adff68a42cd102e9992e60b2f8925df01927
│ └── layer.tar --- Layer File
└── manifest.json --- Image Tar Archive Description
3 directories, 5 files

docker load 一把

1
2
3
4
5
6
tar -cf nginx-new.tar *
> docker load -i nginx-new.tar
ef68f6734aa4: Loading layer [==================================================>] 58.44MB/58.44MB
876456b96423: Loading layer [==================================================>] 54.38MB/54.38MB
9a8f339aeebe: Loading layer [==================================================>] 3.584kB/3.584kB
Loaded image: nginx:latest

docker load 各 layer, 其中左侧的 hex 值为 layer sha256 hash value

可见的确未受影响,所以 docker load 并未依赖 docker save 时保留的 layer metadata 信息

1
2
3
4
5
6
7
8
9
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:ef68f6734aa485edf13a8509fe60e4272428deaf63f446a441b79d47fc5d17d3",
"sha256:876456b964239fb297770341ec7e4c2630e42b64b7bbad5112becb1bd2c72795",
"sha256:9a8f339aeebe1e8bcef322376e1274360653fb802abd4b94c69ea45a54f71a2b"
]
}

另外对于镜像 Config file 中说明的 rootfs.diff_ids, 其中的 sha256 hash 值均为各 layer 的 sha256 hash 值

1
2
3
ef68f6734aa485edf13a8509fe60e4272428deaf63f446a441b79d47fc5d17d3  ecfed8505473c79ab6831d6272a1adff68a42cd102e9992e60b2f8925df01927/layer.tar
876456b964239fb297770341ec7e4c2630e42b64b7bbad5112becb1bd2c72795 9fbc75679caed833594370d2effbdbba4e09eb6ee7a87f9d2e94b41627d56831/layer.tar
9a8f339aeebe1e8bcef322376e1274360653fb802abd4b94c69ea45a54f71a2b 2daafd635a629218204652bd3b10ddd23ae5e33abe1ebc3c26c01103e33369de/layer.tar

https://www.huweihuang.com/article/docker/docker-commands-principle/

如果你要持久化一个镜像,可以使用 docker save 指令
它与 docker export 的区别在于其保留了所有元数据和历史层
另外 docker export 用于容器,而不是镜像

docker inspect 用于查看镜像最顶层的 metadata

docker images -a 该指令用作列出镜像的所有镜像层。镜像层的排序以每个顶层镜像 ID 为首,依次列出每个镜像下的所有镜像层

docker history 查看该镜像 ID 下的所有历史镜像

Summary

对比发现,其实在 Image Tar Archive 中,layer 的 parent-child 关系实际上就是定义于镜像 Config file 中的 rootfs.diff_ids 顺序

[0] <- [1] <- [2] <- …

以 nginx:latest 为例

  1. ef68f6734aa485edf13a8509fe60e4272428deaf63f446a441b79d47fc5d17d3 (base layer)
  2. 876456b964239fb297770341ec7e4c2630e42b64b7bbad5112becb1bd2c72795
  3. 9a8f339aeebe1e8bcef322376e1274360653fb802abd4b94c69ea45a54f71a2b

这下就恍然大悟了

Creating an Image Filesystem Changeset

https://github.com/moby/moby/blob/master/image/spec/v1.md#creating-an-image-filesystem-changeset

描述了如何 Creating an Image Filesystem Changeset

每层 layers file 仅可能有如下的情况

  • add
  • update
  • deleted

例如

1
2
3
Added:      /etc/my-app.d/default.cfg
Modified: /bin/my-app-tools
Deleted: /etc/my-app-config

对于 changeset 来说,会生成如下文件

1
2
3
/etc/my-app.d/default.cfg
/bin/my-app-tools
/etc/.wh.my-app-config

.wh. i.e. without

Loading an Image Filesystem Changeset

那么我们又如何 Loading an Image Filesystem Changeset

https://github.com/moby/moby/blob/master/image/spec/v1.md#loading-an-image-filesystem-changeset

  1. 找到 the root ancestor changeset
  2. 从 root ancestor changeset 开始,逐级解压 layer’s filesystem changeset archive 到目录 (将被使用来作为 the root of a container filesystem)
    1.1 每层解压之后再遍历一次目录,删除已被标记删除的目录 removing any files with the prefix .wh. and the corresponding file or directory named without this prefix

Owner and Group

另外尝试改变文件属主

changeset 也算作文件 update

untar 的时候注意 --same-owner

这里有个新问题,就是 docker load 是如何处理 Image Filesystem Changeset 中的属主的

实际测试得需要 root 用户 tar --same-owner -xvf 才行, 解压出来的属主和 group 也仅为 id 值,毕竟宿主机上不一定有该 owner 和 group

1
2
3
4
5
ash-3.2$ ls -al
total 0
drwxr-xr-x 3 zrss staff 102 11 17 19:24 .
drwxr-xr-x 10 zrss staff 340 11 17 18:39 ..
drwxr-xr-x 13 101 101 442 11 17 19:25 var

101 nginx

Permission

改变文件权限

changeset 也算作文件 update

直接解压即可,可以保留原权限

Scan Image Tar Archive

业界做法扫描 layer,

https://docs.docker.com/ee/dtr/user/manage-images/scan-images-for-vulnerabilities/

而不是将 layer combine 成 container root fs 之后,再全文件扫描

当然可能因为是病毒扫描,这样做比较简单

话说有没有必要组成 root fs 之后再扫描呢,因为毕竟可能之前 layer 的漏洞,在下一 layer 被修复了,感觉可能是会误报的 ? 细节上不知道可以如何实现

倒是可以看下 coreos clair 是如何实现的

🙄 其实也是一样的,把 layer 解压之后,扫文件,比对数据库

Summary

其实是有点儿疑惑的, 业界镜像扫描解决方案 (当然是针对病毒扫描) 都是直接扫描 image layer

暂未发现有按照 Loading an Image Filesystem Changeset 描述的过程那样,挂载出 container root fs 之后,再扫描的解决方案

当然描述的过程感觉其实只是好理解,实际上 dockerd 再组织镜像 root fs 时,是需要根据不同的 storage driver 的实现,调用不同的命令实现的挂载 (或者换一个说法,storage driver 本质上实现了描述的过程 …)

  1. overlay2

https://terriblecode.com/blog/how-docker-images-work-union-file-systems-for-dummies/

1
2
3
4
5
6
mkdir base diff overlay workdir
sudo mount \
-t overlay \
-o lowerdir=base,upperdir=diff,workdir=workdir \
overlay \
overlay

这哥们没讲太细

  1. aufs

https://coolshell.cn/articles/17061.html?spm=a2c4e.11153940.blogcont62949.21.53a61eearfeDBm

有文件删除的话,在可写层放个 .wh.[file-name],文件就被隐藏了。和直接 rm 是一样的

  1. devicemapper

https://coolshell.cn/articles/17200.html?spm=a2c4e.11153940.blogcont62949.22.53a61eearfeDBm

描述如何用 devicemapper 实现 layers 挂载成 union file system 的,各层可以通过 devicemapper 的 snapshot 技术实现,对用户来说就是单一的 fs

0%