最近接触了一些 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

最近看了看 knative-build-controller 中使用的 workqueue,不得不说是个设计复杂

而且对于 controller 来说,非常好用的一个东西。因为 controller 中会使用到 informer,而 informer 产生的待 reconcile key 可以放入 workqueue 中,等待 controller 逐一处理

workqueue 的实现是由 client-go https://github.com/kubernetes/client-go 库提供的,目录位于 util/workqueue

其中 knative-build-controller 使用的为 rate_limitting_queue.go,也就是通过

1
2
3
4
5
6
func NewRateLimitingQueue(rateLimiter RateLimiter) RateLimitingInterface {
return &rateLimitingType{
DelayingInterface: NewDelayingQueue(),
rateLimiter: rateLimiter,
}
}

创建出来的 queue

使用 default_rate_limiters.go,也就是通过

1
2
3
4
5
6
7
func DefaultControllerRateLimiter() RateLimiter {
return NewMaxOfRateLimiter(
NewItemExponentialFailureRateLimiter(5*time.Millisecond, 1000*time.Second),
// 10 qps, 100 bucket size. This is only for retry speed and its only the overall factor (not per item)
&BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
)
}

创建出来的 limiter,用来计算限流时间的,会取 NewItemExponentialFailureRateLimiterBucketRateLimiter 中限流时间的大者

咱们重点看 queue 的实现

RateLimitingQueue 提供了 rate limit (限流) 的功能,而我们知道 knative-build-controller (当然 k8s 中许多其他的 controller 也是如此模型) 的处理模型为循环从 queue 中获取 item,并 reconcile 之

从 api 来看,rate_limitting_queue 组合了 DelayingInterface 接口,并提供了

  • AddRateLimited
  • Forget
  • NumRequeues

方法

其中 DelayingInterface 接口又组合了 Interface 接口,并提供了

  • AddAfter

方法

最后 Interface 接口提供基本的 queue 功能

  • Add
  • Len
  • Get
  • Done
  • Shutdown
  • ShuttingDown

RateLimittingQueue

限流队列

AddRateLimited

实际上是调用了限流算法,根据重试次数计算出当前限流时间,在该时间之后,再将 item 加入 queue 中

1
2
3
func (q *rateLimitingType) AddRateLimited(item interface{}) {
q.DelayingInterface.AddAfter(item, q.rateLimiter.When(item))
}

因此这个实现依赖于 DelayingQueue 的 AddAfter 方法

Forget

这个方法有点儿特殊,Forget 是啥语义?忘了?我 item 好端端的,为啥要忘了我

😂 一开始我也是一脸懵逼,不过想想呐,咱们调用了 AddRateLimited,而这个方法在计算限流时间时,需要使用到 item 的重试次数的,没有这个重试次数,当然计算限流时间就没有意义了

所以 Forget 呢,实际上是清除 item 的重试次数,这样下次再将这个 item AddRateLimited 时,就不会受限流的影响了

Forget 在 ItemExponentialFailureRateLimiter 中的实现

1
2
3
4
5
func (r *ItemExponentialFailureRateLimiter) Forget(item interface{}) {
r.failuresLock.Lock()
defer r.failuresLock.Unlock()
delete(r.failures, item) // 从重试统计次数表中删除 item
}

以及计算限流时间的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (r *ItemExponentialFailureRateLimiter) When(item interface{}) time.Duration {
r.failuresLock.Lock()
defer r.failuresLock.Unlock()
exp := r.failures[item] // 当 item 不在表中时,exp 为 0
// 将重试次数设置为 1,也就是每次递增 1
r.failures[item] = r.failures[item] + 1
// 使用指数算法计算限流时间
backoff := float64(r.baseDelay.Nanoseconds()) * math.Pow(2, float64(exp))
if backoff > math.MaxInt64 {
return r.maxDelay
}
calculated := time.Duration(backoff)
if calculated > r.maxDelay {
return r.maxDelay
}
return calculated
}

DelayingQueue

如此我们首先查看 DelayingInterface 接口的实现之一 delaying_queue.go

我们看,这个 queue 想实现如何的功能?

AddAfter ? 所谓的在 n time 之后的再将 item 加入 queue 的功能

为了实现这个功能,首先要考虑 AddAfter 是同步的,亦或是异步的方法 ?

当前实现是异步的方法

AddAfter

