导入docker源码(docker基础教程)
本文目录一览:
怎样将Docker容器中的文件导入到主机
从Docker容器内拷贝文件到主机上
docker cp :/file/path/within/container /host/path/target
从主机上拷贝文件到容器内
参考自:
1.用-v挂载主机数据卷到容器内
docker run -v /path/to/hostdir:/mnt $container
在容器内拷贝
cp /mnt/sourcefile /path/to/destfile
2.直接在主机上拷贝到容器物理存储系统
A. 获取容器名称或者id :
$ docker ps
B. 获取整个容器的id
$ docker inspect -f '{{.Id}}' 步骤A获取的名称或者id
C. 在主机上拷贝文件:
$ sudo cp path-file-host /var/lib/docker/aufs/mnt/FULL_CONTAINER_ID/PATH-NEW-FILE
或者
$ sudo cp path-file-host /var/lib/docker/devicemapper/mnt/123abc/rootfs/root
例子:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d8e703d7e303 solidleon/ssh:latest /usr/sbin/sshd -D cranky_pare
$ docker inspect -f '{{.Id}}' cranky_pare
or
$ docker inspect -f '{{.Id}}' d8e703d7e303
d8e703d7e3039a6df6d01bd7fb58d1882e592a85059eb16c4b83cf91847f88e5
$ sudo cp file.txt /var/lib/docker/aufs/mnt/**d8e703d7e3039a6df6d01bd7fb58d1882e592a85059eb16c4b83cf91847f88e5
3.用输入输出符
docker run -i Ubuntu /bin/bash -c 'cat /path/to/container/file' /path/to/host/file/
或者
docker exec -it bash -c 'cat /path/to/container/file' /path/to/host/file/
如何进行Docker源码调试
经过研究docker的官方编译脚步,发现本地编译也很简单,只需要在docker源码的目录下执行如下命令即可:
./hack/make.sh binary
上面这条命令就只会生成docker的二进制文件,不过肯定不会这么顺利的,执行这个命令你就会发现错误。如果第一次执行报的错误应该是找不到相应的go依赖包。那么现在就开始解决第一个问题,go依赖包。
解决go依赖包最直接的方法就一个一个去github或者其他地方去下载到本地,但是这样做很麻烦,docker依赖的go语言包很多,然后依赖包可能又依赖其他包。这里有一个简单实用的办法,也是go语言管理项目的方便之处。通过go get命令来自动下载,例如发现报错的是docker某一个目录下的依赖包,那么可以如下执行:
go get -v ./src/github.com/docker/docker/...
这条命令执行以后整个docker目录下源文件依赖的包都会被自动下载。如果发现其他目录下源文件也报同样的错误,可以按照次方法解决。不过这里需要强调一点, 这些下载都是会下载最新的包,如果编译老的docker肯定会出问题 ,如果编译最新的docker代码肯定不会有问题,因为官方的编译是这种方式。
上面执行的命令都是建立在go语言环境建立成功的基础上,我安装的go遇到是1.3.3版本的,采用源码方式安装。安装在/export/servers/go下面,然后所有的go语言工程源码目录放在 /export/servers/gopath。然后配置环境变量在用户的根目录下的.bashrc文件里面如下:
export GOPATH=/export/servers/gopath
export GOROOT=/export/servers/go
export GOARCH=amd64
export GOOS=linux
如何从Docker Registry中导出镜像
一、目录结构
Registry的配置文件中可以指定registry的运行目录(实验用本地文件系统作为后端存储),registry会在这个目录中建立相应的目录结构,我在本地启动一个registry服务,然后只push一个centos镜像上去。镜像名称是localhost:5000/library/centos:latest,然后registry在本地创建了如图1所示的目录。
图 1 registry目录
为了显示方便,我只截取了64位ID的前一部分。可以看到,目录大体分为两个:一个是blobs,一个是repositories。blobs中主要存放数据文件,可以看出都是经过sha256计算后的ID。repositories目录中放镜像的描述信息,记录了一个镜像有哪些layer,tag对应的manifest文件,link文件是一个文本文件,内容是一个形如“sha256:cf34a09a90b54c…”的64位ID,这个ID对应在blob中的文件其实就是这个image的manifest文件。
二、Manifest文件
manifest文件描述了一个镜像的元信息,包括了layer的数据ID,layer的配置等,文件格式是json形式的文本文件。
docker镜像可以分为V1和V2,在1.9以后镜像格式有一些变化。为了向前兼容V1版本的docker,docekr
registryV2使用的manifest也对应地分为Schema1和Schema2,两者可以通过官方对于manifest的解释可以参考[1]和[2]。这里实验都是在schema1上做的。
Schema1主要包含如下信息:
name:image的仓库(repository)名,比如localhost:5000/library/centos:latest这个镜像的repository name是library/centos
tag:该镜像的tag
architecture:指该镜像的宿主机的操作系统架构,如“amd64”
fsLayers:该字段是一个数组,数组中的元素分别指明了各层对应的数据文件的sha256ID,数组的第1个就是镜像的最顶层,第2个是次顶层…以此类推,值得注意的是,不同层的fsLayer
ID
有可能一样,是因为有些层是空的,只有一些配置信息。当执行了一个不涉及文件操作的命令,这时候就会形成空fsLayer,空fsLayer计算出来的sha256ID也都是一样的了。镜像的一个layer,是由文件系统(比如新增的文件)fsLayer以及配置信息构成的,layer在docker的代码层面又被称为image,因为任意一个layer都可以作为顶层layer,被docker
image信息引用,从而成为一个image。所以需要区分fsLayer与layer。
history:该字段也是一个数组,是为了兼容v1而设置的,指明了每个layer的配置信息,数组第一项对应的是镜像的最顶层,与fslayer一起构成了一个layer。数组元素是一个json格式的map对象,key为“v1Compatibility”,值为一个字符串,该字符串就是layer的配置信息,可以直接用json.Unmrashal成为一个V1Image结构体(定义可以参考代码github.com/docker/docker/image/image.go
L31)
schemaVersion:该manifest的版本,一个int型,如 1。
三、Tar包形式的镜像
Docker中有个save和load命令。save命令可以将一个docker镜像导出,把这个镜像从最顶层到最底层的所有layer一起导出到一个tar包中,然后就可以随意拷贝、发送这个tar包到别的机器,最后可以用load命令把这个镜像重新加载进docker。
如果我们把一个镜像从registry里拿出来,按照save成的tar包格式来组织,然后使用load命令加载,这样就实现了不通过docker pull命令来下载镜像,可以根据这个原理做第三方镜像下载工具。
所以我们先来看一下镜像的tar包形式是什么样的,我使用save命令导出了centos镜像,解压后目录结构如图2所示:
图 2 镜像tar包解压后的目录
下面对各文件进行解释:
根目录下的repositories文件,描述了这个镜像的名字,tage,还有顶层layer的id
不同的文件夹代表了不同的layer。
json:layer的配置信息,如创建时间,执行命令等。
layer.tar:layer中包含的文件,如果是空layer,layer.tar解压后就是空的。
VERSION: 版本信息。
四、从registry导出镜像
我们对比tar包中的文件和registry中的文件,不难发现其中的对应关系,json、VERSION还有repositories文件都是可以从manifest中导出。
json文件其实就是之前提到的history字段中v1Compatibilitiy,不同的是manifest中的这个字段中有很多转义符,我们需要去掉这些转义符,方法是先Unmarshal成为一个V1Image结构体,然后在json.Marshal转回字符串就好。
layer.tar其实就是blobs中对应的data文件,直接复制出来然后改个名字就可以。
VERSION是manifest中的schemaVersion。
repositories文件内容很简单,格式是{“imageName”:{“tag”:”topLayerID”}},所以按照这个格式从manifest中找到对应的数据填进去就ok。
这些文件都准备好了以后,就可以准备打包成tar包了,直接使用linux中自带的tar命令,这里需要注意的一点是,应该使用“只打包不压缩”的选项。生成tar包后就可以直接使用docker load命令导入了。
我这么做了,是可以成功地导入一个镜像。但是发现存在一个问题:使用上述方式导入的镜像,每个layer的ID和我直接用docker
pull命令下载下的不一样,而且docker
pull得到的ID从未在manifest和registry中的任何地方出现过。而且不管我使用新的机器还是重新pull,得到的ID都是一样的。经过阅读docker的代码我才发现,layerID不是随机生成的,也不是manifest中写道的id,而是算出来的。下面就说一下计算过程。
我们最终需要的layerID在docker源码中叫做StrongID,StrongID是把一个byte数组做Hash后得到的,这个byte数组的生成需要三个对象:v1Compatibility,blobSum(manifest中的fsLayerID),parent(父layer的StrongID),数组生成方法参考image.go中的MakeImageConfig方法(docker
1.9),基本操作就是把一个json对象转成字节数组。因为有parent字段的存在,需要从最底层的layer开始计算,逐步迭代,最终的到top
layer的ID。最后要做的工作就是替换json文件中的id字段和parent字段成为新计算出来的ID即可。同样地,文件夹名也要做相应改变。
增量导入:如果本地已经存在某些layer的情况,我们只用打包新的layer即可,因为导入时候docker会检测这个layer是否存在,而且有parent信息来保证layer之间的关系。
怎样进入docker镜像导入文件
只能先使用以后的镜像创建容器,然后进入运行的容器后,进行相应的修改,在使用修改后的容器进行镜像的生成,这样,生成后的镜像与之前那个镜像的区别就是,你的修改内容。
如何编译Docker源码
本文根据docker官方给出的docker代码编译环境搭建指南做更深入的分析。官方给出的指导比较简单,但是由于国内的网络问题经常会编译失败,了解了编译步骤后,也可以结合自身遇到的网络问题进行“规避”。
docker的编译环境实际上是创建一个docker容器,在容器中对代码进行编译。 如果想快速的查看编译环境搭建指导,而不关注环境搭建的机制和细节,可以直接跳到最后一章“总结”。
前提
机器上已经安装了docker,因为编译环境是个docker容器,所以要事先有docker(daemon),后面会创建个编译环境容器,在容器里面编译代码。本文中使用物理机,物理机上运行着docker (daemon)。
机器(物理机)上安装了git 。 后续使用git下载docker源码
机器(物理机)上安装了make。
下载ubuntu 14.04的docker镜像
下载docker源码
git clone
会把代码下载到当前目录下,后面会把代码拷贝到容器中。
编译前分析
官方给的编译方法是make build 和 make binary等。下面先分析Makefile,看懂Makefile后,编译环境的准备流程就比较清楚了。
Makefile
在下载的docker源码中可以看到它的Makefile,Makefile中比较关键的几个参数:
DOCKER_MOUNT := $(if $(BIND_DIR),-v "$(CURDIR)/$(BIND_DIR):/go/src/github.com/docker/docker/$(BIND_DIR)") DOCKER_MOUNT 表示创建容器时的mount参数。因为编译环境是一个容器,在后续的步骤中启动容器时使用DOCKER_MOUNT参数,会将物理机上的目录mount给容器容器,容器中该目录是编译生成docker二进制文件的目录。
DOCKER_FLAGS := docker run --rm -i --privileged $(DOCKER_ENVS) $(DOCKER_MOUNT) 这是后面创建docker容器时的命令行的一部分,其中包含了前面的DOCKER_MOUNT参数。
DOCKER_IMAGE := docker-dev$(if $(GIT_BRANCH),:$(GIT_BRANCH)) 这是docker image参数,镜像的名字是docker-dev,以当前git中docker版本作为tag名。这个镜像是在make build一步做出来的。
DOCKER_RUN_DOCKER := $(DOCKER_FLAGS) "$(DOCKER_IMAGE)" 创建docker容器的命令行,组合了前面的DOCKER_FLAGS 和 DOCKER_IMAGE 。 从命令行中可以看出,启动容器使用的参数有 --rm -i --privileged,使用了一些环境变量,还有使用了-v参数把物理机上目录mount给容器,在容器中编译好二进制文件后放到该目录中,在物理机上就能获得docker二进制文件。启动的的docker 容器镜像名字是docker-dev。下文会介绍docker-dev镜像是怎么来的。
由于官方给出的“构建编译环境”的方法是执行 make build,下面在Makefile中看到build分支是这样的:
make build时会调用 docker build -t "$(DOCKER_IMAGE)" . 去制作一个叫做DOCKER_IMAGE的镜像。
进行源码编译的方式是执行 make binary来编译代码,在Makefile中make binary的分支如下:
make binary除了进行 make build以外,会执行$(DOCKER_RUN_DOCKER),即上文提到的docker run命令行。由于执行过了build,会build出来docker-dev镜像,所以在docker run时直接使用前面build出来的镜像。docker run时的命令行参数是hack/make.sh binary。make binary的过程实际上是创建一个容器,在容器中执行hack/make.sh binary脚本。接下来会详细介绍make build和make binary所做的内容。
make build
根据官方的指导,先执行make build来搭建编译环境。上面分析了,make build实际上是制作了一个镜像,这个镜像里会包含编译代码所需的环境。下面来介绍下这个镜像。
Dockerfile
在和Makefile相同的目录下(源码的根目录),有Dockerfile。执行make build 相当于调用docker build,使用的就是该Dockerfile。Dockerfile中的几个主要步骤(有些步骤这里略过):
FROM ubuntu:14.04 使用ubuntu 14.04作为基础镜像;在宿主机上,要事先下载好ubuntu 14.04镜像。
安装一些编译需要的软件;
用git下载lvm2源码,并编译安装;
下载并安装GO 1.5.1;
安装GO相关的tools 可以做code coverage test 、 go lint等代码检查
安装registry和notary server;
安装docker-py 后面跑集成测试用的
将物理机的contrib/download-frozen-image.sh 脚本拷贝到镜像中/go/src/github.com/docker/docker/contrib/
运行contrib/download-frozen-image.sh 制作镜像 实际上这一步只是下载了3个镜像的tar文件。注意:docker build相当于创建一个临时的容器(在临时的容器中执行Dockerfile中的每一步,最后在保存成镜像),“运行contrib/download-frozen-image.sh 制作镜像”这个动作出现在Dockerfile中,相当于在docker build所创建的临时的容器中下载docker镜像,有docker-in-docker容器嵌套的概念。下一小节会对download-frozen-image.sh脚本做详细分析。
ENTRYPOINT ["hack/dind"] 做出来的镜像,使用它启动的容器可以自动运行源码目录中的hack/dind脚本。 dind这个脚本是a wrapper script which allows docker to be run inside a docker container 。后面的小节会对hack/dind脚本做详细的分析。
COPY . /go/src/github.com/docker/docker 把物理机上的docker源码文件打入到镜像中
download-frozen-image.sh脚本
上一小节里提到,在Dockerfile中,有一步会调用contrib/download-frozen-image.sh ,它主要作用是下载3个镜像的tar包,供后续docker load。在Dockerfile中的调用方式如下:
download-frozen-image.sh脚本中会依次解析参数,其中/docker-frozen-images作为base dir,后面下载的东西全放到这里。之后的3个参数是镜像,里面包含了镜像名(例如busybox)、镜像tag(例如latest)、镜像id(例如d7057cb020844f245031d27b76cb18af05db1cc3a96a29fa7777af75f5ac91a3),后面会在循环中依次下载这3个镜像的tar文件。
download-frozen-image.sh脚本中会通过curl从registry上获取如下信息:
token:获取token,后面curl获取的其他信息时都需要使用token。例如本例中 token='signature=9088f0552b1b147364e07bdd48857dd77c0d94ee,repository="library/busybox",access=read'
ancestryJson:把镜像相关联的历史层次的id也都获取到,因为每一层的tar都需要下载。本例中 ancestryJson='["d7057cb020844f245031d27b76cb18af05db1cc3a96a29fa7777af75f5ac91a3", "cfa753dfea5e68a24366dfba16e6edf573daa447abf65bc11619c1a98a3aff54"]'
这里可以看到这个镜像只有2层,两层的id这里都列了出来。 每个镜像包含的层数不同,例如。第三个镜像jess/unshare共有10层。
VERSION、json、tar: 每一层镜像id的目录下,都下载这3个文件,其中VERSION文件内容目前都是“1.0”,json文件是该层镜像的json文件,tar文件是该层镜像的真正内容,以.tar保存。
下载好的各层镜像目录结构如下:
$ls
$tree
hack/dind脚本
在Dockerfile中,ENTRYPOINT ["hack/dind"] ,表示在镜像启动后,运行该脚本,下面分析一下这个脚本的功能。
脚本在代码根目录下的hack目录中,作者对脚本的描述是 DinD: a wrapper script which allows docker to be run inside a docker container.
就是可以在docker容器中创建docker容器。它就做了一个事,那就是在容器中创建好cgroup目录,并把各个cgroup子系统mount上来。
为了方便理解,我们可以先看看物理机。在宿主机上如果创建docker容器,需要宿主机上必须事先mount cgroup子系统,因为cgroup是docker容器的一个依赖。同理docker-in-docker也要求外层的docker容器中有cgroup子系统,dind脚本在容器启动后,先去/proc/1/cgroup中获取cgroup子系统,然后依次使用mount命令,将cgroup mount上来,例如mount -n -t cgroup -o "cpuset" cgroup "/cgroup/cpuset"
最终在运行make build后,会制作出一个叫docker-dev的镜像。
make binary
执行make binary 就可以编译出docker二进制文件。编译出来的二进制文件在源码目录下的bundles/1.10.0-dev/binary/docker-1.10.0-dev ,其中还包含md5和sha256文件。
Makefile中的binary
Makefile中关于make binary流程是
先执行build,即上一节介绍的,制作docker-dev编译环境镜像。
再执行DOCKER_RUN_DOCKER,创建容器,DOCKER_RUN_DOCKER就是执行docker run,使用docker-dev镜像启动容器,并且会mount -v 将容器生成二进制文件的路径与宿主机共享。DOCKER_RUN_DOCKER在“编译前分析”一章中有介绍。启动的容器运行的命令行是 hack/make.sh binary 。docker run完整的形式如下:
docker run --rm -i --privileged -e BUILDFLAGS -e DOCKER_CLIENTONLY -e DOCKER_DEBUG -e DOCKER_EXECDRIVER -e DOCKER_EXPERIMENTAL -e DOCKER_REMAP_ROOT -e DOCKER_GRAPHDRIVER -e DOCKER_STORAGE_OPTS -e DOCKER_USERLANDPROXY -e TESTDIRS -e TESTFLAGS -e TIMEOUT -v "/home/mubai/src/docker/docker/bundles:/go/src/github.com/docker/docker/bundles" -t "docker-dev:master" hack/make.sh binary
hack/make.sh脚本
上一节提到的make binary中创建的容器启动命令是hack/make.sh binary,运行容器中的(docker源码目录下的)hack/make.sh脚本,参数为binary。
make.sh中根据传入的参数组装后续编译用的flags(BUILDFLAGS),最后根据传入的参数依次调用 hack/make/目录下对应的脚本。例如我们的操作中传入的参数只有一个binary。那么在make.sh的最后,会调用hack/make/binary脚本。
hack/make/binary脚本中,就是直接调用go build进行编译了,其中会使用BUILDFLAGS LDFLAGS LDFLAGS_STATIC_DOCKER等编译选项。
如果最终生成的docker二进制文件不在bundles/1.10.0-dev/binary/目录下,那么可能是编译参数BINDDIR设置的不正确,可以在执行make binary时增加BINDDIR参数,例如
make BINDDIR=. binary , 将BINDDIR设置为当前目录。
总结
编译步骤总结:
1、编译前在物理机上安装好make、git,并下载好docker代码。下载好ubuntu:14.04镜像
2、执行make build 。这步执行完会在物理机上创建出一个docker-dev的镜像。
3、执行make binary 。 这步会使用docker-dev镜像启动一个容器,在容器中编译docker代码。编译完成后在物理机上直接可以看到二进制文件。默认二进制文件在 bundles/1.10.0-dev/binary/目录下
4、docker代码里有很多test,可以使用此套编译环境执行test,例如 make test 。 更多参数可以看Makefile
搭建环境心得:
1、在make build时,使用Dockerfile创建制作镜像,这个镜像有40多层,其中一层失败就会导致整个build过程失败。由于Dockerfile中很多步骤是要连到国外的网站去下载东西,很容易失败。好在docker build有cache机制,如果前面的层成功了,下次重新build时会使用cache跳过,节省了很多时间。所以如果make build中途失败(一般是由于国内连国外的网络原因),只要重新执行make build就会在上次失败的地方继续,多试几次可以成功。
2、如果其他人已经build出了docker-dev镜像,可以把它下载到自己的环境上。这样在自己make build时,会跳过那些已经在本地存在的层,可以节省时间。
3、每一次编译会自动删除掉前面已经生成的二进制文件,所以不用担心二进制文件不是最新的问题。