■  磁盘空间和docker资源之间的关系(devicemapper驱动)

  通过docker info | grep Space可以看到Docker占据的磁盘空间的信息。其中Data Space Used表示实际上docker资源占用掉的磁盘空间。相对应的Metadata Space Used是表示了所谓的metadata占据空间的大小。

  data和metadata默认情况下是放在/var/lib/docker/devicemapper/devicemapper中。由于一般/var属于系统盘,而系统盘的大小是比较小的,所以docker很容易占满整个系统盘的空间。所以最好在启动docker的时候指定一个数据盘上的另一个目录作为docker运行时的根目录。通过dockerd的-g或者--graph参数指定。

  另一方面,我们还可以在docker应用的这个层面限制一下docker资源最多可占用的空间的大小。这个数据体现在docker info中的Data Space Total,这个数字是docker启动时配置项可配置的,不是真的磁盘中的总空间。也就是说这个数字有可能是大于docker运行时目录所在磁盘的总空间的,它从docker应用层限制了docker发展的大小。

  这个数字的设置方法是dockerd --storage-opt dm.loopdatasize=500G --storage-opt dm.loopmetadatasize=20G --storage-opt dm.basesize=8G,三个参数限制的分别是docker资源总大小,metadata总大小,单个镜像的最大大小。在启动时配合docker运行时根目录所在磁盘的实际空间,设置一个合适的大小,可以避免docker把磁盘挤满这种事情的发生。

  如果使用systemctl的方式来启动docker的话可以修改/lib/systemd/system/docker.service文件,这个文件是通过systemctl启动dockerd时的各个参数,修改其中的ExecStart加上前面说的几个参数即可。

  ●  扩容

  如果在docker运行过程中发现了大小不够要扩容了,遵循上面的原理,大概可以这么操作。1. 备份所有镜像和容器,因为到时候会删掉原有data文件并创建新的。2.关停docker服务,像上面说的那样进行启动参数的更改。 3. 删除原有的数据文件,并利用dd命令生成一个指定大小的数据文件,参考【】:

sudo rm -rf /var/lib/docker
sudo mkdir -p /var/lib/docker/devicemapper/devicemapper/
sudo dd if=/dev/zero of=/var/lib/docker/devicemapper/devicemapper/data bs=1M count=0 seek=8192
sudo dd if=/dev/zero of=/var/lib/docker/devicemapper/devicemapper/metadata bs=1M count=0 seek=4096

  然后重启docker服务即可:

sudo systemctl daemon-reload
sudo systemctl start docker

  对于空间不够等情况,除了要扩容,更重要的应该是及时清理不要用的容器和镜像。对于镜像,在Linux上可以简单的认为是一个文件,删掉就会减少占用空间,新增就会增加占用空间。在其他一些系统如OSX上则可能会出现删掉了镜像但是空间没有空出来这种事。 * 其实在Linux上也不尽然,生成的镜像即使立刻删除,从dockerinfo的结果来看,似乎也总是会留一点东西在系统中。随着镜像的不断增多,系统的空间肯定是会一点一点变小的。

  对于容器,当一个容器被生成,容器中的内容自然是作为docker数据的一部分被写入磁盘的。容器在生命周期中,在磁盘中空间占用情况是只增不减的。也就是说当我在容器中建了一个1G的文件,磁盘自然多了1G占用,如果再建一个0.5G的文件,磁盘占用就到了1.5G。此时再把第二个文件删掉,虽然容器中看起来占据空间减少了,但是外面的磁盘中依然维持1.5G占用。除非把这个容器删除。  