计算出 readyAt 时间,将该时间与 item 一并存入 waitingForAddCh,这个 ch 的大小为 1000,也就是说未达到 1000 时,AddAfter 是不会被阻塞的

细心的同学可能会问,如果 AddAfter 的时间为 0 甚至为负怎么办,当然这种情况直接加入 queue 即可,就不需要再加入 waitingForAddCh 了

waitingLoop

当然为了实现 AddAfter 这个功能,免不了 queue 需要做一些额外的维护事情,最重要的就是 queue 初始化时,开始用协程执行 waitingLoop 方法

这个方法是实现 delaying_queue 功能的核心逻辑

注意到待加入 queue 的 item 位于 waitingForAddCh 中,waitingLoop 当可以从 waitingForAddCh 获取到 item 时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
case waitEntry := <-q.waitingForAddCh:
if waitEntry.readyAt.After(q.clock.Now()) {
insert(waitingForQueue, waitingEntryByData, waitEntry)
} else {
q.Add(waitEntry.data)
}
drained := false
for !drained {
select {
case waitEntry := <-q.waitingForAddCh:
if waitEntry.readyAt.After(q.clock.Now()) {
insert(waitingForQueue, waitingEntryByData, waitEntry)
} else {
q.Add(waitEntry.data)
}
default:
drained = true
}
}
}

首先会判断这个 item 是否可以加入 queue 了,如果时候还没到,那么将该 item 加入以 readyAt 为排序关键的优先队列中。若时候到了,则加入 queue。处理完第一个 item 之后,会将 waitingForAddCh 中剩余的 item 均按照相同的逻辑处理之

这个 item 加入优先队列时,还有一个讲究的地方,注意到下述代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// insert adds the entry to the priority queue, or updates the readyAt if it already exists in the queue
func insert(q *waitForPriorityQueue, knownEntries map[t]*waitFor, entry *waitFor) {
// if the entry already exists, update the time only if it would cause the item to be queued sooner
existing, exists := knownEntries[entry.data]
if exists {
if existing.readyAt.After(entry.readyAt) {
existing.readyAt = entry.readyAt
heap.Fix(q, existing.index)
}
return
}
heap.Push(q, entry)
knownEntries[entry.data] = entry
}

如果加入的 item 已经存在,并且新加入 item 的 readyAt 时间比已经存在的 item 的时间晚,那么不好意思哈,这个 item 会被直接丢弃。只有新加入的 item 的 readyAt 时间比已存在的 item 时间要早,才会更新已存在的 item 的 readyAt 时间,并调整 item 在优先队列中的位置

所以看到这里,delaying_queue 实际上还有去重功能

回到 waitingLoop 的 loop 来,loop 首先要执行的操作,即为从优先队列中依次 Peek item,即不断从优先队列中取出第一个 item,这个 item 最有可能到触发时间了

1
2
3
4
5
6
7
8
9
10
// Add ready entries
for waitingForQueue.Len() > 0 {
entry := waitingForQueue.Peek().(*waitFor)
if entry.readyAt.After(now) {
break
}
entry = heap.Pop(waitingForQueue).(*waitFor)
q.Add(entry.data)
delete(waitingEntryByData, entry.data)
}

显然如果到达了 Add 时间,那么就将其加入 queue 中,并执行一些清理工作。若终于遍历到未到触发时间的 item 了,这个时候可以退出遍历优先队列的循环了

因为这个时候,所有当前可以 Add queue 的 item 都已经处理完了,所以接下来,可以执行一段优化的逻辑,加快 item 的处理

1
2
3
4
5
6
// Set up a wait for the first item's readyAt (if one exists)
nextReadyAt := never
if waitingForQueue.Len() > 0 {
entry := waitingForQueue.Peek().(*waitFor)
nextReadyAt = q.clock.After(entry.readyAt.Sub(now))
}

这个有点意思,都处理完了是吧,那我选优先队列中的第一个 item,用它的 readyAt 时间设置一个定时器,这样的话,一旦到达需要 Add 的时间,waitLoop 就会处理了 (当然用户态的程序,定时上都有些许偏差,不会特别特别精确)

