1 笨拙的docker registry

由于我厂离线交付的特殊性,我们需要在客户现场运行一个私有registry,该registry包含我们产品的所有app的镜像;有registry的好处是,当kubernets上的pod在不同节点上漂移时,只要从私有registry上docker pull镜像即可。

私有registry的搭建比较简单,可以参考这篇文章,但这样搭建出来的registry中没有任何docker镜像,因此需要:

  1. 开发打包时,将产品所有app都docker save为对应app的tar包;
  2. 客户现场部署时,启动私有registry;
  3. 客户现场部署时,将各app的tar包先docker load到本地,然后docker push到私有registry。

我把这个过程叫做”回放”。

这个方案的优势是,多个开发人员同时打包时,互相之间不受影响;但有一些缺点:

  • 产品交付件会比较大。我们知道docker镜像是分层的,多个app会有复用的layer;但由于每个app都是独立的tar包,因此每个tar包里都有一个复用layer的副本,自然交付件会比较大,比较浪费。
  • regitry回放时,docker load 和docker push都非常耗时,在我们不算差的服务器上基本需要半个小时的时间,体验很糟糕。

2 灵活一点:打一个大包

n in 1

还记得小时候玩的一张卡带300游戏的红白机小霸王吗?拿到以后是不是感觉特别爽,300个游戏啊!但其实,90坦克和疯狂坦克可能只是敌方坦克的速度不同,超级马里奥和水管玛丽可能只是衣服颜色换了换。300个游戏,其实只是几个游戏的衍生版。

到k8s这,其实也差不多;我们的改进,也可以参考下小霸王游戏机。

改进点1:

前面我们提到docker镜像是分层的,如果将所有镜像都打包为一个文件,自然会节省很大的空间。

改进点2:

docker registry中保存的信息,通常保存在/var/lib/registry中,其中包含了2大信息:存储在blobs中的image layer,和存储在repositories的image元数据(有哪些project、哪些image等)。

因此在开发打包时,在开发环境拉起一个私有registry,将产品所有app从harbor上docker pull下来以后,再docker push到该私有registry,并将registry的/var/lib/registry打包;在客户现场部署时,只要将这个包解封后挂载到新的私有registry,就可以完成回放了。

通过这两个改进,最终交付的registry离线包就只有1个大包,而且这个大包只要在客户现场解封后挂载到新docker registry容器中即可;这样耗时的操作放到了频率很低的开发打包阶段,能够提高部署的效率;包的size也变小了很多,我们实测的结果是,之前10GB的app在合并后只有3GB多一点,这“压缩比”太可观了。

但将”回放“移到开发打包阶段的坏处是,多个开发人员同时打包时,都在操作同一个docker registry,势必会互相影响:最终打包的不知道是张三的版本还是李四的版本,这取决于张三和李四谁push的更晚。

开发者们需要隔离。

要隔离不同的开发者,一个可选的方案是使用虚拟机,独门独院,在虚拟机中启动独立的registry容器,不同开发人员完全隔离,但我们没这个条件(无法动态的创建、删除虚拟机);另一个可选的方案是,使用docker in docker。

3 隔离:docker in docker

顾名思义,docker in docker是指在docker容器中运行docker。我们可以在docker容器里运行web应用、mysql服务甚至spark,那么自然也可以在docker容器里运行docker服务本身。

在dind(docker in docker)容器里,可以看到一个新的docker daemon,它与宿主机上的docker daemon没有任何关系。我们可以在dind容器里docker pull/push拉取推送镜像,也可以docker run启动一个新的docker,宛如一个虚拟机。

有了dind,前面所说的隔离就很好解决了。

3.1 开发打包registry.tar

1 启动docker in docker

docker run --privileged -v {registry_dir}:/root/registry:rw -v {pwd}:/root:rw --name {name} -d {image} --insecure-registry docker.ieevee.com

