写在最前

偶然整理,翻出来14年刚开始学docker的时候的好多资料。当时docker刚刚进入国内,还有很多的问题。当时我们的思考方式很简单,docker确实是个好的工具,虽然还不成熟。但是不能因为短时间内造桥不行,就不过河了。我们的方式很简单,先造个小船划过去。由于各种条件的局限,所以很多方法真的是因陋就简,土法上马,一切就是为了抓紧落地。时代更迭、版本变迁,这其中的很多技术方案本身可能已经无法为现有的方案提供有力的帮助了。但是解决问题的思路和原理可能还能为大家提供一点参考吧。这于我自己,也是一个整理回顾。所以我计划写成一个小的系列文章,这个系列直接取名为土法搞docker。

当时遇到的第一个问题,就是docker的底层graph driver,在centos 6下的devicemapper不稳定,有很大的概率会造成内核崩溃。但是如果不解决这个问题,是绝对无法将docker上到生产环境中的。以我贫瘠的内核知识和存储知识,完全无力解决。那怎么办,那就用土办法,自己写一个graph driver。之所以叫自制而不叫自研,因为真的没有多少可以称之为研究的东西,完全是拼凑而成。自制的这个driver本身没有多少技术含量,但是需要深入了解docker的运行原理和底层的存储方式,然后寻找一种恰当的方式来解决它。

graph driver原理

graph driver的原理和接口从1.3到现在的最新版本,基本没有什么变化。这也有赖于docker当时优秀的设计。首先说graph driver是干什么的。我们都知道docker的镜像/容器是由多层组成。graph driver其实就是负责了层文件的管理工作。

这里是driver接口的一些方法:

// ProtoDriver defines the basic capabilities of a driver.
// This interface exists solely to be a minimum set of methods
// for client code which choose not to implement the entire Driver
// interface and use the NaiveDiffDriver wrapper constructor.
//
// Use of ProtoDriver directly by client code is not recommended.
type ProtoDriver interface {
// String returns a string representation of this driver.
String() string
// Create creates a new, empty, filesystem layer with the
// specified id and parent. Parent may be "".
Create(id, parent string) error
// Remove attempts to remove the filesystem layer with this id.
Remove(id string) error
// Get returns the mountpoint for the layered filesystem referred
// to by this id. You can optionally specify a mountLabel or "".
// Returns the absolute path to the mounted layered filesystem.
Get(id, mountLabel string) (dir string, err error)
// Put releases the system resources for the specified id,
// e.g, unmounting layered filesystem.
Put(id string)
// Exists returns whether a filesystem layer with the specified
// ID exists on this driver.
Exists(id string) bool
// Status returns a set of key-value pairs which give low
// level diagnostic status about this driver.
Status() [][2]string
// Cleanup performs necessary tasks to release resources
// held by the driver, e.g., unmounting all layered filesystems
// known to this driver.
Cleanup() error
}

为了方便,我们举一个例子。假设某个镜像由两层组成,layer1(lower)和layer2(upper)。layer1中有一个文件A,layer2中有一个文件B。那么对单一的layer2来说,其实只有一个文件也就是B。但是通过联合文件系统将layer1和layer2联合起来时,得到layer1+2,将layer1+2挂载起来,那么获得的挂载点文件夹下应该包含了A和B两个文件(本文中将这种挂载点称为联合挂载点)。

这里结合这个例子分别对这些中比较重要的方法进行一下介绍:

  • Create: 创建一层,比如创建layer2
  • Remove: 移除某一层,比如移除layer2
  • Get: 将id及其下层的所有层通过联合文件系统联合起来的layer1+2,将layer1+2挂载起来,返回挂载点
  • Put: 将id及其下层的所有层通过联合文件系统联合起来的layer1+2的挂载点umount掉
  • Exists: 判断id层是否存在
  • Cleanup: 将所有的挂载起来的全部卸载掉

其实docker对于层文件的操作,都是通过这些接口组合而成的。比如docker创建容器时,最终需要用Create为容器创建一个新的可读写的层。而docker运行容器时,需要通过Get接口获取容器及其镜像所有层联合起来的文件从而形成容器的rootfs。

在分析这些接口时,我们其实可以发现一个问题,其实接口中没有获取单层,比如只获取layer2的接口。比如docker save镜像时,因为要导出每一层的单独的文件,这又是如何实现的呢?其基本原理其实算是Get(layer1)以及Get(layer2),然后将两层的挂载的文件夹进行diff,从而得到只归属于layer2层的文件。