都准备好之后,waitLoop 就进入等待数据/事件的逻辑了

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
select {
case <-q.stopCh:
return
case <-q.heartbeat.C():
// continue the loop, which will add ready items
// 这里是心跳,所谓的最大等待时间,目前是 10s,即如果 10s 内啥都没发生的话,也会执行一般 waitLoop 中的循环逻辑
case <-nextReadyAt:
// continue the loop, which will add ready items
// 这里是优先队列的第一个 item 的定时器触发了
case waitEntry := <-q.waitingForAddCh:
// 这是我们说的,首先判断第一个 item 是否可以加入 queue
if waitEntry.readyAt.After(q.clock.Now()) {
insert(waitingForQueue, waitingEntryByData, waitEntry)
} else {
q.Add(waitEntry.data)
}
// 接下来会处理仍然处于 waitingForAddCh 中的 item,与上述逻辑一致
drained := false
for !drained {
select {
case waitEntry := <-q.waitingForAddCh:
if waitEntry.readyAt.After(q.clock.Now()) {
insert(waitingForQueue, waitingEntryByData, waitEntry)
} else {
q.Add(waitEntry.data)
}
default:
drained = true
}
}
}

ok,至此 delaying_queue 的要点就说完了,下面提几点使用的时候的注意点,避免踩坑

  • AddAfter 为异步方法,所以现象为调用 AddAfter 之后,item 并不会立即被加入到 queue 中
  • AddAfter 会做去重处理,在 queue 中依然有相同的 item 时,如果新加入 item 的 readyAt time 靠后的话,新加入的 item 会被丢弃

Queue

代码实现位于 queue.go 中

好了,在经过 DelayQueue 的延时加入策略之后,最终 item 还是被加入 queue 中的,而 queue 的实现也多有讲究,来一探究竟吧

queue 内部有两个重要的数据结构

  • processing set
  • dirty set

有一个比较特殊的方法

  • Done

首先来看一下 queue 的 Add 方法

Add

代码不多,直接上了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (q *Type) Add(item interface{}) {
// cond 加锁
q.cond.L.Lock()
defer q.cond.L.Unlock()
if q.shuttingDown {
return
}
// 如果在 dirty set 中,那么该 item 被忽略
if q.dirty.has(item) {
return
}
q.metrics.add(item)
// dirty set 标记
q.dirty.insert(item)
// 如果当前 item 仍然在处理中,则被忽略
if q.processing.has(item) {
return
}
// 加入 queue
q.queue = append(q.queue, item)
// 通知 cond lock 的协程
q.cond.Signal()
}

Get

queue 的出口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (q *Type) Get() (item interface{}, shutdown bool) {
// cond 加锁
q.cond.L.Lock()
defer q.cond.L.Unlock()
// 如果 queue 为空,并且 queue 未关闭,则等待
// 即 Get 方法在这种情况下,是阻塞的
for len(q.queue) == 0 && !q.shuttingDown {
q.cond.Wait()
}
// 这种情况是 queue 关闭的时候
if len(q.queue) == 0 {
// We must be shutting down.
return nil, true
}
// 取 queue 中第一个 item 返回
item, q.queue = q.queue[0], q.queue[1:]
q.metrics.get(item)
// 标记 item 为处理中
q.processing.insert(item)
// 去除 item dirty 标记
q.dirty.delete(item)
return item, false
}

看了 Add 与 Get 实现后,我们得到几个结论

  • queue 也实现了去重: Add 相同 item,若该 item 未被 Get,那仅会被加入 queue 一次
  • queue.Get 方法在 queue 中没数据时,是阻塞的,即你可以这写
1
2
3
4
5
for {
obj := queue.Get()
// obj 一定存在
// blabla
}

Done

1
2
3
4
5
6
7
8
9
10
func (q *Type) Done(item interface{}) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
q.metrics.done(item)
q.processing.delete(item)
if q.dirty.has(item) { // 如果 item 被标记为 dirty,则重新加入 queue 中
q.queue = append(q.queue, item)
q.cond.Signal()
}
}

Done 方法会去掉 item 的 processing 标记,并且如果 item 被标记为 dirty,那会再将 item 加入 queue。有这个逻辑那我们应该如何使用 queue ?

我们首先来考虑这种场景

itemA 被 Add queue,此时 X 协程 Get 时,获取到了 itemA,在这种情况下,itemA 被标记为 processing,而没有 dirty 的标记,若此时另一 Y 协程又再次调用 Add 方法将 itemA Add queue,这时 itemA 就被标记为 dirty 了,并且因为有 processing 标记,所以并不会被加入 queue 中

过了一些时间后,X 协程执行 ok 了,调用 Done(itemA) 方法,去除 itemA 的 processing 标记,因为 itemA 为 dirty,所以将其重新加入 queue 中,等待被 Get 并处理

