写在最前

偶然整理,翻出来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. [转]c中按位分配int的方法

    从网上看到这样一段c代码,让我发觉我的C基本功还是不行啊~~ typedef struct xp { int a:2; int b:2; unsigned int c:1; } xp; 不知道大家对i ...

  2. Thinking in React(翻译)

    下面是React官方文档中的Thinking inReact文章的翻译,第一次翻译英文的文章,肯定有非常多不对的地方,还望多多包涵. 原文地址:https://facebook.github.io/r ...

  3. Git多账号登陆

        最近工作上遇到了使用git+repo的情况,需要用公司的邮箱和账号名重新申请ssh公私密钥,而我本身在github上也有一些开源项目,这里就是记录一下我是如何实现git多账号登陆的.   取消 ...

  4. location.href用法总结

    javascript中的location.href有很多种用法,主要如下. self.location.href=”/url” 当前页面打开URL页面 location.href=”/url” 当前页 ...

  5. 【转载】lvs为何不能完全替代DNS轮询

    上一篇文章"一分钟了解负载均衡的一切"引起了不少同学的关注,评论中大家争论的比较多的一个技术点是接入层负载均衡技术,部分同学持这样的观点: 1)nginx前端加入lvs和keepa ...

  6. PHP中的多行字符串传递给JavaScript方法两则

    PHP和JavaScript都是初学.近期有这么个需求: 例如说有一个PHP的多行字符串: $a = <<<EOF thy38 csdn blog EOF; 传递给JavaScrip ...

  7. 笔记整理--LibCurl开发

    LibCurl开发_未了的雨_百度空间 - Google Chrome (2013/7/26 21:11:15) LibCurl开发 一:LibCurl 编程流程1.调用curl_global_ini ...

  8. mysql读写分离(主从复制)实现

    mysql主从复制 怎么安装mysql数据库,这里不说了,仅仅说它的主从复制.过程例如以下: 主从最好都是同一种系统比方都是linux,或者都是windows,当然混合着也是能够成功,不解释了 1.主 ...

  9. SWT 安装

    下载地址: http://www.eclipse.org/windowbuilder/download.php 看到页面如下:选择图片标记的 3.7 (Indigo)  update site lin ...

  10. 提升自身的iOS编程水平 (转载)

    阅读博客 在现在这个碎片化阅读流行的年代,博客的风头早已被微博盖过.而我却坚持写作博客,并且大量地阅读同行的iOS开发博客.博客的文章长度通常在3000字左右,许多iOS开发知识都至少需要这样的篇幅才 ...