func (gdw *NaiveDiffDriver) Diff(id, parent string) (arch archive.Archive, err error) {
driver := gdw.ProtoDriver
//获取id的联合挂载点
layerFs, err := driver.Get(id, "")
...
//获取parent的联合挂载点
parentFs, err := driver.Get(parent, "")
...
//遍历两个挂载点内的所有文件并进行比较,得到二者的差异文件
//则差异文件就是只属于id层的文件列表
changes, err := archive.ChangesDirs(layerFs, parentFs)
...

这里我们可以特别想下,接口没有获取单层,也就是获取layer2这种层的接口,那么其实就意味着docker其实并不真的需要一个联合文件系统。这也就是我们能够自制vdisk的基础。

那么大家可能会有个小疑问,既然不一定真的需要联合文件系统,那么使用或者不使用联合文件系统有什么差别呢?差别并不在Get接口上,而是在Create接口上。使用联合文件系统时,创建一个新的单层可以非常快速,因为新的层的内容为空。而不使用联合文件系统呢,则需要将所有父层的文件全部拷贝到新的层中,以便在Get接口调用时可以快速挂载。这样二者的创建效率就一目了然了。

docker自身也支持一个默认的非联合文件系统的graph driver,也就是vfs。

vfs这个驱动简单明了。我当年就是从这里开始graph driver的理解和学习的。

vdisk原理

我们的实际需求其实是要在centos下用一个非联合文件系统的方式来取代devicemapper,实现一个稳定可靠的底层存储。那么如何实现,其实有几种路线选择。vfs足够简单稳定,但是无法限制用户对于磁盘的使用量。使用不同的lvm盘来存储每层,由于需要预分配足够的磁盘空间,又会导致磁盘空间的浪费。最终,我们选择了一个折中的方案。就是使用稀疏文件来存储每一层,然后通过loop设备挂载,来表达联合文件系统的挂载效果。

那么同上一个例子,对于layer1的所在层,我们其实可以创建稀疏文件file1,并在其中存储文件A。而对于layer2的所在层如何处理呢?因为接口中没有获取单层文件的接口,我们因此可以创建file2,并在其中存储文件A和B,也就是layer1+2,来实现layer1和layer2的联合。而对于只导出layer2时,只需要将file1和file2的文件进行diff就可以处理了(同上文所说)。

明白了这个原理后,其实代码就好写了。这也是我当时刚学golang后写的第一个docker功能。代码原理上我参考了vfs的实现,也参考了dm驱动的deviceset进行loop设备的管理。其实完全是东拼西凑来的,这里就不献丑了,回头我传到github (https://github.com/xuxinkun) 上去,有兴趣的再来围观吧。

vdisk的弊端

这个驱动因为使用的是稀疏文件和loop设备,因此我命名为loopfile,后来被改名为vdisk。这个驱动原是想应急使用。但是因为足够简单,所以足够稳定。在线上几乎是零故障。虽然后来修复了devicemapper的bug,但是在JDOS 1.0的集群上仍然大规模使用的是这个。当然这其中的一个重要原因其实是因为1.0(基于openstack,采用nova+docker方式管理)还是将容器当做虚拟机来使用,实际创建完容器,仍然需要用户通过部署平台来部署脚本。因此对于容器创建时间不是那么敏感。同时由于镜像预分发,所以创建时间并不是太大的问题。

但是如果镜像层数过多,因为每层的文件中要包含全部父层的文件,存在很大的冗余空间占用。为了解决Dockerfile或者多次commit导致的镜像多层问题,我还为docker增加了compress功能,用以将多层压缩为一层。这个的实现方式我将在后续文章中讲述。

后来,进入到JDOS 2.0时代,这种方式就完全无法应付快速启动容器的需求了。dm的问题也由团队后来的内核专家进行了解决。从此我们就跨入了dm的时代。当然这些就是后话了。

土法搞docker系列之自制docker的graph driver vdisk的更多相关文章

  1. docker系列四之docker镜像与容器的常用命令

    docker镜像与容器的常用命令 一.概述   docker的镜像于容器是docker中两个至关重要的概念,首先给各位读者解释一下笔者对于这两个概念的理解.镜像,我们从字面意思上看,镜子里成像,我们人 ...

  2. 8天入门docker系列 —— 第一天 docker出现前的困惑和简单介绍

    docker出来也有很多年了,但用到的公司其实并不是很多,docker对传统开发是一个革命性的,几乎颠覆了之前我们传统的开发方法和部署模式,而大多 公司保守起见或不到万不得已基本上不会去变更现有模式. ...

  3. Docker系列(七):Docker图形化管理和监控

    Docker管理工具之官方三剑客 Docker Machine是什么鬼 从前 现在 你需要登录主机,按照主机及操作系统特有的安装以及配置步骤安装Docker,使其 能运行Docker容器. Docke ...

  4. Docker系列二:Docker的基本结构

    Docker的基本结构 Docker 的三大基础组件 Docker有三个重要的概念:仓库 , 镜像 和 容器 ,它们是Docker的三大基出组件 Docker的组织结构 Docker处于操作系统和虚拟 ...

  5. Docker系列一:Docker基本概念及指令介绍

    1. Docker是什么? Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化.容器是完全使用 ...

  6. Docker系列一:Docker的介绍和安装

    Docker介绍 Docker是指容器化技术,用于支持创建和实验Linux Container.借助Docker,你可以将容器当做重量轻.模块化的虚拟机来使用,同时,你还将获得高度的灵活性,从而实现对 ...

  7. Docker系列(五):Docker网络机制(上)

    Linux路由机制打通网络 路由机制是效率最好的 docker128上修改Docker0的网络地址,与docker130不冲突 vi /usr/lib/systemd/system/docker.se ...

  8. Docker系列(三):Docker自定义容器镜像

    将容器编程镜像: docker commit [repo:tag] 网上有这句话:当我们在制作自己的镜像的时候,会在container中安装一些工具.修改配置,如果不做commit保存 起来,那么co ...

  9. Docker系列(二):Docker基础命令

    docker的部署安装(Linux kernel至少3.8以上): yum install docker docker1.8安装:(下面 是两个命令) # cat >/etc/yum.repos ...

随机推荐

  1. 第24章、OnLongClickListener长按事件(从零开始学Android)

    在Android App应用中,OnLongClick事件表示长按2秒以上触发的事件,本章我们通过长按图像设置为墙纸来理解其具体用法. 知识点:OnLongClickListener OnLongCl ...

  2. Python基础语法07--面向对象+正则表达式

    Python 面向对象 Python从设计之初就已经是一门面向对象的语言,正因为如此,在Python中创建一个类和对象是很容易的.本章节我们将详细介绍Python的面向对象编程. 如果你以前没有接触过 ...

  3. 源代码方式向openssl中加入新算法完整具体步骤(演示样例:摘要算法SM3)【非engine方式】

    openssl简单介绍 openssl是一个功能丰富且自包括的开源安全工具箱.它提供的主要功能有:SSL协议实现(包括SSLv2.SSLv3和TLSv1).大量软算法(对称/非对称/摘要).大数运算. ...

  4. 设计模式入门之原型模式Prototype

    //原型模式:用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象 //简单来说,当进行面向接口编程时,假设须要复制这一接口对象时.因为不知道他的详细类型并且不能实例化一个接口 //这时就须要 ...

  5. iOS 相似淘宝商品详情查看翻页效果的实现

    基本思路: 1.设置一个 UIScrollView 作为视图底层,而且设置分页为两页 2.然后在第一个分页上加入一个 UITableView 而且设置表格可以上提载入(上拉操作即为让视图滚动到下一页) ...

  6. OC小实例关于init 方法不小心的错误

    OC小实例关于init 方法不小心的错误  正视遇到的每一个错误 在一个遥控器类操控小车玩具的小实例项目中,我采用组合的方式,将遥控器拥有小汽车对象(has a)关系,而不是继承(is a)关系. 想 ...

  7. 1987年国际C语言混乱代码大赛获奖的一行代码

    macb() ? lpcbyu(&gbcq/_\021%ocq\012\0_=w(gbcq)/_dak._=}_ugb_[0q60)s+ 这是CoolShell博主之前做了一个非常有意思的在线 ...

  8. 在git push前怎样遗弃掉历史commit

    今天写了一天代码,然后 git hub commit 了 多达 7 次. 可是都没有把改动正式推送上去. 结果最后要推送的时候发现中间有一个提交文件超过了100M. 是 vs 的代码性能分析报告 .v ...

  9. 【bzoj4554】[Tjoi2016&Heoi2016]游戏

    现在问题有硬石头和软石头的限制 所以要对地图进行预处理 分行做,把有#隔开的*(x)形成联通块的存储下来. 分列作,把有#隔开的*(x)形成联通块的存储下来. 求出所有的行联通个数和列联通个数 作为二 ...

  10. 在Java中如何正确地终止一个线程

    1.使用Thread.stop()? 极力不推荐此方式,此函数不安全且已废弃,具体可参考Java API文档 2.设置终止标识,例如: import static java.lang.System.o ...