所以在这种场景下就好理解多了,实际上 queue 还有个特性,即 queue 中不会有重复的 item,但是仅允许 item 被 Get 之后,Done 之前,被 Add 一次,在这种情况下,Done 的时候,会重新将 item 加入 queue 中

可以理解为如果这个 item 正在被处理时,queue 允许至多缓存一次相同的 item

所以再总结一次 queue 的特性

  • 添加 item 调用 Add 方法
  • 获取 item 调用 Get 方法
  • 处理 item 之后调用 Done 方法。否则再次 Add 相同 item 时,若该 item 仍未被 Get 则直接被忽略。若该 item 已被 Get,则被打上 dirty 标记,在其被调用 Done 时,该 item 才会被重新加入 queue 中
  • 本质上 queue 中不会有重复的 item

Summary

在看了这几种 queue 的实现之后,是否更了解 rate_limmiting_queue.go 该如何使用了?

例如在 knative-build-controller 中它被如此初始化 (天下代码一大抄)

1
workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Builds"),

具体使用时

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
func (c *Controller) processNextWorkItem() bool {
// 从 queue 中取 item
obj, shutdown := c.workqueue.Get()
if shutdown {
return false
}
if err := func(obj interface{}) error {
// 处理结束之后,需要调用 Done
defer c.workqueue.Done(obj)

key, ok := obj.(string)
if !ok {
c.workqueue.Forget(obj) // Fatal 错误,调用 Forget,没有重试的必要
runtime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj))
return nil
}

if err := c.syncHandler(key); err != nil {
// 处理失败时,不调用 Forget,增加 item 的重试次数
return fmt.Errorf("error syncing '%s': %s", key, err.Error())
}
// 处理成功调用 Forget,清除 item 的重试次数,使得下次相同的 item 不受 rate limit 影响
c.workqueue.Forget(obj)
c.logger.Infof("Successfully synced '%s'", key)
return nil
}(obj); err != nil {
runtime.HandleError(err)
}
return true
}

所以再说一遍浓缩用法

  • 添加 item 调用 Add 方法
  • 获取 item 调用 Get 方法
  • 处理 item 之后调用 Done 方法
  • 不增加 item 重试次数调用 Forget 方法

再说一遍 rate limit queue 重点,切莫踩坑

  • Add 是异步方法
  • Add 有去重功能
    • 先经过 DelayQueue 去重处理,对于新加入的 item,在其优先队列中依然有相同的 item 时,如果新加入 item 的 readyAt time 较原 item 的 readyAt 时间靠后的话,新加入的 item 会被丢弃
    • 再经过 Queue 去重处理,如果 queue 中有相同 item 则直接被丢弃。若 queue 中没有相同 item,但是 item 处于被处理中,即未被调用 Done 时,会将 item 标记为 dirty,待 item 被调用 Done 时,重新加入 queue
  • 处理 item 结束之后,无论如何调用 Done,标识该 item 已被处理结束
  • 若不需要增加 item 的重试次数,则结束之后调用 Forget 方法,清除该 item 的重试次数统计
  • 如果需要调用 Forget,则先调用 Forget 再调用 Done,确保再次 Add 的时候不受限流影响

之所以关注到这个问题,是因为在写 build-controller 一个 bugfix 的 ut 时,各种坑,遂研究了下 workqueue 的细节,关于这个 bugfix 的讨论看这个链接 Timeout of build may have problem

Thanks for your time 😁

占个坑,还是在 ut 的时候被坑到了,后续有时间补上,这个我认为 k8s 最成功的地方

不是容器调度,而是这套基于 etcd 抽象出来的 api 😃

  • informer
  • lister

这 informer 呀,其实就是提供 add/update/create 回调的封装 (所谓 informer)

而这 lister 呢,是对象 cache,从 lister 中可以获取到对象。lister 由 informer 提供。

其实还挺自然的,想想 informer 回调 add/update/create 的时候,基本思路

  • list-watch kube-apiserver
  • watch 到对象变化,根据 cache 计算 diff,然后相应回调 add/update/create

所以它是需要 lister 这样的 local cache 的

所以如果自己要实现一个 k8s style 的 controller,需要使用到 informer 时,首先初始化 informer,初始化 ok 之后,如果需要查对象,那可以通过 informer 的 lister 来获取

0%