■  关于利用python的subprocess模块与docker exec进行互动

  今天遇到个很操蛋的需求,要批量修改容器中的root用户密码。本以为不是什么难事,结果没想到那台主机不能接外网且没有paramiko,docker-py等其他模块。想来想去恐怕只能使用subprocess这个内建的模块来做一做。

  因为是subprocess,所以无疑是在主机本地做操作,而从本地最方便的办法就是通过类似于docker exec -ti <container> /bin/bash来进入到容器里面操作。手动尝试了下,更加操蛋的是容器都用了一个镜像且这个镜像里居然没有passwd命令。。视图装Passwd搞了半天都失败,只好用chpasswd来做。但是chpasswd直接docker exec -ti <container> 后面跟chpasswd的一些操作时总是无法顺利将容器内密码修改(相反还改了宿主机密码好几次)。最终还是绕回到用subprocess上来。

  一开始用subprocess的经典方法,运行这条命令时总是报错stdin is not tty之类的错误,后来注意到要去掉-t参数。所以通过subprocess修改一个容器的用户的密码的代码如下,如果将chpasswd命令部分换成其他命令也可以看成是通过subprocess,在宿主机本地来和容器做交互的一种途径:

from subprocess import *

p = Popen('docker exec -i test /bin/bash',shell=True,stdin=PIPE,stdout=PIPE,stderr=PIPE)
# 去掉了-t参数,test为容器名,stdin为PIPE是为了communicate时能够向身为子进程的容器伪终端传递更多命令

out,err = p.communicate(input='echo root:123456|chpasswd\nexit\n')
# 此时密码已经改变,原先为终端中的两行命令通过换行符放在一行中

■  关于容器内外时间同步性问题

  容器内时间和容器外时间(即宿主机的时间)可能会发生不同步的情况。如果刚好差了八个小时,可能是因为两者没用采取统一的时区制。比较常见的情况是容器时间比宿主时间晚八个小时。即容器中采用的是UTC而宿主机使用了CTS-8时区。

  此时解决的办法是修改Dockerfile或者在容器启动时加入参数,把宿主机的/etc/localtime挂载到容器的/etc/localtime,此时容器就可以保持和宿主的时间一致了。  

  另一种反过来的问题是,docker是对文件系统的虚拟,容器和宿主机使用的仍然是一套相同的linux内核。然而时间日期调取是内核方法,使得比如当使用date -s修改宿主机or容器的时间时,另一边也会被修改。如何才能做到容器和宿主机时间不同步?

  在网上找到了一个比较好的解决方案:https://github.com/wolfcw/libfaketime/,将这个libfaketime项目下载下来,然后用make,make install的方式安装到容器中(注意容器中要有gcc以及make相关组件,如果没有建议yum安装下),然后在容器中设置一个环境变量:

export LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1

   之后在这个shell环境中使用date命令,或者其他的一些什么程序调用内核获取时间日期的方法就可以修改了。修改的方式是在获取之前设置一个名为FAKETIME的环境变量,如export FAKETIME="+8h"。这就是表示容器中的时间保持原时间基础上加上8个小时的时间。export过后在容器内外分别运行date命令就可以看到两者的不同了。+8h这块的语法可以参见项目主页的文档。

  * 在用了这个方案之后,容器将默认用回UTC时间。可能是因为libfaketime这个库绕开了内核获取时间的同时也绕开了读取/etc/localtime。这个不知道该怎么解决比较好,但是在faketime的基础上可以先export FAKETIME="+8h",先使得容器内外时间同步且为北京时间。知晓这个的基础上,我们在设置不同的FAKETIME来设置不同的容器内时间。

  值得一提的是,FAKETIME可以设置成一个固定的时间,格式是%Y-%m-%d %X,但是这是固定的,如果设置那么之后date命令的返回永远是这个值不会变。在一般情况下项目给出了@%Y-%m-%d %X这样的FAKETIME设置形式来设置时间从一个时间点开始流逝,但是对于docker环境似乎并不适用。github上也有issue了,看怎么解决了。总结一下就是用相对时间的加加减减一些<数字><单位>如+5m,-1.5d,+20h都是可以用的。 

■  关于devicemapper/mnt和容器对应关系(基于Docker 1.12)

  不是讲解devicemapper的原理… 但是我们知道devicemapper下的mnt目录下面有一堆散列名目录。这些目录往往都一对一地对应着某个容器/镜像的文件系统结构。

  那么怎么把通过这个散列找到对应容器呢?

  其实可以查看docker inspect 某个容器的信息中的GraphDriver.Data.DeviceName,后面有一串所谓的pool名,就是这个散列值。找出对应的就好了。如果嫌散列值比较难看的话也可以在devicemapper/metadata中找到对应散列值的json配置文件,里面标注了device_id,和GraphDriver.Data.DeviceId一致的就是了 