说明几点。

  1. dind容器必须要privileged高权限
  2. {registry_dir}映射到/root/registry,这个目录用来存储docker registry的blogs和repositories。之后在dind中启动docker registry时需要将/root/registry再映射到docker registry的/var/lib/registry目录,通过2层-v映射,打到离线registry的目的。
  3. 由于需要在dind中pull harbor的镜像,而我们的harbor并没有启动https,所以需要传一个insecure-registry参数给dind容器,该参数最终会传给dind中启动的docker daemon。
  4. 不需要设置--storage-driver。默认dind的docker daemon使用的是vfs(据说会有点慢),如果你的os是ubuntu,那么可以传一个--storage-driver=aufs给dind,但如果你的os是centos 7.2,传--storage-driver=devicemapper给docker daemon,会造成在dind中docker pull时报错。统一起见,不要设置了。

2 在dind中pull产品所有app

在宿主机上:

docker exec -it {dind id} sh -c docker pull {image}

3 在dind容器中拉起docker registry容器

在宿主机上:

docker exec -it {dind id} sh -c docker run -d -p 80:5000 --restart always -v /root/registry:/var/lib/registry:rw --name registry registry:2

4 在dind容器中将所有app镜像push到docker registry容器

在宿主机上:

docker exec -it {dind id} sh -c docker push {image}

5 将1中映射的{registry_dir}打包,但不要压缩

tar cf registry.tar {registry_dir}

3.2 客户现场恢复registry.tar

客户现场的操作比较简单了,只需2步:

tar xf {repo_tar} -C /home/
docker run -d -p 80:5000 --restart always -v /home/{registry_dir}:/var/lib/registry:rw --name registry registry:2

由于打包时没有压缩,这里tar的速度还是挺快的。

4 docker in docker的问题

虽然dind很方便,作者Jérôme Petazzoni建议,不要用docker in docker来做CI。

具体解释一下,这里是指,CI运行在容器中,如果要启动新的app容器,那么势必就要在容器中启动容器了,也就是docker in Docker。

Jérôme Petazzoni 在其blog中提到了dind的3个问题:

1 dind跟Linux Security Modules (LSM)配合不好。

when starting a container, the “inner Docker” might try to apply security profiles that will conflict or confuse the “outer Docker.”

不同linux distribution的LSM不同,有的是AppArmor,有的是SELinux,内部docker启动时会修改外部docker的安全配置,如果内外不同可能会有问题。

我这里倒是ok,因为centos上的SELinux是Disable的。

2 storage driver

外部docker daemon的storage driver通常是一个传统的文件系统,例如我们用的是ext4(–storage-opt dm.fs=ext4),但内部docker daemon只能用copy-on-write文件系统,如aufs,devicemapper, brtfs等。内外docker daemon的stroage driver不是随意配对的,只能固定组合。aufs on aufs很好用,但brtfs on brtfs在subvolume时会造成父volume卸载失败,而Devicemapper则干脆就不支持namespace。

我们的解决办法是使用vfs,目前来看没遇到什么问题。

3 build cache

由于dind容器是个全新的环境,一个docker镜像都没有,一切都需要从头开始pull。dotCloud将/var/lib/docker共享给了多个docker daemon,但遇到了数据损坏的问题。

这个没有很好的解决方法,只能先忍了。

5 docker in docker优化

Jérôme Petazzoni 给了一个优化方法:将宿主机的docker.sock挂载到dind容器里去。

docker run -v /var/run/docker.sock:/var/run/docker.sock -ti docker

这样dind中的docker client连接的是宿主机的docker daemon,新创建的docker容器,不再是dind容器的子容器,而是dind容器的兄弟姐妹:因为它们是宿主机的docker daemon创建的!

这样能够解决CI in docker的问题,因为CI要求的,其实只是能在docker中跑CI即可,至于CI拉起的app容器,并不要求一定要在docker里面。

但这正是我想避开的:多个docker registry在同一个宿主机上,他们无法隔离,甚至因为registry都需要映射80端口,实际只能跑一个registry。

6 综述

总的来说,虽然docker in docker有一些缺点,但我这里都不是事;用docker in docker 来做docker registry的离线,体验还不错,值得推荐。