写在最前

偶然整理,翻出来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. jQuery开发之Ajax

    1.load()方法 (1)load()方法是jQuery中最经常使用和最简单的Ajax方法,能载入远程HTML代码,并插入代码中. 它的结构为: load(url [,data] [,callbac ...

  2. 全国省市区三级联动js

    function Dsy(){ this.Items = {}; } Dsy.prototype.add = function(id,iArray){ this.Items[id] = iArray; ...

  3. Mysql 数据库中间件

    读写分离:简单的说是把对数据库读和写的操作分开对应不同的数据库服务器,这样能有效地减轻数据库压力,也能减轻io压力.主数据库提供写操作,从数据库提供读操作,其实在很多系统中,主要是读的操作.当主数据库 ...

  4. grunt简单教程

    Grunt简单教程 1.grunt简单介绍 Grunt是一个基于任务的命令行工具.依赖于node.js环境. 它能帮你合并js文件,压缩js文件,验证js.编译less,合并css.还能够配置自己主动 ...

  5. MUI-折叠面板效果accordion

    在做开发的过程中我们经经常使用到折叠面板. 那我们来看下折叠面板到底是怎么使用. 废话不多说. 代码粘下来: <!DOCTYPE html> <html> <head&g ...

  6. ViewFlow增强onItemClick功能及ViewFlow AbsListView源代码分析

    先看实现效果,上图:  ViewFlow是一个非常好用的,用于不确定item个数的水平滑动切换的开源项目. 可是从github上下载的ViewFlow事实上是不支持onItemClick功能的,tou ...

  7. 基于第三方微信授权登录的iOS代码分析

    本文转载至 http://www.cocoachina.com/ios/20140922/9715.html 微信已经深入到每一个APP的缝隙,最常用的莫过分享和登录了,接下来就以代码的形式来展开微信 ...

  8. ruby 学习网站

    Ruby on Rails官网: http://rubyonrails.org/ Rails Guides:http://guides.rubyonrails.org/ -中文版: http://gu ...

  9. IOS中UIActionSheet使用方法详解

    一.初始化方法 - (instancetype)initWithTitle:(NSString *)title delegate:(id<UIActionSheetDelegate>)de ...

  10. p1199八数码问题

    oj上简化的八数码问题,最强的数据仅仅是20步: 根据曼哈顿距离构造启发函数: 主算法:IDA*:(使用方法好像不太对......) 未用位运算优化: #include<iostream> ...