■  关于aufs、devicemapper和overlay 区别的简单理解

  之前一直没注意到,其实docker在将容器/镜像内容存储到宿主机这一方面存在多种方式。每种方式对应这一种存储驱动。副标题中的aufs, devicemapper, overlay都是一种驱动方式而已。三者是按照从旧到新的顺序写出来的。aufs这种驱动在centos6上就已经存在了,最为古老;devicemapper常见于centos7上,通过epelyum源直接安装的docker采用的就是这种驱动;overlay比较新,也常用于centos7上。

  说到三者的特点,overlay驱动最为优化。采取overlay驱动的容器/镜像相关数据保存在docker根目录(通常是/var/lib/docker)的overlay目录下。这个目录下有很多经过点窜的目录名,代表的是一个个文件系统层。对于容器层的每个目录下都有lowerid记录着它下一层的数据的ID,还有就是upper目录记录的是这一层中做出的文件的新增和改变的情况。overlay的一个重要特点就是容器内外文件的完全同步。如果在容器中删除掉一些新增的文件,那么在相应的容器的upper目录下面,这些文件也会被删除,因此可以实时释放宿主机的磁盘空间。  * 但是需要注意,不应该在宿主机中直接对upper目录中的内容进行删除。容器中看到的是其实是merged视图(综合了lowerdir和upperdir两个文件系统层的内容),其中通过image层直接引用或者container层覆盖了image层 or container层自有的文件,这些文件在容器里显示的inode和在宿主机相应点窜目录中查看inode一致,如图所示:

docker prometheu数据本地化 docker metadata_shell

  但是在overlay文件系统中,似乎存在这样一种机制。从merged视图中发起的删除,会将文件对应block需要删除标记告知给宿主机文件系统,因此容器中删除文件会直接释放宿主机的磁盘空间。但是宿主机中删除某个文件,无从告知容器中的文件系统(想想也是,宿主机里有那么多和容器无关的文件,很难做到告知),导致容器中会认为,原先文件对应的block块已经没有了inode的引用,但是又不能释放,此时会出现一些异常现象。比如一个文件明明ls看不到,但touch一下也不会出现,程序显示写入文件正常,但是又无从看到写入的内容等等。

  相比之下,devicemapper驱动是类似于数据库的数据文件那样,首先在系统中创建一个可扩容的数据文件data用来保存容器/镜像数据,这个文件的最大大小默认通常是100G,位于docker根目录devicemapper/devicemapper/中。这个目录下还有metadata文件。之后所有的容器/镜像实质内容数据都是被写入到data和metadata两个文件中的,其他的一些配置和相关的内容可能会存放在devicemapper目录下的其他地方。devicemapper驱动的一个不好的地方就是无法实时地将宿主机的磁盘空间释放出来(也是我上面提到过的)。因为数据增加之后data文件扩大,但是将容器内的内容删除之后data文件不会自动缩回原来的大小。相当于整个data文件只会变大不会变小直到达到指定的最大大小,除非在这个过程中进行docker服务的重启或者容器的删除重建。

  aufs驱动现在用的已经不多了,主要是centos6通过yum安装docker-io的时候会用到这种驱动。在/var/lib/docker/aufs中会有diff,layers,mnt三个目录。layers目录保存了一些以层ID命名的文件,其中记录了这个层的下面有哪些层作为基础。diff目录下则是一些以层ID命名的子目录,每个目录下是本层针对所有基础层所作出的变化。私以为aufs比较好理解,因为它把包括一个容器对应的镜像的所有层,再加上容器本身的读写层都原原本本地展示出来,理解docker原理的人可以很轻松的根据层级去寻找自己想要的信息。mnt目录下则是各个容器根目录的挂载点,容器层ID为名的那个子目录下面的内容就是和容器中根目录完全对应的。